mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-19 07:36:52 +00:00
ci: Full nightly coverage report — unit + integration + E2E + Python merged into one lcov (no-changelog) (#31945)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,5 +65,5 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: backend-integration-postgres
|
||||
name: backend-integration-postgres
|
||||
files: ./packages/cli/coverage/cobertura-coverage.xml
|
||||
files: ./packages/cli/coverage/lcov.info
|
||||
fail_ci_if_error: false
|
||||
|
||||
@@ -10,6 +10,19 @@ concurrency:
|
||||
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
|
||||
@@ -24,6 +37,7 @@ jobs:
|
||||
# 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 }}
|
||||
@@ -31,15 +45,15 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup and Build janitor
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: pnpm turbo run build --filter=@n8n/playwright-janitor
|
||||
|
||||
- name: Generate matrix (10 shards, duration-weighted)
|
||||
- name: Generate matrix (16 shards, duration-weighted)
|
||||
id: gen
|
||||
run: |
|
||||
MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 10 --orchestrate)
|
||||
MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 16 --orchestrate)
|
||||
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
||||
|
||||
e2e:
|
||||
@@ -60,9 +74,114 @@ jobs:
|
||||
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
|
||||
artifact-path: packages/cli/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
|
||||
needs: [e2e, unit-coverage, python-coverage]
|
||||
if: always() && needs.e2e.result != 'skipped' && needs.e2e.result != 'cancelled'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
@@ -72,21 +191,34 @@ jobs:
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
||||
- name: Download shard artifacts
|
||||
- name: Download E2E shard artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: coverage-shard-*
|
||||
path: /tmp/shards/
|
||||
|
||||
- name: Build janitor (tested merge-coverage)
|
||||
# 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
|
||||
|
||||
# Report (shard lcovs → unified lcov) + impact map (per-spec lcovs →
|
||||
# spec-keyed map), both via the property-tested janitor merge-coverage.
|
||||
# Logic lives in the tested aggregate-coverage.mjs, not inline bash.
|
||||
# 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 --out=coverage
|
||||
run: |
|
||||
node scripts/aggregate-coverage.mjs \
|
||||
--shards=/tmp/shards \
|
||||
--unit-shards=/tmp/unit-lcovs \
|
||||
--out=coverage
|
||||
|
||||
- name: Upload Coverage Report Artifact
|
||||
if: always()
|
||||
@@ -96,14 +228,14 @@ jobs:
|
||||
path: packages/testing/playwright/coverage/
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload E2E Coverage to Codecov
|
||||
- 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: frontend-e2e
|
||||
name: playwright-e2e
|
||||
flags: nightly-full
|
||||
name: nightly-full-coverage
|
||||
fail_ci_if_error: false
|
||||
|
||||
# Codecov needs ~60-90s to finish indexing the fresh upload before its
|
||||
|
||||
@@ -160,6 +160,8 @@ jobs:
|
||||
packages/testing/playwright/playwright-report/
|
||||
packages/testing/playwright/coverage/lcov.info
|
||||
packages/testing/playwright/coverage/by-spec/
|
||||
packages/testing/playwright/coverage/index.html
|
||||
packages/testing/playwright/coverage/assets/
|
||||
retention-days: 1
|
||||
if-no-files-found: ignore
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: backend-unit
|
||||
name: backend-unit
|
||||
files: ./packages/**/coverage/cobertura-coverage.xml,./coverage/cobertura-coverage.xml
|
||||
files: ./packages/**/coverage/lcov.info
|
||||
|
||||
integration-test-backend:
|
||||
name: Backend Integration Tests
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: backend-integration
|
||||
name: backend-integration
|
||||
files: ./packages/**/coverage/cobertura-coverage.xml,./coverage/cobertura-coverage.xml
|
||||
files: ./packages/**/coverage/lcov.info
|
||||
|
||||
unit-test-nodes:
|
||||
name: Nodes Unit Tests
|
||||
@@ -194,7 +194,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: nodes-unit
|
||||
name: nodes-unit
|
||||
files: ./packages/**/coverage/cobertura-coverage.xml,./coverage/cobertura-coverage.xml
|
||||
files: ./packages/**/coverage/lcov.info
|
||||
|
||||
unit-test-frontend:
|
||||
name: Frontend (${{ matrix.shard }}/2)
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: frontend
|
||||
name: frontend-shard-${{ matrix.shard }}
|
||||
files: ./packages/**/coverage/cobertura-coverage.xml,./coverage/cobertura-coverage.xml
|
||||
files: ./packages/**/coverage/lcov.info
|
||||
|
||||
unit-test:
|
||||
name: Unit tests
|
||||
|
||||
@@ -17,6 +17,8 @@ coverage:
|
||||
project:
|
||||
default:
|
||||
threshold: 0.5
|
||||
flags:
|
||||
- nightly-full
|
||||
|
||||
github_checks:
|
||||
annotations: false
|
||||
@@ -36,6 +38,10 @@ flags:
|
||||
carryforward: true
|
||||
python:
|
||||
carryforward: true
|
||||
nightly-full:
|
||||
carryforward: true
|
||||
nightly-python:
|
||||
carryforward: true
|
||||
|
||||
component_management:
|
||||
default_rules:
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
const { pathsToModuleNameMapper } = require('ts-jest');
|
||||
const { compilerOptions } = require('get-tsconfig').getTsconfig().config;
|
||||
const { resolve } = require('path');
|
||||
const coverageExcludes = require('./jest.coverage-excludes');
|
||||
|
||||
/** @type {import('ts-jest').TsJestTransformerOptions} */
|
||||
const tsJestOptions = {
|
||||
@@ -100,7 +101,7 @@ const config = {
|
||||
};
|
||||
|
||||
if (process.env.CI === 'true') {
|
||||
config.collectCoverageFrom = ['src/**/*.ts'];
|
||||
config.collectCoverageFrom = ['src/**/*.ts', ...coverageExcludes];
|
||||
config.reporters = ['default', 'jest-junit'];
|
||||
config.coverageReporters = ['cobertura'];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Shared `collectCoverageFrom` exclusions for all jest packages. `**`-anchored
|
||||
// so they apply regardless of the source roots a package collects from.
|
||||
// Mirrored by packages/@n8n/vitest-config/coverage-excludes.ts (vitest) and the
|
||||
// V8 path filter in packages/testing/playwright/coverage-options.ts — keep in sync.
|
||||
module.exports = [
|
||||
'!**/*.spec.ts',
|
||||
'!**/*.test.ts',
|
||||
'!**/__tests__/**',
|
||||
'!**/__mocks__/**',
|
||||
'!**/*.d.ts',
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Files to drop from coverage. vitest 4 ships an empty default `exclude` and
|
||||
* only auto-excludes the test files it runs, so type decls, mocks and test
|
||||
* helpers otherwise land in the denominator at 0%. Spread these onto
|
||||
* `coverageConfigDefaults.exclude` in each config.
|
||||
*
|
||||
* Mirrors the jest equivalent at the repo root (jest.coverage-excludes.js) —
|
||||
* same intent, plain globs here vs `!`-negated globs there. Keep them in sync.
|
||||
*/
|
||||
export const coverageExcludes = [
|
||||
'**/*.spec.ts',
|
||||
'**/*.test.ts',
|
||||
'**/__tests__/**',
|
||||
'**/__mocks__/**',
|
||||
'**/*.d.ts',
|
||||
];
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { coverageConfigDefaults, defineConfig } from 'vitest/config';
|
||||
import type { InlineConfig } from 'vitest/node';
|
||||
|
||||
import { coverageExcludes } from './coverage-excludes.js';
|
||||
|
||||
export const createVitestConfig = (options: InlineConfig = {}) => {
|
||||
const vitestConfig = defineConfig({
|
||||
test: {
|
||||
@@ -13,6 +15,7 @@ export const createVitestConfig = (options: InlineConfig = {}) => {
|
||||
coverage: {
|
||||
enabled: false,
|
||||
include: ['src/**/*.{ts,vue}'],
|
||||
exclude: [...coverageConfigDefaults.exclude, ...coverageExcludes],
|
||||
provider: 'v8',
|
||||
reporter: ['text-summary', 'lcov', 'html-spa'],
|
||||
},
|
||||
@@ -29,8 +32,8 @@ export const createVitestConfig = (options: InlineConfig = {}) => {
|
||||
const { coverage } = vitestConfig.test;
|
||||
coverage.enabled = true;
|
||||
if (process.env.CI === 'true' && coverage.provider === 'v8') {
|
||||
coverage.include = ['src/**'];
|
||||
coverage.reporter = ['cobertura'];
|
||||
coverage.include = ['src/**/*.{ts,vue}'];
|
||||
coverage.reporter = ['lcov'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { coverageConfigDefaults, defineConfig } from 'vitest/config';
|
||||
import type { InlineConfig } from 'vitest/node';
|
||||
|
||||
import { coverageExcludes } from './coverage-excludes.js';
|
||||
|
||||
/**
|
||||
* Shared test options without the outer defineConfig wrapper.
|
||||
* Use this when you need to spread the config into workspace projects.
|
||||
@@ -16,8 +18,9 @@ export const createBaseInlineConfig = (options: InlineConfig = {}): InlineConfig
|
||||
coverage: {
|
||||
enabled: true,
|
||||
provider: 'v8',
|
||||
reporter: process.env.CI === 'true' ? 'cobertura' : 'text-summary',
|
||||
reporter: process.env.CI === 'true' ? 'lcov' : 'text-summary',
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: [...coverageConfigDefaults.exclude, ...coverageExcludes],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -5,7 +5,12 @@ process.env.TZ = 'UTC';
|
||||
module.exports = {
|
||||
...require('../../jest.config'),
|
||||
testPathIgnorePatterns: ['/dist/', '/node_modules/', '\\.integration\\.test\\.ts$'],
|
||||
collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'],
|
||||
collectCoverageFrom: [
|
||||
'credentials/**/*.ts',
|
||||
'nodes/**/*.ts',
|
||||
'utils/**/*.ts',
|
||||
...require('../../jest.coverage-excludes'),
|
||||
],
|
||||
globalSetup: '<rootDir>/test/globalSetup.ts',
|
||||
setupFilesAfterEnv: ['jest-expect-message', '<rootDir>/test/setup.ts'],
|
||||
};
|
||||
|
||||
@@ -27,9 +27,16 @@ export const coverageOptions: CoverageReportOptions = {
|
||||
entry.url.includes('/assets/') || /\/packages\/[^/]+(?:\/[^/]+)?\/dist\//.test(entry.url),
|
||||
// Keep first-party application source after source-map expansion; drop deps
|
||||
// and any unmapped dist files. NB nodes-base sources live under `nodes/`,
|
||||
// `credentials/` etc — not `src/` — so don't require `/src/`.
|
||||
// `credentials/` etc — not `src/` — so don't require `/src/`. The test/mock
|
||||
// exclusions mirror jest.coverage-excludes.js — keep them in sync.
|
||||
sourceFilter: (sourcePath) =>
|
||||
!sourcePath.includes('node_modules') && !sourcePath.includes('/dist/'),
|
||||
!sourcePath.includes('node_modules') &&
|
||||
!sourcePath.includes('/dist/') &&
|
||||
!sourcePath.endsWith('.d.ts') &&
|
||||
!sourcePath.endsWith('.spec.ts') &&
|
||||
!sourcePath.endsWith('.test.ts') &&
|
||||
!sourcePath.includes('/__tests__/') &&
|
||||
!sourcePath.includes('/__mocks__/'),
|
||||
// Key Codecov + the impact map on repo-relative `packages/.../src/...`.
|
||||
// Backend map-sources resolve to absolute repo paths (package-qualified);
|
||||
// the frontend bundle resolves relative, so its `src/...` sources have no
|
||||
|
||||
@@ -77,11 +77,25 @@ export const v8CoverageFixtures = {
|
||||
const specDir = join(BY_SPEC_DIR, slugify(spec));
|
||||
mkdirSync(specDir, { recursive: true });
|
||||
// Unique per test so multiple tests in one spec file accumulate (don't clobber).
|
||||
try {
|
||||
writeFileSync(
|
||||
join(specDir, `raw-${slugify(testInfo.testId)}.json`),
|
||||
JSON.stringify(perSpecRaw),
|
||||
);
|
||||
writeFileSync(join(specDir, '.spec'), spec);
|
||||
} catch (error) {
|
||||
// V8 coverage entries include full script source text. Tests that navigate
|
||||
// extensively (e.g. signout + signin in one test with resetOnNavigation:false)
|
||||
// accumulate enough script entries that JSON.stringify hits V8's ~536MB string
|
||||
// limit (RangeError: Invalid string length). The shard-level sharedReport
|
||||
// already received the data above, so aggregate coverage is unaffected — only
|
||||
// this test's per-spec impact map entry is dropped. Re-throw anything else: a
|
||||
// real write failure (disk full, permissions) must not silently lose attribution.
|
||||
if (!(error instanceof RangeError)) throw error;
|
||||
console.warn(
|
||||
`[coverage] per-spec raw write skipped for ${testInfo.titlePath.join(' > ')}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -262,7 +262,15 @@ export function getProjects(): Project[] {
|
||||
testIgnore: INSTANCE_AI_E2E_IGNORE,
|
||||
timeout: 60000,
|
||||
fullyParallel: true,
|
||||
use: { containerConfig: {} },
|
||||
use: {
|
||||
containerConfig: {},
|
||||
// Capture only on failure (global default is `on`). The shard artifact
|
||||
// is downloaded and aggregated each run, so keep it to coverage data
|
||||
// plus failure diagnostics, not full traces/videos for every test.
|
||||
trace: 'retain-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
});
|
||||
|
||||
projects.push({
|
||||
|
||||
@@ -7,21 +7,38 @@
|
||||
* `merge-coverage`. Replaces brittle inline `find`/`cp` bash in the workflows —
|
||||
* the janitor CLI path lives here, in one place, instead of per-YAML-string.
|
||||
*
|
||||
* node aggregate-coverage.mjs --shards=<dir> --out=<dir>
|
||||
* node aggregate-coverage.mjs --shards=<dir> --out=<dir> [--unit-shards=<dir>]
|
||||
*
|
||||
* --unit-shards: directory of downloaded unit + integration + frontend lcov
|
||||
* artifacts. Jest and vitest write absolute SF: paths in CI
|
||||
* (/home/runner/work/n8n/n8n/packages/cli/src/foo.ts); they are normalised
|
||||
* to repo-root-relative before merging with the E2E lcovs so MCR sees the
|
||||
* same key across all three suites.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, rmSync } from 'node:fs';
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
rmSync,
|
||||
} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const JANITOR_CLI = path.resolve(__dirname, '..', '..', 'janitor', 'dist', 'cli.js');
|
||||
// scripts/ → playwright/ → testing/ → packages/ → repo root
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..') + path.sep;
|
||||
|
||||
const arg = (name, fallback) =>
|
||||
process.argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3) ?? fallback;
|
||||
|
||||
const SHARDS = arg('shards', '/tmp/shards');
|
||||
const UNIT_SHARDS = arg('unit-shards', null);
|
||||
const OUT = arg('out', './coverage');
|
||||
|
||||
/** Recursively collect files under `dir` matching `predicate(filename)`. */
|
||||
@@ -35,12 +52,26 @@ function findFiles(dir, predicate, acc = []) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
/** Stage matching files into a fresh temp dir with unique names, return the dir. */
|
||||
/**
|
||||
* Rewrite absolute SF: paths to repo-root-relative. Jest and vitest write
|
||||
* absolute paths in CI; E2E lcovs are already repo-root-relative so the
|
||||
* regex is a no-op for them. Merging everything through the same transform
|
||||
* means MCR sees identical keys across all three suites.
|
||||
*/
|
||||
function normalizeLcov(content) {
|
||||
return content.replace(/^SF:(.+)$/gm, (line, p) =>
|
||||
p.startsWith(REPO_ROOT) ? `SF:${p.slice(REPO_ROOT.length)}` : line,
|
||||
);
|
||||
}
|
||||
|
||||
/** Stage files into a fresh temp dir with unique names, normalising SF: paths. */
|
||||
function stage(label, files) {
|
||||
const dir = path.join('/tmp', `agg-${label}`);
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
mkdirSync(dir, { recursive: true });
|
||||
files.forEach((f, i) => copyFileSync(f, path.join(dir, `${label}-${i}.lcov`)));
|
||||
files.forEach((f, i) => {
|
||||
writeFileSync(path.join(dir, `${label}-${i}.lcov`), normalizeLcov(readFileSync(f, 'utf8')));
|
||||
});
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -60,16 +91,25 @@ function merge(inputsDir, outLcov, outMap) {
|
||||
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
// 1. Report: shard-level lcovs (frontend + backend) → unified lcov for Codecov.
|
||||
const shardLcovs = findFiles(SHARDS, (name) => name === 'lcov.info');
|
||||
console.log(`Report: ${shardLcovs.length} shard lcov(s)`);
|
||||
if (shardLcovs.length) {
|
||||
merge(stage('report', shardLcovs), path.join(OUT, 'lcov.info'), '/tmp/agg-report-map.json');
|
||||
const unitLcovs = UNIT_SHARDS ? findFiles(UNIT_SHARDS, (name) => name === 'lcov.info') : [];
|
||||
const allReportLcovs = [...shardLcovs, ...unitLcovs];
|
||||
|
||||
// 1. Combined report → lcov.info (all layers: E2E + unit + integration + frontend).
|
||||
// Uploaded to Codecov as a single `nightly-full` flag — one authoritative number
|
||||
// rather than per-layer flags that require an approximated union at query time.
|
||||
console.log(
|
||||
`Report: ${shardLcovs.length} E2E shard lcov(s), ${unitLcovs.length} unit/integration/frontend lcov(s)`,
|
||||
);
|
||||
if (allReportLcovs.length) {
|
||||
merge(stage('report', allReportLcovs), path.join(OUT, 'lcov.info'), '/tmp/agg-report-map.json');
|
||||
} else {
|
||||
console.warn(' ⚠ no shard lcov.info found — skipping report');
|
||||
console.warn(' ⚠ no lcovs found — skipping report');
|
||||
}
|
||||
|
||||
// 2. Impact map: per-spec frontend lcovs (TN-tagged) → spec-keyed map for selection.
|
||||
// 2. Impact map: per-spec E2E lcovs (TN-tagged) → spec-keyed map for PR selection.
|
||||
// Unit/integration lcovs are not TN-tagged per test so they stay out of the
|
||||
// impact map for now — cross-layer per-test attribution is Phase 3 (DEVP-293).
|
||||
const specLcovs = findFiles(
|
||||
SHARDS,
|
||||
(name, p) => name.endsWith('.lcov') && p.includes(`${path.sep}by-spec${path.sep}`),
|
||||
|
||||
Reference in New Issue
Block a user