first commit
Some checks failed
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Some checks failed
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
50
.github/workflows/backport.yml
vendored
Normal file
50
.github/workflows/backport.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
install-command: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
- name: Compute backport targets
|
||||
id: targets
|
||||
run: node .github/scripts/compute-backport-targets.mjs
|
||||
|
||||
- name: Backport
|
||||
if: steps.targets.outputs.target_branches != ''
|
||||
uses: korthout/backport-action@c656f5d5851037b2b38fb5db2691a03fa229e3b2 # v4.0.1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
source_pr_number: ${{ github.event.pull_request.number }}
|
||||
target_branches: ${{ steps.targets.outputs.target_branches }}
|
||||
pull_description: |-
|
||||
# Description
|
||||
Backport of #${pull_number} to `${target_branch}`
|
||||
|
||||
# Original description
|
||||
|
||||
${pull_description}
|
||||
pull_title: ${pull_title} (backport to ${target_branch})
|
||||
add_author_as_assignee: true
|
||||
copy_assignees: true
|
||||
copy_requested_reviewers: true
|
||||
copy_labels_pattern: '^(?!Backport to\b).+' # Copy everything except backport labels
|
||||
experimental: >
|
||||
{
|
||||
"conflict_resolution": "draft_commit_conflicts"
|
||||
}
|
||||
71
.github/workflows/build-base-image.yml
vendored
Normal file
71
.github/workflows/build-base-image.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: 'Build: Base Image'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'docker/images/n8n-base/Dockerfile'
|
||||
- '.github/workflows/build-base-image.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docker/images/n8n-base/Dockerfile'
|
||||
- '.github/workflows/build-base-image.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
push:
|
||||
description: 'Push to registries'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node_version: ['22', '24.13.1', '25']
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to DHI Registry (for pulling base images)
|
||||
uses: ./.github/actions/docker-registry-login
|
||||
with:
|
||||
login-ghcr: 'false'
|
||||
login-dhi: 'true'
|
||||
dockerhub-username: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to Docker registries (for pushing)
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push == true)
|
||||
uses: ./.github/actions/docker-registry-login
|
||||
with:
|
||||
login-ghcr: 'true'
|
||||
login-dockerhub: 'true'
|
||||
dockerhub-username: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/images/n8n-base/Dockerfile
|
||||
build-args: |
|
||||
NODE_VERSION=${{ matrix.node_version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
provenance: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push == true) }}
|
||||
sbom: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push == true) }}
|
||||
push: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push == true) }}
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/base:${{ matrix.node_version }}-${{ github.sha }}
|
||||
${{ secrets.DOCKER_USERNAME }}/base:${{ matrix.node_version }}
|
||||
ghcr.io/${{ github.repository_owner }}/base:${{ matrix.node_version }}-${{ github.sha }}
|
||||
ghcr.io/${{ github.repository_owner }}/base:${{ matrix.node_version }}
|
||||
no-cache: true
|
||||
41
.github/workflows/build-benchmark-image.yml
vendored
Normal file
41
.github/workflows/build-benchmark-image.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: 'Build: Benchmark Image'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/@n8n/benchmark/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- '.github/workflows/build-benchmark-image.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: ./.github/actions/docker-registry-login
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/@n8n/benchmark/Dockerfile
|
||||
platforms: linux/amd64
|
||||
provenance: false
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/n8n-benchmark:latest
|
||||
100
.github/workflows/build-unit-test-pr-comment.yml
vendored
Normal file
100
.github/workflows/build-unit-test-pr-comment.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
name: 'Build: Unit Test PR Comment'
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
actions: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
validate_and_dispatch:
|
||||
name: Validate user and dispatch CI workflow
|
||||
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/build-unit-test')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate user permissions and collect PR data
|
||||
id: check_permissions
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const commenter = context.actor;
|
||||
const body = (context.payload.comment.body || '').trim();
|
||||
const isCommand = body.startsWith('/build-unit-test');
|
||||
const allowedPermissions = ['admin', 'write', 'maintain'];
|
||||
const commentId = context.payload.comment.id;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
async function react(content) {
|
||||
try {
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
content,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Failed to add reaction '${content}': ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
core.setOutput('proceed', 'false');
|
||||
core.setOutput('headSha', '');
|
||||
core.setOutput('prNumber', '');
|
||||
|
||||
if (!context.payload.issue.pull_request || !isCommand) {
|
||||
console.log('Comment is not /build-unit-test on a pull request. Skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
let permission;
|
||||
try {
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username: commenter,
|
||||
});
|
||||
permission = data.permission;
|
||||
} catch (error) {
|
||||
console.log(`Could not verify permissions for @${commenter}: ${error.message}`);
|
||||
await react('confused');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowedPermissions.includes(permission)) {
|
||||
console.log(`User @${commenter} has '${permission}' permission; requires admin/write/maintain.`);
|
||||
await react('-1');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const prNumber = context.issue.number;
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
await react('+1');
|
||||
core.setOutput('proceed', 'true');
|
||||
core.setOutput('headSha', pr.head.sha);
|
||||
core.setOutput('prNumber', String(prNumber));
|
||||
} catch (error) {
|
||||
console.log(`Failed to fetch PR details for PR #${context.issue.number}: ${error.message}`);
|
||||
await react('confused');
|
||||
}
|
||||
|
||||
- name: Dispatch build/unit test workflow
|
||||
if: ${{ steps.check_permissions.outputs.proceed == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_SHA: ${{ steps.check_permissions.outputs.headSha }}
|
||||
PR_NUMBER: ${{ steps.check_permissions.outputs.prNumber }}
|
||||
run: |
|
||||
gh workflow run ci-manual-unit-tests.yml \
|
||||
--repo "${{ github.repository }}" \
|
||||
-f ref="${HEAD_SHA}" \
|
||||
-f pr_number="${PR_NUMBER}"
|
||||
96
.github/workflows/build-windows.yml
vendored
Normal file
96
.github/workflows/build-windows.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: 'Build: Windows'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
notify_on_failure:
|
||||
description: 'Send Slack notification on build failure'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_call:
|
||||
inputs:
|
||||
notify_on_failure:
|
||||
description: 'Send Slack notification on build failure'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
QBOT_SLACK_TOKEN:
|
||||
required: false
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- '**/package.json'
|
||||
- '**/turbo.json'
|
||||
- '.github/workflows/build-windows.yml'
|
||||
- '.github/actions/setup-nodejs/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Node.js and Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: pnpm build
|
||||
|
||||
- name: Smoke test pnpm start -- -- --version
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Running smoke test: pnpm start -- -- --version"
|
||||
|
||||
pnpm start -- -- --version
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "`n❌ Smoke test failed (exit code: $LASTEXITCODE)"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "`n✓ Smoke test passed"
|
||||
|
||||
- name: Send Slack notification on failure
|
||||
if: failure() && inputs.notify_on_failure == true
|
||||
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.QBOT_SLACK_TOKEN }}
|
||||
payload: |
|
||||
{
|
||||
"channel": "C035KBDA917",
|
||||
"text": "🚨 Windows build failed for `${{ github.repository }}` on branch `${{ github.ref_name }}`",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": { "type": "plain_text", "text": "🚨 Windows Build Failed" }
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{ "type": "mrkdwn", "text": "*Repository:*\n<${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}>" },
|
||||
{ "type": "mrkdwn", "text": "*Branch:*\n`${{ github.ref_name }}`" },
|
||||
{ "type": "mrkdwn", "text": "*Commit:*\n`${{ github.sha }}`" },
|
||||
{ "type": "mrkdwn", "text": "*Trigger:*\n${{ github.event_name }}" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": { "type": "mrkdwn", "text": ":warning: *Cross-platform compatibility issue detected*\nThis likely indicates Unix-specific commands in package.json scripts or build configuration that don't work on Windows." }
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": { "type": "plain_text", "text": ":github: View Workflow Run" },
|
||||
"style": "danger",
|
||||
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
111
.github/workflows/ci-check-eligibility-reusable.yml
vendored
Normal file
111
.github/workflows/ci-check-eligibility-reusable.yml
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
# Determines if conditions are met for running subsequent jobs on a Pull Request.
|
||||
#
|
||||
# !! IMPORTANT !!
|
||||
# This workflow RELIES on being called from a parent workflow triggered by
|
||||
# a `pull_request` or `pull_request_target` event. It uses `github.event`
|
||||
# to access PR details.
|
||||
#
|
||||
# It checks if all the following conditions are TRUE:
|
||||
# 1. The PR is NOT from a fork (i.e., it's an internal PR).
|
||||
# 2. The PR has been approved by a maintainer (`is_pr_approved_by_maintainer`).
|
||||
# 3. The PR's source branch does NOT match an excluded pattern.
|
||||
# 4. The PR includes relevant file changes (`paths_filter_patterns`).
|
||||
#
|
||||
# It outputs `should_run` as 'true' if ALL conditions pass, 'false' otherwise.
|
||||
|
||||
name: 'CI: Check Eligibility'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
is_pr_approved_by_maintainer:
|
||||
required: true
|
||||
type: boolean
|
||||
paths_filter_patterns:
|
||||
description: "Path filter patterns for 'paths-filter-action'."
|
||||
required: false
|
||||
type: string
|
||||
default: |
|
||||
not_ignored:
|
||||
- '!.devcontainer/**'
|
||||
- '!.github/*'
|
||||
- '!.github/scripts/*'
|
||||
- '!.github/workflows/benchmark-*'
|
||||
- '!.github/workflows/check-*'
|
||||
- '!.vscode/**'
|
||||
- '!docker/**'
|
||||
- '!packages/@n8n/benchmark/**'
|
||||
- '!packages/@n8n/task-runner-python/**'
|
||||
- '!**/*.md'
|
||||
excluded_source_branch_patterns:
|
||||
description: 'Newline-separated list of glob patterns for source branches to EXCLUDE.'
|
||||
required: false
|
||||
type: string
|
||||
default: |
|
||||
release/*
|
||||
master
|
||||
|
||||
outputs:
|
||||
should_run:
|
||||
description: "Outputs 'true' if all eligibility checks pass, otherwise 'false'."
|
||||
value: ${{ jobs.evaluate_conditions.outputs.run_decision }}
|
||||
|
||||
jobs:
|
||||
evaluate_conditions:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
run_decision: ${{ steps.evaluate.outputs.should_run }}
|
||||
steps:
|
||||
- name: Check out current commit
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Determine changed files
|
||||
uses: tomi/paths-filter-action@32c62f5ca100c1110406e3477d5b3ecef4666fec # v3.0.2
|
||||
id: changed
|
||||
with:
|
||||
filters: ${{ inputs.paths_filter_patterns }}
|
||||
predicate-quantifier: 'every'
|
||||
|
||||
- name: Evaluate Conditions & Set Output
|
||||
id: evaluate
|
||||
env:
|
||||
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
|
||||
IS_APPROVED: ${{ inputs.is_pr_approved_by_maintainer }}
|
||||
FILES_CHANGED: ${{ steps.changed.outputs.not_ignored == 'true' }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
EXCLUDED_PATTERNS: ${{ inputs.excluded_source_branch_patterns }}
|
||||
run: |
|
||||
if [[ "$IS_FORK" == "true" ]]; then
|
||||
is_community="true"
|
||||
else
|
||||
is_community="false"
|
||||
fi
|
||||
|
||||
source_branch_excluded="false"
|
||||
while IFS= read -r pattern; do
|
||||
# shellcheck disable=SC2053
|
||||
if [[ -n "$pattern" && "$HEAD_REF" == $pattern ]]; then
|
||||
source_branch_excluded="true"
|
||||
break
|
||||
fi
|
||||
done <<< "$EXCLUDED_PATTERNS"
|
||||
|
||||
echo "--- Checking Conditions ---"
|
||||
echo "Is NOT Community PR: $([[ "$is_community" == "false" ]] && echo true || echo false)"
|
||||
echo "Files Changed: $FILES_CHANGED"
|
||||
echo "Source Branch Excluded: $source_branch_excluded"
|
||||
echo "Is Approved: $IS_APPROVED"
|
||||
echo "-------------------------"
|
||||
|
||||
if [[ "$is_community" == "false" && \
|
||||
"$FILES_CHANGED" == "true" && \
|
||||
"$source_branch_excluded" == "false" && \
|
||||
"$IS_APPROVED" == "true" ]]; then
|
||||
echo "Decision: Conditions met. Setting should_run=true."
|
||||
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Decision: Conditions not met. Setting should_run=false."
|
||||
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
21
.github/workflows/ci-check-pr-title.yml
vendored
Normal file
21
.github/workflows/ci-check-pr-title.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: 'CI: Check PR Title'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
branches:
|
||||
- 'master'
|
||||
- '1.x'
|
||||
|
||||
jobs:
|
||||
check-pr-title:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Validate PR title
|
||||
uses: n8n-io/validate-n8n-pull-request-title@c3b6fd06bda12eebd57a592c0cf3b747d5b73569 # v2.4.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
135
.github/workflows/ci-manual-unit-tests.yml
vendored
Normal file
135
.github/workflows/ci-manual-unit-tests.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
name: 'CI: Manual Unit Tests'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Commit SHA or ref to check out
|
||||
required: true
|
||||
pr_number:
|
||||
description: PR number (optional, for check reporting)
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
create-check-run:
|
||||
name: Create Check Run
|
||||
runs-on: ubuntu-latest
|
||||
if: inputs.pr_number != ''
|
||||
outputs:
|
||||
check_run_id: ${{ steps.create.outputs.check_run_id }}
|
||||
steps:
|
||||
- name: Create pending check run on PR
|
||||
id: create
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { data: checkRun } = await github.rest.checks.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'Build & Unit Tests - Checks',
|
||||
head_sha: '${{ inputs.ref }}',
|
||||
status: 'in_progress',
|
||||
output: {
|
||||
title: 'Build & Unit Tests - Checks',
|
||||
summary: 'Running build, unit tests, and lint...'
|
||||
}
|
||||
});
|
||||
|
||||
core.setOutput('check_run_id', checkRun.id);
|
||||
console.log(`Created check run ${checkRun.id} on commit ${{ inputs.ref }}`);
|
||||
|
||||
install-and-build:
|
||||
name: Install & Build
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Setup and Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
||||
- name: Run format check
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Run typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
unit-tests:
|
||||
name: Unit tests
|
||||
needs: install-and-build
|
||||
uses: ./.github/workflows/test-unit-reusable.yml
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
collectCoverage: true
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
BUILD_STATS_WEBHOOK_URL: ${{ secrets.BUILD_STATS_WEBHOOK_URL }}
|
||||
BUILD_STATS_WEBHOOK_USER: ${{ secrets.BUILD_STATS_WEBHOOK_USER }}
|
||||
BUILD_STATS_WEBHOOK_PASSWORD: ${{ secrets.BUILD_STATS_WEBHOOK_PASSWORD }}
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: install-and-build
|
||||
uses: ./.github/workflows/test-linting-reusable.yml
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
post-build-unit-tests:
|
||||
name: Build & Unit Tests - Checks
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-check-run, install-and-build, unit-tests, lint]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Update check run on PR (if triggered from PR comment)
|
||||
if: inputs.pr_number != ''
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const checkRunId = '${{ needs.create-check-run.outputs.check_run_id }}';
|
||||
|
||||
if (!checkRunId) {
|
||||
console.log('No check run ID found, skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
const buildResult = '${{ needs.install-and-build.result }}';
|
||||
const testResult = '${{ needs.unit-tests.result }}';
|
||||
const lintResult = '${{ needs.lint.result }}';
|
||||
|
||||
const conclusion = (buildResult === 'success' && testResult === 'success' && lintResult === 'success')
|
||||
? 'success'
|
||||
: 'failure';
|
||||
|
||||
const summary = `
|
||||
**Build**: ${buildResult}
|
||||
**Unit Tests**: ${testResult}
|
||||
**Lint**: ${lintResult}
|
||||
`;
|
||||
|
||||
await github.rest.checks.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
check_run_id: parseInt(checkRunId),
|
||||
status: 'completed',
|
||||
conclusion: conclusion,
|
||||
output: {
|
||||
title: 'Build & Unit Tests - Checks',
|
||||
summary: summary
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Updated check run ${checkRunId} with conclusion: ${conclusion}`);
|
||||
|
||||
- name: Fail if any job failed
|
||||
if: needs.install-and-build.result == 'failure' || needs.unit-tests.result == 'failure' || needs.lint.result == 'failure'
|
||||
run: exit 1
|
||||
66
.github/workflows/ci-master.yml
vendored
Normal file
66
.github/workflows/ci-master.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: 'CI: Master (Build, Test, Lint)'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 1.x
|
||||
paths-ignore:
|
||||
- packages/@n8n/task-runner-python/**
|
||||
|
||||
jobs:
|
||||
build-github:
|
||||
name: Build for Github Cache
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
BUILD_STATS_WEBHOOK_URL: ${{ secrets.BUILD_STATS_WEBHOOK_URL }}
|
||||
BUILD_STATS_WEBHOOK_USER: ${{ secrets.BUILD_STATS_WEBHOOK_USER }}
|
||||
BUILD_STATS_WEBHOOK_PASSWORD: ${{ secrets.BUILD_STATS_WEBHOOK_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup and Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
||||
unit-test:
|
||||
name: Unit tests
|
||||
uses: ./.github/workflows/test-unit-reusable.yml
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [22.x, 24.13.1, 25.x]
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
nodeVersion: ${{ matrix.node-version }}
|
||||
collectCoverage: ${{ matrix.node-version == '24.13.1' }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
BUILD_STATS_WEBHOOK_URL: ${{ secrets.BUILD_STATS_WEBHOOK_URL }}
|
||||
BUILD_STATS_WEBHOOK_USER: ${{ secrets.BUILD_STATS_WEBHOOK_USER }}
|
||||
BUILD_STATS_WEBHOOK_PASSWORD: ${{ secrets.BUILD_STATS_WEBHOOK_PASSWORD }}
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
uses: ./.github/workflows/test-linting-reusable.yml
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
|
||||
performance:
|
||||
name: Performance
|
||||
uses: ./.github/workflows/test-bench-reusable.yml
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
|
||||
notify-on-failure:
|
||||
name: Notify Slack on failure
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unit-test, lint, performance, build-github]
|
||||
steps:
|
||||
- name: Notify Slack on failure
|
||||
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
||||
if: failure()
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
channel: '#alerts-build'
|
||||
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
message: ${{ github.ref_name }} branch (build or test or lint) failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
188
.github/workflows/ci-pull-requests.yml
vendored
Normal file
188
.github/workflows/ci-pull-requests.yml
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
name: 'CI: Pull Requests (Build, Test, Lint)'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.event.pull_request.number || github.event.merge_group.head_sha || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
install-and-build:
|
||||
name: Install & Build
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-2vcpu-ubuntu-2204' }}
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
BUILD_STATS_WEBHOOK_URL: ${{ secrets.BUILD_STATS_WEBHOOK_URL }}
|
||||
BUILD_STATS_WEBHOOK_USER: ${{ secrets.BUILD_STATS_WEBHOOK_USER }}
|
||||
BUILD_STATS_WEBHOOK_PASSWORD: ${{ secrets.BUILD_STATS_WEBHOOK_PASSWORD }}
|
||||
outputs:
|
||||
ci: ${{ fromJSON(steps.ci-filter.outputs.results).ci == true }}
|
||||
unit: ${{ fromJSON(steps.ci-filter.outputs.results).unit == true }}
|
||||
e2e: ${{ fromJSON(steps.ci-filter.outputs.results).e2e == true }}
|
||||
workflows: ${{ fromJSON(steps.ci-filter.outputs.results).workflows == true }}
|
||||
db: ${{ fromJSON(steps.ci-filter.outputs.results).db == true }}
|
||||
design_system: ${{ fromJSON(steps.ci-filter.outputs.results)['design-system'] == true }}
|
||||
performance: ${{ fromJSON(steps.ci-filter.outputs.results).performance == true }}
|
||||
commit_sha: ${{ steps.commit-sha.outputs.sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
# Use merge_group SHA when in merge queue, otherwise PR merge ref
|
||||
ref: ${{ github.event_name == 'merge_group' && github.event.merge_group.head_sha || format('refs/pull/{0}/merge', github.event.pull_request.number) }}
|
||||
|
||||
- name: Capture commit SHA for cache consistency
|
||||
id: commit-sha
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check for relevant changes
|
||||
uses: ./.github/actions/ci-filter
|
||||
id: ci-filter
|
||||
with:
|
||||
mode: filter
|
||||
filters: |
|
||||
ci:
|
||||
**
|
||||
!packages/@n8n/task-runner-python/**
|
||||
!.github/**
|
||||
unit:
|
||||
**
|
||||
!packages/@n8n/task-runner-python/**
|
||||
!packages/testing/playwright/**
|
||||
!.github/**
|
||||
e2e:
|
||||
.github/workflows/test-e2e-*.yml
|
||||
.github/scripts/cleanup-ghcr-images.mjs
|
||||
packages/testing/playwright/**
|
||||
packages/testing/containers/**
|
||||
workflows: .github/**
|
||||
design-system:
|
||||
packages/frontend/@n8n/design-system/**
|
||||
packages/frontend/@n8n/chat/**
|
||||
packages/frontend/@n8n/storybook/**
|
||||
.github/workflows/test-visual-chromatic.yml
|
||||
performance:
|
||||
packages/testing/performance/**
|
||||
packages/workflow/src/**
|
||||
.github/workflows/test-bench-reusable.yml
|
||||
db:
|
||||
packages/cli/src/databases/**
|
||||
packages/cli/src/modules/*/database/**
|
||||
packages/cli/src/modules/**/*.entity.ts
|
||||
packages/cli/src/modules/**/*.repository.ts
|
||||
packages/cli/test/integration/**
|
||||
packages/cli/test/migration/**
|
||||
packages/cli/test/shared/db/**
|
||||
packages/@n8n/db/**
|
||||
packages/cli/**/__tests__/**
|
||||
packages/testing/containers/services/postgres.ts
|
||||
.github/workflows/test-db-reusable.yml
|
||||
|
||||
- name: Setup and Build
|
||||
if: fromJSON(steps.ci-filter.outputs.results).ci
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
||||
- name: Run format check
|
||||
if: fromJSON(steps.ci-filter.outputs.results).ci
|
||||
run: pnpm format:check
|
||||
|
||||
unit-test:
|
||||
name: Unit tests
|
||||
if: needs.install-and-build.outputs.unit == 'true'
|
||||
uses: ./.github/workflows/test-unit-reusable.yml
|
||||
needs: install-and-build
|
||||
with:
|
||||
ref: ${{ needs.install-and-build.outputs.commit_sha }}
|
||||
collectCoverage: true
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
BUILD_STATS_WEBHOOK_URL: ${{ secrets.BUILD_STATS_WEBHOOK_URL }}
|
||||
BUILD_STATS_WEBHOOK_USER: ${{ secrets.BUILD_STATS_WEBHOOK_USER }}
|
||||
BUILD_STATS_WEBHOOK_PASSWORD: ${{ secrets.BUILD_STATS_WEBHOOK_PASSWORD }}
|
||||
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
if: needs.install-and-build.outputs.ci == 'true'
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
needs: install-and-build
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.install-and-build.outputs.commit_sha }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: pnpm typecheck
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
if: needs.install-and-build.outputs.ci == 'true'
|
||||
uses: ./.github/workflows/test-linting-reusable.yml
|
||||
needs: install-and-build
|
||||
with:
|
||||
ref: ${{ needs.install-and-build.outputs.commit_sha }}
|
||||
|
||||
e2e-tests:
|
||||
name: E2E Tests
|
||||
needs: install-and-build
|
||||
if: (needs.install-and-build.outputs.ci == 'true' || needs.install-and-build.outputs.e2e == 'true') && github.repository == 'n8n-io/n8n'
|
||||
uses: ./.github/workflows/test-e2e-ci-reusable.yml
|
||||
with:
|
||||
branch: ${{ needs.install-and-build.outputs.commit_sha }}
|
||||
playwright-only: ${{ needs.install-and-build.outputs.e2e == 'true' && needs.install-and-build.outputs.unit == 'false' }}
|
||||
secrets: inherit
|
||||
|
||||
db-tests:
|
||||
name: DB Tests
|
||||
needs: install-and-build
|
||||
if: needs.install-and-build.outputs.db == 'true'
|
||||
uses: ./.github/workflows/test-db-reusable.yml
|
||||
with:
|
||||
ref: ${{ needs.install-and-build.outputs.commit_sha }}
|
||||
|
||||
performance:
|
||||
name: Performance
|
||||
needs: install-and-build
|
||||
if: needs.install-and-build.outputs.performance == 'true' && github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/test-bench-reusable.yml
|
||||
with:
|
||||
ref: ${{ needs.install-and-build.outputs.commit_sha }}
|
||||
|
||||
security-checks:
|
||||
name: Security Checks
|
||||
needs: install-and-build
|
||||
if: needs.install-and-build.outputs.workflows == 'true'
|
||||
uses: ./.github/workflows/sec-ci-reusable.yml
|
||||
with:
|
||||
ref: ${{ needs.install-and-build.outputs.commit_sha }}
|
||||
secrets: inherit
|
||||
|
||||
chromatic:
|
||||
name: Chromatic
|
||||
needs: install-and-build
|
||||
if: needs.install-and-build.outputs.design_system == 'true'
|
||||
uses: ./.github/workflows/test-visual-chromatic.yml
|
||||
with:
|
||||
ref: ${{ needs.install-and-build.outputs.commit_sha }}
|
||||
secrets: inherit
|
||||
|
||||
# This job is required by GitHub branch protection rules.
|
||||
# PRs cannot be merged unless this job passes.
|
||||
required-checks:
|
||||
name: Required Checks
|
||||
needs: [install-and-build, unit-test, typecheck, lint, e2e-tests, db-tests, performance, security-checks, chromatic]
|
||||
if: always()
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
sparse-checkout: .github/actions/ci-filter
|
||||
sparse-checkout-cone-mode: false
|
||||
- name: Validate required checks
|
||||
uses: ./.github/actions/ci-filter
|
||||
with:
|
||||
mode: validate
|
||||
job-results: ${{ toJSON(needs) }}
|
||||
56
.github/workflows/ci-python.yml
vendored
Normal file
56
.github/workflows/ci-python.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: 'CI: Python'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/@n8n/task-runner-python/**
|
||||
- .github/workflows/ci-python.yml
|
||||
push:
|
||||
paths:
|
||||
- packages/@n8n/task-runner-python/**
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
name: Checks
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/@n8n/task-runner-python
|
||||
steps:
|
||||
- name: Check out project
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # 6.5.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3.0.0
|
||||
|
||||
- name: Install Python
|
||||
run: uv python install 3.13
|
||||
|
||||
- name: Install project dependencies
|
||||
run: just sync-all
|
||||
|
||||
- name: Format check
|
||||
run: just format-check
|
||||
|
||||
- name: Typecheck
|
||||
run: just typecheck
|
||||
|
||||
- name: Lint
|
||||
run: just lint
|
||||
|
||||
- name: Python unit tests
|
||||
run: uv run pytest --cov=src --cov-report=xml --cov-report=term-missing
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: packages/@n8n/task-runner-python/coverage.xml
|
||||
flags: tests
|
||||
name: task-runner-python
|
||||
fail_ci_if_error: false
|
||||
84
.github/workflows/ci-restrict-private-merges.yml
vendored
Normal file
84
.github/workflows/ci-restrict-private-merges.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: 'CI: Check merge source and destination'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check_branch:
|
||||
if: ${{ github.repository == 'n8n-io/n8n-private' }}
|
||||
name: enforce-bundle-branches-only-in-private
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Validate head branch
|
||||
id: validate
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
head="${{ github.head_ref }}"
|
||||
if [[ "$head" == bundle/* ]]; then
|
||||
echo "allowed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "allowed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Comment on PR (blocked)
|
||||
if: ${{ steps.validate.outputs.allowed == 'false' }}
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
const head = context.payload.pull_request.head.ref;
|
||||
const base = context.payload.pull_request.base.ref;
|
||||
|
||||
const marker = "<!-- bundle-branch-only -->";
|
||||
const body =
|
||||
`${marker}\n` +
|
||||
`🚫 **Merge blocked**: PRs into \`${base}\` are only allowed from branches named \`bundle/*\`.\n\n` +
|
||||
`Current source branch: \`${head}\`\n\n` +
|
||||
`Merge your developments into a bundle branch instead of directly merging to master.`;
|
||||
|
||||
// Find an existing marker comment (to update instead of spamming)
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Fail (blocked)
|
||||
if: ${{ steps.validate.outputs.allowed == 'false' }}
|
||||
run: |
|
||||
echo "::error::You can only merge to master from a bundle/* branch. Got '${{ github.head_ref }}'."
|
||||
exit 1
|
||||
|
||||
- name: Allowed
|
||||
if: ${{ steps.validate.outputs.allowed == 'true' }}
|
||||
run: |
|
||||
echo "OK: '${{ github.head_ref }}' can merge into '${{ github.base_ref }}'"
|
||||
69
.github/workflows/create-patch-release-branch.yml
vendored
Normal file
69
.github/workflows/create-patch-release-branch.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
name: Create Branch For Patch Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commit_shas:
|
||||
description: 'Comma-separated commit SHAs'
|
||||
required: true
|
||||
old_version:
|
||||
description: 'Old version to be patched'
|
||||
required: true
|
||||
default: '1.0.0'
|
||||
new_version:
|
||||
description: 'The new patch version'
|
||||
required: true
|
||||
default: '1.0.1'
|
||||
resumeUrl:
|
||||
description: 'n8n workflow resume URL'
|
||||
required: true
|
||||
jobs:
|
||||
create-branch:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Validate inputs
|
||||
run: |
|
||||
if ! [[ "${{ inputs.old_version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]]; then
|
||||
echo "Invalid old version format: ${{ inputs.old_version }}"
|
||||
exit 1
|
||||
fi
|
||||
if ! [[ "${{ inputs.new_version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]]; then
|
||||
echo "Invalid new version format: ${{ inputs.new_version }}"
|
||||
exit 1
|
||||
fi
|
||||
- name: Notify if inputs are invalid
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
curl -X POST -H "Content-Type: application/json" -d '{ "success": false, "message": "The old or new version you provided is invalid, make sure they both follow the SemVer format" }' ${{ inputs.resumeUrl }}
|
||||
exit 1
|
||||
- name: Setup, cherry-pick and push branch
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git switch "n8n@${{ inputs.old_version }}" --detach
|
||||
BRANCH="patch/${{ inputs.new_version }}"
|
||||
git checkout -b "$BRANCH"
|
||||
IFS=',' read -ra SHAS <<< "${{ inputs.commit_shas }}"
|
||||
for sha in "${SHAS[@]}"; do
|
||||
sha=$(echo "$sha" | xargs)
|
||||
if ! git merge-base --is-ancestor "$sha" HEAD; then
|
||||
echo "Cherry-picking commit $sha"
|
||||
git cherry-pick "$sha"
|
||||
else
|
||||
echo "Commit $sha is already in the branch, skipping"
|
||||
fi
|
||||
done
|
||||
git push -f origin "$BRANCH"
|
||||
- name: Notify if cherry-pick is successful
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
curl -X POST -H "Content-Type: application/json" -d '{ "success": true }' ${{ inputs.resumeUrl }}
|
||||
- name: Notify if cherry-pick is not successful
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
curl -X POST -H "Content-Type: application/json" -d '{ "success": false, "message": "There was a conflict when trying to create the branch, please do the cherry-pick and resolve the conflicts manually or do not include the PRs that caused the conflict" }' ${{ inputs.resumeUrl }}
|
||||
422
.github/workflows/docker-build-push.yml
vendored
Normal file
422
.github/workflows/docker-build-push.yml
vendored
Normal file
@@ -0,0 +1,422 @@
|
||||
# This workflow is used to build and push the Docker image for n8nio/n8n and n8nio/runners
|
||||
#
|
||||
# - Uses docker-config.mjs for context determination, this determines what needs to be built based on the trigger
|
||||
# - Uses docker-tags.mjs for tag generation, this generates the tags for the images
|
||||
|
||||
name: 'Docker: Build and Push'
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=7168'
|
||||
NODE_VERSION: '24.13.1'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
workflow_call:
|
||||
inputs:
|
||||
n8n_version:
|
||||
description: 'N8N version to build'
|
||||
required: true
|
||||
type: string
|
||||
release_type:
|
||||
description: 'Release type (stable, nightly, dev)'
|
||||
required: false
|
||||
type: string
|
||||
default: 'stable'
|
||||
push_enabled:
|
||||
description: 'Whether to push the built images'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
push_enabled:
|
||||
description: 'Push image to registry'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
success_url:
|
||||
description: 'URL to call after the build is successful'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
determine-build-context:
|
||||
name: Determine Build Context
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_type: ${{ steps.context.outputs.release_type }}
|
||||
n8n_version: ${{ steps.context.outputs.version }}
|
||||
push_enabled: ${{ steps.context.outputs.push_enabled }}
|
||||
push_to_docker: ${{ steps.context.outputs.push_to_docker }}
|
||||
build_matrix: ${{ steps.context.outputs.build_matrix }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Determine build context
|
||||
id: context
|
||||
run: |
|
||||
node .github/scripts/docker/docker-config.mjs \
|
||||
--event "${{ github.event_name }}" \
|
||||
--pr "${{ github.event.pull_request.number }}" \
|
||||
--branch "${{ github.ref_name }}" \
|
||||
--version "${{ inputs.n8n_version }}" \
|
||||
--release-type "${{ inputs.release_type }}" \
|
||||
--push-enabled "${{ inputs.push_enabled }}"
|
||||
|
||||
build-and-push-docker:
|
||||
name: Build App, then Build and Push Docker Image (${{ matrix.platform }})
|
||||
needs: determine-build-context
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 25
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.determine-build-context.outputs.build_matrix) }}
|
||||
outputs:
|
||||
image_ref: ${{ steps.determine-tags.outputs.n8n_primary_tag }}
|
||||
primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.n8n_primary_tag }}
|
||||
runners_primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.runners_primary_tag }}
|
||||
runners_distroless_primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.runners_distroless_primary_tag }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup and Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: pnpm build:n8n
|
||||
enable-docker-cache: 'true'
|
||||
env:
|
||||
RELEASE: ${{ needs.determine-build-context.outputs.n8n_version }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Determine Docker tags for all images
|
||||
id: determine-tags
|
||||
run: |
|
||||
node .github/scripts/docker/docker-tags.mjs \
|
||||
--all \
|
||||
--version "${{ needs.determine-build-context.outputs.n8n_version }}" \
|
||||
--platform "${{ matrix.docker_platform }}" \
|
||||
${{ needs.determine-build-context.outputs.push_to_docker == 'true' && '--include-docker' || '' }}
|
||||
|
||||
echo "=== Generated Docker Tags ==="
|
||||
cat "$GITHUB_OUTPUT" | grep "_tags=" | while IFS='=' read -r key value; do
|
||||
echo "${key}: ${value%%,*}..." # Show first tag for brevity
|
||||
done
|
||||
|
||||
- name: Login to Docker registries
|
||||
if: needs.determine-build-context.outputs.push_enabled == 'true'
|
||||
uses: ./.github/actions/docker-registry-login
|
||||
with:
|
||||
login-ghcr: true
|
||||
login-dockerhub: ${{ needs.determine-build-context.outputs.push_to_docker == 'true' }}
|
||||
dockerhub-username: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push n8n Docker image
|
||||
id: build-n8n
|
||||
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/images/n8n/Dockerfile
|
||||
build-args: |
|
||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
||||
N8N_VERSION=${{ needs.determine-build-context.outputs.n8n_version }}
|
||||
N8N_RELEASE_TYPE=${{ needs.determine-build-context.outputs.release_type }}
|
||||
platforms: ${{ matrix.docker_platform }}
|
||||
provenance: false # Disabled - using SLSA L3 generator for isolated provenance
|
||||
sbom: true
|
||||
push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }}
|
||||
tags: ${{ steps.determine-tags.outputs.n8n_tags }}
|
||||
|
||||
- name: Build and push task runners Docker image (Alpine)
|
||||
id: build-runners
|
||||
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/images/runners/Dockerfile
|
||||
build-args: |
|
||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
||||
N8N_VERSION=${{ needs.determine-build-context.outputs.n8n_version }}
|
||||
N8N_RELEASE_TYPE=${{ needs.determine-build-context.outputs.release_type }}
|
||||
platforms: ${{ matrix.docker_platform }}
|
||||
provenance: false # Disabled - using SLSA L3 generator for isolated provenance
|
||||
sbom: true
|
||||
push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }}
|
||||
tags: ${{ steps.determine-tags.outputs.runners_tags }}
|
||||
|
||||
- name: Build and push task runners Docker image (distroless)
|
||||
id: build-runners-distroless
|
||||
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/images/runners/Dockerfile.distroless
|
||||
build-args: |
|
||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
||||
N8N_VERSION=${{ needs.determine-build-context.outputs.n8n_version }}
|
||||
N8N_RELEASE_TYPE=${{ needs.determine-build-context.outputs.release_type }}
|
||||
platforms: ${{ matrix.docker_platform }}
|
||||
provenance: false # Disabled - using SLSA L3 generator for isolated provenance
|
||||
sbom: true
|
||||
push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }}
|
||||
tags: ${{ steps.determine-tags.outputs.runners_distroless_tags }}
|
||||
|
||||
create_multi_arch_manifest:
|
||||
name: Create Multi-Arch Manifest
|
||||
needs: [determine-build-context, build-and-push-docker]
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
needs.build-and-push-docker.result == 'success' &&
|
||||
needs.determine-build-context.outputs.push_enabled == 'true'
|
||||
outputs:
|
||||
n8n_digest: ${{ steps.get-digests.outputs.n8n_digest }}
|
||||
n8n_image: ${{ steps.get-digests.outputs.n8n_image }}
|
||||
runners_digest: ${{ steps.get-digests.outputs.runners_digest }}
|
||||
runners_image: ${{ steps.get-digests.outputs.runners_image }}
|
||||
runners_distroless_digest: ${{ steps.get-digests.outputs.runners_distroless_digest }}
|
||||
runners_distroless_image: ${{ steps.get-digests.outputs.runners_distroless_image }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to Docker registries
|
||||
uses: ./.github/actions/docker-registry-login
|
||||
with:
|
||||
login-ghcr: true
|
||||
login-dockerhub: ${{ needs.determine-build-context.outputs.push_to_docker == 'true' }}
|
||||
dockerhub-username: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Create GHCR multi-arch manifests
|
||||
run: |
|
||||
RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}"
|
||||
|
||||
# Function to create manifest for an image
|
||||
create_manifest() {
|
||||
local IMAGE_NAME=$1
|
||||
local MANIFEST_TAG=$2
|
||||
|
||||
if [[ -z "$MANIFEST_TAG" ]]; then
|
||||
echo "Skipping $IMAGE_NAME - no manifest tag"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Creating GHCR manifest for $IMAGE_NAME: $MANIFEST_TAG"
|
||||
|
||||
# For branch builds, only AMD64 is built
|
||||
if [[ "$RELEASE_TYPE" == "branch" ]]; then
|
||||
docker buildx imagetools create \
|
||||
--tag "$MANIFEST_TAG" \
|
||||
"${MANIFEST_TAG}-amd64"
|
||||
else
|
||||
docker buildx imagetools create \
|
||||
--tag "$MANIFEST_TAG" \
|
||||
"${MANIFEST_TAG}-amd64" \
|
||||
"${MANIFEST_TAG}-arm64"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create manifests for all images
|
||||
create_manifest "n8n" "${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}"
|
||||
create_manifest "runners" "${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}"
|
||||
create_manifest "runners-distroless" "${{ needs.build-and-push-docker.outputs.runners_distroless_primary_ghcr_manifest_tag }}"
|
||||
|
||||
- name: Create Docker Hub manifests
|
||||
if: needs.determine-build-context.outputs.push_to_docker == 'true'
|
||||
run: |
|
||||
VERSION="${{ needs.determine-build-context.outputs.n8n_version }}"
|
||||
DOCKER_BASE="${{ secrets.DOCKER_USERNAME }}"
|
||||
|
||||
# Create manifests for each image type
|
||||
declare -A images=(
|
||||
["n8n"]="${VERSION}"
|
||||
["runners"]="${VERSION}"
|
||||
["runners-distroless"]="${VERSION}-distroless"
|
||||
)
|
||||
|
||||
for image in "${!images[@]}"; do
|
||||
TAG_SUFFIX="${images[$image]}"
|
||||
IMAGE_NAME="${image//-distroless/}" # Remove -distroless from image name
|
||||
|
||||
echo "Creating Docker Hub manifest for $image"
|
||||
docker buildx imagetools create \
|
||||
--tag "${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}" \
|
||||
"${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}-amd64" \
|
||||
"${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}-arm64"
|
||||
done
|
||||
|
||||
- name: Get manifest digests for attestation
|
||||
id: get-digests
|
||||
env:
|
||||
N8N_TAG: ${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}
|
||||
RUNNERS_TAG: ${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}
|
||||
DISTROLESS_TAG: ${{ needs.build-and-push-docker.outputs.runners_distroless_primary_ghcr_manifest_tag }}
|
||||
run: node .github/scripts/docker/get-manifest-digests.mjs
|
||||
|
||||
call-success-url:
|
||||
name: Call Success URL
|
||||
needs: [create_multi_arch_manifest]
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.create_multi_arch_manifest.result == 'success' || needs.create_multi_arch_manifest.result == 'skipped'
|
||||
steps:
|
||||
- name: Call Success URL
|
||||
env:
|
||||
SUCCESS_URL: ${{ github.event.inputs.success_url }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.success_url != '' }}
|
||||
run: |
|
||||
echo "Calling success URL: ${{ env.SUCCESS_URL }}"
|
||||
curl -v "${{ env.SUCCESS_URL }}" || echo "Failed to call success URL"
|
||||
shell: bash
|
||||
|
||||
# SLSA L3 Provenance - Must use version tags (@vX.Y.Z), NOT SHAs
|
||||
provenance-n8n:
|
||||
name: SLSA Provenance (n8n)
|
||||
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
|
||||
if: |
|
||||
needs.create_multi_arch_manifest.result == 'success' &&
|
||||
needs.create_multi_arch_manifest.outputs.n8n_digest != ''
|
||||
permissions:
|
||||
id-token: write
|
||||
packages: write
|
||||
actions: read
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: ${{ needs.create_multi_arch_manifest.outputs.n8n_image }}
|
||||
digest: ${{ needs.create_multi_arch_manifest.outputs.n8n_digest }}
|
||||
registry-username: ${{ github.actor }}
|
||||
secrets:
|
||||
registry-password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
provenance-runners:
|
||||
name: SLSA Provenance (runners)
|
||||
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
|
||||
if: |
|
||||
needs.create_multi_arch_manifest.result == 'success' &&
|
||||
needs.create_multi_arch_manifest.outputs.runners_digest != ''
|
||||
permissions:
|
||||
id-token: write
|
||||
packages: write
|
||||
actions: read
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: ${{ needs.create_multi_arch_manifest.outputs.runners_image }}
|
||||
digest: ${{ needs.create_multi_arch_manifest.outputs.runners_digest }}
|
||||
registry-username: ${{ github.actor }}
|
||||
secrets:
|
||||
registry-password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
provenance-runners-distroless:
|
||||
name: SLSA Provenance (runners-distroless)
|
||||
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
|
||||
if: |
|
||||
needs.create_multi_arch_manifest.result == 'success' &&
|
||||
needs.create_multi_arch_manifest.outputs.runners_distroless_digest != ''
|
||||
permissions:
|
||||
id-token: write
|
||||
packages: write
|
||||
actions: read
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: ${{ needs.create_multi_arch_manifest.outputs.runners_distroless_image }}
|
||||
digest: ${{ needs.create_multi_arch_manifest.outputs.runners_distroless_digest }}
|
||||
registry-username: ${{ github.actor }}
|
||||
secrets:
|
||||
registry-password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# VEX Attestation - Documents which CVEs affect us (security/vex.openvex.json)
|
||||
vex-attestation:
|
||||
name: VEX Attestation
|
||||
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest, provenance-n8n, provenance-runners, provenance-runners-distroless]
|
||||
if: |
|
||||
always() &&
|
||||
needs.create_multi_arch_manifest.result == 'success' &&
|
||||
(needs.determine-build-context.outputs.release_type == 'stable' ||
|
||||
needs.determine-build-context.outputs.release_type == 'rc' ||
|
||||
needs.determine-build-context.outputs.release_type == 'nightly')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Attest VEX to n8n image
|
||||
if: needs.create_multi_arch_manifest.outputs.n8n_digest != ''
|
||||
run: |
|
||||
cosign attest --yes \
|
||||
--type openvex \
|
||||
--predicate security/vex.openvex.json \
|
||||
${{ needs.create_multi_arch_manifest.outputs.n8n_image }}@${{ needs.create_multi_arch_manifest.outputs.n8n_digest }}
|
||||
|
||||
- name: Attest VEX to runners image
|
||||
if: needs.create_multi_arch_manifest.outputs.runners_digest != ''
|
||||
run: |
|
||||
cosign attest --yes \
|
||||
--type openvex \
|
||||
--predicate security/vex.openvex.json \
|
||||
${{ needs.create_multi_arch_manifest.outputs.runners_image }}@${{ needs.create_multi_arch_manifest.outputs.runners_digest }}
|
||||
|
||||
- name: Attest VEX to runners-distroless image
|
||||
if: needs.create_multi_arch_manifest.outputs.runners_distroless_digest != ''
|
||||
run: |
|
||||
cosign attest --yes \
|
||||
--type openvex \
|
||||
--predicate security/vex.openvex.json \
|
||||
${{ needs.create_multi_arch_manifest.outputs.runners_distroless_image }}@${{ needs.create_multi_arch_manifest.outputs.runners_distroless_digest }}
|
||||
|
||||
security-scan:
|
||||
name: Security Scan
|
||||
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
|
||||
if: |
|
||||
success() &&
|
||||
(needs.determine-build-context.outputs.release_type == 'stable' ||
|
||||
needs.determine-build-context.outputs.release_type == 'nightly' ||
|
||||
needs.determine-build-context.outputs.release_type == 'rc')
|
||||
uses: ./.github/workflows/security-trivy-scan-callable.yml
|
||||
with:
|
||||
image_ref: ${{ needs.build-and-push-docker.outputs.image_ref }}
|
||||
secrets: inherit
|
||||
|
||||
security-scan-runners:
|
||||
name: Security Scan (runners)
|
||||
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
|
||||
if: |
|
||||
success() &&
|
||||
(needs.determine-build-context.outputs.release_type == 'stable' ||
|
||||
needs.determine-build-context.outputs.release_type == 'nightly' ||
|
||||
needs.determine-build-context.outputs.release_type == 'rc')
|
||||
uses: ./.github/workflows/security-trivy-scan-callable.yml
|
||||
with:
|
||||
image_ref: ${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}
|
||||
secrets: inherit
|
||||
|
||||
notify-on-failure:
|
||||
name: Notify Cats on nightly build failure
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-and-push-docker]
|
||||
if: needs.build-and-push-docker.result == 'failure' && github.event_name == 'schedule'
|
||||
steps:
|
||||
- uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
||||
with:
|
||||
status: ${{ needs.build-and-push-docker.result }}
|
||||
channel: '#team-catalysts'
|
||||
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
message: Nightly Docker build failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
15
.github/workflows/release-create-minor-pr.yml
vendored
Normal file
15
.github/workflows/release-create-minor-pr.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: 'Release: Create Minor Release PR'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# schedule:
|
||||
# - cron: 0 13 * * 1
|
||||
|
||||
jobs:
|
||||
create-release-pr:
|
||||
name: Create release PR
|
||||
uses: ./.github/workflows/release-create-pr.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
base-branch: master
|
||||
release-type: minor
|
||||
119
.github/workflows/release-create-pr.yml
vendored
Normal file
119
.github/workflows/release-create-pr.yml
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
name: 'Release: Create Pull Request'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
base-branch:
|
||||
description: 'The branch, tag, or commit to create this release PR from.'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
release-type:
|
||||
description: 'A SemVer release type.'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base-branch:
|
||||
description: 'The branch, tag, or commit to create this release PR from.'
|
||||
required: true
|
||||
default: 'master'
|
||||
|
||||
release-type:
|
||||
description: 'A SemVer release type.'
|
||||
required: true
|
||||
type: choice
|
||||
default: 'minor'
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
- experimental
|
||||
- premajor
|
||||
|
||||
jobs:
|
||||
create-release-pr:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
with:
|
||||
app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
|
||||
private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
# Checkout base branch via separate step to prevent unsafe actions/checkout ref usage.
|
||||
# poutine: untrusted_checkout_exec
|
||||
- name: Switch to base branch
|
||||
env:
|
||||
BASE_BRANCH: ${{ inputs.base-branch }}
|
||||
run: git checkout "$BASE_BRANCH"
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
|
||||
- run: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.33
|
||||
corepack enable
|
||||
|
||||
- name: Bump package versions
|
||||
run: |
|
||||
echo "NEXT_RELEASE=$(node .github/scripts/bump-versions.mjs)" >> "$GITHUB_ENV"
|
||||
env:
|
||||
RELEASE_TYPE: ${{ inputs.release-type }}
|
||||
|
||||
- name: Update Changelog
|
||||
run: node .github/scripts/update-changelog.mjs
|
||||
|
||||
- name: Push the base branch
|
||||
env:
|
||||
BASE_BRANCH: ${{ inputs.base-branch }}
|
||||
run: |
|
||||
git push -f origin "refs/remotes/origin/${{ env.BASE_BRANCH }}:refs/heads/release/${{ env.NEXT_RELEASE }}"
|
||||
|
||||
- name: Generate PR body
|
||||
id: generate-body
|
||||
run: |
|
||||
set -e
|
||||
CHANGELOG_FILE="CHANGELOG-${{ env.NEXT_RELEASE }}.md"
|
||||
DELIMITER="EOF_$(uuidgen)"
|
||||
|
||||
if [ -f "${CHANGELOG_FILE}" ]; then
|
||||
{
|
||||
echo "content<<${DELIMITER}"
|
||||
cat "${CHANGELOG_FILE}"
|
||||
echo "${DELIMITER}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "content=No changelog generated. Likely points to fixes in our CI." >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Push the release branch, and Create the PR
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
base: 'release/${{ env.NEXT_RELEASE }}'
|
||||
branch: 'release-pr/${{ env.NEXT_RELEASE }}'
|
||||
commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}'
|
||||
delete-branch: true
|
||||
labels: release,release:${{ inputs.release-type }}
|
||||
title: ':rocket: Release ${{ env.NEXT_RELEASE }}'
|
||||
body: ${{ steps.generate-body.outputs.content }}
|
||||
53
.github/workflows/release-publish-post-release.yml
vendored
Normal file
53
.github/workflows/release-publish-post-release.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: 'Release: Publish: Post-release'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
track:
|
||||
description: 'Release track acquired from determine-version-info. (e.g. stable, beta)'
|
||||
required: true
|
||||
type: string
|
||||
version:
|
||||
description: 'Release version acquired from determine-version-info. (e.g. 2.9.3, 1.123.23)'
|
||||
required: true
|
||||
type: string
|
||||
bump:
|
||||
description: 'Release bump size acquired from determine-version-info. (e.g. minor, patch)'
|
||||
required: true
|
||||
type: string
|
||||
new_stable_version:
|
||||
description: 'New stable version acquired from determine-version-info. (e.g. 2.9.3, null (on patch releases))'
|
||||
required: true
|
||||
type: string
|
||||
release_type:
|
||||
description: 'Release type acquired from determine-version-info. (stable or rc)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
push-new-release-to-channel:
|
||||
name: Push new release to channel
|
||||
if: inputs.release_type != 'rc'
|
||||
uses: ./.github/workflows/release-push-to-channel.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
release-channel: ${{ inputs.track }}
|
||||
|
||||
promote-previous-beta-to-stable:
|
||||
name: Promote previous beta to stable
|
||||
if: |
|
||||
inputs.release_type != 'rc' &&
|
||||
inputs.bump == 'minor'
|
||||
uses: ./.github/workflows/release-push-to-channel.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
version: ${{ inputs.new_stable_version }}
|
||||
release-channel: stable
|
||||
|
||||
ensure-release-candidate-branches:
|
||||
name: 'Ensure release candidate branches'
|
||||
if: |
|
||||
inputs.release_type != 'rc'
|
||||
uses: ./.github/workflows/util-ensure-release-candidate-branches.yml
|
||||
secrets: inherit
|
||||
213
.github/workflows/release-publish.yml
vendored
Normal file
213
.github/workflows/release-publish.yml
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
name: 'Release: Publish'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- 'release/*'
|
||||
|
||||
jobs:
|
||||
determine-version-info:
|
||||
name: Determine publishing track
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
outputs:
|
||||
track: ${{ steps.determine-info.outputs.track }}
|
||||
version: ${{ steps.determine-info.outputs.version }}
|
||||
bump: ${{ steps.determine-info.outputs.bump }}
|
||||
new_stable_version: ${{ steps.determine-info.outputs.new_stable_version }}
|
||||
release_type: ${{ steps.determine-info.outputs.release_type }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
install-command: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
- name: Determine track from package version number
|
||||
id: determine-info
|
||||
run: node .github/scripts/determine-version-info.mjs
|
||||
|
||||
publish-to-npm:
|
||||
name: Publish to NPM
|
||||
needs: [determine-version-info]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 20
|
||||
environment: npm
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
RELEASE: ${{ needs.determine-version-info.outputs.version }} # Used by Vite build process
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup and Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
env:
|
||||
N8N_FAIL_ON_POPULARITY_FETCH_ERROR: true
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Check for new unpublished packages
|
||||
run: node .github/scripts/detect-new-packages.mjs
|
||||
|
||||
- name: Dry-run publishing
|
||||
run: |
|
||||
pnpm --filter n8n publish --no-git-checks --dry-run
|
||||
pnpm publish -r --filter '!n8n' --no-git-checks --dry-run
|
||||
|
||||
- name: Pre publishing changes
|
||||
run: |
|
||||
node .github/scripts/trim-fe-packageJson.js
|
||||
node .github/scripts/ensure-provenance-fields.mjs
|
||||
cp README.md packages/cli/README.md
|
||||
sed -i "s/default: 'dev'/default: '${{ needs.determine-version-info.outputs.release_type }}'/g" packages/cli/dist/config/schema.js
|
||||
|
||||
- name: Publish n8n to NPM with rc tag
|
||||
env:
|
||||
PUBLISH_BRANCH: ${{ github.event.pull_request.base.ref }}
|
||||
run: pnpm --filter n8n publish --publish-branch "$PUBLISH_BRANCH" --access public --tag rc --no-git-checks
|
||||
|
||||
- name: Publish other packages to NPM with latest tag
|
||||
env:
|
||||
PUBLISH_BRANCH: ${{ github.event.pull_request.base.ref }}
|
||||
run: pnpm publish -r --filter '!n8n' --publish-branch "$PUBLISH_BRANCH" --access public --no-git-checks
|
||||
|
||||
- name: Cleanup rc tag
|
||||
run: npm dist-tag rm n8n rc
|
||||
continue-on-error: true
|
||||
|
||||
publish-to-docker-hub:
|
||||
name: Publish to DockerHub
|
||||
needs: [determine-version-info]
|
||||
uses: ./.github/workflows/docker-build-push.yml
|
||||
with:
|
||||
n8n_version: ${{ needs.determine-version-info.outputs.version }}
|
||||
release_type: ${{ needs.determine-version-info.outputs.release_type }}
|
||||
secrets: inherit
|
||||
|
||||
create-github-release:
|
||||
name: Create a GitHub Release
|
||||
needs: [determine-version-info, publish-to-npm, publish-to-docker-hub]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 5
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Create a Release on GitHub
|
||||
uses: ncipollo/release-action@1c89adf39833729d8f85a31ccbc451b078733c80 # v1
|
||||
with:
|
||||
commit: ${{github.event.pull_request.base.ref}}
|
||||
tag: 'n8n@${{ needs.determine-version-info.outputs.version }}'
|
||||
prerelease: ${{ needs.determine-version-info.outputs.track == 'beta' }}
|
||||
makeLatest: ${{ needs.determine-version-info.outputs.track == 'stable' }}
|
||||
body: ${{github.event.pull_request.body}}
|
||||
|
||||
move-track-tag:
|
||||
name: Move track tag
|
||||
needs: [determine-version-info, create-github-release]
|
||||
if: github.event.pull_request.merged == true
|
||||
uses: ./.github/workflows/release-update-pointer-tag.yml
|
||||
with:
|
||||
track: ${{ needs.determine-version-info.outputs.track }}
|
||||
version-tag: 'n8n@${{ needs.determine-version-info.outputs.version }}'
|
||||
secrets: inherit
|
||||
|
||||
promote-stable-tag:
|
||||
name: Promote stable tag (minor bump)
|
||||
needs: [determine-version-info, create-github-release]
|
||||
if: |
|
||||
github.event.pull_request.merged == true &&
|
||||
needs.determine-version-info.outputs.new_stable_version != ''
|
||||
uses: ./.github/workflows/release-update-pointer-tag.yml
|
||||
with:
|
||||
track: stable
|
||||
version-tag: 'n8n@${{ needs.determine-version-info.outputs.new_stable_version }}'
|
||||
secrets: inherit
|
||||
|
||||
generate-and-attach-sbom:
|
||||
name: Generate and Attach SBOM to Release
|
||||
needs: [determine-version-info, create-github-release]
|
||||
uses: ./.github/workflows/sbom-generation-callable.yml
|
||||
with:
|
||||
n8n_version: ${{ needs.determine-version-info.outputs.version }}
|
||||
release_tag_ref: 'n8n@${{ needs.determine-version-info.outputs.version }}'
|
||||
secrets: inherit
|
||||
|
||||
merge-release-tag-to-master:
|
||||
name: Merge release tag to master
|
||||
needs: [determine-version-info, publish-to-npm, create-github-release]
|
||||
if: |
|
||||
github.event.pull_request.merged == true &&
|
||||
needs.determine-version-info.outputs.bump == 'minor' &&
|
||||
needs.determine-version-info.outputs.release_type != 'rc'
|
||||
runs-on: ubuntu-latest
|
||||
environment: minor-release-tag-merge
|
||||
env:
|
||||
VERSION: ${{ needs.determine-version-info.outputs.version }}
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.RELEASE_TAG_MERGE_APP_ID }}
|
||||
private-key: ${{ secrets.RELEASE_TAG_MERGE_PRIVATE_KEY }}
|
||||
skip-token-revoke: false
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: master
|
||||
fetch-depth: 500
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Verify release tag exists
|
||||
run: |
|
||||
if ! git ls-remote --tags origin "refs/tags/n8n@${VERSION}" | grep -q .; then
|
||||
echo "::error::Tag n8n@${VERSION} not found on remote"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Fetch release tag
|
||||
run: git fetch origin "refs/tags/n8n@${VERSION}:refs/tags/n8n@${VERSION}"
|
||||
|
||||
- name: Merge release tag to master
|
||||
run: |
|
||||
git config user.name "n8n-release-tag-merge[bot]"
|
||||
git config user.email "256767729+n8n-release-tag-merge[bot]@users.noreply.github.com"
|
||||
git merge --ff "n8n@${VERSION}"
|
||||
|
||||
- name: Push to master
|
||||
run: git push origin HEAD:master
|
||||
|
||||
- name: Notify Slack on failure
|
||||
if: failure()
|
||||
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
channel: '#updates-and-product-releases'
|
||||
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
message: |
|
||||
<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}| Release tag merge to master failed for n8n@${{ needs.determine-version-info.outputs.version }} >
|
||||
post-release:
|
||||
name: Run Post-release actions
|
||||
needs: [determine-version-info, publish-to-npm, create-github-release]
|
||||
uses: ./.github/workflows/release-publish-post-release.yml
|
||||
with:
|
||||
track: ${{ needs.determine-version-info.outputs.track }}
|
||||
version: ${{ needs.determine-version-info.outputs.version }}
|
||||
bump: ${{ needs.determine-version-info.outputs.bump }}
|
||||
new_stable_version: ${{ needs.determine-version-info.outputs.new_stable_version }}
|
||||
release_type: ${{ needs.determine-version-info.outputs.release_type }}
|
||||
secrets: inherit
|
||||
161
.github/workflows/release-push-to-channel.yml
vendored
Normal file
161
.github/workflows/release-push-to-channel.yml
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
name: 'Release: Push to Channel'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: 'n8n Release version to push to a channel (e.g., 1.2.3 or 1.2.3-beta.4)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
release-channel:
|
||||
description: 'Release channel'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'n8n Release version to push to a channel (e.g., 1.2.3 or 1.2.3-beta.4)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
release-channel:
|
||||
description: 'Release channel'
|
||||
required: true
|
||||
type: choice
|
||||
default: 'beta'
|
||||
options:
|
||||
- beta
|
||||
- stable
|
||||
|
||||
jobs:
|
||||
validate-inputs:
|
||||
name: Validate Inputs
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.check_version.outputs.version }}
|
||||
release_channel: ${{ inputs.release-channel }}
|
||||
steps:
|
||||
- name: Check Version Format
|
||||
id: check_version
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
input_version="${{ env.INPUT_VERSION }}"
|
||||
version_regex='^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$'
|
||||
|
||||
if [[ "$input_version" =~ $version_regex ]]; then
|
||||
echo "Version format is valid: $input_version"
|
||||
echo "version=$input_version" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::error::Invalid version format provided: '$input_version'. Must match regex '$version_regex'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Block RC promotion to stable/beta
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
CHANNEL: ${{ inputs.release-channel }}
|
||||
run: |
|
||||
if [[ "$INPUT_VERSION" == *"-rc."* ]]; then
|
||||
echo "::error::RC versions cannot be promoted to '$CHANNEL' channel. Version '$INPUT_VERSION' contains '-rc.'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Version '$INPUT_VERSION' is allowed for '$CHANNEL' channel"
|
||||
|
||||
release-to-npm:
|
||||
name: Release to NPM
|
||||
runs-on: ubuntu-latest
|
||||
needs: validate-inputs
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
|
||||
# Remove after https://github.com/npm/cli/issues/8547 gets resolved
|
||||
- run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
|
||||
- name: Add beta/next tags to NPM
|
||||
if: needs.validate-inputs.outputs.release_channel == 'beta'
|
||||
run: |
|
||||
npm dist-tag add "n8n@${{ needs.validate-inputs.outputs.version }}" next
|
||||
npm dist-tag add "n8n@${{ needs.validate-inputs.outputs.version }}" beta
|
||||
|
||||
- name: Add latest/stable tags to NPM
|
||||
if: needs.validate-inputs.outputs.release_channel == 'stable'
|
||||
run: |
|
||||
npm dist-tag add "n8n@${{ needs.validate-inputs.outputs.version }}" latest
|
||||
npm dist-tag add "n8n@${{ needs.validate-inputs.outputs.version }}" stable
|
||||
|
||||
release-to-docker-hub:
|
||||
name: Release to DockerHub
|
||||
runs-on: ubuntu-latest
|
||||
needs: validate-inputs
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: ./.github/actions/docker-registry-login
|
||||
with:
|
||||
login-ghcr: false
|
||||
login-dockerhub: true
|
||||
dockerhub-username: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Tag stable/latest Docker image
|
||||
if: needs.validate-inputs.outputs.release_channel == 'stable'
|
||||
run: |
|
||||
docker buildx imagetools create -t "${{ secrets.DOCKER_USERNAME }}/n8n:stable" "${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "${{ secrets.DOCKER_USERNAME }}/n8n:latest" "${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "${{ secrets.DOCKER_USERNAME }}/runners:stable" "${{ secrets.DOCKER_USERNAME }}/runners:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "${{ secrets.DOCKER_USERNAME }}/runners:latest" "${{ secrets.DOCKER_USERNAME }}/runners:${{ needs.validate-inputs.outputs.version }}"
|
||||
|
||||
- name: Tag beta/next Docker image
|
||||
if: needs.validate-inputs.outputs.release_channel == 'beta'
|
||||
run: |
|
||||
docker buildx imagetools create -t "${{ secrets.DOCKER_USERNAME }}/n8n:beta" "${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "${{ secrets.DOCKER_USERNAME }}/n8n:next" "${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "${{ secrets.DOCKER_USERNAME }}/runners:beta" "${{ secrets.DOCKER_USERNAME }}/runners:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "${{ secrets.DOCKER_USERNAME }}/runners:next" "${{ secrets.DOCKER_USERNAME }}/runners:${{ needs.validate-inputs.outputs.version }}"
|
||||
|
||||
release-to-github-container-registry:
|
||||
name: Release to GitHub Container Registry
|
||||
runs-on: ubuntu-latest
|
||||
needs: validate-inputs
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: ./.github/actions/docker-registry-login
|
||||
|
||||
- name: Tag stable/latest GHCR image
|
||||
if: needs.validate-inputs.outputs.release_channel == 'stable'
|
||||
run: |
|
||||
docker buildx imagetools create -t "ghcr.io/${{ github.repository_owner }}/n8n:stable" "ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "ghcr.io/${{ github.repository_owner }}/n8n:latest" "ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "ghcr.io/${{ github.repository_owner }}/runners:stable" "ghcr.io/${{ github.repository_owner }}/runners:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "ghcr.io/${{ github.repository_owner }}/runners:latest" "ghcr.io/${{ github.repository_owner }}/runners:${{ needs.validate-inputs.outputs.version }}"
|
||||
|
||||
- name: Tag beta/next GHCR image
|
||||
if: needs.validate-inputs.outputs.release_channel == 'beta'
|
||||
run: |
|
||||
docker buildx imagetools create -t "ghcr.io/${{ github.repository_owner }}/n8n:beta" "ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "ghcr.io/${{ github.repository_owner }}/n8n:next" "ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "ghcr.io/${{ github.repository_owner }}/runners:beta" "ghcr.io/${{ github.repository_owner }}/runners:${{ needs.validate-inputs.outputs.version }}"
|
||||
docker buildx imagetools create -t "ghcr.io/${{ github.repository_owner }}/runners:next" "ghcr.io/${{ github.repository_owner }}/runners:${{ needs.validate-inputs.outputs.version }}"
|
||||
|
||||
update-docs:
|
||||
name: Update latest and next in the docs
|
||||
runs-on: ubuntu-latest
|
||||
needs: [validate-inputs, release-to-npm, release-to-docker-hub]
|
||||
steps:
|
||||
- continue-on-error: true
|
||||
run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/update-latest-next'
|
||||
60
.github/workflows/release-standalone-package.yml
vendored
Normal file
60
.github/workflows/release-standalone-package.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: 'Release: Standalone Package'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Package to release'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- '@n8n/codemirror-lang'
|
||||
- '@n8n/codemirror-lang-html'
|
||||
- '@n8n/codemirror-lang-sql'
|
||||
- '@n8n/create-node'
|
||||
- '@n8n/eslint-plugin-community-nodes'
|
||||
- '@n8n/node-cli'
|
||||
- '@n8n/scan-community-package'
|
||||
# All packages listed above require OIDC enabled in NPM. https://docs.npmjs.com/trusted-publishers
|
||||
|
||||
concurrency:
|
||||
group: release-package-${{ github.event.inputs.package }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
CACHE_KEY: ${{ github.sha }}-${{ github.event.inputs.package }}-build
|
||||
|
||||
jobs:
|
||||
publish-to-npm:
|
||||
name: Publish to NPM
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
environment: npm
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
steps:
|
||||
- name: Check branch
|
||||
if: github.ref != 'refs/heads/master'
|
||||
run: |
|
||||
echo "::error::This workflow can only be run from the master branch"
|
||||
exit 1
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup and Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: 'pnpm turbo build --filter "...${{ github.event.inputs.package }}"'
|
||||
|
||||
- name: Pre publishing changes
|
||||
run: |
|
||||
node .github/scripts/ensure-provenance-fields.mjs
|
||||
|
||||
- name: Publish package
|
||||
env:
|
||||
PACKAGE: ${{ github.event.inputs.package }}
|
||||
run: pnpm --filter "$PACKAGE" publish --access public --no-git-checks --publish-branch master
|
||||
66
.github/workflows/release-update-pointer-tag.yml
vendored
Normal file
66
.github/workflows/release-update-pointer-tag.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: 'Release: Update pointer tag'
|
||||
run-name: 'Update pointer tag: ${{ inputs.track }} -> ${{ inputs.version-tag }}'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
track:
|
||||
required: true
|
||||
type: string
|
||||
version-tag:
|
||||
required: true
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
track:
|
||||
description: 'Release Track'
|
||||
required: true
|
||||
type: choice
|
||||
options: [stable, beta, v1]
|
||||
version-tag:
|
||||
description: 'Version tag (e.g. n8n@2.7.0). Track tag will point to this version tag.'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-pointer-tags:
|
||||
name: Update pointer tags
|
||||
runs-on: ubuntu-slim
|
||||
environment: minor-release-tag-merge
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.RELEASE_TAG_MERGE_APP_ID }}
|
||||
private-key: ${{ secrets.RELEASE_TAG_MERGE_PRIVATE_KEY }}
|
||||
skip-token-revoke: false
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
# We can manage with a shallow clone, since `ensureTagExists` in github-helpers.mjs
|
||||
# does a targeted fetch for the tags it needs.
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
install-command: npm install --prefix=.github/scripts --no-package-lock
|
||||
build-command: ''
|
||||
|
||||
- name: Configure git author
|
||||
run: |
|
||||
git config user.name "n8n-release-tag-merge[bot]"
|
||||
git config user.email "256767729+n8n-release-tag-merge[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Move track tag
|
||||
env:
|
||||
TRACK: ${{ inputs.track }}
|
||||
VERSION_TAG: ${{ inputs.version-tag }}
|
||||
run: node ./.github/scripts/move-track-tag.mjs
|
||||
88
.github/workflows/sbom-generation-callable.yml
vendored
Normal file
88
.github/workflows/sbom-generation-callable.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
name: 'Release: Attach SBOM'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
n8n_version:
|
||||
description: 'N8N version to generate SBOM for'
|
||||
required: true
|
||||
type: string
|
||||
release_tag_ref:
|
||||
description: 'Git reference to checkout (e.g. n8n@1.2.3)'
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
SLACK_WEBHOOK_URL:
|
||||
required: true
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
n8n_version:
|
||||
description: 'N8N version to generate SBOM for'
|
||||
required: true
|
||||
type: string
|
||||
release_tag_ref:
|
||||
description: 'Git reference to checkout (e.g. n8n@1.2.3)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
generate-sbom:
|
||||
name: Generate and Attach SBOM to Release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout release tag
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ inputs.release_tag_ref }}
|
||||
|
||||
- name: Setup Node.js and install dependencies
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
|
||||
- name: Generate CycloneDX SBOM for source code
|
||||
uses: anchore/sbom-action@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6
|
||||
with:
|
||||
path: ./
|
||||
format: cyclonedx-json
|
||||
output-file: sbom-source.cdx.json
|
||||
|
||||
- name: Attest SBOM for source release
|
||||
uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0
|
||||
with:
|
||||
subject-path: './package.json'
|
||||
sbom-path: 'sbom-source.cdx.json'
|
||||
|
||||
- name: Attach SBOM and VEX files to release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Upload SBOM and VEX files to the existing release
|
||||
gh release upload "${{ inputs.release_tag_ref }}" \
|
||||
sbom-source.cdx.json \
|
||||
security/vex.openvex.json \
|
||||
--clobber
|
||||
|
||||
COMPONENT_COUNT=$(jq '.components | length' sbom-source.cdx.json 2>/dev/null || echo "unknown")
|
||||
VEX_STATEMENTS=$(jq '.statements | length' security/vex.openvex.json 2>/dev/null || echo "0")
|
||||
echo "SBOM and VEX attached to release"
|
||||
echo " - SBOM: $COMPONENT_COUNT components"
|
||||
echo " - VEX: $VEX_STATEMENTS CVE statements"
|
||||
|
||||
- name: Notify Slack on failure
|
||||
if: failure()
|
||||
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
channel: '#alerts-build'
|
||||
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
message: |
|
||||
<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}| SBOM generation and attachment failed for release ${{ inputs.release_tag_ref }} >
|
||||
23
.github/workflows/sec-ci-reusable.yml
vendored
Normal file
23
.github/workflows/sec-ci-reusable.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: 'Sec: CI Checks'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: GitHub ref to scan.
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
poutine-scan:
|
||||
name: Poutine Security Scan
|
||||
uses: ./.github/workflows/sec-poutine-reusable.yml
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
secrets: inherit
|
||||
|
||||
# Future security checks can be added here:
|
||||
# - dependency-scan:
|
||||
# - secret-detection:
|
||||
# - container-scan:
|
||||
44
.github/workflows/sec-poutine-reusable.yml
vendored
Normal file
44
.github/workflows/sec-poutine-reusable.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: 'Sec: Poutine Scan'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: GitHub ref to scan.
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
poutine_scan:
|
||||
name: Poutine Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Run Poutine Security Scanner
|
||||
uses: boostsecurityio/poutine-action@84c0a0d32e8d57ae12651222be1eb15351429228 # v0.15.2
|
||||
|
||||
- name: Fail on error-level findings
|
||||
run: |
|
||||
# Check SARIF for error-level findings
|
||||
if jq -e '.runs[].results[] | select(.level == "error")' results.sarif > /dev/null 2>&1; then
|
||||
echo "::error::Poutine found error-level security findings:"
|
||||
jq -r '.runs[].results[] | select(.level == "error") | " - \(.ruleId): \(.message.text)"' results.sarif
|
||||
exit 1
|
||||
fi
|
||||
echo "No error-level findings detected"
|
||||
|
||||
- name: Upload SARIF results
|
||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
if: github.repository == 'n8n-io/n8n'
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
61
.github/workflows/sec-publish-fix.yml
vendored
Normal file
61
.github/workflows/sec-publish-fix.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: 'Security: Publish fix'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
sync-security-fix:
|
||||
if: github.repository == 'n8n-io/n8n-private' && github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
with:
|
||||
app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
|
||||
private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
|
||||
owner: n8n-io
|
||||
repositories: n8n,n8n-private
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Open PR to public repo
|
||||
run: |
|
||||
COMMIT_TO_PUBLISH=$(git rev-parse HEAD)
|
||||
BRANCH_NAME="private-$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
git remote add public-repo https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/n8n-io/n8n.git
|
||||
git fetch public-repo master
|
||||
git checkout -b "$BRANCH_NAME" public-repo/master
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git cherry-pick "$COMMIT_TO_PUBLISH"
|
||||
git push public-repo "$BRANCH_NAME"
|
||||
gh pr create \
|
||||
--repo n8n-io/n8n \
|
||||
--base master \
|
||||
--head "$BRANCH_NAME" \
|
||||
--title "$PR_TITLE" \
|
||||
--body "Cherry-picked from n8n-private. Original PR: $PR_URL"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
channel: '#alerts-security'
|
||||
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
message: 'Security fix PR creation failed. Run "Security: Sync from Public" workflow, rebase your branch, reopen PR. (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
|
||||
62
.github/workflows/sec-sync-public-to-private.yml
vendored
Normal file
62
.github/workflows/sec-sync-public-to-private.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Sync n8n-io/n8n to n8n-io/n8n-private
|
||||
#
|
||||
# Runs hourly to keep private in sync with public.
|
||||
# Can also be triggered manually for conflict recovery.
|
||||
#
|
||||
# Scheduled runs only sync if private is not ahead of public.
|
||||
# Manual runs always sync (for conflict recovery after failed cherry-pick).
|
||||
|
||||
name: 'Security: Sync from Public'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force:
|
||||
description: Sync even if private is ahead (for conflict recovery)
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
sync-from-public:
|
||||
if: github.repository == 'n8n-io/n8n-private'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Generate App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
|
||||
private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Sync from public
|
||||
run: |
|
||||
git fetch https://github.com/n8n-io/n8n.git master:public-master
|
||||
|
||||
# Check if private is ahead of public
|
||||
AHEAD_COUNT=$(git rev-list public-master..HEAD --count)
|
||||
|
||||
if [ "$AHEAD_COUNT" -gt 0 ]; then
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
echo "Private is $AHEAD_COUNT commit(s) ahead of public, skipping scheduled sync"
|
||||
exit 0
|
||||
elif [ "${{ inputs.force }}" != "true" ]; then
|
||||
echo "Private is $AHEAD_COUNT commit(s) ahead of public, skipping (force not enabled)"
|
||||
exit 0
|
||||
else
|
||||
echo "Private is $AHEAD_COUNT commit(s) ahead of public, force syncing anyway"
|
||||
fi
|
||||
fi
|
||||
|
||||
git reset --hard public-master
|
||||
git push origin master --force-with-lease
|
||||
349
.github/workflows/security-trivy-scan-callable.yml
vendored
Normal file
349
.github/workflows/security-trivy-scan-callable.yml
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
name: Security - Scan Docker Image With Trivy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
image_ref:
|
||||
description: 'Full image reference to scan e.g. ghcr.io/n8n-io/n8n:latest'
|
||||
required: true
|
||||
default: 'ghcr.io/n8n-io/n8n:latest'
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_ref:
|
||||
type: string
|
||||
description: 'Full image reference to scan e.g. ghcr.io/n8n-io/n8n:latest'
|
||||
required: true
|
||||
secrets:
|
||||
QBOT_SLACK_TOKEN:
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
QBOT_SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
|
||||
SLACK_CHANNEL_ID: C0A6Y62BH9T #notify-security-scan-outputs
|
||||
SLACK_CHANNEL_CRITICAL: C042WDXPTEZ #mission-security
|
||||
|
||||
jobs:
|
||||
security_scan:
|
||||
name: Security - Scan Docker Image With Trivy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout for VEX file
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
sparse-checkout: |
|
||||
security/vex.openvex.json
|
||||
security/trivy.yaml
|
||||
security/trivy-ignore-policy.rego
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Pull Docker image with retry
|
||||
run: |
|
||||
for i in {1..4}; do
|
||||
docker pull "${{ inputs.image_ref }}" && break
|
||||
[ "$i" -lt 4 ] && echo "Retry $i failed, waiting..." && sleep 15
|
||||
done
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # v0.34.1
|
||||
id: trivy_scan
|
||||
with:
|
||||
image-ref: ${{ inputs.image_ref }}
|
||||
version: 'v0.69.2'
|
||||
format: 'json'
|
||||
output: 'trivy-results.json'
|
||||
severity: 'CRITICAL,HIGH,MEDIUM,LOW'
|
||||
ignore-unfixed: false
|
||||
exit-code: '0'
|
||||
trivy-config: 'security/trivy.yaml'
|
||||
|
||||
- name: Calculate vulnerability counts
|
||||
id: process_results
|
||||
run: |
|
||||
if [ ! -s trivy-results.json ] || [ "$(jq '.Results | length' trivy-results.json)" -eq 0 ]; then
|
||||
echo "No vulnerabilities found."
|
||||
echo "vulnerabilities_found=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Calculate counts by severity
|
||||
CRITICAL_COUNT=$(jq '([.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length)' trivy-results.json)
|
||||
HIGH_COUNT=$(jq '([.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length)' trivy-results.json)
|
||||
MEDIUM_COUNT=$(jq '([.Results[]?.Vulnerabilities[]? | select(.Severity == "MEDIUM")] | length)' trivy-results.json)
|
||||
LOW_COUNT=$(jq '([.Results[]?.Vulnerabilities[]? | select(.Severity == "LOW")] | length)' trivy-results.json)
|
||||
TOTAL_VULNS=$((CRITICAL_COUNT + HIGH_COUNT + MEDIUM_COUNT + LOW_COUNT))
|
||||
|
||||
# Get unique CVE count
|
||||
UNIQUE_CVES=$(jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | length' trivy-results.json)
|
||||
|
||||
# Get affected packages count
|
||||
AFFECTED_PACKAGES=$(jq -r '[.Results[]?.Vulnerabilities[]? | .PkgName] | unique | length' trivy-results.json)
|
||||
|
||||
{
|
||||
echo "vulnerabilities_found=$( [ "$TOTAL_VULNS" -gt 0 ] && echo 'true' || echo 'false' )"
|
||||
echo "total_count=$TOTAL_VULNS"
|
||||
echo "critical_count=$CRITICAL_COUNT"
|
||||
echo "high_count=$HIGH_COUNT"
|
||||
echo "medium_count=$MEDIUM_COUNT"
|
||||
echo "low_count=$LOW_COUNT"
|
||||
echo "unique_cves=$UNIQUE_CVES"
|
||||
echo "affected_packages=$AFFECTED_PACKAGES"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate GitHub Job Summary
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo "# 🛡️ Trivy Security Scan Results"
|
||||
echo ""
|
||||
echo "**Image:** \`${{ inputs.image_ref }}\`"
|
||||
echo "**Scan Date:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ ! -s trivy-results.json ]; then
|
||||
{
|
||||
echo "⚠️ **Scan did not produce results.** Check the 'Run Trivy vulnerability scanner' step for errors."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
elif [ "${{ steps.process_results.outputs.vulnerabilities_found }}" == "false" ]; then
|
||||
{
|
||||
echo "✅ **No vulnerabilities found!**"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
{
|
||||
echo "## 📊 Summary"
|
||||
echo "| Metric | Count |"
|
||||
echo "|--------|-------|"
|
||||
echo "| 🔴 Critical Vulnerabilities | ${{ steps.process_results.outputs.critical_count }} |"
|
||||
echo "| 🟠 High Vulnerabilities | ${{ steps.process_results.outputs.high_count }} |"
|
||||
echo "| 🟡 Medium Vulnerabilities | ${{ steps.process_results.outputs.medium_count }} |"
|
||||
echo "| 🟡 Low Vulnerabilities | ${{ steps.process_results.outputs.low_count }} |"
|
||||
echo "| 📋 Unique CVEs | ${{ steps.process_results.outputs.unique_cves }} |"
|
||||
echo "| 📦 Affected Packages | ${{ steps.process_results.outputs.affected_packages }} |"
|
||||
echo ""
|
||||
echo "## 🚨 Top Vulnerabilities"
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
{
|
||||
# Generate detailed vulnerability table
|
||||
jq -r --arg image_ref "${{ inputs.image_ref }}" '
|
||||
# Collect all vulnerabilities
|
||||
[.Results[] | select(.Vulnerabilities != null) | .Vulnerabilities[]] |
|
||||
# Group by CVE ID to avoid duplicates
|
||||
group_by(.VulnerabilityID) |
|
||||
map({
|
||||
cve: .[0].VulnerabilityID,
|
||||
severity: .[0].Severity,
|
||||
cvss: (.[0].CVSS.nvd.V3Score // "N/A"),
|
||||
cvss_sort: (.[0].CVSS.nvd.V3Score // 0),
|
||||
packages: [.[] | "\(.PkgName)@\(.InstalledVersion)"] | unique | join(", "),
|
||||
fixed: (.[0].FixedVersion // "No fix available"),
|
||||
description: (.[0].Description // "No description available") | split("\n")[0] | .[0:150]
|
||||
}) |
|
||||
# Sort by severity (CRITICAL, HIGH, MEDIUM, LOW) and CVSS score
|
||||
sort_by(
|
||||
if .severity == "CRITICAL" then 0
|
||||
elif .severity == "HIGH" then 1
|
||||
elif .severity == "MEDIUM" then 2
|
||||
elif .severity == "LOW" then 3
|
||||
else 4 end,
|
||||
-.cvss_sort
|
||||
) |
|
||||
# Take top 15
|
||||
.[:15] |
|
||||
# Generate markdown table
|
||||
"| CVE | Severity | CVSS | Package(s) | Fix Version | Description |",
|
||||
"|-----|----------|------|------------|-------------|-------------|",
|
||||
(.[] | "| [\(.cve)](https://nvd.nist.gov/vuln/detail/\(.cve)) | \(.severity) | \(.cvss) | `\(.packages)` | `\(.fixed)` | \(.description) |")
|
||||
' trivy-results.json
|
||||
|
||||
echo ""
|
||||
echo "---"
|
||||
echo "🔍 **View detailed logs above for full analysis**"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Generate Slack Blocks JSON
|
||||
if: steps.process_results.outputs.vulnerabilities_found == 'true'
|
||||
id: generate_blocks
|
||||
run: |
|
||||
BLOCKS_JSON=$(jq -c --arg image_ref "${{ inputs.image_ref }}" \
|
||||
--arg repo_url "${{ github.server_url }}/${{ github.repository }}" \
|
||||
--arg repo_name "${{ github.repository }}" \
|
||||
--arg run_url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||||
--arg critical_count "${{ steps.process_results.outputs.critical_count }}" \
|
||||
--arg high_count "${{ steps.process_results.outputs.high_count }}" \
|
||||
--arg medium_count "${{ steps.process_results.outputs.medium_count }}" \
|
||||
--arg low_count "${{ steps.process_results.outputs.low_count }}" \
|
||||
--arg unique_cves "${{ steps.process_results.outputs.unique_cves }}" \
|
||||
'
|
||||
# Function to create a vulnerability block with emoji indicators
|
||||
def vuln_block: {
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "\(if .Severity == "CRITICAL" then ":red_circle:" elif .Severity == "HIGH" then ":large_orange_circle:" elif .Severity == "MEDIUM" then ":large_yellow_circle:" else ":large_green_circle:" end) *<https://nvd.nist.gov/vuln/detail/\(.VulnerabilityID)|\(.VulnerabilityID)>* (CVSS: `\(.CVSS.nvd.V3Score // "N/A")`)\n*Package:* `\(.PkgName)@\(.InstalledVersion)` → `\(.FixedVersion // "No fix available")`"
|
||||
}
|
||||
};
|
||||
|
||||
# Main structure
|
||||
[
|
||||
{
|
||||
"type": "header",
|
||||
"text": { "type": "plain_text", "text": ":warning: Trivy Scan: Vulnerabilities Detected" }
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{ "type": "mrkdwn", "text": "*Repository:*\n<\($repo_url)|\($repo_name)>" },
|
||||
{ "type": "mrkdwn", "text": "*Image:*\n`\($image_ref)`" },
|
||||
{ "type": "mrkdwn", "text": "*Critical:*\n:red_circle: \($critical_count)" },
|
||||
{ "type": "mrkdwn", "text": "*High:*\n:large_orange_circle: \($high_count)" },
|
||||
{ "type": "mrkdwn", "text": "*Medium:*\n:large_yellow_circle: \($medium_count)" },
|
||||
{ "type": "mrkdwn", "text": "*Low:*\n:large_green_circle: \($low_count)" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{ "type": "mrkdwn", "text": ":shield: \($unique_cves) unique CVEs affecting packages" }
|
||||
]
|
||||
},
|
||||
{ "type": "divider" }
|
||||
] +
|
||||
(
|
||||
# Group vulnerabilities by CVE to avoid duplicates in notification
|
||||
[.Results[] | select(.Vulnerabilities != null) | .Vulnerabilities[]] |
|
||||
group_by(.VulnerabilityID) |
|
||||
map(.[0]) |
|
||||
sort_by(
|
||||
(if .Severity == "CRITICAL" then 0
|
||||
elif .Severity == "HIGH" then 1
|
||||
elif .Severity == "MEDIUM" then 2
|
||||
elif .Severity == "LOW" then 3
|
||||
else 4 end),
|
||||
-((.CVSS.nvd.V3Score // 0) | tonumber? // 0)
|
||||
) |
|
||||
.[:8] |
|
||||
map(. | vuln_block)
|
||||
) +
|
||||
[
|
||||
{ "type": "divider" },
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": { "type": "plain_text", "text": ":github: View Full Report" },
|
||||
"style": "primary",
|
||||
"url": $run_url
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
' trivy-results.json)
|
||||
|
||||
echo "slack_blocks=$BLOCKS_JSON" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Send Slack Notification
|
||||
if: steps.process_results.outputs.vulnerabilities_found == 'true'
|
||||
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.QBOT_SLACK_TOKEN }}
|
||||
payload: |
|
||||
channel: ${{ env.SLACK_CHANNEL_ID }}
|
||||
text: "🚨 Trivy Scan: ${{ steps.process_results.outputs.critical_count }} Critical, ${{ steps.process_results.outputs.high_count }} High, ${{ steps.process_results.outputs.medium_count }} Medium, ${{ steps.process_results.outputs.low_count }} Low vulnerabilities found in ${{ inputs.image_ref }}"
|
||||
blocks: ${{ steps.generate_blocks.outputs.slack_blocks }}
|
||||
|
||||
- name: Generate Critical Vulnerability Blocks for Mission Security
|
||||
if: steps.process_results.outputs.vulnerabilities_found == 'true' && steps.process_results.outputs.critical_count != '0'
|
||||
id: generate_critical_blocks
|
||||
run: |
|
||||
CRITICAL_BLOCKS_JSON=$(jq -c --arg image_ref "${{ inputs.image_ref }}" \
|
||||
--arg repo_url "${{ github.server_url }}/${{ github.repository }}" \
|
||||
--arg repo_name "${{ github.repository }}" \
|
||||
--arg run_url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||||
--arg critical_count "${{ steps.process_results.outputs.critical_count }}" \
|
||||
--arg unique_cves "${{ steps.process_results.outputs.unique_cves }}" \
|
||||
'
|
||||
# Function to create a detailed critical vulnerability block
|
||||
def critical_vuln_block:
|
||||
# Build references section if available
|
||||
(if (.References | length) > 0 then
|
||||
"\n*References:* " + ([.References[0:2][] | "<\(.)|\(. | split("/")[-1])>"] | join(", ")) + (if (.References | length) > 2 then " and \((.References | length) - 2) more" else "" end)
|
||||
else "" end) as $refs |
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":red_circle: *<https://nvd.nist.gov/vuln/detail/\(.VulnerabilityID)|\(.VulnerabilityID)>*\n*Severity:* CRITICAL | *CVSS Score:* `\(.CVSS.nvd.V3Score // "N/A")` | *Vector:* `\(.CVSS.nvd.V3Vector // "N/A")`\n*Package:* `\(.PkgName)@\(.InstalledVersion)` → `\(.FixedVersion // "No fix available")`\n*Published:* \(.PublishedDate // "Unknown" | split("T")[0])\n*Description:* \(.Description // "No description available" | split("\n")[0] | .[0:200])\($refs)"
|
||||
}
|
||||
};
|
||||
|
||||
# Main structure for critical vulnerabilities
|
||||
[
|
||||
{
|
||||
"type": "header",
|
||||
"text": { "type": "plain_text", "text": ":rotating_light: CRITICAL Vulnerabilities Detected" }
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{ "type": "mrkdwn", "text": "*Repository:*\n<\($repo_url)|\($repo_name)>" },
|
||||
{ "type": "mrkdwn", "text": "*Image:*\n`\($image_ref)`" },
|
||||
{ "type": "mrkdwn", "text": "*Critical Vulnerabilities:*\n:red_circle: \($critical_count)" },
|
||||
{ "type": "mrkdwn", "text": "*Scan Time:*\n<!date^\(now | floor)^{date_num} {time_secs}|Unknown>" }
|
||||
]
|
||||
},
|
||||
{ "type": "divider" },
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*:warning: Critical CVE Details:*"
|
||||
}
|
||||
}
|
||||
] +
|
||||
(
|
||||
# Filter only CRITICAL vulnerabilities
|
||||
[.Results[] | select(.Vulnerabilities != null) | .Vulnerabilities[] | select(.Severity == "CRITICAL")] |
|
||||
# Group by CVE to avoid duplicates
|
||||
group_by(.VulnerabilityID) |
|
||||
map(.[0]) |
|
||||
# Sort by CVSS score descending
|
||||
sort_by(-((.CVSS.nvd.V3Score // 0) | tonumber? // 0)) |
|
||||
# Include all critical vulnerabilities
|
||||
map(. | critical_vuln_block)
|
||||
) +
|
||||
[
|
||||
{ "type": "divider" },
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": { "type": "plain_text", "text": ":github: View Full Scan Report" },
|
||||
"style": "danger",
|
||||
"url": $run_url
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
' trivy-results.json)
|
||||
|
||||
echo "critical_blocks=$CRITICAL_BLOCKS_JSON" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Send Critical Vulnerability Notification to Mission Security
|
||||
if: steps.process_results.outputs.critical_count != '0'
|
||||
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.QBOT_SLACK_TOKEN }}
|
||||
payload: |
|
||||
channel: ${{ env.SLACK_CHANNEL_CRITICAL }}
|
||||
text: "🚨 CRITICAL: ${{ steps.process_results.outputs.critical_count }} critical vulnerabilities found in ${{ inputs.image_ref }}"
|
||||
blocks: ${{ steps.generate_critical_blocks.outputs.critical_blocks }}
|
||||
35
.github/workflows/test-bench-reusable.yml
vendored
Normal file
35
.github/workflows/test-bench-reusable.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: 'Test: Benchmarks'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: GitHub ref to test.
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Branch or ref to benchmark (defaults to the workflow's branch).
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
name: Benchmarks
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-2vcpu-ubuntu-2204' }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
||||
- name: Run benchmarks
|
||||
uses: CodSpeedHQ/action@2ac572851726409c88c02a307f1ea2632a9ea59b # v4.11.0
|
||||
with:
|
||||
mode: simulation
|
||||
run: CODSPEED=true pnpm --filter=@n8n/performance bench
|
||||
39
.github/workflows/test-benchmark-destroy-nightly.yml
vendored
Normal file
39
.github/workflows/test-benchmark-destroy-nightly.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: 'Test: Benchmark Destroy Env'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: benchmark
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment: benchmarking
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Azure login
|
||||
uses: azure/login@6c251865b4e6290e7b78be643ea2d005bc51f69a # v2.1.1
|
||||
with:
|
||||
client-id: ${{ secrets.BENCHMARK_ARM_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.BENCHMARK_ARM_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.BENCHMARK_ARM_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Setup Node.js and install dependencies
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
|
||||
- name: Destroy cloud env
|
||||
run: pnpm destroy-cloud-env
|
||||
working-directory: packages/@n8n/benchmark
|
||||
115
.github/workflows/test-benchmark-nightly.yml
vendored
Normal file
115
.github/workflows/test-benchmark-nightly.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
name: 'Test: Benchmark Nightly'
|
||||
run-name: Benchmark ${{ inputs.n8n_tag || 'nightly' }}
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1,2,3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug:
|
||||
description: 'Use debug logging'
|
||||
required: true
|
||||
default: 'false'
|
||||
n8n_tag:
|
||||
description: 'Name of the n8n docker tag to run the benchmark against.'
|
||||
required: true
|
||||
default: 'nightly'
|
||||
benchmark_tag:
|
||||
description: 'Name of the benchmark cli docker tag to run the benchmark with.'
|
||||
required: true
|
||||
default: 'latest'
|
||||
|
||||
env:
|
||||
ARM_CLIENT_ID: ${{ secrets.BENCHMARK_ARM_CLIENT_ID }}
|
||||
ARM_SUBSCRIPTION_ID: ${{ secrets.BENCHMARK_ARM_SUBSCRIPTION_ID }}
|
||||
ARM_TENANT_ID: ${{ secrets.BENCHMARK_ARM_TENANT_ID }}
|
||||
N8N_TAG: ${{ inputs.n8n_tag || 'nightly' }}
|
||||
N8N_BENCHMARK_TAG: ${{ inputs.benchmark_tag || 'latest' }}
|
||||
DEBUG: ${{ inputs.debug == 'true' && '--debug' || '' }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: benchmark
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment: benchmarking
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Setup Node.js and install dependencies
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
|
||||
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3
|
||||
with:
|
||||
terraform_version: '1.8.5'
|
||||
|
||||
- name: Azure login
|
||||
uses: azure/login@6c251865b4e6290e7b78be643ea2d005bc51f69a # v2.1.1
|
||||
with:
|
||||
client-id: ${{ env.ARM_CLIENT_ID }}
|
||||
tenant-id: ${{ env.ARM_TENANT_ID }}
|
||||
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Destroy any existing environment
|
||||
run: pnpm destroy-cloud-env
|
||||
working-directory: packages/@n8n/benchmark
|
||||
|
||||
- name: Provision the environment
|
||||
run: pnpm provision-cloud-env ${{ env.DEBUG }}
|
||||
working-directory: packages/@n8n/benchmark
|
||||
|
||||
- name: Run the benchmark
|
||||
id: benchmark
|
||||
env:
|
||||
BENCHMARK_RESULT_WEBHOOK_URL: ${{ secrets.BENCHMARK_RESULT_WEBHOOK_URL }}
|
||||
BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER: ${{ secrets.BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER }}
|
||||
N8N_LICENSE_CERT: ${{ secrets.N8N_BENCHMARK_LICENSE_CERT }}
|
||||
run: |
|
||||
pnpm benchmark-in-cloud \
|
||||
--vus 5 \
|
||||
--duration 1m \
|
||||
--n8nTag ${{ env.N8N_TAG }} \
|
||||
--benchmarkTag ${{ env.N8N_BENCHMARK_TAG }} \
|
||||
${{ env.DEBUG }}
|
||||
working-directory: packages/@n8n/benchmark
|
||||
|
||||
# We need to login again because the access token expires
|
||||
- name: Azure login
|
||||
if: always()
|
||||
uses: azure/login@6c251865b4e6290e7b78be643ea2d005bc51f69a # v2.1.1
|
||||
with:
|
||||
client-id: ${{ env.ARM_CLIENT_ID }}
|
||||
tenant-id: ${{ env.ARM_TENANT_ID }}
|
||||
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Destroy the environment
|
||||
if: always()
|
||||
run: pnpm destroy-cloud-env ${{ env.DEBUG }}
|
||||
working-directory: packages/@n8n/benchmark
|
||||
|
||||
- name: Fail `build` job if `benchmark` step failed
|
||||
if: steps.benchmark.outcome == 'failure'
|
||||
run: exit 1
|
||||
|
||||
notify-on-failure:
|
||||
name: Notify Cats on failure
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
if: failure()
|
||||
steps:
|
||||
- uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
channel: '#team-catalysts'
|
||||
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
message: Benchmark run failed for n8n tag `${{ inputs.n8n_tag || 'nightly' }}` - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
51
.github/workflows/test-db-reusable.yml
vendored
Normal file
51
.github/workflows/test-db-reusable.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: 'Test: DB Integration'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: SQLite Pooled
|
||||
runner: blacksmith-2vcpu-ubuntu-2204
|
||||
test-cmd: pnpm test:sqlite
|
||||
migration-cmd: pnpm test:sqlite:migrations
|
||||
- name: Postgres 16
|
||||
runner: blacksmith-4vcpu-ubuntu-2204
|
||||
test-cmd: pnpm test:postgres:integration:tc
|
||||
migration-cmd: pnpm test:postgres:migrations:tc
|
||||
TEST_IMAGE_POSTGRES: 'postgres:16'
|
||||
env:
|
||||
TEST_IMAGE_POSTGRES: ${{ matrix.TEST_IMAGE_POSTGRES }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Setup and Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
||||
- name: Pre-pull Test Container Images
|
||||
run: pnpm tsx packages/testing/containers/pull-test-images.ts || true
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: packages/cli
|
||||
run: ${{ matrix.test-cmd }}
|
||||
|
||||
- name: Run Migration Tests
|
||||
working-directory: packages/cli
|
||||
run: ${{ matrix.migration-cmd }}
|
||||
153
.github/workflows/test-e2e-ci-reusable.yml
vendored
Normal file
153
.github/workflows/test-e2e-ci-reusable.yml
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
name: 'Test: E2E CI'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'GitHub branch/ref to test'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
playwright-only:
|
||||
description: 'Only Playwright files changed — run impacted tests only'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
DOCKER_IMAGE: ghcr.io/${{ github.repository }}:ci-${{ github.run_id }}
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: 'Prepare E2E'
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
outputs:
|
||||
matrix: ${{ steps.generate-matrix.outputs.matrix }}
|
||||
skip-tests: ${{ steps.generate-matrix.outputs.skip-tests }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push to GHCR
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: 'pnpm build:docker'
|
||||
enable-docker-cache: true
|
||||
env:
|
||||
INCLUDE_TEST_CONTROLLER: 'true'
|
||||
IMAGE_BASE_NAME: ghcr.io/${{ github.repository }}
|
||||
IMAGE_TAG: ci-${{ github.run_id }}
|
||||
RUNNERS_IMAGE_BASE_NAME: ghcr.io/${{ github.repository_owner }}/runners
|
||||
BUILD_STATS_WEBHOOK_URL: ${{ secrets.BUILD_STATS_WEBHOOK_URL }}
|
||||
BUILD_STATS_WEBHOOK_USER: ${{ secrets.BUILD_STATS_WEBHOOK_USER }}
|
||||
BUILD_STATS_WEBHOOK_PASSWORD: ${{ secrets.BUILD_STATS_WEBHOOK_PASSWORD }}
|
||||
DOCKER_STATS_WEBHOOK_URL: ${{ secrets.DOCKER_STATS_WEBHOOK_URL }}
|
||||
|
||||
- name: Get changed files for impact analysis
|
||||
if: ${{ inputs.playwright-only }}
|
||||
id: changed-files
|
||||
run: |
|
||||
git fetch --depth=1 origin ${{ github.event.pull_request.base.ref || 'master' }}
|
||||
echo "list=$(git diff --name-only FETCH_HEAD HEAD | tr '\n' ',' | sed 's/,$//')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate shard matrix
|
||||
id: generate-matrix
|
||||
env:
|
||||
CHANGED_FILES: ${{ steps.changed-files.outputs.list }}
|
||||
run: |
|
||||
ARGS=(--matrix 16 --orchestrate)
|
||||
if [[ "${{ inputs.playwright-only }}" == "true" ]]; then
|
||||
ARGS+=(--impact "--files=$CHANGED_FILES")
|
||||
fi
|
||||
MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs "${ARGS[@]}")
|
||||
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
||||
echo "skip-tests=$(node -e "process.stdout.write(JSON.parse(process.argv[1])[0]?.skip === true ? 'true' : 'false')" "$MATRIX")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
sqlite-sanity:
|
||||
needs: [prepare]
|
||||
name: 'SQLite: Sanity Check'
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
uses: ./.github/workflows/test-e2e-reusable.yml
|
||||
with:
|
||||
branch: ${{ inputs.branch }}
|
||||
test-mode: docker-pull
|
||||
docker-image: ghcr.io/${{ github.repository }}:ci-${{ github.run_id }}
|
||||
test-command: pnpm --filter=n8n-playwright test:container:sqlite:e2e tests/e2e/building-blocks/workflow-entry-points.spec.ts
|
||||
shards: 1
|
||||
runner: blacksmith-2vcpu-ubuntu-2204
|
||||
workers: '1'
|
||||
pre-generated-matrix: '[{"shard":1,"images":""}]'
|
||||
|
||||
# Multi-main: postgres + redis + caddy + 2 mains + 1 worker
|
||||
# Only runs for internal PRs (not community/fork PRs)
|
||||
# Pulls pre-built Docker image from GHCR
|
||||
multi-main-e2e:
|
||||
needs: [prepare]
|
||||
name: 'Multi-Main: E2E'
|
||||
if: ${{ !github.event.pull_request.head.repo.fork && needs.prepare.outputs.skip-tests != 'true' }}
|
||||
uses: ./.github/workflows/test-e2e-reusable.yml
|
||||
with:
|
||||
branch: ${{ inputs.branch }}
|
||||
test-mode: docker-pull
|
||||
docker-image: ghcr.io/${{ github.repository }}:ci-${{ github.run_id }}
|
||||
test-command: pnpm --filter=n8n-playwright test:container:multi-main:e2e
|
||||
shards: 16
|
||||
runner: blacksmith-2vcpu-ubuntu-2204
|
||||
workers: '1'
|
||||
use-custom-orchestration: true
|
||||
pre-generated-matrix: ${{ needs.prepare.outputs.matrix }}
|
||||
secrets: inherit
|
||||
|
||||
# Community PR tests: Local mode with SQLite (no container building, no secrets required)
|
||||
# Runs on GitHub-hosted runners without Currents reporting
|
||||
community-e2e:
|
||||
name: 'Community: E2E'
|
||||
if: ${{ github.event.pull_request.head.repo.fork }}
|
||||
uses: ./.github/workflows/test-e2e-reusable.yml
|
||||
with:
|
||||
branch: ${{ inputs.branch }}
|
||||
test-mode: local
|
||||
test-command: pnpm --filter=n8n-playwright test:local:e2e-only
|
||||
shards: 7
|
||||
runner: ubuntu-latest
|
||||
workers: '1'
|
||||
upload-failure-artifacts: true
|
||||
|
||||
# Cleanup ephemeral Docker image from GHCR after tests complete
|
||||
# Local runner cleanup is handled by each test shard in test-e2e-reusable.yml
|
||||
cleanup-ghcr:
|
||||
name: 'Cleanup GHCR Image'
|
||||
needs: [prepare, multi-main-e2e, sqlite-sanity]
|
||||
if: ${{ !failure() && !cancelled() && !github.event.pull_request.head.repo.fork }}
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Delete images from GHCR
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GHCR_ORG: ${{ github.repository_owner }}
|
||||
GHCR_REPO: ${{ github.event.repository.name }}
|
||||
run: node .github/scripts/cleanup-ghcr-images.mjs --tag ci-${{ github.run_id }}
|
||||
84
.github/workflows/test-e2e-coverage-weekly.yml
vendored
Normal file
84
.github/workflows/test-e2e-coverage-weekly.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: 'Test: E2E Coverage Weekly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * 1' # Every Monday at 2 AM
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=16384
|
||||
PLAYWRIGHT_WORKERS: 4
|
||||
PLAYWRIGHT_BROWSERS_PATH: packages/testing/playwright/.playwright-browsers
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2204
|
||||
name: Coverage Tests
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
env:
|
||||
INCLUDE_TEST_CONTROLLER: 'true'
|
||||
|
||||
- name: Build Docker Image with Coverage
|
||||
run: pnpm build:docker:coverage
|
||||
env:
|
||||
INCLUDE_TEST_CONTROLLER: 'true'
|
||||
|
||||
- name: Install Browsers
|
||||
run: pnpm turbo run install-browsers --filter=n8n-playwright
|
||||
|
||||
- name: Run Container Coverage Tests
|
||||
id: coverage-tests
|
||||
run: |
|
||||
pnpm --filter n8n-playwright test:container:sqlite \
|
||||
--workers=${{ env.PLAYWRIGHT_WORKERS }}
|
||||
env:
|
||||
BUILD_WITH_COVERAGE: 'true'
|
||||
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
|
||||
CURRENTS_PROJECT_ID: 'LRxcNt'
|
||||
QA_PERFORMANCE_METRICS_WEBHOOK_URL: ${{ secrets.QA_PERFORMANCE_METRICS_WEBHOOK_URL }}
|
||||
QA_PERFORMANCE_METRICS_WEBHOOK_USER: ${{ secrets.QA_PERFORMANCE_METRICS_WEBHOOK_USER }}
|
||||
QA_PERFORMANCE_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_PERFORMANCE_METRICS_WEBHOOK_PASSWORD }}
|
||||
|
||||
- name: Generate Coverage Report
|
||||
if: always() && steps.coverage-tests.outcome != 'skipped'
|
||||
run: pnpm --filter n8n-playwright coverage:report
|
||||
|
||||
- name: Upload Coverage Report Artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: coverage-report
|
||||
path: packages/testing/playwright/coverage/
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload E2E Coverage to Codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: packages/testing/playwright/coverage/lcov.info
|
||||
flags: frontend-e2e
|
||||
name: playwright-e2e
|
||||
fail_ci_if_error: false
|
||||
|
||||
- name: Analyse Coverage Gaps
|
||||
if: always() && steps.coverage-tests.outcome != 'skipped'
|
||||
env:
|
||||
CODECOV_API_TOKEN: ${{ secrets.CODECOV_API_TOKEN }}
|
||||
run: |
|
||||
node packages/testing/playwright/scripts/coverage-analysis.mjs \
|
||||
--md --top=15 --out-json=coverage-gaps.json >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload Coverage Gap Report
|
||||
if: always() && steps.coverage-tests.outcome != 'skipped'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: coverage-gap-report
|
||||
path: coverage-gaps.json
|
||||
retention-days: 21
|
||||
38
.github/workflows/test-e2e-docker-pull-reusable.yml
vendored
Normal file
38
.github/workflows/test-e2e-docker-pull-reusable.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: 'Test: E2E Docker Pull'
|
||||
# This workflow is used to run Playwright tests in a Docker container pulled from the registry
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
shards:
|
||||
description: 'Shards for parallel execution'
|
||||
required: false
|
||||
default: 1
|
||||
type: number
|
||||
image:
|
||||
description: 'Image to use'
|
||||
required: false
|
||||
default: 'n8nio/n8n:nightly'
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
shards:
|
||||
description: 'Shards for parallel execution'
|
||||
required: false
|
||||
default: 1
|
||||
type: number
|
||||
image:
|
||||
description: 'Image to use'
|
||||
required: false
|
||||
default: 'n8nio/n8n:nightly'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
uses: ./.github/workflows/test-e2e-reusable.yml
|
||||
with:
|
||||
test-mode: docker-pull
|
||||
shards: ${{ inputs.shards }}
|
||||
docker-image: ${{ inputs.image }}
|
||||
test-command: pnpm --filter=n8n-playwright test:container:standard
|
||||
secrets: inherit
|
||||
173
.github/workflows/test-e2e-helm.yml
vendored
Normal file
173
.github/workflows/test-e2e-helm.yml
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
name: 'Test: E2E Helm Chart'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/test-e2e-helm.yml'
|
||||
- 'packages/testing/containers/helm-stack.ts'
|
||||
- 'packages/testing/containers/helm-start-stack.ts'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch to test'
|
||||
required: false
|
||||
default: ''
|
||||
helm-chart-ref:
|
||||
description: 'n8n-hosting branch/tag for Helm chart'
|
||||
required: false
|
||||
default: 'main'
|
||||
mode:
|
||||
description: 'Deployment mode to test'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- standalone
|
||||
- queue
|
||||
default: 'queue'
|
||||
|
||||
env:
|
||||
DOCKER_IMAGE: ghcr.io/${{ github.repository }}:ci-${{ github.run_id }}
|
||||
HELM_CHART_REF: ${{ inputs.helm-chart-ref || 'main' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 'Build Docker Image'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push to GHCR
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: 'pnpm build:docker'
|
||||
enable-docker-cache: true
|
||||
env:
|
||||
INCLUDE_TEST_CONTROLLER: 'true'
|
||||
IMAGE_BASE_NAME: ghcr.io/${{ github.repository }}
|
||||
IMAGE_TAG: ci-${{ github.run_id }}
|
||||
RUNNERS_IMAGE_BASE_NAME: ghcr.io/${{ github.repository_owner }}/runners
|
||||
|
||||
helm-e2e:
|
||||
name: "Helm E2E (${{ inputs.mode || 'queue' }})"
|
||||
needs: build
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: 'pnpm build'
|
||||
|
||||
- name: Install Browsers
|
||||
working-directory: packages/testing/playwright
|
||||
run: pnpm exec playwright install chromium
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@bf6a7d304bc2fdb57e0331155b7ebf2c504acf0a # v4
|
||||
|
||||
- name: Install kubectl
|
||||
uses: azure/setup-kubectl@c0c8b32d33a5244f1e5947304550403b63930415 # v4
|
||||
|
||||
- name: Start K3s + Helm stack
|
||||
env:
|
||||
TESTCONTAINERS_RYUK_DISABLED: 'true'
|
||||
run: |
|
||||
npx tsx packages/testing/containers/helm-start-stack.ts \
|
||||
--env E2E_TESTS=true --env NODE_ENV=development \
|
||||
--mode "${{ inputs.mode || 'queue' }}" \
|
||||
--image "${{ env.DOCKER_IMAGE }}" \
|
||||
--chart-ref "${{ env.HELM_CHART_REF }}" \
|
||||
--url-file /tmp/n8n-helm-url.txt \
|
||||
--kubeconfig-file /tmp/n8n-helm-kubeconfig.txt
|
||||
|
||||
N8N_URL=$(cat /tmp/n8n-helm-url.txt)
|
||||
echo "n8n URL: ${N8N_URL}"
|
||||
echo "N8N_BASE_URL=${N8N_URL}" >> "$GITHUB_ENV"
|
||||
|
||||
KUBECONFIG_PATH=$(cat /tmp/n8n-helm-kubeconfig.txt)
|
||||
echo "KUBECONFIG=${KUBECONFIG_PATH}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run Building Blocks E2E Tests
|
||||
working-directory: packages/testing/playwright
|
||||
run: |
|
||||
N8N_BASE_URL="${{ env.N8N_BASE_URL }}" \
|
||||
RESET_E2E_DB=true \
|
||||
npx playwright test \
|
||||
--project=e2e \
|
||||
tests/e2e/building-blocks/ \
|
||||
--workers=1 \
|
||||
--retries=2 \
|
||||
--reporter=list
|
||||
|
||||
- name: Debug K8s State
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Pod Status ==="
|
||||
kubectl get pods -o wide
|
||||
echo ""
|
||||
echo "=== Pod Descriptions ==="
|
||||
kubectl describe pods -l app.kubernetes.io/name=n8n
|
||||
echo ""
|
||||
echo "=== Recent Events ==="
|
||||
kubectl get events --sort-by=.lastTimestamp | tail -30
|
||||
echo ""
|
||||
echo "=== n8n Pod Logs (last 100 lines) ==="
|
||||
kubectl logs -l app.kubernetes.io/name=n8n --tail=100 || true
|
||||
echo ""
|
||||
echo "=== Health Check ==="
|
||||
curl -s -o /dev/null -w "HTTP %{http_code}" "${{ env.N8N_BASE_URL }}/healthz/readiness" || echo "UNREACHABLE"
|
||||
|
||||
- name: Upload Failure Artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: helm-e2e-report-${{ inputs.mode || 'queue' }}
|
||||
path: |
|
||||
packages/testing/playwright/test-results/
|
||||
packages/testing/playwright/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
cleanup-ghcr:
|
||||
name: 'Cleanup GHCR Image'
|
||||
needs: [build, helm-e2e]
|
||||
if: always()
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Delete images from GHCR
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GHCR_ORG: ${{ github.repository_owner }}
|
||||
GHCR_REPO: ${{ github.event.repository.name }}
|
||||
run: node .github/scripts/cleanup-ghcr-images.mjs --tag ci-${{ github.run_id }}
|
||||
20
.github/workflows/test-e2e-performance-reusable.yml
vendored
Normal file
20
.github/workflows/test-e2e-performance-reusable.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: 'Test: E2E Performance'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Runs daily at midnight
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/test-e2e-performance-reusable.yml'
|
||||
|
||||
jobs:
|
||||
build-and-test-performance:
|
||||
uses: ./.github/workflows/test-e2e-reusable.yml
|
||||
with:
|
||||
test-mode: docker-build
|
||||
test-command: pnpm --filter=n8n-playwright test:performance
|
||||
shards: 1
|
||||
currents-project-id: 'O9BJaN'
|
||||
secrets: inherit
|
||||
196
.github/workflows/test-e2e-reusable.yml
vendored
Normal file
196
.github/workflows/test-e2e-reusable.yml
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
name: 'Test: E2E'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'GitHub branch to test.'
|
||||
required: false
|
||||
type: string
|
||||
test-mode:
|
||||
description: 'Test mode: local (pnpm start from local), docker-build, or docker-pull'
|
||||
required: false
|
||||
default: 'local'
|
||||
type: string
|
||||
test-command:
|
||||
description: 'Test command to run'
|
||||
required: false
|
||||
default: 'pnpm --filter=n8n-playwright test:local'
|
||||
type: string
|
||||
shards:
|
||||
description: 'Number of parallel shards'
|
||||
required: false
|
||||
default: 8
|
||||
type: number
|
||||
docker-image:
|
||||
description: 'Docker image to use (for docker-pull mode). The runners image is derived automatically from the n8n image.'
|
||||
required: false
|
||||
default: 'n8nio/n8n:nightly'
|
||||
type: string
|
||||
workers:
|
||||
description: 'Number of parallel workers'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
runner:
|
||||
description: 'GitHub runner to use'
|
||||
required: false
|
||||
default: 'blacksmith-2vcpu-ubuntu-2204'
|
||||
type: string
|
||||
use-custom-orchestration:
|
||||
description: 'Use duration-based custom orchestration instead of Playwright sharding'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
upload-failure-artifacts:
|
||||
description: 'Upload test failure artifacts (screenshots, traces, videos). Enable for community PRs without Currents access.'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
currents-project-id:
|
||||
description: 'Currents project ID for reporting'
|
||||
required: false
|
||||
default: 'LRxcNt'
|
||||
type: string
|
||||
pre-generated-matrix:
|
||||
description: 'Pre-generated shard matrix JSON (skips matrix job if provided)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
secrets:
|
||||
CURRENTS_RECORD_KEY:
|
||||
required: false
|
||||
CURRENTS_API_KEY:
|
||||
required: false
|
||||
QA_PERFORMANCE_METRICS_WEBHOOK_URL:
|
||||
required: false
|
||||
QA_PERFORMANCE_METRICS_WEBHOOK_USER:
|
||||
required: false
|
||||
QA_PERFORMANCE_METRICS_WEBHOOK_PASSWORD:
|
||||
required: false
|
||||
N8N_LICENSE_ACTIVATION_KEY:
|
||||
required: false
|
||||
N8N_LICENSE_CERT:
|
||||
required: false
|
||||
N8N_ENCRYPTION_KEY:
|
||||
required: false
|
||||
CONTAINER_TELEMETRY_WEBHOOK:
|
||||
required: false
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: ${{ contains(inputs.runner, '2vcpu') && '--max-old-space-size=6144' || '' }}
|
||||
PLAYWRIGHT_WORKERS: ${{ inputs.workers != '' && inputs.workers || '2' }}
|
||||
# Browser cache location - must match install-browsers script
|
||||
PLAYWRIGHT_BROWSERS_PATH: packages/testing/playwright/.playwright-browsers
|
||||
TEST_IMAGE_N8N: ${{ inputs.test-mode == 'docker-build' && 'n8nio/n8n:local' || inputs.docker-image }}
|
||||
N8N_SKIP_LICENSES: 'true'
|
||||
CURRENTS_CI_BUILD_ID: ${{ github.repository }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
CURRENTS_PROJECT_ID: ${{ inputs.currents-project-id }}
|
||||
|
||||
jobs:
|
||||
matrix:
|
||||
if: ${{ inputs.pre-generated-matrix == '' }}
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-2vcpu-ubuntu-2204' }}
|
||||
outputs:
|
||||
matrix: ${{ steps.generate.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
|
||||
- name: Generate shard matrix
|
||||
id: generate
|
||||
run: echo "matrix=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix ${{ inputs.shards }} ${{ inputs.use-custom-orchestration && '--orchestrate' || '' }})" >> "$GITHUB_OUTPUT"
|
||||
|
||||
test:
|
||||
needs: matrix
|
||||
if: ${{ !cancelled() }}
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || inputs.runner }}
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
packages: read
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(inputs.pre-generated-matrix || needs.matrix.outputs.matrix) }}
|
||||
name: Shard ${{ matrix.shard }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
# docker-build: build app + docker image locally
|
||||
# docker-pull: no build needed, image is pre-built
|
||||
# local: build app for local server
|
||||
build-command: ${{ inputs.test-mode == 'docker-build' && 'pnpm build:docker' || 'pnpm build' }}
|
||||
enable-docker-cache: ${{ inputs.test-mode == 'docker-build' }}
|
||||
env:
|
||||
INCLUDE_TEST_CONTROLLER: ${{ inputs.test-mode == 'docker-build' && 'true' || '' }}
|
||||
|
||||
- name: Install Browsers
|
||||
run: pnpm turbo run install-browsers --filter=n8n-playwright
|
||||
|
||||
- name: Login to GHCR
|
||||
if: ${{ inputs.test-mode == 'docker-pull' }}
|
||||
uses: ./.github/actions/docker-registry-login
|
||||
|
||||
- name: Pre-pull Test Container Images
|
||||
if: ${{ !contains(inputs.test-command, 'test:local') }}
|
||||
run: npx tsx packages/testing/containers/pull-test-images.ts ${{ matrix.images }} || true
|
||||
|
||||
- name: Run Tests
|
||||
# Uses pre-distributed specs if orchestration enabled, otherwise falls back to Playwright sharding
|
||||
run: ${{ inputs.test-command }} --workers=${{ env.PLAYWRIGHT_WORKERS }} ${{ matrix.specs || format('--shard={0}/{1}', matrix.shard, strategy.job-total) }}
|
||||
env:
|
||||
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
|
||||
QA_PERFORMANCE_METRICS_WEBHOOK_URL: ${{ secrets.QA_PERFORMANCE_METRICS_WEBHOOK_URL }}
|
||||
QA_PERFORMANCE_METRICS_WEBHOOK_USER: ${{ secrets.QA_PERFORMANCE_METRICS_WEBHOOK_USER }}
|
||||
QA_PERFORMANCE_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_PERFORMANCE_METRICS_WEBHOOK_PASSWORD }}
|
||||
N8N_LICENSE_ACTIVATION_KEY: ${{ secrets.N8N_LICENSE_ACTIVATION_KEY }}
|
||||
N8N_LICENSE_CERT: ${{ secrets.N8N_LICENSE_CERT }}
|
||||
N8N_ENCRYPTION_KEY: ${{ secrets.N8N_ENCRYPTION_KEY }}
|
||||
CONTAINER_TELEMETRY_WEBHOOK: ${{secrets.CONTAINER_TELEMETRY_WEBHOOK}}
|
||||
|
||||
- name: Upload Failure Artifacts
|
||||
if: ${{ failure() && inputs.upload-failure-artifacts }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: playwright-report-shard-${{ matrix.shard }}
|
||||
path: |
|
||||
packages/testing/playwright/test-results/
|
||||
packages/testing/playwright/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
- name: Cleanup cached CI images
|
||||
if: ${{ inputs.test-mode == 'docker-pull' }}
|
||||
continue-on-error: true
|
||||
run: |
|
||||
docker images --format '{{.Repository}}:{{.Tag}}' | grep -E 'ghcr\.io/n8n-io/(n8n|runners):(ci|pr)-' | xargs -r docker rmi || true
|
||||
docker system prune -f || true
|
||||
|
||||
- name: Cancel Currents run if workflow is cancelled
|
||||
if: ${{ cancelled() }}
|
||||
env:
|
||||
CURRENTS_API_KEY: ${{ secrets.CURRENTS_API_KEY }}
|
||||
run: |
|
||||
if [ -n "$CURRENTS_API_KEY" ]; then
|
||||
curl --location --request PUT \
|
||||
"https://api.currents.dev/v1/runs/cancel-ci/github" \
|
||||
--header "Authorization: Bearer $CURRENTS_API_KEY" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "{\"githubRunId\": \"${{ github.run_id }}\", \"githubRunAttempt\": \"${{ github.run_attempt }}\"}"
|
||||
fi
|
||||
60
.github/workflows/test-evals-ai-release.yml
vendored
Normal file
60
.github/workflows/test-evals-ai-release.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: 'Test: Evals AI (Release)'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
check-minor-release:
|
||||
name: Check Minor Release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_minor: ${{ steps.check.outputs.is_minor }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
steps:
|
||||
- name: Check if minor release
|
||||
id: check
|
||||
run: |
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
echo "Release tag: $TAG"
|
||||
|
||||
# Check if it's a minor release (e.g., v1.2.0, not v1.2.1)
|
||||
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.0$ ]]; then
|
||||
echo "is_minor=true" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "Minor release detected: $TAG"
|
||||
else
|
||||
echo "is_minor=false" >> "$GITHUB_OUTPUT"
|
||||
echo "version=" >> "$GITHUB_OUTPUT"
|
||||
echo "Not a minor release, skipping evals"
|
||||
fi
|
||||
|
||||
run-pairwise-evals:
|
||||
name: Run Pairwise (Spec) Evaluations
|
||||
needs: check-minor-release
|
||||
if: needs.check-minor-release.outputs.is_minor == 'true'
|
||||
uses: ./.github/workflows/test-evals-ai-reusable.yml
|
||||
with:
|
||||
branch: ${{ github.event.release.tag_name }}
|
||||
suite: pairwise
|
||||
dataset: notion-pairwise-fresh
|
||||
# Spec evals on minor release: 2 reps, 3 judges
|
||||
repetitions: 2
|
||||
judges: 3
|
||||
experiment_name_prefix: CI_${{ needs.check-minor-release.outputs.version }}
|
||||
secrets: inherit
|
||||
|
||||
run-llm-judge-evals:
|
||||
name: Run LLM Judge (Matrix) Evaluations
|
||||
needs: check-minor-release
|
||||
if: needs.check-minor-release.outputs.is_minor == 'true'
|
||||
uses: ./.github/workflows/test-evals-ai-reusable.yml
|
||||
with:
|
||||
branch: ${{ github.event.release.tag_name }}
|
||||
suite: llm-judge
|
||||
dataset: workflow-builder-canvas-prompts
|
||||
# Matrix evals always use 3 reps, 3 judges
|
||||
repetitions: 3
|
||||
judges: 3
|
||||
experiment_name_prefix: CI_${{ needs.check-minor-release.outputs.version }}
|
||||
secrets: inherit
|
||||
108
.github/workflows/test-evals-ai-reusable.yml
vendored
Normal file
108
.github/workflows/test-evals-ai-reusable.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
name: 'Test: Evals AI'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'GitHub branch to test.'
|
||||
type: string
|
||||
default: 'master'
|
||||
suite:
|
||||
description: 'Evaluation suite to run (pairwise or llm-judge).'
|
||||
type: string
|
||||
default: 'pairwise'
|
||||
dataset:
|
||||
description: 'LangSmith dataset to use.'
|
||||
type: string
|
||||
required: true
|
||||
repetitions:
|
||||
description: 'Number of repetitions to run.'
|
||||
type: number
|
||||
default: 1
|
||||
judges:
|
||||
description: 'Number of judges to use.'
|
||||
type: number
|
||||
default: 1
|
||||
concurrency:
|
||||
description: 'Max concurrent evaluations.'
|
||||
type: number
|
||||
default: 10
|
||||
experiment_name_prefix:
|
||||
description: 'Prefix for the experiment name. If empty, will be auto-generated from branch name.'
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
evals:
|
||||
name: Run ${{ inputs.suite }} Evaluations
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
env:
|
||||
N8N_AI_ANTHROPIC_KEY: ${{ secrets.EVALS_ANTHROPIC_KEY }}
|
||||
LANGSMITH_TRACING: true
|
||||
LANGSMITH_ENDPOINT: ${{ secrets.EVALS_LANGSMITH_ENDPOINT }}
|
||||
LANGSMITH_API_KEY: ${{ secrets.EVALS_LANGSMITH_API_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
|
||||
- name: Generate experiment name
|
||||
id: experiment
|
||||
run: |
|
||||
DATE=$(date +%Y_%m_%d)
|
||||
PREFIX="${{ inputs.experiment_name_prefix }}"
|
||||
|
||||
if [ -n "$PREFIX" ]; then
|
||||
NAME="${PREFIX}_${DATE}"
|
||||
else
|
||||
# Extract ticket ID from branch name (e.g., AI-1234 from ai-1234-feature-name)
|
||||
BRANCH="${{ inputs.branch }}"
|
||||
TICKET=$(echo "$BRANCH" | grep -oE '^[Aa][Ii]-[0-9]+' | tr '[:lower:]' '[:upper:]' || true)
|
||||
if [ -n "$TICKET" ]; then
|
||||
NAME="${TICKET}_${DATE}"
|
||||
else
|
||||
# Sanitize branch name for experiment name
|
||||
SANITIZED_BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9_.-]/_/g' | sed 's/__*/_/g')
|
||||
NAME="CI_${SANITIZED_BRANCH}_${DATE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "name=$NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "Generated experiment name: $NAME"
|
||||
|
||||
- name: Setup and Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # 6.5.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3.0.0
|
||||
|
||||
- name: Install Python
|
||||
working-directory: packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python
|
||||
run: uv python install 3.11
|
||||
|
||||
- name: Install workflow comparison dependencies
|
||||
working-directory: packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python
|
||||
run: just sync-all
|
||||
|
||||
- name: Export Node Types
|
||||
run: |
|
||||
./packages/cli/bin/n8n export:nodes --output ./packages/@n8n/ai-workflow-builder.ee/evaluations/nodes.json
|
||||
|
||||
- name: Run Evaluations
|
||||
working-directory: packages/@n8n/ai-workflow-builder.ee/evaluations
|
||||
run: |
|
||||
pnpm eval \
|
||||
--suite "${{ inputs.suite }}" \
|
||||
--backend langsmith \
|
||||
--dataset "${{ inputs.dataset }}" \
|
||||
--repetitions ${{ inputs.repetitions }} \
|
||||
--judges ${{ inputs.judges }} \
|
||||
--concurrency ${{ inputs.concurrency }} \
|
||||
--name "${{ steps.experiment.outputs.name }}" \
|
||||
${{ secrets.EVALS_WEBHOOK_URL && format('--webhook-url "{0}"', secrets.EVALS_WEBHOOK_URL) || '' }} \
|
||||
${{ secrets.EVALS_WEBHOOK_SECRET && format('--webhook-secret "{0}"', secrets.EVALS_WEBHOOK_SECRET) || '' }}
|
||||
161
.github/workflows/test-evals-ai.yml
vendored
Normal file
161
.github/workflows/test-evals-ai.yml
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
name: 'Test: Evals AI'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/@n8n/ai-workflow-builder.ee/src/prompts/**'
|
||||
- 'packages/@n8n/ai-workflow-builder.ee/**/*.prompt.ts'
|
||||
- '.github/workflows/test-evals-ai.yml'
|
||||
- '.github/workflows/test-evals-ai-reusable.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'GitHub branch to test.'
|
||||
required: false
|
||||
default: 'master'
|
||||
eval_type:
|
||||
description: 'Which evaluations to run.'
|
||||
required: false
|
||||
default: 'both'
|
||||
type: choice
|
||||
options:
|
||||
- both
|
||||
- spec
|
||||
- matrix
|
||||
spec_repetitions:
|
||||
description: 'Number of repetitions for spec (pairwise) evals.'
|
||||
required: false
|
||||
default: '1'
|
||||
spec_judges:
|
||||
description: 'Number of judges for spec (pairwise) evals.'
|
||||
required: false
|
||||
default: '1'
|
||||
|
||||
jobs:
|
||||
check-skip:
|
||||
name: Check Skip Label
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_skip: ${{ steps.check.outputs.should_skip }}
|
||||
steps:
|
||||
- name: Check for no-prompt-changes opt-out
|
||||
id: check
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const SKIP_TAG = '(no-prompt-changes)';
|
||||
const SKIP_LABEL = 'no-prompt-changes';
|
||||
|
||||
// Only check for push events (merges to master)
|
||||
if (context.eventName !== 'push') {
|
||||
core.setOutput('should_skip', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check commit message for skip tag
|
||||
const commitMessage = context.payload.head_commit?.message || '';
|
||||
if (commitMessage.includes(SKIP_TAG)) {
|
||||
console.log(`Found ${SKIP_TAG} in commit message, skipping evals`);
|
||||
core.setOutput('should_skip', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the PR associated with this merge commit
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: context.sha
|
||||
});
|
||||
|
||||
if (prs.length === 0) {
|
||||
console.log('No PR found for this commit, running evals');
|
||||
core.setOutput('should_skip', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
const pr = prs[0];
|
||||
console.log(`PR #${pr.number}: "${pr.title}"`);
|
||||
|
||||
// Check PR title for skip tag
|
||||
if (pr.title.includes(SKIP_TAG)) {
|
||||
console.log(`Found ${SKIP_TAG} in PR title, skipping evals`);
|
||||
core.setOutput('should_skip', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check PR labels for skip label
|
||||
const labels = pr.labels.map(l => l.name);
|
||||
console.log(`PR labels: ${labels.join(', ') || '(none)'}`);
|
||||
|
||||
if (labels.includes(SKIP_LABEL)) {
|
||||
console.log(`Found ${SKIP_LABEL} label, skipping evals`);
|
||||
core.setOutput('should_skip', 'true');
|
||||
} else {
|
||||
console.log('No skip indicator found, running evals');
|
||||
core.setOutput('should_skip', 'false');
|
||||
}
|
||||
|
||||
determine-config:
|
||||
name: Determine Configuration
|
||||
needs: check-skip
|
||||
if: needs.check-skip.outputs.should_skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
branch: ${{ steps.config.outputs.branch }}
|
||||
spec_repetitions: ${{ steps.config.outputs.spec_repetitions }}
|
||||
spec_judges: ${{ steps.config.outputs.spec_judges }}
|
||||
experiment_prefix: ${{ steps.config.outputs.experiment_prefix }}
|
||||
steps:
|
||||
- name: Set configuration based on trigger
|
||||
id: config
|
||||
run: |
|
||||
EVENT_NAME="${{ github.event_name }}"
|
||||
|
||||
if [ "$EVENT_NAME" = "push" ]; then
|
||||
# Merge to master: spec evals use 1 rep, 1 judge
|
||||
{
|
||||
echo "branch=${{ github.ref_name }}"
|
||||
echo "spec_repetitions=1"
|
||||
echo "spec_judges=1"
|
||||
echo "experiment_prefix="
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Manual dispatch: use provided values for spec evals
|
||||
{
|
||||
echo "branch=${{ inputs.branch || 'master' }}"
|
||||
echo "spec_repetitions=${{ inputs.spec_repetitions || '1' }}"
|
||||
echo "spec_judges=${{ inputs.spec_judges || '1' }}"
|
||||
echo "experiment_prefix=CI_manual"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
run-pairwise-evals:
|
||||
name: Run Pairwise (Spec) Evaluations
|
||||
needs: determine-config
|
||||
if: github.event_name == 'push' || inputs.eval_type == 'both' || inputs.eval_type == 'spec'
|
||||
uses: ./.github/workflows/test-evals-ai-reusable.yml
|
||||
with:
|
||||
branch: ${{ needs.determine-config.outputs.branch }}
|
||||
suite: pairwise
|
||||
dataset: notion-pairwise-fresh
|
||||
repetitions: ${{ fromJson(needs.determine-config.outputs.spec_repetitions) }}
|
||||
judges: ${{ fromJson(needs.determine-config.outputs.spec_judges) }}
|
||||
experiment_name_prefix: ${{ needs.determine-config.outputs.experiment_prefix }}
|
||||
secrets: inherit
|
||||
|
||||
run-llm-judge-evals:
|
||||
name: Run LLM Judge (Matrix) Evaluations
|
||||
needs: determine-config
|
||||
if: github.event_name == 'push' || inputs.eval_type == 'both' || inputs.eval_type == 'matrix'
|
||||
uses: ./.github/workflows/test-evals-ai-reusable.yml
|
||||
with:
|
||||
branch: ${{ needs.determine-config.outputs.branch }}
|
||||
suite: llm-judge
|
||||
dataset: workflow-builder-canvas-prompts
|
||||
# Matrix evals always use 3 reps, 3 judges regardless of trigger
|
||||
repetitions: 3
|
||||
judges: 3
|
||||
experiment_name_prefix: ${{ needs.determine-config.outputs.experiment_prefix }}
|
||||
secrets: inherit
|
||||
56
.github/workflows/test-evals-python.yml
vendored
Normal file
56
.github/workflows/test-evals-python.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: 'Test: Evals Python'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python/**
|
||||
- .github/workflows/test-evals-python.yml
|
||||
push:
|
||||
paths:
|
||||
- packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python/**
|
||||
|
||||
jobs:
|
||||
workflow-comparison:
|
||||
name: Workflow Comparison Python
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python
|
||||
steps:
|
||||
- name: Check out project
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # 6.5.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3.0.0
|
||||
|
||||
- name: Install Python
|
||||
run: uv python install 3.11
|
||||
|
||||
- name: Install project dependencies
|
||||
run: just sync-all
|
||||
|
||||
- name: Format check
|
||||
run: just format-check
|
||||
|
||||
- name: Typecheck
|
||||
run: just typecheck
|
||||
|
||||
- name: Lint
|
||||
run: just lint
|
||||
|
||||
- name: Python unit tests
|
||||
run: uv run pytest --cov=src --cov-report=xml --cov-report=term-missing
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python/coverage.xml
|
||||
flags: tests
|
||||
name: workflow-comparison-python
|
||||
fail_ci_if_error: false
|
||||
33
.github/workflows/test-linting-reusable.yml
vendored
Normal file
33
.github/workflows/test-linting-reusable.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: 'Test: Linting'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: GitHub ref to lint.
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
nodeVersion:
|
||||
description: Version of node to use.
|
||||
required: false
|
||||
type: string
|
||||
default: 24.13.1
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=7168
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Build and Test
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: pnpm lint
|
||||
node-version: ${{ inputs.nodeVersion }}
|
||||
217
.github/workflows/test-unit-reusable.yml
vendored
Normal file
217
.github/workflows/test-unit-reusable.yml
vendored
Normal file
@@ -0,0 +1,217 @@
|
||||
name: 'Test: Unit'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: GitHub ref to test.
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
nodeVersion:
|
||||
description: Version of node to use.
|
||||
required: false
|
||||
type: string
|
||||
default: 24.13.1
|
||||
collectCoverage:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
description: 'Codecov upload token.'
|
||||
required: false
|
||||
BUILD_STATS_WEBHOOK_URL:
|
||||
description: 'Webhook URL for test stats.'
|
||||
required: false
|
||||
BUILD_STATS_WEBHOOK_USER:
|
||||
description: 'Basic auth username for test stats webhook.'
|
||||
required: false
|
||||
BUILD_STATS_WEBHOOK_PASSWORD:
|
||||
description: 'Basic auth password for test stats webhook.'
|
||||
required: false
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=7168
|
||||
|
||||
jobs:
|
||||
unit-test-backend:
|
||||
name: Backend Unit Tests
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
env:
|
||||
COVERAGE_ENABLED: ${{ inputs.collectCoverage }} # Coverage collected when true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
node-version: ${{ inputs.nodeVersion }}
|
||||
|
||||
- name: Test Unit
|
||||
run: pnpm test:ci:backend:unit --summarize
|
||||
|
||||
- name: Send Test Stats
|
||||
if: ${{ !cancelled() }}
|
||||
run: node .github/scripts/send-build-stats.mjs || true
|
||||
env:
|
||||
BUILD_STATS_WEBHOOK_URL: ${{ secrets.BUILD_STATS_WEBHOOK_URL }}
|
||||
BUILD_STATS_WEBHOOK_USER: ${{ secrets.BUILD_STATS_WEBHOOK_USER }}
|
||||
BUILD_STATS_WEBHOOK_PASSWORD: ${{ secrets.BUILD_STATS_WEBHOOK_PASSWORD }}
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
report-type: test_results
|
||||
name: backend-unit
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: env.COVERAGE_ENABLED == 'true'
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: backend-unit
|
||||
name: backend-unit
|
||||
|
||||
integration-test-backend:
|
||||
name: Backend Integration Tests
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
env:
|
||||
COVERAGE_ENABLED: ${{ inputs.collectCoverage }} # Coverage collected when true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
node-version: ${{ inputs.nodeVersion }}
|
||||
|
||||
- name: Test Integration
|
||||
run: pnpm test:ci:backend:integration --summarize
|
||||
|
||||
- name: Send Test Stats
|
||||
if: ${{ !cancelled() }}
|
||||
run: node .github/scripts/send-build-stats.mjs || true
|
||||
env:
|
||||
BUILD_STATS_WEBHOOK_URL: ${{ secrets.BUILD_STATS_WEBHOOK_URL }}
|
||||
BUILD_STATS_WEBHOOK_USER: ${{ secrets.BUILD_STATS_WEBHOOK_USER }}
|
||||
BUILD_STATS_WEBHOOK_PASSWORD: ${{ secrets.BUILD_STATS_WEBHOOK_PASSWORD }}
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
name: backend-integration
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: env.COVERAGE_ENABLED == 'true'
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: backend-integration
|
||||
name: backend-integration
|
||||
|
||||
unit-test-nodes:
|
||||
name: Nodes Unit Tests
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
env:
|
||||
COVERAGE_ENABLED: ${{ inputs.collectCoverage }} # Coverage collected when true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
node-version: ${{ inputs.nodeVersion }}
|
||||
|
||||
- name: Test Nodes
|
||||
run: pnpm turbo test --filter=n8n-nodes-base --summarize
|
||||
|
||||
- name: Send Test Stats
|
||||
if: ${{ !cancelled() }}
|
||||
run: node .github/scripts/send-build-stats.mjs || true
|
||||
env:
|
||||
BUILD_STATS_WEBHOOK_URL: ${{ secrets.BUILD_STATS_WEBHOOK_URL }}
|
||||
BUILD_STATS_WEBHOOK_USER: ${{ secrets.BUILD_STATS_WEBHOOK_USER }}
|
||||
BUILD_STATS_WEBHOOK_PASSWORD: ${{ secrets.BUILD_STATS_WEBHOOK_PASSWORD }}
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
name: nodes-unit
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: env.COVERAGE_ENABLED == 'true'
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: nodes-unit
|
||||
name: nodes-unit
|
||||
|
||||
unit-test-frontend:
|
||||
name: Frontend (${{ matrix.shard }}/2)
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
env:
|
||||
COVERAGE_ENABLED: ${{ inputs.collectCoverage }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
node-version: ${{ inputs.nodeVersion }}
|
||||
|
||||
- name: Test
|
||||
run: pnpm test:ci:frontend --summarize -- --shard=${{ matrix.shard }}/2
|
||||
env:
|
||||
VITEST_SHARD: ${{ matrix.shard }}/2
|
||||
|
||||
- name: Send Test Stats
|
||||
if: ${{ !cancelled() }}
|
||||
run: node .github/scripts/send-build-stats.mjs || true
|
||||
env:
|
||||
BUILD_STATS_WEBHOOK_URL: ${{ secrets.BUILD_STATS_WEBHOOK_URL }}
|
||||
BUILD_STATS_WEBHOOK_USER: ${{ secrets.BUILD_STATS_WEBHOOK_USER }}
|
||||
BUILD_STATS_WEBHOOK_PASSWORD: ${{ secrets.BUILD_STATS_WEBHOOK_PASSWORD }}
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
name: frontend-shard-${{ matrix.shard }}
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: env.COVERAGE_ENABLED == 'true'
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: frontend
|
||||
name: frontend-shard-${{ matrix.shard }}
|
||||
|
||||
unit-test:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unit-test-backend, integration-test-backend, unit-test-nodes, unit-test-frontend]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Fail if tests failed
|
||||
if: needs.unit-test-backend.result == 'failure' || needs.integration-test-backend.result == 'failure' || needs.unit-test-nodes.result == 'failure' || needs.unit-test-frontend.result == 'failure'
|
||||
run: exit 1
|
||||
37
.github/workflows/test-visual-chromatic.yml
vendored
Normal file
37
.github/workflows/test-visual-chromatic.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: 'Test: Visual Chromatic'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: GitHub ref to check out.
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
chromatic:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: pnpm run build --filter=@n8n/utils --filter=@n8n/vitest-config --filter=@n8n/chat --filter=@n8n/design-system
|
||||
|
||||
- name: Publish to Chromatic
|
||||
uses: chromaui/action@1cfa065cbdab28f6ca3afaeb3d761383076a35aa # v11
|
||||
id: chromatic_tests
|
||||
with:
|
||||
workingDir: packages/frontend/@n8n/storybook
|
||||
buildScriptName: build
|
||||
autoAcceptChanges: 'master'
|
||||
skip: 'release/**'
|
||||
onlyChanged: true
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
exitZeroOnChanges: false
|
||||
54
.github/workflows/test-visual-storybook.yml
vendored
Normal file
54
.github/workflows/test-visual-storybook.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: 'Test: Visual Storybook'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/frontend/@n8n/storybook/**'
|
||||
- 'packages/frontend/@n8n/chat/**'
|
||||
- 'packages/frontend/@n8n/design-system/**'
|
||||
- '.github/workflows/test-visual-storybook.yml'
|
||||
concurrency:
|
||||
group: storybook-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cloudflare:
|
||||
name: Cloudflare Pages
|
||||
if: |
|
||||
!contains(github.event.pull_request.labels.*.name, 'community')
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: pnpm run build --filter=@n8n/utils --filter=@n8n/vitest-config --filter=@n8n/design-system
|
||||
|
||||
- name: Build
|
||||
working-directory: packages/frontend/@n8n/storybook
|
||||
run: |
|
||||
pnpm build
|
||||
|
||||
- name: Setup Wrangler Pre-requisites
|
||||
run: |
|
||||
echo "ignore-workspace-root-check=true" >> .npmrc
|
||||
pnpm add --global wrangler
|
||||
|
||||
- name: Deploy
|
||||
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
|
||||
id: cloudflare_deployment
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy packages/frontend/@n8n/storybook/storybook-static --project-name=storybook --branch=${{github.ref_name}} --commit-hash=${{ github.sha }}
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
55
.github/workflows/test-workflows-callable.yml
vendored
Normal file
55
.github/workflows/test-workflows-callable.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: 'Test: Workflows'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
git_ref:
|
||||
description: 'The Git ref (branch, tag, or SHA) to checkout and test.'
|
||||
required: true
|
||||
type: string
|
||||
compare_schemas:
|
||||
description: 'Set to "true" to enable schema comparison during tests.'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: string
|
||||
secrets:
|
||||
ENCRYPTION_KEY:
|
||||
description: 'Encryption key for n8n operations.'
|
||||
required: true
|
||||
CURRENTS_RECORD_KEY:
|
||||
description: 'Currents record key for uploading test results.'
|
||||
required: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=3072
|
||||
|
||||
jobs:
|
||||
run_workflow_tests:
|
||||
name: Run Workflow Tests with Snapshots
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.git_ref }}
|
||||
|
||||
- name: Set up Environment
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
||||
- name: Set up Workflow Tests
|
||||
run: pnpm --filter=n8n-playwright test:workflows:setup
|
||||
env:
|
||||
N8N_ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
|
||||
|
||||
- name: Run Workflow Tests
|
||||
run: pnpm --filter=n8n-playwright test:workflows --workers 4
|
||||
env:
|
||||
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
|
||||
CURRENTS_PROJECT_ID: 'mpLFH9'
|
||||
|
||||
- name: Run Workflow Schema Tests
|
||||
if: ${{ inputs.compare_schemas == 'true' }}
|
||||
run: pnpm --filter=n8n-playwright test:workflows:schema
|
||||
env:
|
||||
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
|
||||
CURRENTS_PROJECT_ID: 'mpLFH9'
|
||||
20
.github/workflows/test-workflows-nightly.yml
vendored
Normal file
20
.github/workflows/test-workflows-nightly.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: 'Test: Workflows Nightly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
git_ref_to_test:
|
||||
description: 'The Git ref (branch, tag, or SHA) to run tests against.'
|
||||
required: true
|
||||
type: string
|
||||
default: 'master'
|
||||
|
||||
jobs:
|
||||
run_workflow_tests:
|
||||
name: Run Workflow Tests
|
||||
uses: ./.github/workflows/test-workflows-callable.yml
|
||||
with:
|
||||
git_ref: ${{ github.event_name == 'schedule' && 'master' || github.event.inputs.git_ref_to_test }}
|
||||
secrets: inherit
|
||||
110
.github/workflows/test-workflows-pr-comment.yml
vendored
Normal file
110
.github/workflows/test-workflows-pr-comment.yml
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
name: 'Test: Workflows PR Comment'
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
handle_comment_command:
|
||||
name: Handle /test-workflows Command
|
||||
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/test-workflows')
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
permission_granted: ${{ steps.pr_check_and_details.outputs.permission_granted }}
|
||||
git_ref: ${{ steps.pr_check_and_details.outputs.head_sha }}
|
||||
pr_number: ${{ steps.pr_check_and_details.outputs.pr_number_string }}
|
||||
|
||||
steps:
|
||||
- name: Validate User, Get PR Details, and React
|
||||
id: pr_check_and_details
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const commenter = context.actor;
|
||||
const issueOwner = context.repo.owner;
|
||||
const issueRepo = context.repo.repo;
|
||||
const commentId = context.payload.comment.id;
|
||||
const prNumber = context.issue.number; // In issue_comment on a PR, issue.number is the PR number
|
||||
|
||||
// Function to add a reaction to the comment
|
||||
async function addReaction(content) {
|
||||
try {
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: issueOwner,
|
||||
repo: issueRepo,
|
||||
comment_id: commentId,
|
||||
content: content
|
||||
});
|
||||
} catch (reactionError) {
|
||||
// Log if reaction fails but don't fail the script for this
|
||||
console.log(`Failed to add reaction '${content}': ${reactionError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize outputs to a non-triggering state
|
||||
core.setOutput('permission_granted', 'false');
|
||||
core.setOutput('head_sha', '');
|
||||
core.setOutput('pr_number_string', '');
|
||||
|
||||
// 1. Check user permissions
|
||||
try {
|
||||
const { data: permissions } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: issueOwner,
|
||||
repo: issueRepo,
|
||||
username: commenter
|
||||
});
|
||||
|
||||
const allowedPermissions = ['admin', 'write', 'maintain'];
|
||||
if (!allowedPermissions.includes(permissions.permission)) {
|
||||
console.log(`User @${commenter} has '${permissions.permission}' permission. Needs 'admin', 'write', or 'maintain'.`);
|
||||
await addReaction('-1'); // User does not have permission
|
||||
return; // Exit script, tests will not be triggered
|
||||
}
|
||||
console.log(`User @${commenter} has '${permissions.permission}' permission.`);
|
||||
} catch (error) {
|
||||
console.log(`Could not verify permissions for @${commenter}: ${error.message}`);
|
||||
await addReaction('confused'); // Error checking permissions
|
||||
return; // Exit script
|
||||
}
|
||||
|
||||
// 2. Fetch PR details (if permission check passed)
|
||||
let headSha;
|
||||
try {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: issueOwner,
|
||||
repo: issueRepo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
headSha = pr.head.sha;
|
||||
console.log(`Workspaced PR details: SHA - ${headSha}, PR Number - ${prNumber}`);
|
||||
|
||||
// Set outputs for the next job
|
||||
core.setOutput('permission_granted', 'true');
|
||||
core.setOutput('head_sha', headSha);
|
||||
core.setOutput('pr_number_string', prNumber.toString());
|
||||
await addReaction('+1'); // Command accepted, tests will be triggered
|
||||
|
||||
} catch (error) {
|
||||
console.log(`Failed to fetch PR details for PR #${prNumber}: ${error.message}`);
|
||||
core.setOutput('permission_granted', 'false'); // Ensure this is false if PR fetch fails
|
||||
await addReaction('confused'); // Error fetching PR details
|
||||
}
|
||||
|
||||
trigger_reusable_tests:
|
||||
name: Trigger Reusable Test Workflow
|
||||
needs: handle_comment_command
|
||||
|
||||
if: >
|
||||
always() &&
|
||||
needs.handle_comment_command.result != 'skipped' &&
|
||||
needs.handle_comment_command.outputs.permission_granted == 'true' &&
|
||||
needs.handle_comment_command.outputs.git_ref != ''
|
||||
uses: ./.github/workflows/test-workflows-callable.yml
|
||||
with:
|
||||
git_ref: ${{ needs.handle_comment_command.outputs.git_ref }}
|
||||
secrets: inherit
|
||||
37
.github/workflows/util-check-docs-urls.yml
vendored
Normal file
37
.github/workflows/util-check-docs-urls.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: 'Util: Check Docs URLs'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-docs-urls:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: turbo build --filter=*nodes*
|
||||
|
||||
- run: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
- name: Test URLs
|
||||
run: node .github/scripts/validate-docs-links.js
|
||||
|
||||
- name: Notify Slack on failure
|
||||
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
||||
if: failure()
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
channel: '#alerts-build'
|
||||
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
message: |
|
||||
<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}| Documentation URLs check failed >
|
||||
194
.github/workflows/util-claude-task.yml
vendored
Normal file
194
.github/workflows/util-claude-task.yml
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
name: 'Util: Claude Task Runner'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
task:
|
||||
description: 'Task description - what should Claude do?'
|
||||
required: true
|
||||
type: string
|
||||
resumeUrl:
|
||||
description: 'Optional callback URL to call with the result when the workflow completes'
|
||||
required: false
|
||||
type: string
|
||||
model:
|
||||
description: 'Claude model to use (e.g., claude-opus-4-6, claude-sonnet-4-5, claude-haiku-4-5)'
|
||||
required: false
|
||||
type: string
|
||||
default: 'claude-sonnet-4-5-20250929'
|
||||
use_raw_prompt:
|
||||
description: 'Pass the task input directly as the prompt without wrapping it with guidelines and instructions'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
max_turns:
|
||||
description: 'Maximum conversation turns before Claude stops gracefully'
|
||||
required: false
|
||||
type: number
|
||||
default: 50
|
||||
suppress_output:
|
||||
description: 'Suppress console output and job summary'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
ref:
|
||||
description: 'Git ref (branch/tag/SHA) to check out and work on'
|
||||
required: false
|
||||
type: string
|
||||
default: 'master'
|
||||
|
||||
jobs:
|
||||
run-claude-task:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
with:
|
||||
app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
|
||||
private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 1
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Configure git remote with token
|
||||
run: git remote set-url origin "https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/${{ github.repository }}.git"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
|
||||
- name: Create working branch
|
||||
run: |
|
||||
BRANCH_NAME="claude/task-${{ github.run_id }}-${{ github.run_attempt }}"
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_ENV"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
|
||||
- name: Configure git author
|
||||
run: |
|
||||
git config user.name "${{ github.actor }}"
|
||||
git config user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
|
||||
echo "Git author configured as: ${{ github.actor }}"
|
||||
|
||||
- name: Prepare Claude prompt
|
||||
env:
|
||||
INPUT_TASK: ${{ inputs.task }}
|
||||
USE_RAW_PROMPT: ${{ inputs.use_raw_prompt }}
|
||||
run: node .github/scripts/claude-task/prepare-claude-prompt.mjs
|
||||
|
||||
- name: Create MCP config
|
||||
run: |
|
||||
cat > /tmp/mcp-config.json << 'MCPEOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"linear": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"mcp-remote",
|
||||
"https://mcp.linear.app/sse",
|
||||
"--header",
|
||||
"Authorization:${AUTH_HEADER}"
|
||||
],
|
||||
"env": {
|
||||
"AUTH_HEADER": "Bearer MCP_LINEAR_API_KEY_PLACEHOLDER"
|
||||
}
|
||||
},
|
||||
"notion": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@notionhq/notion-mcp-server"],
|
||||
"env": {
|
||||
"OPENAPI_MCP_HEADERS": "{\"Authorization\":\"Bearer MCP_NOTION_TOKEN_PLACEHOLDER\",\"Notion-Version\":\"2025-02-19\"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MCPEOF
|
||||
sed -i "s|MCP_LINEAR_API_KEY_PLACEHOLDER|${{ secrets.MCP_LINEAR_API_KEY }}|g" /tmp/mcp-config.json
|
||||
sed -i "s|MCP_NOTION_TOKEN_PLACEHOLDER|${{ secrets.MCP_NOTION_TOKEN }}|g" /tmp/mcp-config.json
|
||||
|
||||
- name: Run Claude
|
||||
id: claude
|
||||
continue-on-error: true
|
||||
uses: anthropics/claude-code-action@d668cc452deb931732f24c6b1bbf24d97bc0c586 # v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ steps.generate_token.outputs.token }}
|
||||
prompt: ${{ env.CLAUDE_PROMPT }}
|
||||
show_full_output: ${{ inputs.suppress_output != true }}
|
||||
display_report: false
|
||||
settings: |
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash",
|
||||
"Read",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"WebFetch",
|
||||
"WebSearch",
|
||||
"TodoWrite",
|
||||
"Skill",
|
||||
"Task",
|
||||
"mcp__linear__*",
|
||||
"mcp__notion__*"
|
||||
]
|
||||
}
|
||||
}
|
||||
claude_args: |
|
||||
--model ${{ inputs.model }} --max-turns ${{ inputs.max_turns }} --mcp-config /tmp/mcp-config.json
|
||||
|
||||
- name: Push branch
|
||||
if: always()
|
||||
run: |
|
||||
if ! git diff --quiet "${{ inputs.ref }}" 2>/dev/null; then
|
||||
git push -u origin "$BRANCH_NAME"
|
||||
echo "::notice::Changes pushed to branch $BRANCH_NAME"
|
||||
else
|
||||
echo "::notice::No changes to push."
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
env:
|
||||
CLAUDE_OUTCOME: ${{ steps.claude.outcome }}
|
||||
CLAUDE_SESSION_ID: ${{ steps.claude.outputs.session_id }}
|
||||
run: |
|
||||
{
|
||||
echo "## Claude Task Runner"
|
||||
echo "**Branch:** ${BRANCH_NAME:-N/A}"
|
||||
echo "**Claude outcome:** $CLAUDE_OUTCOME"
|
||||
if [ -n "$CLAUDE_SESSION_ID" ]; then
|
||||
echo "**Session ID:** \`$CLAUDE_SESSION_ID\`"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Call resume url
|
||||
if: always() && inputs.resumeUrl != ''
|
||||
env:
|
||||
RESUME_URL: ${{ inputs.resumeUrl }}
|
||||
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
|
||||
CLAUDE_OUTCOME: ${{ steps.claude.outcome }}
|
||||
CLAUDE_SESSION_ID: ${{ steps.claude.outputs.session_id }}
|
||||
run: node .github/scripts/claude-task/resume-callback.mjs
|
||||
|
||||
- name: Check final status
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.claude.outcome }}" = "failure" ]; then
|
||||
echo "::error::Claude task failed. Check the 'Run Claude' step logs for details."
|
||||
exit 1
|
||||
fi
|
||||
48
.github/workflows/util-claude.yml
vendored
Normal file
48
.github/workflows/util-claude.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: 'Util: Claude'
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude-code-action:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@d668cc452deb931732f24c6b1bbf24d97bc0c586 # v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Or use OAuth token instead:
|
||||
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
timeout_minutes: '60'
|
||||
# mode: tag # Default: responds to @claude mentions
|
||||
# Optional: Restrict network access to specific domains only
|
||||
# experimental_allowed_domains: |
|
||||
# .anthropic.com
|
||||
# .github.com
|
||||
# api.github.com
|
||||
# .githubusercontent.com
|
||||
# bun.sh
|
||||
# registry.npmjs.org
|
||||
# .blob.core.windows.net
|
||||
40
.github/workflows/util-cleanup-abandoned-release-branches.yml
vendored
Normal file
40
.github/workflows/util-cleanup-abandoned-release-branches.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: 'Util: Cleanup abandoned release branches'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
delete-release-branch:
|
||||
# Only if PR was closed without merge
|
||||
if: >
|
||||
github.event.pull_request.merged == false
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
|
||||
private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
install-command: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
- name: Cleanup release branch if PR qualifies
|
||||
run: node .github/scripts/cleanup-release-branch.mjs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
GITHUB_EVENT_PATH: ${{ github.event_path }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
27
.github/workflows/util-cleanup-pr-images.yml
vendored
Normal file
27
.github/workflows/util-cleanup-pr-images.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: 'Util: Cleanup CI Docker Images'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Daily cleanup at 3 AM UTC
|
||||
- cron: '0 3 * * *'
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: 'Delete stale CI images'
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Delete stale CI images
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GHCR_ORG: ${{ github.repository_owner }}
|
||||
GHCR_REPO: ${{ github.event.repository.name }}
|
||||
run: node .github/scripts/cleanup-ghcr-images.mjs --stale 1
|
||||
84
.github/workflows/util-data-tooling.yml
vendored
Normal file
84
.github/workflows/util-data-tooling.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: 'Util: Data Tooling'
|
||||
|
||||
# TODO: Uncomment this after it works on a manual invocation
|
||||
# on:
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - '**'
|
||||
# - '!release/*'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sqlite-export-sqlite-import:
|
||||
name: sqlite database export -> sqlite database import
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
outputs:
|
||||
db_changed: ${{ steps.paths-filter.outputs.db == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
# with:
|
||||
# ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- name: Check for frontend changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: paths-filter
|
||||
with:
|
||||
filters: |
|
||||
db:
|
||||
- packages/@n8n/db/**
|
||||
- packages/cli/**
|
||||
- name: Setup, build and export sqlite
|
||||
if: steps.paths-filter.outputs.db == 'true'
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: |
|
||||
pnpm build
|
||||
./packages/cli/bin/n8n export:entities --outputDir packages/cli/commands/export/outputs
|
||||
./packages/cli/bin/n8n import:entities --inputDir packages/cli/commands/export/outputs --truncateTables
|
||||
postgres-export-postgres-import:
|
||||
name: postgres export -> postgres import
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
outputs:
|
||||
db_changed: ${{ steps.paths-filter.outputs.db == 'true' }}
|
||||
env:
|
||||
DB_TYPE: postgresdb
|
||||
DB_POSTGRESDB_DATABASE: n8n
|
||||
DB_POSTGRESDB_HOST: localhost
|
||||
DB_POSTGRESDB_PORT: 5432
|
||||
DB_POSTGRESDB_USER: postgres
|
||||
DB_POSTGRESDB_PASSWORD: password
|
||||
DB_POSTGRESDB_POOL_SIZE: 1 # Detect connection pooling deadlocks
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
# with:
|
||||
# ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- name: Check for db changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: paths-filter
|
||||
with:
|
||||
filters: |
|
||||
db:
|
||||
- packages/@n8n/db/**
|
||||
- packages/cli/**
|
||||
|
||||
- name: Setup and Build
|
||||
if: steps.paths-filter.outputs.db == 'true'
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
||||
- name: Start Postgres
|
||||
if: steps.paths-filter.outputs.db == 'true'
|
||||
uses: isbang/compose-action@802a148945af6399a338c7906c267331b39a71af # v2.0.0
|
||||
with:
|
||||
compose-file: ./.github/docker-compose.yml
|
||||
services: |
|
||||
postgres
|
||||
|
||||
- name: Export postgres
|
||||
if: steps.paths-filter.outputs.db == 'true'
|
||||
run: ./packages/cli/bin/n8n export:entities --outputDir packages/cli/commands/export/outputs
|
||||
- name: Import postgres
|
||||
if: steps.paths-filter.outputs.db == 'true'
|
||||
run: ./packages/cli/bin/n8n import:entities --inputDir packages/cli/commands/export/outputs --truncateTables
|
||||
45
.github/workflows/util-determine-current-version.yml
vendored
Normal file
45
.github/workflows/util-determine-current-version.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: 'Util: Determine current versions'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
outputs:
|
||||
stable:
|
||||
description: 'Stable release version'
|
||||
value: ${{ jobs.get-versions.outputs.stable }}
|
||||
beta:
|
||||
description: 'Beta release version'
|
||||
value: ${{ jobs.get-versions.outputs.beta }}
|
||||
v1:
|
||||
description: 'v1 release version'
|
||||
value: ${{ jobs.get-versions.outputs.v1 }}
|
||||
|
||||
jobs:
|
||||
get-versions:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
stable: ${{ steps.get-tags.outputs.stable }}
|
||||
beta: ${{ steps.get-tags.outputs.beta }}
|
||||
v1: ${{ steps.get-tags.outputs.v1 }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
install-command: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
- name: Extract release versions
|
||||
id: get-tags
|
||||
run: node ./.github/scripts/get-release-versions.mjs
|
||||
|
||||
- name: Print detected versions
|
||||
run: |
|
||||
echo "Stable: ${{ steps.get-tags.outputs.stable }}"
|
||||
echo "Beta: ${{ steps.get-tags.outputs.beta }}"
|
||||
echo "v1: ${{ steps.get-tags.outputs.v1 }}"
|
||||
27
.github/workflows/util-ensure-release-candidate-branches.yml
vendored
Normal file
27
.github/workflows/util-ensure-release-candidate-branches.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: 'Util: Ensure release candidate branches'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
ensure-release-candidate-branches:
|
||||
name: Ensure release-candidate branches
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
install-command: npm install --prefix=.github/scripts --no-package-lock
|
||||
build-command: ''
|
||||
|
||||
- name: Ensure release-candidate branches
|
||||
run: node ./.github/scripts/ensure-release-candidate-branches.mjs
|
||||
29
.github/workflows/util-notify-pr-status.yml
vendored
Normal file
29
.github/workflows/util-notify-pr-status.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: 'Util: Notify PR Status'
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
(github.event_name == 'pull_request_review' && github.event.review.state == 'approved') ||
|
||||
(github.event_name == 'pull_request_review' && github.event.review.state == 'dismissed') ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true) ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == false && github.event.action == 'closed')
|
||||
env:
|
||||
NOTIFY_URL: ${{ secrets.N8N_NOTIFY_PR_STATUS_CHANGED_URL }}
|
||||
steps:
|
||||
- uses: fjogeleit/http-request-action@bf78da14118941f7e940279dd58f67e863cbeff6 # v1
|
||||
if: ${{ env.NOTIFY_URL != '' && !contains(github.event.pull_request.labels.*.name, 'community') }}
|
||||
name: Notify
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
with:
|
||||
url: ${{ secrets.N8N_NOTIFY_PR_STATUS_CHANGED_URL }}
|
||||
method: 'POST'
|
||||
customHeaders: '{ "x-api-token": "${{ secrets.N8N_NOTIFY_PR_STATUS_CHANGED_TOKEN }}" }'
|
||||
data: '{ "event_name": "${{ github.event_name }}", "pr_url": "${{ env.PR_URL }}", "event": ${{ toJSON(github.event) }} }'
|
||||
155
.github/workflows/util-sync-api-docs.yml
vendored
Normal file
155
.github/workflows/util-sync-api-docs.yml
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
name: 'Util: Sync API Docs'
|
||||
|
||||
on:
|
||||
# Triggers for the master branch if relevant Public API files have changed
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
# Trigger if:
|
||||
# - any of the public API files change
|
||||
- 'packages/cli/src/public-api/**/*.{css,yaml,yml}'
|
||||
# - the build script or dependencies change
|
||||
- 'packages/cli/package.json'
|
||||
# - any main dependencies change
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
# Allow manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync-public-api:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # Needed for `gh` to read the workflow history
|
||||
|
||||
steps:
|
||||
- name: Checkout Main n8n Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for git log
|
||||
|
||||
- name: Setup Node.js and install dependencies
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
|
||||
- name: Build Public API Schema
|
||||
run: pnpm run build:data
|
||||
working-directory: ./packages/cli
|
||||
|
||||
- name: Verify OpenAPI schema exists
|
||||
id: verify_file
|
||||
run: |
|
||||
if [[ -f "packages/cli/dist/public-api/v1/openapi.yml" ]]; then
|
||||
echo "OpenAPI file found: packages/cli/dist/public-api/v1/openapi.yml"
|
||||
echo "file_exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ERROR: OpenAPI file not found at packages/cli/dist/public-api/v1/openapi.yml after build."
|
||||
echo "file_exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
if: steps.verify_file.outputs.file_exists == 'true'
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
with:
|
||||
app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
|
||||
private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: n8n-docs
|
||||
|
||||
- name: Checkout Docs Repository
|
||||
if: steps.verify_file.outputs.file_exists == 'true'
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: n8n-io/n8n-docs
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
path: public-docs
|
||||
|
||||
- name: Copy OpenAPI file to Docs Repo
|
||||
if: steps.verify_file.outputs.file_exists == 'true'
|
||||
run: |
|
||||
# Destination path within the 'public-docs' checkout directory
|
||||
DOCS_TARGET_PATH="public-docs/docs/api/v1/openapi.yml"
|
||||
|
||||
echo "Copying 'packages/cli/dist/public-api/v1/openapi.yml' to '${DOCS_TARGET_PATH}'"
|
||||
cp packages/cli/dist/public-api/v1/openapi.yml "${DOCS_TARGET_PATH}"
|
||||
|
||||
- name: Find Relevant Source Commits
|
||||
id: find_source_commits
|
||||
if: steps.verify_file.outputs.file_exists == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
echo "Finding last successful workflow run..."
|
||||
LAST_SUCCESS_SHA=$(gh run list \
|
||||
--workflow "${{ github.workflow }}" \
|
||||
--branch "${{ github.ref_name }}" \
|
||||
--status "success" \
|
||||
--json "headSha" \
|
||||
--jq '.[0].headSha // empty' \
|
||||
-L 1)
|
||||
|
||||
RELEVANT_COMMITS=""
|
||||
if [[ -n "$LAST_SUCCESS_SHA" ]]; then
|
||||
echo "Last successful commit: $LAST_SUCCESS_SHA"
|
||||
echo "Current commit: ${{ github.sha }}"
|
||||
|
||||
# Find all commits between the last success and now that touched the relevant files
|
||||
# NOTE: The pathspecs here mirror the workflow's 'paths' trigger for precision
|
||||
RELEVANT_COMMITS=$(git log --pretty=format:"- [%h](https://github.com/${{ github.repository }}/commit/%H) %s" "${LAST_SUCCESS_SHA}..${{ github.sha }}" -- \
|
||||
'packages/cli/src/public-api/**/*.css' \
|
||||
'packages/cli/src/public-api/**/*.yaml' \
|
||||
'packages/cli/src/public-api/**/*.yml')
|
||||
fi
|
||||
|
||||
if [[ -z "$RELEVANT_COMMITS" ]]; then
|
||||
if [[ -z "$LAST_SUCCESS_SHA" ]]; then
|
||||
echo "No previous successful run found. Using current commit as source."
|
||||
else
|
||||
echo "No commits touching source files found. Linking to trigger commit (likely a dependency update)."
|
||||
fi
|
||||
FULL_SHA="${{ github.sha }}"
|
||||
SHORT_SHA="${FULL_SHA:0:7}"
|
||||
SOURCE_LINK="Source commit: [$SHORT_SHA](https://github.com/${{ github.repository }}/commit/$FULL_SHA)"
|
||||
else
|
||||
echo "Found relevant source commits:"
|
||||
echo "$RELEVANT_COMMITS"
|
||||
# Build the string manually to avoid heredoc capturing leading whitespace from YAML
|
||||
SOURCE_LINK="Source commit(s):"$'\n'"$RELEVANT_COMMITS"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "source_link<<EOF"
|
||||
echo "$SOURCE_LINK"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create PR in Docs Repo
|
||||
if: steps.verify_file.outputs.file_exists == 'true'
|
||||
|
||||
# Pin v7.0.8
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
path: public-docs
|
||||
commit-message: 'feat(public-api): Update Public API schema'
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
|
||||
# Create a single branch for multiple PRs
|
||||
branch: 'chore/sync-public-api-schema'
|
||||
delete-branch: false
|
||||
|
||||
title: 'chore: Update Public API schema'
|
||||
body: |
|
||||
Automated update of the Public API OpenAPI YAML schema.
|
||||
|
||||
This PR was generated by a GitHub Action in the [${{ github.repository }} repository](https://github.com/${{ github.repository }}).
|
||||
${{ steps.find_source_commits.outputs.source_link }}
|
||||
|
||||
Please review the changes and merge if appropriate.
|
||||
67
.github/workflows/util-update-node-popularity.yml
vendored
Normal file
67
.github/workflows/util-update-node-popularity.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: 'Util: Update Node Popularity'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every Monday at 00:00 UTC
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch: # Allow manual trigger for testing
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-popularity:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'schedule' && github.repository == 'n8n-io/n8n')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Node.js and Dependencies
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: '' # Skip build, we only need to fetch data
|
||||
|
||||
- name: Fetch node popularity data
|
||||
run: |
|
||||
cd packages/frontend/editor-ui
|
||||
node scripts/fetch-node-popularity.mjs
|
||||
env:
|
||||
N8N_FAIL_ON_POPULARITY_FETCH_ERROR: 'false' # Don't fail if API is down
|
||||
|
||||
- name: Format generated file
|
||||
run: pnpm biome format --write packages/frontend/editor-ui/data/node-popularity.json
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
if git diff --quiet packages/frontend/editor-ui/data/node-popularity.json; then
|
||||
echo "No changes to popularity data"
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Popularity data has changed"
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check-changes.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
branch-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: Update node popularity data'
|
||||
title: 'chore: Update node popularity data'
|
||||
body: |
|
||||
This automated PR updates the node popularity data used for sorting nodes in the node creator panel.
|
||||
|
||||
The data is fetched weekly from the n8n telemetry endpoint to reflect current usage patterns.
|
||||
|
||||
_Generated by the weekly node popularity update workflow._
|
||||
branch: update-node-popularity
|
||||
base: master
|
||||
delete-branch: true
|
||||
reviewers: Matsuuu
|
||||
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
Reference in New Issue
Block a user