name: 'Test: E2E Coverage Nightly' on: schedule: - cron: '0 2 * * *' # Nightly at 02:00 UTC workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # Shared build so the Turbo remote cache is warm before any test job starts. # Downstream jobs call setup-nodejs with the default build-command and get # ~30s cache hits rather than racing to build in parallel from cold. build: name: Install & Build runs-on: blacksmith-4vcpu-ubuntu-2204 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup and Build uses: ./.github/actions/setup-nodejs prepare-docker: name: Prepare Docker (coverage) uses: ./.github/workflows/prepare-docker-reusable.yml with: build-variant: coverage runner: blacksmith-8vcpu-ubuntu-2204 secrets: inherit # Distribute the full e2e suite across shards by historical duration # (janitor orchestrator) instead of Playwright's count-based --shard=N/total. # Count-based sharding stacked the heavy + flaky specs onto one shard, which # then blew past the timeout; duration-weighting keeps every shard even. generate-matrix: name: Generate shard matrix needs: build runs-on: blacksmith-2vcpu-ubuntu-2204 outputs: matrix: ${{ steps.gen.outputs.matrix }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup uses: ./.github/actions/setup-nodejs with: build-command: pnpm turbo run build --filter=@n8n/playwright-janitor - name: Generate matrix (16 shards, duration-weighted) id: gen run: | MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 16 --orchestrate) echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" e2e: name: E2E (coverage) needs: [prepare-docker, generate-matrix] uses: ./.github/workflows/test-e2e-reusable.yml with: test-mode: docker-artifact # Runs the coverage project + resolves this shard's V8 to lcov/per-spec # (keeps test-e2e-reusable coverage-agnostic). See run-coverage-shard.mjs. test-command: pnpm --filter=n8n-playwright coverage:shard workers: '1' runner: blacksmith-4vcpu-ubuntu-2204 timeout-minutes: 55 pre-generated-matrix: ${{ needs.generate-matrix.outputs.matrix }} artifact-prefix: coverage build-variant: coverage currents-project-id: 'LRxcNt' secrets: inherit # Unit + integration coverage, one leg per suite — no change-scoping, run in # parallel with the E2E shards (the shared `build` job keeps Turbo cache warm). # The integration leg adds a Postgres pre-pull and runs from packages/cli to # reach branches a mocked DB can't (real query paths, multi-main, migrations). unit-coverage: name: ${{ matrix.name }} needs: build runs-on: blacksmith-4vcpu-ubuntu-2204 strategy: fail-fast: false matrix: include: - name: Unit Coverage (backend + nodes) artifact: unit-coverage test-command: pnpm test:ci:backend:unit artifact-path: packages/**/coverage/lcov.info - name: Unit Coverage (frontend) artifact: frontend-coverage test-command: pnpm test:ci:frontend artifact-path: packages/**/coverage/lcov.info - name: Unit Coverage (CLI integration + Postgres) artifact: integration-coverage test-command: pnpm test:postgres:integration:tc # Glob (not the single file) so the artifact keeps its packages// # path, which aggregate-coverage.mjs uses to qualify SF: keys. artifact-path: packages/**/coverage/lcov.info working-directory: packages/cli pre-pull-containers: true env: COVERAGE_ENABLED: 'true' NODE_OPTIONS: --max-old-space-size=7168 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup uses: ./.github/actions/setup-nodejs - name: Pre-pull Test Container Images if: matrix.pre-pull-containers run: pnpm tsx packages/testing/containers/pull-test-images.ts || true - name: Run tests working-directory: ${{ matrix.working-directory || github.workspace }} run: ${{ matrix.test-command }} - name: Upload coverage artifact if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ matrix.artifact }} path: ${{ matrix.artifact-path }} retention-days: 3 # Both Python packages in one job — they're fast and share the uv install # (each still pins its own Python version: 3.13 task-runner, 3.11 evals). python-coverage: name: Python Coverage (task-runner + ai-workflow-builder) runs-on: ubuntu-latest steps: - name: Checkout 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 3.13 (task-runner) working-directory: packages/@n8n/task-runner-python run: uv python install 3.13 - name: Install task-runner dependencies working-directory: packages/@n8n/task-runner-python run: just sync-all - name: Test task-runner-python working-directory: packages/@n8n/task-runner-python run: uv run pytest --cov=src --cov-report=xml --cov-report=term-missing - name: Install Python 3.11 (ai-workflow-builder evals) working-directory: packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python run: uv python install 3.11 - name: Install ai-workflow-builder eval dependencies working-directory: packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python run: just sync-all - name: Test ai-workflow-builder evals working-directory: packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python run: uv run pytest --cov=src --cov-report=xml --cov-report=term-missing - name: Upload Python Coverage to Codecov if: always() uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: | packages/@n8n/task-runner-python/coverage.xml packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python/coverage.xml flags: nightly-python name: nightly-python-coverage fail_ci_if_error: false aggregate: name: Aggregate Coverage needs: [e2e, unit-coverage, python-coverage] if: always() && needs.e2e.result != 'skipped' && needs.e2e.result != 'cancelled' runs-on: blacksmith-4vcpu-ubuntu-2204 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Environment uses: ./.github/actions/setup-nodejs - name: Download E2E shard artifacts uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: pattern: coverage-shard-* path: /tmp/shards/ # No merge-multiple: packages/cli/coverage/lcov.info exists in both # unit-coverage and integration-coverage — separate artifact subdirs # prevent clobbering, and findFiles recurses into all of them so MCR # unions unit + integration coverage for CLI correctly. - name: Download unit + integration + frontend coverage artifacts uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: pattern: '*-coverage' path: /tmp/unit-lcovs/ - name: Build janitor (merge-coverage) run: pnpm turbo run build --filter=@n8n/playwright-janitor # Merges all lcovs into one lcov.info (SF: paths normalised to repo-root- # relative) and builds the per-spec E2E impact map for PR selection. - name: Aggregate coverage + build impact map working-directory: packages/testing/playwright run: | node scripts/aggregate-coverage.mjs \ --shards=/tmp/shards \ --unit-shards=/tmp/unit-lcovs \ --out=coverage - 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 Combined 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: nightly-full name: nightly-full-coverage fail_ci_if_error: false # Codecov needs ~60-90s to finish indexing the fresh upload before its # report API will return the new flag data. Without this wait the # gap-analysis step that follows runs into 503s and bails. - name: Wait for Codecov to index upload if: always() shell: bash run: sleep 60 # Advisory only: a Codecov API hiccup (e.g. a 503) must not red the whole # nightly nor block the impact-map refresh, which runs off the built map. - name: Analyse Coverage Gaps if: always() continue-on-error: true 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() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: coverage-gap-report path: coverage-gaps.json retention-days: 21 # Refresh the committed impact map PRs read for E2E selection. Without this the # map only updates on manual commits and drifts, so newly-covered source→spec # couplings get declared uncovered and their E2E specs skipped. Gated on ALL # shards passing (complete map) + aggregate having built it — a partial shard # set yields a degraded map, worse than a stale one. commit-impact-map: name: Commit refreshed impact map needs: [e2e, aggregate] if: needs.e2e.result == 'success' && needs.aggregate.result == 'success' runs-on: ubuntu-latest # Least-privilege GITHUB_TOKEN: checkout + download-artifact only need read; # the branch push + PR are done with the scoped app token below, not this token. permissions: contents: read actions: read 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 }} # Scope the token to only what the branch push + PR need. permission-contents: write permission-pull-requests: write - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: token: ${{ steps.generate-token.outputs.token }} # Don't persist the token in .git/config (avoids leaking it via artifacts); # create-pull-request pushes with its own token input, not the git remote creds. persist-credentials: false - name: Download coverage report artifact uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: coverage-report path: /tmp/coverage-report - name: Copy impact map into place run: | MAP=/tmp/coverage-report/impact-map.json # Guard: a missing/empty map is a build hiccup — keep the committed map # rather than clobber it with a degraded one. node -e ' const fs = require("fs"); const m = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const n = Array.isArray(m.specs) ? m.specs.length : 0; if (!n) { console.error("impact map has no specs — refusing to commit"); process.exit(1); } console.log("impact map ok: " + n + " specs"); ' "$MAP" # Raw copy: the map is intentionally minified and biome-ignored # (.github/test-metrics/**). Do NOT format it. cp "$MAP" .github/test-metrics/e2e-impact-map.json - name: Check for changes id: check-changes run: | if git diff --quiet .github/test-metrics/e2e-impact-map.json; then echo "Impact map unchanged" echo "has_changes=false" >> "$GITHUB_OUTPUT" else echo "Impact map 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 e2e impact map' labels: 'automation:scheduled-update' title: 'chore: Update e2e impact map' body: | Automated refresh of the committed E2E impact map from the latest nightly coverage run. PRs use this map to scope which E2E specs run; refreshing it keeps newly-covered source→spec couplings visible so changes aren't declared uncovered and their specs silently skipped. _Generated by the E2E coverage nightly._ branch: update-e2e-impact-map 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: [commit-impact-map] if: needs.commit-impact-map.outputs.pull-request-number != '' uses: ./.github/workflows/util-approve-and-set-automerge.yml secrets: inherit with: pull-request-number: ${{ needs.commit-impact-map.outputs.pull-request-number }}