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 }} 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 }} skip_tests: ${{ steps.generate-matrix.outputs.skip-tests }} 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/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 pnpm-workspace.yaml packages/@n8n/*/package.json 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/** 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-instance-ai*.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 || 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: 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" 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 # 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.ci == 'true' || needs.install-and-build.outputs.e2e == 'true' || needs.install-and-build.outputs.e2e_performance == 'true') uses: ./.github/workflows/prepare-docker-reusable.yml with: branch: ${{ needs.install-and-build.outputs.commit_sha }} secrets: inherit # Internal-only 1-spec fail-fast sanity check on sqlite. sqlite-sanity: name: 'SQLite: Sanity Check' needs: [install-and-build, prepare-docker] if: >- needs.prepare-docker.result == 'success' && (needs.install-and-build.outputs.ci == 'true' || needs.install-and-build.outputs.e2e == 'true') && github.repository == 'n8n-io/n8n' && github.event_name != 'merge_group' && github.event.pull_request.head.repo.fork != true uses: ./.github/workflows/test-e2e-reusable.yml with: branch: ${{ needs.install-and-build.outputs.commit_sha }} test-mode: docker-artifact test-command: pnpm --filter=n8n-playwright test:container:sqlite:e2e tests/e2e/building-blocks/workflow-entry-points.spec.ts workers: '1' 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.ci == 'true' || needs.install-and-build.outputs.e2e == 'true') && 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 }} 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 }} 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). if: >- needs.prepare-docker.result == 'success' && needs.install-and-build.outputs.e2e_performance == 'true' && 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 # Depends on prepare-docker so the eval workflow can load the SHA-keyed image cache. # prepare-docker may be skipped (its filter excludes .github/**); the eval falls back to a local build. instance-ai-workflow-evals: name: Instance AI Workflow Evals needs: [install-and-build, prepare-docker] if: >- !cancelled() && needs.install-and-build.result == 'success' && (needs.prepare-docker.result == 'success' || needs.prepare-docker.result == 'skipped') && needs.install-and-build.outputs.instance_ai_workflow_eval == 'true' && github.repository == 'n8n-io/n8n' && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) uses: ./.github/workflows/test-evals-instance-ai.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, sqlite-sanity, e2e, dev-server-smoke, db-tests, performance, security-checks, workflow-scripts, ] 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) }} # 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] if: always() 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,docker-image-size-n8n,docker-image-size-runners secrets: inherit