n8n/.github/workflows/ci-pull-requests.yml
Declan Carroll 20d8e90c95
ci: Refactor and optimizes E2E test runs in CI (#28968)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 07:09:32 +00:00

322 lines
13 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-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 }}
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 }}
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/**
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 }}
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=FETCH_HEAD)
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 }}
upload-failure-artifacts: ${{ github.event.pull_request.head.repo.fork == true }}
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
instance-ai-workflow-evals:
name: Instance AI Workflow Evals
needs: install-and-build
if: >-
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,
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