mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-19 16:55:02 +02:00
The DB matrix's SQLite Pooled leg runs the full cli integration suite with the same command and environment as the scoped step in the unit workflow. Skip the scoped step on those runs. Master runs keep the default and do not change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
458 lines
20 KiB
YAML
458 lines
20 KiB
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-4vcpu-ubuntu-2204' }}
|
|
env:
|
|
NODE_OPTIONS: '--max-old-space-size=7168'
|
|
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 }}
|
|
runtime: ${{ fromJSON(steps.ci-filter.outputs.results).runtime == true }}
|
|
unit: ${{ fromJSON(steps.ci-filter.outputs.results).unit == true }}
|
|
e2e: ${{ fromJSON(steps.ci-filter.outputs.results).e2e == true }}
|
|
dev_server_smoke: ${{ fromJSON(steps.ci-filter.outputs.results)['dev-server-smoke'] == 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 }}
|
|
performance: ${{ fromJSON(steps.ci-filter.outputs.results).performance == true }}
|
|
e2e_performance: ${{ fromJSON(steps.ci-filter.outputs.results)['e2e-performance'] == true }}
|
|
instance_ai_workflow_eval: ${{ fromJSON(steps.ci-filter.outputs.results)['instance-ai-workflow-eval'] == true }}
|
|
commit_sha: ${{ steps.commit-sha.outputs.sha }}
|
|
merge_base: ${{ steps.ci-filter.outputs.merge-base }}
|
|
matrix: ${{ steps.generate-matrix.outputs.matrix }}
|
|
db_test_matrix: ${{ steps.generate-db-test-matrix.outputs.db_test_matrix }}
|
|
skip_tests: ${{ steps.generate-matrix.outputs.skip-tests }}
|
|
affected_packages: ${{ steps.affected-packages.outputs.list }}
|
|
changed_files: ${{ steps.ci-filter.outputs.changed-files }}
|
|
steps:
|
|
- uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
|
|
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/**
|
|
!**/*.md
|
|
!**/LICENSE
|
|
!**/CHANGELOG.md
|
|
runtime:
|
|
**
|
|
!packages/@n8n/task-runner-python/**
|
|
!.github/**
|
|
!**/*.md
|
|
!**/LICENSE
|
|
!**/CHANGELOG.md
|
|
!**/*.test.ts
|
|
!**/*.spec.ts
|
|
!packages/testing/playwright/**
|
|
!packages/frontend/@n8n/storybook/**
|
|
!.devcontainer/**
|
|
!scripts/agent-setup.mjs
|
|
!scripts/backend-module/**
|
|
!scripts/cloud-session.mjs
|
|
!scripts/licenses/**
|
|
!scripts/mutation-health/**
|
|
!scripts/sync-agent-skill-links.mjs
|
|
unit:
|
|
**
|
|
!packages/@n8n/task-runner-python/**
|
|
!packages/testing/playwright/**
|
|
!.github/**
|
|
!**/*.md
|
|
!**/LICENSE
|
|
!**/CHANGELOG.md
|
|
!.devcontainer/**
|
|
!scripts/agent-setup.mjs
|
|
!scripts/backend-module/**
|
|
!scripts/cloud-session.mjs
|
|
!scripts/licenses/**
|
|
!scripts/mutation-health/**
|
|
!scripts/sync-agent-skill-links.mjs
|
|
e2e:
|
|
.github/workflows/test-e2e-*.yml
|
|
.github/workflows/prepare-docker-reusable.yml
|
|
.github/actions/build-n8n-docker/**
|
|
.github/actions/load-n8n-docker/**
|
|
packages/testing/playwright/**
|
|
packages/testing/containers/**
|
|
dev-server-smoke:
|
|
packages/frontend/editor-ui/vite.config.mts
|
|
# If this job becomes too slow, use a smaller glob. Do not delete the assertion.
|
|
packages/frontend/editor-ui/src/**
|
|
pnpm-workspace.yaml
|
|
packages/@n8n/*/package.json
|
|
packages/frontend/@n8n/frontend-vite-config/**
|
|
packages/testing/playwright/tests/dev-server-smoke/**
|
|
packages/testing/playwright/playwright.config.ts
|
|
packages/testing/playwright/playwright-projects.ts
|
|
packages/testing/playwright/package.json
|
|
.github/workflows/test-dev-server-smoke-reusable.yml
|
|
workflows: .github/**
|
|
workflow-scripts:
|
|
.github/scripts/**
|
|
OWNERS
|
|
scripts/licenses/**
|
|
scripts/mutation-health/**
|
|
cubic.yaml
|
|
.agents/review-rules/**
|
|
performance:
|
|
packages/testing/performance/**
|
|
packages/workflow/src/**
|
|
packages/@n8n/expression-runtime/src/**
|
|
.github/workflows/test-bench-reusable.yml
|
|
e2e-performance:
|
|
packages/testing/playwright/tests/performance/**
|
|
packages/testing/playwright/utils/performance-helper.ts
|
|
packages/testing/containers/**
|
|
.github/workflows/test-e2e-performance-reusable.yml
|
|
instance-ai-workflow-eval:
|
|
packages/@n8n/instance-ai/src/**
|
|
packages/@n8n/instance-ai/evaluations/**
|
|
packages/cli/src/modules/instance-ai/**
|
|
packages/core/src/execution-engine/eval-mock-helpers.ts
|
|
.github/workflows/test-evals-discovery.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/cli/test/setup-testcontainers.js
|
|
packages/cli/test/teardown-testcontainers.js
|
|
packages/@n8n/db/**
|
|
packages/@n8n/backend-test-utils/**
|
|
packages/@n8n/scheduler/src/**
|
|
packages/cli/**/__tests__/**
|
|
packages/testing/containers/services/postgres.ts
|
|
packages/testing/containers/postgres-versions.json
|
|
packages/testing/containers/test-containers.ts
|
|
.github/workflows/test-db-reusable.yml
|
|
.github/scripts/db-test-matrix.mjs
|
|
docs/generated/**
|
|
.tbls.sqlite.yml
|
|
.tbls.postgres.yml
|
|
|
|
- name: Setup and Build
|
|
if: fromJSON(steps.ci-filter.outputs.results).ci || fromJSON(steps.ci-filter.outputs.results).e2e
|
|
uses: ./.github/actions/setup-nodejs
|
|
with:
|
|
build-command: ${{ fromJSON(steps.ci-filter.outputs.results).ci && 'pnpm build' || 'pnpm turbo run build --filter=@n8n/playwright-janitor' }}
|
|
|
|
- name: Report build critical path
|
|
if: fromJSON(steps.ci-filter.outputs.results).ci
|
|
run: node scripts/turbo-critical-path.mjs >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Run format check
|
|
if: fromJSON(steps.ci-filter.outputs.results).ci
|
|
run: pnpm format:check
|
|
|
|
- name: Generate shard matrix
|
|
id: generate-matrix
|
|
if: fromJSON(steps.ci-filter.outputs.results).ci || fromJSON(steps.ci-filter.outputs.results).e2e
|
|
env:
|
|
CHANGED_FILES: ${{ steps.ci-filter.outputs.changed-files }}
|
|
MERGE_BASE: ${{ steps.ci-filter.outputs.merge-base }}
|
|
run: |
|
|
FILES_CSV=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//')
|
|
MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 16 --orchestrate --impact "--files=$FILES_CSV" "--base=$MERGE_BASE")
|
|
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"
|
|
|
|
# Generated here because a reusable workflow cannot resolve its own matrix.
|
|
- name: Generate DB test matrix
|
|
id: generate-db-test-matrix
|
|
if: fromJSON(steps.ci-filter.outputs.results).db
|
|
run: echo "db_test_matrix=$(node .github/scripts/db-test-matrix.mjs)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Compute affected packages
|
|
id: affected-packages
|
|
if: fromJSON(steps.ci-filter.outputs.results).unit
|
|
env:
|
|
CHANGED_FILES: ${{ steps.ci-filter.outputs.changed-files }}
|
|
run: |
|
|
PACKAGES=$(node packages/testing/janitor/dist/cli.js affected-packages | tr '\n' ' ' | sed 's/ *$//')
|
|
echo "Affected packages: $PACKAGES"
|
|
echo "list=$PACKAGES" >> "$GITHUB_OUTPUT"
|
|
|
|
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: ${{ github.event_name != 'merge_group' }}
|
|
affectedPackages: ${{ needs.install-and-build.outputs.affected_packages }}
|
|
changedFiles: ${{ needs.install-and-build.outputs.changed_files }}
|
|
# The DB matrix's "SQLite Pooled" leg runs the full cli integration
|
|
# suite with the same command and environment as the scoped step.
|
|
skipCliIntegration: ${{ needs.install-and-build.outputs.db == '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
|
|
env:
|
|
# Inherited by every worker turbo spawns, so the real budget is
|
|
# concurrency x cap: 2 x 6 GB fits the runner's 15.2 GB.
|
|
NODE_OPTIONS: --max-old-space-size=6144
|
|
steps:
|
|
- uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
|
|
with:
|
|
ref: ${{ needs.install-and-build.outputs.commit_sha }}
|
|
|
|
- name: Setup Node.js
|
|
uses: ./.github/actions/setup-nodejs
|
|
with:
|
|
# turbo directly, not the root `typecheck` script, to keep the
|
|
# concurrency bound CI-only.
|
|
build-command: pnpm turbo typecheck --concurrency=2
|
|
|
|
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: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
|
|
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
|
|
|
|
# Seeds the SHA-keyed Docker image cache once so that downstream e2e jobs
|
|
# (each of which invokes prepare-docker internally) short-circuit to a
|
|
# cache hit instead of racing to rebuild.
|
|
prepare-docker:
|
|
name: Prepare Docker
|
|
needs: install-and-build
|
|
if: >-
|
|
github.repository == 'n8n-io/n8n' &&
|
|
github.event_name != 'merge_group' &&
|
|
(needs.install-and-build.outputs.runtime == 'true'
|
|
|| needs.install-and-build.outputs.e2e == 'true'
|
|
|| needs.install-and-build.outputs.e2e_performance == 'true'
|
|
|| contains(github.event.pull_request.labels.*.name, 'force-e2e')
|
|
|| contains(github.event.pull_request.labels.*.name, 'run-perf-tests'))
|
|
uses: ./.github/workflows/prepare-docker-reusable.yml
|
|
with:
|
|
branch: ${{ needs.install-and-build.outputs.commit_sha }}
|
|
secrets: inherit
|
|
|
|
# Full e2e run. Internal PRs run multi-main (postgres + redis + caddy + 2 mains + 1 worker).
|
|
# Fork PRs run sqlite-only and skip @licensed tests (no enterprise license secrets on forks).
|
|
e2e:
|
|
name: E2E
|
|
needs: [install-and-build, prepare-docker]
|
|
if: >-
|
|
needs.prepare-docker.result == 'success' &&
|
|
(needs.install-and-build.outputs.runtime == 'true'
|
|
|| needs.install-and-build.outputs.e2e == 'true'
|
|
|| contains(github.event.pull_request.labels.*.name, 'force-e2e')) &&
|
|
needs.install-and-build.outputs.skip_tests != 'true' &&
|
|
github.event_name != 'merge_group'
|
|
uses: ./.github/workflows/test-e2e-reusable.yml
|
|
with:
|
|
branch: ${{ needs.install-and-build.outputs.commit_sha }}
|
|
test-mode: docker-artifact
|
|
test-command: ${{ github.event.pull_request.head.repo.fork == true && 'pnpm --filter=n8n-playwright test:container:sqlite:e2e --grep-invert=@licensed' || 'pnpm --filter=n8n-playwright test:container:multi-main:e2e' }}
|
|
workers: '1'
|
|
pre-generated-matrix: ${{ needs.install-and-build.outputs.matrix }}
|
|
artifact-prefix: e2e
|
|
secrets: inherit
|
|
|
|
# Boots the editor-ui against the Vite dev server and fails on any console
|
|
# or page error during load. Catches regressions in dev-mode module
|
|
# resolution (missing Vite alias, broken workspace package interop) that
|
|
# the production-bundle e2e job bundles around.
|
|
dev-server-smoke:
|
|
name: Dev-server boot smoke
|
|
needs: install-and-build
|
|
if: needs.install-and-build.outputs.dev_server_smoke == 'true' && github.event_name != 'merge_group'
|
|
uses: ./.github/workflows/test-dev-server-smoke-reusable.yml
|
|
with:
|
|
ref: ${{ needs.install-and-build.outputs.commit_sha }}
|
|
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 }}
|
|
matrix: ${{ needs.install-and-build.outputs.db_test_matrix }}
|
|
|
|
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 }}
|
|
|
|
e2e-performance:
|
|
name: E2E Performance
|
|
needs: [install-and-build, prepare-docker]
|
|
# Performance is internal-only (license secrets required, not available on forks).
|
|
# Runs when path filter trips OR when the PR carries the `run-perf-tests` label
|
|
# (opt-in coverage for shared-utility / Vue Flow changes that the path filter
|
|
# would otherwise miss).
|
|
if: >-
|
|
needs.prepare-docker.result == 'success' &&
|
|
(needs.install-and-build.outputs.e2e_performance == 'true' ||
|
|
contains(github.event.pull_request.labels.*.name, 'run-perf-tests')) &&
|
|
github.event.pull_request.head.repo.fork != true
|
|
uses: ./.github/workflows/test-e2e-performance-reusable.yml
|
|
secrets: inherit
|
|
|
|
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
|
|
|
|
# In-process discovery eval — asserts the orchestrator reaches for browser/computer-use
|
|
# tools at OAuth/screenshot moments. Lightweight (no Docker), runs in parallel with the
|
|
# heavy workflow eval. Non-blocking initially; promote to required after stability.
|
|
instance-ai-discovery-evals:
|
|
name: Instance AI Discovery Evals
|
|
needs: install-and-build
|
|
if: >-
|
|
!cancelled() &&
|
|
needs.install-and-build.result == 'success' &&
|
|
needs.install-and-build.outputs.instance_ai_workflow_eval == 'true' &&
|
|
github.repository == 'n8n-io/n8n' &&
|
|
github.event_name != 'merge_group' &&
|
|
(github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork)
|
|
uses: ./.github/workflows/test-evals-discovery.yml
|
|
with:
|
|
branch: ${{ 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,
|
|
dev-server-smoke,
|
|
db-tests,
|
|
performance,
|
|
security-checks,
|
|
workflow-scripts,
|
|
]
|
|
if: always()
|
|
runs-on: ubuntu-slim
|
|
steps:
|
|
- uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
|
|
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) }}
|
|
|
|
# A red `chore: Bundle/*` PR blocks publishing every fix batched into it, so the
|
|
# failure is announced instead of waiting to be noticed. Only fires in n8n-private,
|
|
# where bundle branches exist.
|
|
notify-bundle-pr-failure:
|
|
name: Notify Slack on bundle PR failure
|
|
needs: required-checks
|
|
if: >-
|
|
always() &&
|
|
needs.required-checks.result == 'failure' &&
|
|
(github.head_ref == 'bundle/2.x' || github.head_ref == 'bundle/1.x')
|
|
runs-on: ubuntu-slim
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
sparse-checkout: .github/scripts/slack
|
|
sparse-checkout-cone-mode: false
|
|
- name: Notify Slack
|
|
env:
|
|
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
|
|
HEAD_REF: ${{ github.head_ref }}
|
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
run: |
|
|
node .github/scripts/slack/notify.mjs \
|
|
--channel '#alerts-build' \
|
|
--text "<${PR_URL}|Required Checks failed on the ${HEAD_REF} PR> - the batched fixes stay unpublished until it is green."
|
|
|
|
# Posts a QA metrics comparison comment on the PR.
|
|
# Runs after all checks so any job can emit metrics before this reports.
|
|
post-qa-metrics-comment:
|
|
name: QA Metrics
|
|
needs: [required-checks, e2e-performance]
|
|
# Only post when the perf suite actually measured this PR, otherwise the
|
|
# comment fires on trivial PRs and flags master drift/noise that isn't theirs.
|
|
if: always() && needs.e2e-performance.result == 'success'
|
|
uses: ./.github/workflows/util-qa-metrics-comment-reusable.yml
|
|
with:
|
|
metrics: memory-heap-used-baseline,memory-rss-baseline,instance-ai-heap-used-baseline,instance-ai-rss-baseline,agents-heap-used-baseline,agents-rss-baseline,docker-image-size-n8n,docker-image-size-runners,canvas-cold-load-S-ms,canvas-cold-load-M-ms,canvas-cold-load-L-ms,canvas-server-heap-M-mb,canvas-server-heap-L-mb,canvas-browser-heap-M-mb,canvas-browser-heap-L-mb,canvas-pan-frame-p95-M-ms,canvas-pan-frame-p95-L-ms,canvas-tidy-up-M-ms,canvas-tidy-up-L-ms,canvas-exec-heavy-M-ms,canvas-exec-heavy-L-ms
|
|
secrets: inherit
|