n8n-io/n8n
70 workflows · maturity 83% · 15 patterns · GitHub ↗
Practices
✓ Matrix✓ Permissions✓ Security scan✓ AI review○ Cache✓ Concurrency✓ Reusable workflows
Detected patterns
Security dimensions
Tools: aquasecurity/trivy-action, github/codeql-action/upload-sarif
Workflows (70)
backport perms .github/workflows/backport.yml
View raw YAML
name: 'Util: Backport pull request changes'
run-name: Backport pull request ${{ github.event.pull_request.number || inputs.pull-request-id }}
on:
pull_request:
types: [closed]
workflow_dispatch:
inputs:
pull-request-id:
description: 'The ID number of the pull request (e.g. 3342). No #, no extra letters.'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
backport:
if: |
github.event.pull_request.merged == true ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-slim
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 }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.generate-token.outputs.token }}
fetch-depth: 0
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- name: Compute backport targets
id: targets
env:
PULL_REQUEST_ID: ${{ inputs.pull-request-id }}
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
run: node .github/scripts/compute-backport-targets.mjs
- name: Backport
if: steps.targets.outputs.target_branches != ''
uses: korthout/backport-action@4aaf0e03a94ff0a619c9a511b61aeb42adea5b02 # v4.2.0
with:
github_token: ${{ steps.generate-token.outputs.token }}
source_pr_number: ${{ github.event.pull_request.number || inputs.pull-request-id }}
target_branches: ${{ steps.targets.outputs.target_branches }}
pull_description: |-
# Description
Backport of #${pull_number} to `${target_branch}`.
## Checklist for the author (@${pull_author}) to go through.
- [ ] Review the backport changes
- [ ] Fix possible conflicts
- [ ] Merge to target branch
After this PR has been merged, it will be picked up in the next patch release for release track.
# Original description
${pull_description}
pull_title: ${pull_title} (backport to ${target_branch})
add_author_as_assignee: true
add_author_as_reviewer: true
copy_assignees: true
copy_requested_reviewers: false
copy_labels_pattern: '^(?!Backport to\b).+' # Copy everything except backport labels
add_labels: 'automation:backport'
experimental: >
{
"conflict_resolution": "draft_commit_conflicts"
}
build-base-image matrix .github/workflows/build-base-image.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- 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@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.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
build-benchmark-image .github/workflows/build-benchmark-image.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to GitHub Container Registry
uses: ./.github/actions/docker-registry-login
- name: Build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.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
build-windows .github/workflows/build-windows.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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 }}"
}
]
}
]
}
ci-check-pr-title .github/workflows/ci-check-pr-title.yml
View raw YAML
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 }}
ci-check-release-from-fork .github/workflows/ci-check-release-from-fork.yml
View raw YAML
name: 'CI: Block fork PRs to release branches'
on:
pull_request:
branches:
- 'release/**'
types:
- opened
- reopened
- synchronize
- ready_for_review
- edited
jobs:
block-fork-prs:
runs-on: ubuntu-slim
permissions:
pull-requests: write
contents: read
steps:
- name: Check if PR is from a fork
id: check
run: |
if [ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then
echo "fork=true" >> "$GITHUB_OUTPUT"
else
echo "fork=false" >> "$GITHUB_OUTPUT"
fi
- name: Comment on PR explaining the block
if: steps.check.outputs.fork == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
});
const alreadyCommented = comments.some(
(c) => c.user.login === 'github-actions[bot]' && c.body.includes('Pull request blocked')
);
if (!alreadyCommented) {
const body = `
🚫 **Pull request blocked**
Pull requests from **forked repositories** are not allowed to target **release branches** in this repository.
**Target branch:** \`${context.payload.pull_request.base.ref}\`
If you believe this was blocked in error, contact the repository maintainers.
`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body
});
}
- name: Fail workflow if from fork
if: steps.check.outputs.fork == 'true'
run: |
echo "PR from fork targeting a release branch is not allowed."
exit 1
ci-detect-new-packages .github/workflows/ci-detect-new-packages.yml
View raw YAML
name: 'CI: Detect New Packages on Master'
on:
pull_request:
types:
- closed
branches:
- master
jobs:
detect-new-packages:
name: Check for new unpublished packages
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- name: Check for new unpublished packages
id: detect
continue-on-error: true
run: node .github/scripts/detect-new-packages.mjs
- name: Notify Slack about new packages
if: steps.detect.outcome == 'failure' && steps.detect.outputs.packages != ''
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
with:
method: chat.postMessage
token: ${{ secrets.RELEASE_HELPER_SLACK_TOKEN }}
payload: |
channel: C036AELNMV0
text: |-
:warning: *New unpublished packages detected* after merging <${{ github.event.pull_request.html_url }}|PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}>
The following packages do not exist on npm yet: `${{ steps.detect.outputs.packages }}`
*If a package is not intended for npm*, set `"private": true` in its `package.json` to exclude it from future checks.
*Otherwise, to unblock the next release:*
1. Run the <${{ github.server_url }}/${{ github.repository }}/actions/workflows/release-publish-new-package.yml|Release: Publish New Package> workflow for each package
2. Configure Trusted Publishing on npmjs.com (owner: `n8n-io`, repo: `n8n`, workflow: `release-publish.yml`)
ci-master matrix .github/workflows/ci-master.yml
View raw YAML
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 }}
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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: inherit
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 }})
ci-pull-requests .github/workflows/ci-pull-requests.yml
View raw YAML
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 }}
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_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 }}
workflow_scripts: ${{ fromJSON(steps.ci-filter.outputs.results)['workflow-scripts'] == 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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/**
workflow-scripts: .github/scripts/**
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: inherit
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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 }}
check-packaging:
name: Check packaging
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.install-and-build.outputs.commit_sha }}
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
- name: Check packaging
shell: bash
run: |
pnpm -r pack --dry-run
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
workflow-scripts:
name: Workflow scripts
needs: install-and-build
if: needs.install-and-build.outputs.workflow_scripts == 'true'
uses: ./.github/workflows/test-workflow-scripts-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' && github.event_name == 'pull_request'
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,
check-packaging,
e2e-tests,
db-tests,
performance,
security-checks,
workflow-scripts,
chromatic,
]
if: always()
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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) }}
ci-python .github/workflows/ci-python.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/@n8n/task-runner-python/coverage.xml
flags: tests
name: task-runner-python
fail_ci_if_error: false
ci-restrict-private-merges perms .github/workflows/ci-restrict-private-merges.yml
View raw YAML
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
env:
HEAD_REF: ${{ github.head_ref }}
run: |
set -euo pipefail
head="$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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
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' }}
env:
HEAD_REF: ${{ github.head_ref }}
run: |
echo "::error::You can only merge to master from a bundle/* branch. Got '$HEAD_REF'."
exit 1
- name: Allowed
if: ${{ steps.validate.outputs.allowed == 'true' }}
env:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
run: |
echo "OK: '$HEAD_REF' can merge into '$BASE_REF'"
docker-build-push matrix .github/workflows/docker-build-push.yml
View raw YAML
# 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- 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@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a
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@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a
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@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Cosign
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
- name: Login to GHCR
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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 }}
docker-build-smoke .github/workflows/docker-build-smoke.yml
View raw YAML
name: 'Docker Build Smoke Test'
# Verifies the full Docker build chain works without any caching.
# Catches native module compilation failures (e.g., isolated-vm, sqlite3)
# that layer caching can mask in the regular E2E pipeline.
#
# Full chain: pnpm install → pnpm build (no Turbo cache) →
# build base image (no Docker cache) →
# build n8n + runners images (no Docker cache)
on:
schedule:
- cron: '0 3 * * *' # 3:00 AM UTC, after the nightly Docker build at midnight
pull_request:
paths:
- 'docker/images/n8n/**'
- 'docker/images/n8n-base/**'
- 'docker/images/runners/**'
- 'scripts/build-n8n.mjs'
- 'scripts/dockerize-n8n.mjs'
workflow_dispatch:
jobs:
docker-smoke-test:
name: 'Docker Build (no cache)'
runs-on: blacksmith-4vcpu-ubuntu-2204
if: ${{ !github.event.pull_request.head.repo.fork }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to DHI Registry (for base image)
uses: ./.github/actions/docker-registry-login
with:
login-ghcr: 'false'
login-dhi: 'true'
dockerhub-username: ${{ secrets.DOCKER_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build full chain (no cache)
uses: ./.github/actions/setup-nodejs
with:
build-command: 'pnpm build:docker:clean'
enable-docker-cache: true
- name: Verify n8n image starts
run: |
docker run --rm -d --name n8n-smoke-test n8nio/n8n:local
sleep 5
docker logs n8n-smoke-test 2>&1 | tail -20
docker stop n8n-smoke-test
notify-on-failure:
name: Notify on nightly smoke test failure
runs-on: ubuntu-slim
needs: [docker-smoke-test]
if: needs.docker-smoke-test.result == 'failure' && github.event_name == 'schedule'
steps:
- uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
with:
method: chat.postMessage
token: ${{ secrets.QBOT_SLACK_TOKEN }}
payload: |
channel: C0A9RLY8Y20
text: "🚨 Nightly Docker smoke test failed (no-cache build) - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
release-create-github-releases perms .github/workflows/release-create-github-releases.yml
View raw YAML
name: 'Release: Create GitHub Releases'
run-name: 'Creating GitHub Releases for ${{ inputs.version-tag }} (${{ inputs.track }})'
on:
workflow_call:
inputs:
track:
required: true
type: string
version-tag:
required: true
type: string
body:
required: true
type: string
commit:
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).'
required: true
type: string
body:
description: 'Release notes body.'
required: true
type: string
commit:
description: 'Commitish the release points to (e.g. branch name or SHA).'
required: true
type: string
permissions:
contents: write
id-token: write
jobs:
create-github-releases:
name: Create GitHub releases
runs-on: ubuntu-slim
environment: release
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.generate_token.outputs.token }}
fetch-depth: 1
- name: Setup NodeJS
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- name: Create GitHub releases
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
RELEASE_TAG: ${{ inputs.version-tag }}
BODY: ${{ inputs.body }}
IS_PRE_RELEASE: ${{ inputs.track == 'beta' }}
MAKE_LATEST: ${{ inputs.track == 'stable' }}
COMMIT: ${{ inputs.commit }}
ADDITIONAL_TAGS: ${{ inputs.track }}
run: node ./.github/scripts/create-github-release.mjs
release-create-minor-pr .github/workflows/release-create-minor-pr.yml
View raw YAML
name: 'Release: Create Minor Release PR'
on:
workflow_dispatch:
schedule:
- cron: 0 13 * * 1 # 2pm CET (UTC+1), Monday
jobs:
create-release-pr:
name: Create release PR
uses: ./.github/workflows/release-create-pr.yml
secrets: inherit
with:
base-branch: master
release-type: minor
notify-slack:
name: Notify Slack
needs: [create-release-pr]
if: needs.create-release-pr.result == 'success' && needs.create-release-pr.outputs.pull-request-number != ''
runs-on: ubuntu-latest
steps:
- name: Post to Slack
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
with:
method: chat.postMessage
token: ${{ secrets.RELEASE_HELPER_SLACK_TOKEN }}
payload: |
channel: C036AELNMV0
text: ":rocket: Minor release PR created. <${{ github.server_url }}/${{ github.repository }}/pull/${{ needs.create-release-pr.outputs.pull-request-number }}|View PR> — close it to cancel the release."
release-create-patch-pr .github/workflows/release-create-patch-pr.yml
View raw YAML
name: 'Release: Create Patch Release PR'
run-name: 'Release: Create Patch Release PR for track ${{ inputs.track }}'
on:
workflow_call:
inputs:
track:
description: 'Release Track'
required: true
type: string
workflow_dispatch:
inputs:
track:
description: 'Release Track'
required: true
type: choice
options: [stable, beta, v1]
jobs:
determine-version-info:
name: Determine publishing track
runs-on: ubuntu-latest
outputs:
release_candidate_branch: ${{ steps.determine-branch.outputs.release_candidate_branch }}
should_update: ${{ steps.determine-branch.outputs.should_update }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- name: Determine release candidate branch from track
id: determine-branch
env:
TRACK: ${{ inputs.track }}
run: node .github/scripts/determine-release-candidate-branch-for-track.mjs
skip-release-pr:
name: Skip release PR (no new commits)
needs: [determine-version-info]
if: needs.determine-version-info.outputs.should_update != 'true'
runs-on: ubuntu-latest
steps:
- name: Log skip reason
run: echo "No new commits found between the release candidate branch and the current release tag. Skipping PR creation."
create-release-pr:
name: Create release PR
needs: [determine-version-info]
if: needs.determine-version-info.outputs.should_update == 'true'
uses: ./.github/workflows/release-create-pr.yml
secrets: inherit
with:
base-branch: ${{ needs.determine-version-info.outputs.release_candidate_branch }}
release-type: patch
notify-slack:
name: Notify Slack
needs: [create-release-pr]
if: needs.create-release-pr.result == 'success' && needs.create-release-pr.outputs.pull-request-number != ''
runs-on: ubuntu-latest
steps:
- name: Post to Slack
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
with:
method: chat.postMessage
token: ${{ secrets.RELEASE_HELPER_SLACK_TOKEN }}
payload: |
channel: C036AELNMV0
text: ":rocket: Patch release PR created for *${{ inputs.track }}* track. <${{ github.server_url }}/${{ github.repository }}/pull/${{ needs.create-release-pr.outputs.pull-request-number }}|View PR> — close it to cancel the release."
release-create-pr perms .github/workflows/release-create-pr.yml
View raw YAML
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
outputs:
pull-request-number:
description: 'Number of the created pull request'
value: ${{ jobs.create-release-pr.outputs.pull-request-number }}
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
permissions:
contents: write
pull-requests: write
jobs:
create-release-pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
timeout-minutes: 5
outputs:
pull-request-number: ${{ steps.create-pr.outputs.pull-request-number }}
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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"
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- 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@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
id: create-pr
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 }}
approve-and-automerge:
needs: [create-release-pr]
if: |
needs.create-release-pr.outputs.pull-request-number != ''
uses: ./.github/workflows/util-approve-and-set-automerge.yml
secrets: inherit
with:
pull-request-number: ${{ needs.create-release-pr.outputs.pull-request-number }}
release-merge-tag-to-branch .github/workflows/release-merge-tag-to-branch.yml
View raw YAML
name: 'Release: Merge tag to branch'
run-name: 'Merge n8n@${{ inputs.version }} to ${{ inputs.target-branch }}'
on:
workflow_call:
inputs:
version:
description: 'The release version (e.g. 2.10.2)'
required: true
type: string
target-branch:
description: 'The branch to merge the release tag into (e.g. master or release-candidate/2.10.x)'
required: true
type: string
jobs:
merge-tag-to-branch:
name: Merge release tag to ${{ inputs.target-branch }}
runs-on: ubuntu-latest
environment: minor-release-tag-merge
env:
VERSION: ${{ inputs.version }}
TARGET_BRANCH: ${{ inputs.target-branch }}
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.target-branch }}
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 branch
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 ${{ inputs.target-branch }}
run: git push origin "HEAD:${TARGET_BRANCH}"
- 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 ${{ inputs.target-branch }} failed for n8n@${{ inputs.version }} >
release-populate-cloud-with-releases .github/workflows/release-populate-cloud-with-releases.yml
View raw YAML
name: 'Release: Populate cloud with releases'
run-name: 'Populate cloud with version n8n@${{ inputs.version }}'
on:
workflow_dispatch:
inputs:
previous-version:
description: 'The previous release version (e.g. 2.10.2)'
required: true
type: string
version:
description: 'The release version (e.g. 2.11.0)'
required: true
type: string
experimental:
description: 'If publishing experimental version'
type: boolean
default: false
workflow_call:
inputs:
previous-version:
description: 'The previous release version (e.g. 2.10.2)'
required: true
type: string
version:
description: 'The release version (e.g. 2.11.0)'
required: true
type: string
experimental:
description: 'If publishing experimental version'
type: boolean
default: false
jobs:
determine-changes:
runs-on: ubuntu-slim
environment: release
outputs:
has_node_enhancements: ${{ steps.get-changes.outputs.has_node_enhancements }}
has_core_changes: ${{ steps.get-changes.outputs.has_core_changes }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- name: Extract changes
id: get-changes
env:
PREVIOUS_VERSION_TAG: 'n8n@${{ inputs.previous-version }}'
RELEASE_VERSION_TAG: 'n8n@${{ inputs.version }}'
run: node ./.github/scripts/determine-release-version-changes.mjs
- name: Populate databases
id: populate-databases
env:
N8N_POPULATE_CLOUD_WEBHOOK_DATA: ${{ secrets.N8N_POPULATE_CLOUD_WEBHOOK_DATA }}
PAYLOAD: |
{
"target_version_to_update": "${{ inputs.version }}",
"has_node_enhancements": ${{ steps.get-changes.outputs.has_node_enhancements }},
"has_core_changes": ${{ steps.get-changes.outputs.has_core_changes }},
"has_breaking_change": false,
"is_experimental": ${{ inputs.experimental }}
}
run: node ./.github/scripts/populate-cloud-databases.mjs
release-promote-github-release perms .github/workflows/release-promote-github-release.yml
View raw YAML
name: 'Release: Promote GitHub Releases'
run-name: 'Promoting GitHub Release ${{ inputs.version-tag }} to latest'
on:
workflow_call:
inputs:
version-tag:
required: true
type: string
workflow_dispatch:
inputs:
version-tag:
description: 'Version tag (e.g. n8n@2.7.0).'
required: true
type: string
permissions:
contents: write
jobs:
promote-github-releases:
name: Promote GitHub release to latest
runs-on: ubuntu-slim
environment: release
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.generate_token.outputs.token }}
fetch-depth: 1
- name: Setup NodeJS
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- name: Promote GitHub releases
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
RELEASE_TAG: ${{ inputs.version-tag }}
run: node ./.github/scripts/promote-github-release.mjs
release-publish .github/workflows/release-publish.yml
View raw YAML
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 }}
previous_version: ${{ steps.determine-info.outputs.previous_version }}
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 }}
rc_branch: ${{ steps.determine-info.outputs.rc_branch }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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
env:
PUBLISH_BRANCH: ${{ github.event.pull_request.base.ref }}
PUBLISH_TAG: ${{ needs.determine-version-info.outputs.track == 'stable' && 'latest' || needs.determine-version-info.outputs.track }}
run: |
# Prefix version-like track names (e.g. "1", "v1") to avoid npm rejecting them as semver ranges
if [[ "$PUBLISH_TAG" =~ ^v?[0-9] ]]; then
PUBLISH_TAG="release-${PUBLISH_TAG}"
fi
pnpm publish -r --filter '!n8n' --publish-branch "$PUBLISH_BRANCH" --access public --tag "$PUBLISH_TAG" --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 GitHub Release
needs: [determine-version-info, publish-to-npm, publish-to-docker-hub]
if: github.event.pull_request.merged == true
uses: ./.github/workflows/release-create-github-releases.yml
with:
track: ${{ needs.determine-version-info.outputs.track }}
version-tag: 'n8n@${{ needs.determine-version-info.outputs.version }}'
body: ${{ github.event.pull_request.body }}
commit: ${{ github.event.pull_request.base.ref }}
secrets: inherit
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 on minor release
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'
uses: ./.github/workflows/release-merge-tag-to-branch.yml
with:
version: ${{ needs.determine-version-info.outputs.version }}
target-branch: master
secrets: inherit
merge-release-tag-to-rc-branch:
name: Merge release tag to RC branch on patch release
needs: [determine-version-info, publish-to-npm, create-github-release]
if: |
github.event.pull_request.merged == true &&
needs.determine-version-info.outputs.bump == 'patch' &&
needs.determine-version-info.outputs.release_type != 'rc'
uses: ./.github/workflows/release-merge-tag-to-branch.yml
with:
version: ${{ needs.determine-version-info.outputs.version }}
target-branch: ${{ needs.determine-version-info.outputs.rc_branch }}
secrets: inherit
post-release:
name: Run Post-release actions
needs:
[
determine-version-info,
publish-to-npm,
create-github-release,
move-track-tag,
promote-stable-tag,
]
if: |
always() &&
needs.publish-to-npm.result == 'success' &&
needs.create-github-release.result == 'success' &&
(needs.move-track-tag.result == 'success' || needs.move-track-tag.result == 'skipped') &&
(needs.promote-stable-tag.result == 'success' || needs.promote-stable-tag.result == 'skipped')
uses: ./.github/workflows/release-publish-post-release.yml
with:
track: ${{ needs.determine-version-info.outputs.track }}
previous_version: ${{ needs.determine-version-info.outputs.previous_version }}
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
release-publish-new-package .github/workflows/release-publish-new-package.yml
View raw YAML
name: 'Release: Publish New Package'
on:
workflow_dispatch:
inputs:
package-path:
description: 'Path to the package to publish (e.g. packages/@n8n/my-new-package)'
required: true
type: string
concurrency:
group: release-new-package-${{ github.event.inputs.package-path }}
cancel-in-progress: false
jobs:
publish-to-npm:
name: Publish to NPM
runs-on: ubuntu-latest
timeout-minutes: 30
environment: release
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup and Build
uses: ./.github/actions/setup-nodejs
- name: Check package does not already exist on NPM
working-directory: ${{ github.event.inputs.package-path }}
run: |
PACKAGE_NAME=$(node -p "require('./package.json').name")
if [ "$PACKAGE_NAME" = "n8n-monorepo" ]; then
echo "::error::Package 'n8n-monorepo' cannot be published."
exit 1
fi
if npm view "$PACKAGE_NAME" > /dev/null 2>&1; then
echo "::error::Package '$PACKAGE_NAME' already exists on NPM. Use the regular release workflow for updates."
exit 1
fi
echo "Package '$PACKAGE_NAME' does not exist on NPM yet. Proceeding with publish."
- name: Configure NPM token
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_INITIAL_PUBLISH_TOKEN }}" > ~/.npmrc
- name: Publish package
working-directory: ${{ github.event.inputs.package-path }}
run: pnpm publish --access public --no-git-checks
release-publish-post-release .github/workflows/release-publish-post-release.yml
View raw YAML
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
previous_version:
description: 'Previous release version acquired from determine-version-info. (e.g. 2.9.2, 1.123.22)'
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
promote-previous-minor-github-release-to-latest:
name: Promote previous minor Github Release to latest
if: |
inputs.release_type != 'rc' &&
inputs.bump == 'minor'
uses: ./.github/workflows/release-promote-github-release.yml
secrets: inherit
with:
version-tag: 'n8n@${{ inputs.new_stable_version }}'
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
populate-cloud-with-releases:
name: 'Populate cloud database with releases'
uses: ./.github/workflows/release-populate-cloud-with-releases.yml
with:
previous-version: ${{ inputs.previous_version }}
version: ${{ inputs.version }}
experimental: ${{ inputs.release_type == 'rc' }}
secrets: inherit
send-version-release-notification:
name: 'Send version release notifications'
uses: ./.github/workflows/release-version-release-notification.yml
with:
previous-version: ${{ inputs.previous_version }}
version: ${{ inputs.version }}
secrets: inherit
release-push-to-channel .github/workflows/release-push-to-channel.yml
View raw YAML
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
environment: release
permissions:
id-token: write
steps:
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24.13.1
# Remove after https://github.com/npm/cli/issues/8547 gets resolved
- run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- 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
environment: release
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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'
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
docker buildx imagetools create -t "${DOCKER_USERNAME}/n8n:stable" "${DOCKER_USERNAME}/n8n:${{ needs.validate-inputs.outputs.version }}"
docker buildx imagetools create -t "${DOCKER_USERNAME}/n8n:latest" "${DOCKER_USERNAME}/n8n:${{ needs.validate-inputs.outputs.version }}"
docker buildx imagetools create -t "${DOCKER_USERNAME}/runners:stable" "${DOCKER_USERNAME}/runners:${{ needs.validate-inputs.outputs.version }}"
docker buildx imagetools create -t "${DOCKER_USERNAME}/runners:latest" "${DOCKER_USERNAME}/runners:${{ needs.validate-inputs.outputs.version }}"
- name: Tag beta/next Docker image
if: needs.validate-inputs.outputs.release_channel == 'beta'
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
docker buildx imagetools create -t "${DOCKER_USERNAME}/n8n:beta" "${DOCKER_USERNAME}/n8n:${{ needs.validate-inputs.outputs.version }}"
docker buildx imagetools create -t "${DOCKER_USERNAME}/n8n:next" "${DOCKER_USERNAME}/n8n:${{ needs.validate-inputs.outputs.version }}"
docker buildx imagetools create -t "${DOCKER_USERNAME}/runners:beta" "${DOCKER_USERNAME}/runners:${{ needs.validate-inputs.outputs.version }}"
docker buildx imagetools create -t "${DOCKER_USERNAME}/runners:next" "${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
environment: release
permissions:
packages: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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]
environment: release
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'
release-schedule-patch-prs matrix .github/workflows/release-schedule-patch-prs.yml
View raw YAML
name: 'Release: Schedule Patch Release PRs'
on:
workflow_dispatch:
schedule:
- cron: '0 8 * * 2-5' # 9am CET (UTC+1), Tuesday–Friday
jobs:
create-patch-prs:
name: Create patch release PR (${{ matrix.track }})
strategy:
matrix:
track: [stable, beta, v1]
uses: ./.github/workflows/release-create-patch-pr.yml
secrets: inherit
with:
track: ${{ matrix.track }}
release-standalone-package .github/workflows/release-standalone-package.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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
release-update-pointer-tag perms .github/workflows/release-update-pointer-tag.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- 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
release-version-release-notification .github/workflows/release-version-release-notification.yml
View raw YAML
name: 'Release: Send version release notification'
run-name: 'Send version release notification for n8n@${{ inputs.version }}'
on:
workflow_dispatch:
inputs:
previous-version:
description: 'The previous release version (e.g. 2.10.2)'
required: true
type: string
version:
description: 'The release version (e.g. 2.11.0)'
required: true
type: string
workflow_call:
inputs:
previous-version:
description: 'The previous release version (e.g. 2.10.2)'
required: true
type: string
version:
description: 'The release version (e.g. 2.11.0)'
required: true
type: string
jobs:
release-notification:
runs-on: ubuntu-slim
environment: release
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: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- name: Send release notification
env:
N8N_VERSION_RELEASE_NOTIFICATION_DATA: ${{ secrets.N8N_VERSION_RELEASE_NOTIFICATION_DATA }}
PAYLOAD: |
{
"previous_version": "${{ inputs.previous-version }}",
"new_version": "${{ inputs.version }}"
}
run: node ./.github/scripts/send-version-release-notification.mjs
sbom-generation-callable perms .github/workflows/sbom-generation-callable.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
with:
path: ./
format: cyclonedx-json
output-file: sbom-source.cdx.json
- name: Attest SBOM for source release
uses: actions/attest-sbom@07e74fc4e78d1aad915e867f9a094073a9f71527 # v4.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 }} >
sec-ci-reusable .github/workflows/sec-ci-reusable.yml
View raw YAML
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:
sec-poutine-reusable perms security .github/workflows/sec-poutine-reusable.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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
sec-publish-fix .github/workflows/sec-publish-fix.yml
View raw YAML
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@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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 }})'
sec-publish-fix-1x .github/workflows/sec-publish-fix-1x.yml
View raw YAML
name: 'Security: Publish fix (1.x)'
on:
pull_request:
types: [closed]
branches: ['1.x']
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@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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-1x-$(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 1.x
git checkout -b "$BRANCH_NAME" public-repo/1.x
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 1.x \
--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 (1.x). Run "Security: Sync from Public" workflow, rebase your branch, reopen PR. (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
sec-sync-public-to-private .github/workflows/sec-sync-public-to-private.yml
View raw YAML
# 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Sync master from public
run: |
git fetch https://github.com/n8n-io/n8n.git master:public-master
# Check if private is ahead of public, ignore Bundle commits
AHEAD_COUNT=$(git rev-list public-master..HEAD --pretty=oneline --grep="chore: Bundle" --invert-grep --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
- name: Sync 1.x from public
run: |
git fetch https://github.com/n8n-io/n8n.git 1.x:public-1.x
git checkout 1.x
# Check if private is ahead of public, ignore Bundle commits
AHEAD_COUNT=$(git rev-list public-1.x..HEAD --pretty=oneline --grep="chore: Bundle" --invert-grep --count)
if [ "$AHEAD_COUNT" -gt 0 ]; then
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "Private 1.x is $AHEAD_COUNT commit(s) ahead of public, skipping scheduled sync"
exit 0
elif [ "${{ inputs.force }}" != "true" ]; then
echo "Private 1.x is $AHEAD_COUNT commit(s) ahead of public, skipping (force not enabled)"
exit 0
else
echo "Private 1.x is $AHEAD_COUNT commit(s) ahead of public, force syncing anyway"
fi
fi
git reset --hard public-1.x
git push origin 1.x --force-with-lease
- name: Ensure bundle/2.x exists
run: |
if git ls-remote --exit-code origin refs/heads/bundle/2.x; then
echo "bundle/2.x already exists, skipping"
else
echo "bundle/2.x not found, creating from master"
git checkout master
git checkout -b bundle/2.x
git push origin bundle/2.x
fi
- name: Ensure bundle/1.x exists
run: |
if git ls-remote --exit-code origin refs/heads/bundle/1.x; then
echo "bundle/1.x already exists, skipping"
else
echo "bundle/1.x not found, creating from 1.x"
git checkout 1.x
git checkout -b bundle/1.x
git push origin bundle/1.x
fi
security-trivy-scan-callable perms security .github/workflows/security-trivy-scan-callable.yml
View raw YAML
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: C0AHNJU9XFA #updates-security
jobs:
security_scan:
name: Security - Scan Docker Image With Trivy
runs-on: ubuntu-latest
steps:
- name: Checkout for VEX file
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
security/vex.openvex.json
security/trivy.yaml
security/trivy-ignore-policy.rego
.github/scripts/retry.mjs
sparse-checkout-cone-mode: false
- name: Pull Docker image with retry
run: node .github/scripts/retry.mjs --attempts 4 --delay 15 'docker pull "${{ inputs.image_ref }}"'
- 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 }}
test-bench-reusable .github/workflows/test-bench-reusable.yml
View raw YAML
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
if: github.repository == 'n8n-io/n8n'
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-2vcpu-ubuntu-2204' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
- name: Run benchmarks
uses: CodSpeedHQ/action@281164b0f014a4e7badd2c02cecad9b595b70537 # v4.11.1
with:
mode: simulation
run: CODSPEED=true pnpm --filter=@n8n/performance bench
test-benchmark-destroy-nightly perms .github/workflows/test-benchmark-destroy-nightly.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Azure login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
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
test-benchmark-nightly perms .github/workflows/test-benchmark-nightly.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js and install dependencies
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
- uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
with:
terraform_version: '1.8.5'
- name: Azure login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
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@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
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 }}
test-db-reusable matrix .github/workflows/test-db-reusable.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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 }}
test-e2e-ci-reusable .github/workflows/test-e2e-ci-reusable.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Login to GHCR
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
- 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" "--base=FETCH_HEAD")
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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 }}
test-e2e-coverage-weekly .github/workflows/test-e2e-coverage-weekly.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: coverage-report
path: packages/testing/playwright/coverage/
retention-days: 14
- name: Upload E2E Coverage to Codecov
if: always()
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: coverage-gap-report
path: coverage-gaps.json
retention-days: 21
test-e2e-docker-pull-reusable .github/workflows/test-e2e-docker-pull-reusable.yml
View raw YAML
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
test-e2e-helm .github/workflows/test-e2e-helm.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Login to GHCR
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
- name: Install kubectl
uses: azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
- 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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 }}
test-e2e-infrastructure-reusable matrix .github/workflows/test-e2e-infrastructure-reusable.yml
View raw YAML
name: 'Test: E2E Infrastructure'
on:
workflow_call:
workflow_dispatch:
pull_request:
paths:
- 'packages/testing/playwright/tests/infrastructure/**'
- 'packages/testing/playwright/utils/benchmark/**'
- 'packages/testing/playwright/utils/performance-helper.ts'
- 'packages/testing/playwright/reporters/benchmark-summary-reporter.ts'
- 'packages/testing/containers/services/**'
- '.github/workflows/test-e2e-infrastructure-reusable.yml'
concurrency:
group: e2e-infrastructure-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
benchmark:
name: ${{ matrix.profile }}
strategy:
fail-fast: false
matrix:
include:
- profile: benchmark-direct
runner: blacksmith-4vcpu-ubuntu-2204
- profile: benchmark-queue
runner: blacksmith-8vcpu-ubuntu-2204
- profile: benchmark-queue-tuned
runner: blacksmith-8vcpu-ubuntu-2204
uses: ./.github/workflows/test-e2e-reusable.yml
with:
test-mode: docker-build
test-command: pnpm --filter=n8n-playwright test:all --project='${{ matrix.profile }}:infrastructure' --workers=1
shards: 1
runner: ${{ matrix.runner }}
timeout-minutes: 60
secrets: inherit
test-e2e-performance-reusable .github/workflows/test-e2e-performance-reusable.yml
View raw YAML
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
test-e2e-reusable matrix .github/workflows/test-e2e-reusable.yml
View raw YAML
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
timeout-minutes:
description: 'Job timeout in minutes'
required: false
default: 30
type: number
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
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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: ${{ inputs.timeout-minutes }}
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_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 }}
- name: Upload Failure Artifacts
if: ${{ failure() && inputs.upload-failure-artifacts }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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
test-evals-ai .github/workflows/test-evals-ai.yml
View raw YAML
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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
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
test-evals-ai-release .github/workflows/test-evals-ai-release.yml
View raw YAML
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
test-evals-ai-reusable .github/workflows/test-evals-ai-reusable.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.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) || '' }}
test-evals-python .github/workflows/test-evals-python.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
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
test-linting-reusable .github/workflows/test-linting-reusable.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref }}
- name: Build and Test
uses: ./.github/actions/setup-nodejs
with:
build-command: pnpm lint
node-version: ${{ inputs.nodeVersion }}
test-unit-reusable matrix .github/workflows/test-unit-reusable.yml
View raw YAML
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
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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:
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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:
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
name: backend-integration
- name: Upload coverage to Codecov
if: env.COVERAGE_ENABLED == 'true'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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:
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
name: nodes-unit
- name: Upload coverage to Codecov
if: env.COVERAGE_ENABLED == 'true'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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:
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
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
test-visual-chromatic .github/workflows/test-visual-chromatic.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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
test-visual-storybook .github/workflows/test-visual-storybook.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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 }}
test-workflow-scripts-reusable .github/workflows/test-workflow-scripts-reusable.yml
View raw YAML
name: 'Test: Workflow scripts'
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:
workflow-script-tests:
name: Run tests for workflow scripts
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-slim' || 'blacksmith-4vcpu-ubuntu-2204' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- name: Run tests
id: run-tests
run: npm test --prefix=.github/scripts
test-workflows-callable .github/workflows/test-workflows-callable.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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'
test-workflows-nightly .github/workflows/test-workflows-nightly.yml
View raw YAML
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
test-workflows-pr-comment perms .github/workflows/test-workflows-pr-comment.yml
View raw YAML
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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
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
util-approve-and-set-automerge perms .github/workflows/util-approve-and-set-automerge.yml
View raw YAML
name: 'Util: Approve and set Automerge'
run-name: Approve and automerge PR ${{ inputs.pull-request-number }}
on:
workflow_call:
inputs:
pull-request-number:
type: string
required: true
permissions:
contents: write
pull-requests: write
jobs:
approve-and-automerge:
if: |
inputs.pull-request-number != ''
runs-on: ubuntu-slim
environment: release
steps:
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.N8N_RELEASE_HELPER_APP_ID }}
private-key: ${{ secrets.N8N_RELEASE_HELPER_PRIVATE_KEY }}
- name: Approve PR (as the App)
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
gh pr review "${{ inputs.pull-request-number }}" \
--approve \
--repo "${{ github.repository }}"
- name: Enable auto-merge (merge when checks pass)
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
gh pr merge "${{ inputs.pull-request-number }}" \
--auto \
--squash \
--repo "${{ github.repository }}"
util-backport-bundle perms .github/workflows/util-backport-bundle.yml
View raw YAML
name: 'Util: Backport bundle PR to bundle/1.x'
run-name: Backport pull request ${{ github.event.pull_request.number || inputs.pull-request-id }}
on:
pull_request:
types: [closed]
branches:
- 'bundle/2.x'
workflow_dispatch:
inputs:
pull-request-id:
description: 'The ID number of the pull request (e.g. 3342). No #, no extra letters.'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
backport:
if: |
github.repository == 'n8n-io/n8n-private' &&
(github.event.pull_request.merged == true ||
github.event_name == 'workflow_dispatch')
runs-on: ubuntu-slim
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 }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.generate-token.outputs.token }}
fetch-depth: 0
- name: Backport
uses: korthout/backport-action@4aaf0e03a94ff0a619c9a511b61aeb42adea5b02 # v4.2.0
with:
github_token: ${{ steps.generate-token.outputs.token }}
source_pr_number: ${{ github.event.pull_request.number || inputs.pull-request-id }}
target_branches: bundle/1.x
pull_description: |-
# Description
Backport of #${pull_number} to `${target_branch}`.
## Checklist for the author (@${pull_author}) to go through.
- [ ] Review the backport changes
- [ ] Fix possible conflicts
- [ ] Merge to target branch
After this PR has been merged, it will be picked up in the next patch release for release track.
# Original description
${pull_description}
pull_title: ${pull_title} (backport to ${target_branch})
add_author_as_assignee: true
add_author_as_reviewer: true
copy_assignees: true
copy_requested_reviewers: false
copy_labels_pattern: '^(?!Backport to\b).+'
add_labels: 'automation:backport'
experimental: >
{
"conflict_resolution": "draft_commit_conflicts"
}
util-claude AI .github/workflows/util-claude.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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
util-claude-task AI .github/workflows/util-claude-task.yml
View raw YAML
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@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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
env:
MCP_LINEAR_API_KEY: ${{ secrets.MCP_LINEAR_API_KEY }}
MCP_NOTION_TOKEN: ${{ secrets.MCP_NOTION_TOKEN }}
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|${MCP_LINEAR_API_KEY}|g" /tmp/mcp-config.json
sed -i "s|MCP_NOTION_TOKEN_PLACEHOLDER|${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
util-cleanup-abandoned-release-branches .github/workflows/util-cleanup-abandoned-release-branches.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- 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 }}
util-cleanup-pr-images .github/workflows/util-cleanup-pr-images.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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
util-data-tooling .github/workflows/util-data-tooling.yml
View raw YAML
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: ${{ fromJSON(steps.paths-filter.outputs.results).db }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for db changes
uses: ./.github/actions/ci-filter
id: paths-filter
with:
mode: filter
base-ref: master
filters: |
db:
- packages/@n8n/db/**
- packages/cli/**
- name: Setup, build and export sqlite
if: fromJSON(steps.paths-filter.outputs.results).db
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: ${{ fromJSON(steps.paths-filter.outputs.results).db }}
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for db changes
uses: ./.github/actions/ci-filter
id: paths-filter
with:
mode: filter
base-ref: master
filters: |
db:
- packages/@n8n/db/**
- packages/cli/**
- name: Setup and Build
if: fromJSON(steps.paths-filter.outputs.results).db
uses: ./.github/actions/setup-nodejs
- name: Start Postgres
if: fromJSON(steps.paths-filter.outputs.results).db
uses: isbang/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
with:
compose-file: ./.github/docker-compose.yml
services: |
postgres
- name: Export postgres
if: fromJSON(steps.paths-filter.outputs.results).db
run: ./packages/cli/bin/n8n export:entities --outputDir packages/cli/commands/export/outputs
- name: Import postgres
if: fromJSON(steps.paths-filter.outputs.results).db
run: ./packages/cli/bin/n8n import:entities --inputDir packages/cli/commands/export/outputs --truncateTables
util-determine-current-version .github/workflows/util-determine-current-version.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- 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 }}"
util-ensure-release-candidate-branches .github/workflows/util-ensure-release-candidate-branches.yml
View raw YAML
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: 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
fetch-tags: true
- name: Setup NodeJS
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace
- name: Ensure release-candidate branches
run: node ./.github/scripts/ensure-release-candidate-branches.mjs
util-notify-pr-status .github/workflows/util-notify-pr-status.yml
View raw YAML
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@551353b829c3646756b2ec2b3694f819d7957495 # v2.0.0
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) }} }'
util-sync-api-docs .github/workflows/util-sync-api-docs.yml
View raw YAML
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
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.
util-update-node-popularity perms .github/workflows/util-update-node-popularity.yml
View raw YAML
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
outputs:
pull-request-number: ${{ steps.create-pr.outputs.pull-request-number }}
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 repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.generate-token.outputs.token }}
- 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'
id: create-pr
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ steps.generate-token.outputs.token }}
commit-message: 'chore: Update node popularity data'
labels: 'automation:scheduled-update'
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
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
approve-and-automerge:
needs: [update-popularity]
if: |
needs.update-popularity.outputs.pull-request-number != '' &&
(github.event_name == 'workflow_dispatch' ||
(github.event_name == 'schedule' && github.repository == 'n8n-io/n8n'))
uses: ./.github/workflows/util-approve-and-set-automerge.yml
secrets: inherit
with:
pull-request-number: ${{ needs.update-popularity.outputs.pull-request-number }}