mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-28 23:37:00 +02:00
ci: Scope frontend + nodes unit tests via janitor (DEVP-194) (#31096)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
29fd8ccc4e
commit
e495833a64
14
.github/workflows/ci-pull-requests.yml
vendored
14
.github/workflows/ci-pull-requests.yml
vendored
|
|
@ -34,6 +34,8 @@ jobs:
|
|||
merge_base: ${{ steps.ci-filter.outputs.merge-base }}
|
||||
matrix: ${{ steps.generate-matrix.outputs.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: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
|
|
@ -145,6 +147,16 @@ jobs:
|
|||
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"
|
||||
|
||||
- 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'
|
||||
|
|
@ -153,6 +165,8 @@ jobs:
|
|||
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 }}
|
||||
secrets: inherit
|
||||
|
||||
typecheck:
|
||||
|
|
|
|||
37
.github/workflows/test-unit-reusable.yml
vendored
37
.github/workflows/test-unit-reusable.yml
vendored
|
|
@ -17,6 +17,23 @@ on:
|
|||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
affectedPackages:
|
||||
description: |
|
||||
Space-separated list of workspace packages affected by the PR, as
|
||||
computed by `janitor affected-packages` in install-and-build. Empty
|
||||
string means "no scoping data, run everything".
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
changedFiles:
|
||||
description: |
|
||||
Newline-separated list of CHANGED_FILES from ci-filter. Passed to
|
||||
per-package `janitor scope` invocations so test runners can filter
|
||||
to actually-changed source files via jest --findRelatedTests or
|
||||
vitest related.
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=7168
|
||||
|
||||
|
|
@ -26,6 +43,8 @@ jobs:
|
|||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
env:
|
||||
COVERAGE_ENABLED: ${{ inputs.collectCoverage }} # Coverage collected when true
|
||||
AFFECTED_PACKAGES: ${{ inputs.affectedPackages }}
|
||||
CHANGED_FILES: ${{ inputs.changedFiles }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
|
|
@ -69,6 +88,8 @@ jobs:
|
|||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
env:
|
||||
COVERAGE_ENABLED: ${{ inputs.collectCoverage }} # Coverage collected when true
|
||||
AFFECTED_PACKAGES: ${{ inputs.affectedPackages }}
|
||||
CHANGED_FILES: ${{ inputs.changedFiles }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
|
|
@ -112,13 +133,12 @@ jobs:
|
|||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
env:
|
||||
COVERAGE_ENABLED: ${{ inputs.collectCoverage }} # Coverage collected when true
|
||||
MERGE_BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || '' }}
|
||||
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || '' }}
|
||||
AFFECTED_PACKAGES: ${{ inputs.affectedPackages }}
|
||||
CHANGED_FILES: ${{ inputs.changedFiles }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
|
@ -126,12 +146,7 @@ jobs:
|
|||
node-version: ${{ inputs.nodeVersion }}
|
||||
|
||||
- name: Test Nodes
|
||||
run: |
|
||||
if [ -n "${MERGE_BASE:-}" ]; then
|
||||
pnpm turbo test:changed --filter=n8n-nodes-base --affected --summarize
|
||||
else
|
||||
pnpm turbo test --filter=n8n-nodes-base --summarize
|
||||
fi
|
||||
run: pnpm turbo test:changed --filter=n8n-nodes-base --summarize
|
||||
|
||||
- name: Send Test Stats
|
||||
if: ${{ !cancelled() }}
|
||||
|
|
@ -167,6 +182,8 @@ jobs:
|
|||
shard: [1, 2]
|
||||
env:
|
||||
COVERAGE_ENABLED: ${{ inputs.collectCoverage }}
|
||||
AFFECTED_PACKAGES: ${{ inputs.affectedPackages }}
|
||||
CHANGED_FILES: ${{ inputs.changedFiles }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
|
|
@ -178,7 +195,7 @@ jobs:
|
|||
node-version: ${{ inputs.nodeVersion }}
|
||||
|
||||
- name: Test
|
||||
run: pnpm test:ci:frontend --summarize -- --shard=${{ matrix.shard }}/2
|
||||
run: pnpm test:ci:frontend:changed --summarize -- --shard=${{ matrix.shard }}/2
|
||||
env:
|
||||
VITEST_SHARD: ${{ matrix.shard }}/2
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"test": "JEST_JUNIT_CLASSNAME={filepath} turbo run test",
|
||||
"test:ci": "turbo run test --continue --concurrency=1",
|
||||
"test:ci:frontend": "turbo run test --continue --filter='./packages/frontend/**'",
|
||||
"test:ci:frontend:changed": "turbo run test:changed --continue --filter='./packages/frontend/**'",
|
||||
"test:ci:backend": "turbo run test --continue --concurrency=1 --filter='!./packages/frontend/**'",
|
||||
"test:ci:backend:unit": "turbo run test:unit --continue --filter='!./packages/frontend/**'",
|
||||
"test:ci:backend:integration": "turbo run test:integration --continue --concurrency=1 --filter='!./packages/frontend/**'",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"preview": "vite preview",
|
||||
"test:dev": "vitest",
|
||||
"test": "vitest run",
|
||||
"test:changed": "janitor test-scoped --runner=vitest",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"lint": "eslint src --quiet",
|
||||
"lint:fix": "eslint src --fix",
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
"devDependencies": {
|
||||
"@iconify-json/mdi": "^1.1.54",
|
||||
"@n8n/eslint-config": "workspace:*",
|
||||
"@n8n/playwright-janitor": "workspace:*",
|
||||
"@n8n/stylelint-config": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:changed": "janitor test-scoped --runner=vitest",
|
||||
"test:dev": "vitest --silent=false",
|
||||
"lint": "eslint src --quiet",
|
||||
"lint:fix": "eslint src --fix",
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@n8n/eslint-config": "workspace:*",
|
||||
"@n8n/playwright-janitor": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@testing-library/vue": "catalog:frontend",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"typecheck": "vue-tsc --noEmit",
|
||||
"typecheck:watch": "vue-tsc --watch --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:changed": "janitor test-scoped --runner=vitest",
|
||||
"test:dev": "vitest",
|
||||
"chromatic": "chromatic",
|
||||
"format": "biome format --write . && prettier --write . --ignore-path ../../../../.prettierignore",
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@n8n/eslint-config": "workspace:*",
|
||||
"@n8n/playwright-janitor": "workspace:*",
|
||||
"@n8n/stylelint-config": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:changed": "janitor test-scoped --runner=vitest",
|
||||
"test:dev": "vitest --silent=false",
|
||||
"lint": "eslint src --quiet",
|
||||
"lint:fix": "eslint src --fix",
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@n8n/eslint-config": "workspace:*",
|
||||
"@n8n/playwright-janitor": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "catalog:frontend",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:changed": "janitor test-scoped --runner=vitest",
|
||||
"test:dev": "vitest --silent=false",
|
||||
"lint": "eslint src --quiet",
|
||||
"lint:fix": "eslint src --fix",
|
||||
|
|
@ -45,6 +46,7 @@
|
|||
"devDependencies": {
|
||||
"@n8n/eslint-config": "workspace:*",
|
||||
"@n8n/i18n": "workspace:*",
|
||||
"@n8n/playwright-janitor": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "catalog:frontend",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:changed": "janitor test-scoped --runner=vitest",
|
||||
"test:dev": "vitest --silent=false",
|
||||
"lint": "eslint src --quiet",
|
||||
"lint:fix": "eslint src --fix",
|
||||
|
|
@ -37,6 +38,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@n8n/eslint-config": "workspace:*",
|
||||
"@n8n/playwright-janitor": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "catalog:frontend",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"format:check": "biome ci . && prettier --check . --ignore-path ../../../.prettierignore",
|
||||
"serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vite --host 0.0.0.0 --port 8080 dev",
|
||||
"test": "vitest run",
|
||||
"test:changed": "janitor test-scoped --runner=vitest",
|
||||
"test:dev": "vitest --silent=false"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -118,6 +119,7 @@
|
|||
"@faker-js/faker": "^8.0.2",
|
||||
"@iconify/json": "^2.2.349",
|
||||
"@n8n/eslint-config": "workspace:*",
|
||||
"@n8n/playwright-janitor": "workspace:*",
|
||||
"@n8n/stylelint-config": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"lint:fix": "eslint nodes credentials utils test --fix",
|
||||
"watch": "tsc-watch -p tsconfig.build.cjs.json --onCompilationComplete \"pnpm copy-nodes-json && tsc-alias -p tsconfig.build.cjs.json\" --onSuccess \"pnpm n8n-generate-metadata\"",
|
||||
"test": "jest",
|
||||
"test:changed": "node scripts/test-changed.mjs",
|
||||
"test:changed": "janitor test-scoped --runner=jest",
|
||||
"test:dev": "jest --watch",
|
||||
"test:integration:skip": "vitest run --config vitest.integration.config.ts"
|
||||
},
|
||||
|
|
@ -869,6 +869,7 @@
|
|||
"devDependencies": {
|
||||
"@n8n/client-oauth2": "workspace:*",
|
||||
"@n8n/eslint-plugin-community-nodes": "workspace:*",
|
||||
"@n8n/playwright-janitor": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@types/amqplib": "^0.10.1",
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
import { execSync, spawnSync } from 'node:child_process';
|
||||
|
||||
const mergeBase = process.env.MERGE_BASE;
|
||||
const cwd = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
||||
const log = (m) => console.log(`[test-changed] ${m}`);
|
||||
const diff = (range, paths = '') =>
|
||||
execSync(`git diff --name-only ${range} ${paths}`.trim(), { cwd, encoding: 'utf-8' }).trim();
|
||||
const runJest = (extra = []) => {
|
||||
const r = spawnSync('jest', [...extra, ...process.argv.slice(2)], { stdio: 'inherit' });
|
||||
process.exit(r.status ?? 1);
|
||||
};
|
||||
|
||||
if (!mergeBase) {
|
||||
log('MERGE_BASE unset → full');
|
||||
runJest();
|
||||
}
|
||||
|
||||
// Turbo --affected can invoke us when only an upstream package changed, in
|
||||
// which case jest --changedSince would walk an empty in-package diff and run
|
||||
// zero tests — the one direction we must never take.
|
||||
const inPackage = diff(`${mergeBase}...HEAD`, '-- packages/nodes-base/');
|
||||
if (!inPackage) {
|
||||
log('upstream-only change → full');
|
||||
runJest();
|
||||
}
|
||||
|
||||
if (/\/(jest\.config\.js|test\/(setup|globalSetup)\.ts|package\.json)$/m.test(inPackage)) {
|
||||
log('in-package config change → full');
|
||||
runJest();
|
||||
}
|
||||
|
||||
const all = diff(`${mergeBase}...HEAD`);
|
||||
if (/^(pnpm-lock\.yaml|package\.json|packages\/cli\/src\/public-api\/v1\/)/m.test(all)) {
|
||||
log('cross-cutting change → full');
|
||||
runJest();
|
||||
}
|
||||
|
||||
log(`scoping via jest --changedSince=${mergeBase}`);
|
||||
runJest([`--changedSince=${mergeBase}`]);
|
||||
|
|
@ -127,7 +127,7 @@ For existing codebases with many violations, use a baseline to enable incrementa
|
|||
|
||||
```bash
|
||||
# Create baseline of current violations
|
||||
playwright-janitor baseline
|
||||
janitor baseline
|
||||
|
||||
# Commit the baseline
|
||||
git add .janitor-baseline.json
|
||||
|
|
@ -140,10 +140,10 @@ Once a baseline exists, janitor and TCR **only fail on new violations**. Pre-exi
|
|||
|
||||
```bash
|
||||
# This now passes (only checks for NEW violations)
|
||||
playwright-janitor tcr --execute -m="Add new feature"
|
||||
janitor tcr --execute -m="Add new feature"
|
||||
|
||||
# As you fix violations, update the baseline (manual commit required - TCR won't commit baseline changes)
|
||||
playwright-janitor baseline
|
||||
janitor baseline
|
||||
git add .janitor-baseline.json
|
||||
git commit -m "chore: update baseline after cleanup"
|
||||
```
|
||||
|
|
@ -156,13 +156,13 @@ View all available rules with their descriptions:
|
|||
|
||||
```bash
|
||||
# Human-readable list
|
||||
playwright-janitor rules
|
||||
janitor rules
|
||||
|
||||
# JSON output (for AI agents/automation)
|
||||
playwright-janitor rules --json
|
||||
janitor rules --json
|
||||
|
||||
# Verbose (includes target globs)
|
||||
playwright-janitor rules --verbose
|
||||
janitor rules --verbose
|
||||
```
|
||||
|
||||
The JSON output is useful for AI agents that need to understand the rules before writing code.
|
||||
|
|
@ -173,20 +173,76 @@ Discover test specs via AST analysis and distribute them across CI shards:
|
|||
|
||||
```bash
|
||||
# Discover specs and capabilities (JSON output)
|
||||
playwright-janitor discover
|
||||
janitor discover
|
||||
|
||||
# Distribute specs across shards (JSON output)
|
||||
playwright-janitor orchestrate --shards=14
|
||||
janitor orchestrate --shards=14
|
||||
|
||||
# Get specs for a single shard (0-indexed)
|
||||
playwright-janitor orchestrate --shards=14 --shard-index=0
|
||||
janitor orchestrate --shards=14 --shard-index=0
|
||||
|
||||
# Only include specs affected by git changes
|
||||
playwright-janitor orchestrate --shards=14 --impact
|
||||
janitor orchestrate --shards=14 --impact
|
||||
```
|
||||
|
||||
Discovery detects `test.fixme()` and `test.skip()` via AST and excludes them automatically. Capability tags (`@capability:proxy`) are extracted for grouping.
|
||||
|
||||
### Workspace-Wide CI Test Scoping
|
||||
|
||||
The `affected-packages`, `scope`, and `test-scoped` subcommands operate at
|
||||
the workspace level — they do not require a `janitor.config.js` and can be
|
||||
invoked from any package via `pnpm exec janitor ...` (the package's bin is
|
||||
named `janitor`). They replace the need for `turbo run --affected`, which
|
||||
requires git history on the runner and is incompatible with `fetch-depth: 1`
|
||||
checkouts.
|
||||
|
||||
**Pipeline:**
|
||||
|
||||
```
|
||||
ci-filter (in install-and-build)
|
||||
│
|
||||
└─→ CHANGED_FILES (newline-separated)
|
||||
│
|
||||
├─→ janitor affected-packages (walks pnpm workspace dep graph)
|
||||
│ │
|
||||
│ └─→ AFFECTED_PACKAGES (space-separated list)
|
||||
│ │
|
||||
│ └─→ passed to test jobs as workflow input
|
||||
│
|
||||
└─→ CHANGED_FILES forwarded to test jobs
|
||||
│
|
||||
└─→ janitor test-scoped --runner=jest|vitest (per-package)
|
||||
│
|
||||
├─→ SKIP → exit 0 (no in-package changes)
|
||||
├─→ RUN_FULL → spawn runner with no scope flags
|
||||
└─→ scoped → jest --findRelatedTests / vitest related
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
# Walk the workspace dep graph. Output: one package name per line.
|
||||
CHANGED_FILES="packages/workflow/src/x.ts" janitor affected-packages
|
||||
|
||||
# Compute scope for the cwd package. Output: SKIP | RUN_FULL | <files>
|
||||
janitor scope --runner=vitest
|
||||
|
||||
# Compute scope AND spawn the runner. Unrecognised flags forward to runner.
|
||||
janitor test-scoped --runner=vitest --shard=1/2 --coverage
|
||||
```
|
||||
|
||||
**Bailout triggers (force ALL packages):** `pnpm-lock.yaml`, root `package.json`.
|
||||
|
||||
**Per-package bailout (force full suite):** `jest.config.*`, `vitest.config.*`,
|
||||
`vite.config.*` (vitest reads vite config), `package.json`, `tsconfig.*`,
|
||||
plus setup files at `<pkg>/jest.setup.*`, `<pkg>/vitest.setup.*`, and
|
||||
`<pkg>/src/__tests__/setup.*`. The scope analyzer detects these and emits
|
||||
`RUN_FULL`; `test-scoped` then spawns the runner without scope flags.
|
||||
|
||||
**Turbo extra inputs:** `n8n-nodes-base#test`'s declared input
|
||||
`../cli/src/public-api/v1/**/*.yml` is honoured — a change to that yml
|
||||
marks nodes-base as affected.
|
||||
|
||||
## Rules
|
||||
|
||||
### Architecture Rules
|
||||
|
|
|
|||
10
packages/testing/janitor/bin/janitor.mjs
Executable file
10
packages/testing/janitor/bin/janitor.mjs
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env node
|
||||
// Shim that exists in the source tree so pnpm can create the bin symlink at
|
||||
// install time, before `pnpm build` produces dist/cli.js. Re-imports the
|
||||
// built entry at invocation time, by which point the build has run via
|
||||
// turbo's `^build` dependency on consuming tasks.
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
await import(resolve(here, '..', 'dist', 'cli.js'));
|
||||
|
|
@ -4,7 +4,7 @@ import { baseConfig } from '@n8n/eslint-config/base';
|
|||
export default defineConfig(
|
||||
baseConfig,
|
||||
{
|
||||
ignores: ['coverage/**'],
|
||||
ignores: ['coverage/**', 'bin/**'],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@
|
|||
"description": "Static analysis and architecture enforcement for Playwright test suites",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"janitor": "bin/janitor.mjs"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
|
|
@ -30,7 +34,8 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@n8n/rules-engine": "workspace:*",
|
||||
"glob": "^10.0.0"
|
||||
"glob": "^10.0.0",
|
||||
"yaml": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ts-morph": "catalog:"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Playwright Janitor CLI
|
||||
* Janitor CLI
|
||||
*
|
||||
* Usage:
|
||||
* playwright-janitor # Run all rules
|
||||
* playwright-janitor inventory # Show codebase inventory
|
||||
* playwright-janitor impact # Show impact of changes
|
||||
* playwright-janitor tcr # TCR workflow
|
||||
* playwright-janitor --help # Show help
|
||||
* janitor # Run all rules
|
||||
* janitor inventory # Show codebase inventory
|
||||
* janitor impact # Show impact of changes
|
||||
* janitor tcr # TCR workflow
|
||||
* janitor affected-packages # List workspace packages affected by changed files
|
||||
* janitor scope # Compute per-package jest/vitest scope list
|
||||
* janitor --help # Show help
|
||||
*
|
||||
* The `affected-packages` and `scope` subcommands are workspace-wide utilities
|
||||
* for CI test scoping — they do not require a janitor.config.js and can be
|
||||
* invoked from any package via `pnpm exec janitor ...`.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
|
|
@ -25,8 +31,12 @@ import {
|
|||
showTcrHelp,
|
||||
showDiscoverHelp,
|
||||
showOrchestrateHelp,
|
||||
showAffectedPackagesHelp,
|
||||
showScopeHelp,
|
||||
showTestScopedHelp,
|
||||
} from './cli/index.js';
|
||||
import { setConfig, getConfig, defineConfig, type JanitorConfig } from './config.js';
|
||||
import { affectedPackages, findWorkspaceRoot } from './core/affected-packages-analyzer.js';
|
||||
import {
|
||||
generateBaseline,
|
||||
saveBaseline,
|
||||
|
|
@ -64,8 +74,10 @@ import { orchestrate } from './core/orchestrator.js';
|
|||
import { createProject } from './core/project-loader.js';
|
||||
import { toJSON, toConsole, printFixResults } from './core/reporter.js';
|
||||
import { filterToFailedSpecs } from './core/retry-filter.js';
|
||||
import { computeScope, formatScope } from './core/scope-analyzer.js';
|
||||
import { TcrExecutor, formatTcrResultConsole, formatTcrResultJSON } from './core/tcr-executor.js';
|
||||
import { TestDiscoveryAnalyzer } from './core/test-discovery-analyzer.js';
|
||||
import { runTestScoped } from './core/test-scoped-runner.js';
|
||||
import { createDefaultRunner } from './index.js';
|
||||
import type { RunOptions } from './types.js';
|
||||
import { resolveInputPaths } from './utils/paths.js';
|
||||
|
|
@ -576,6 +588,62 @@ async function runOrchestrate(options: CliOptions): Promise<void> {
|
|||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read CHANGED_FILES from --changed-files flag or env. Returns null when
|
||||
* neither is set — callers treat that as "no signal, run everything" so local
|
||||
* dev (`pnpm test:changed` with no env) doesn't silently skip tests.
|
||||
*/
|
||||
function readChangedFiles(options: CliOptions): string[] | null {
|
||||
const flag = options.changedFiles;
|
||||
const env = process.env.CHANGED_FILES;
|
||||
if (flag === undefined && env === undefined) return null;
|
||||
const raw = flag ?? env ?? '';
|
||||
return raw
|
||||
.split(/[\n,]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
}
|
||||
|
||||
function runAffectedPackages(options: CliOptions): void {
|
||||
const result = affectedPackages({
|
||||
rootDir: findWorkspaceRoot(process.cwd()),
|
||||
changedFiles: readChangedFiles(options),
|
||||
});
|
||||
console.log(result.join('\n'));
|
||||
}
|
||||
|
||||
function runTestScopedCmd(options: CliOptions): void {
|
||||
if (!options.runner) {
|
||||
console.error('Error: --runner=jest|vitest is required');
|
||||
process.exit(1);
|
||||
}
|
||||
const packageDir = options.packageDir ?? process.cwd();
|
||||
const changedFiles = readChangedFiles(options);
|
||||
const exitCode = runTestScoped({
|
||||
runner: options.runner,
|
||||
packageDir,
|
||||
rootDir: findWorkspaceRoot(process.cwd()),
|
||||
changedFiles,
|
||||
passthroughArgs: options.passthroughArgs,
|
||||
});
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
function runScope(options: CliOptions): void {
|
||||
if (!options.runner) {
|
||||
console.error('Error: --runner=jest|vitest is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = computeScope({
|
||||
runner: options.runner,
|
||||
packageDir: options.packageDir ?? process.cwd(),
|
||||
changedFiles: readChangedFiles(options),
|
||||
rootDir: findWorkspaceRoot(process.cwd()),
|
||||
});
|
||||
console.log(formatScope(result));
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const options = parseArgs();
|
||||
|
||||
|
|
@ -606,12 +674,35 @@ async function main(): Promise<void> {
|
|||
case 'orchestrate':
|
||||
showOrchestrateHelp();
|
||||
break;
|
||||
case 'affected-packages':
|
||||
showAffectedPackagesHelp();
|
||||
break;
|
||||
case 'scope':
|
||||
showScopeHelp();
|
||||
break;
|
||||
case 'test-scoped':
|
||||
showTestScopedHelp();
|
||||
break;
|
||||
default:
|
||||
showHelp();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Workspace-wide CI utilities: no config file required.
|
||||
if (options.command === 'affected-packages') {
|
||||
runAffectedPackages(options);
|
||||
return;
|
||||
}
|
||||
if (options.command === 'scope') {
|
||||
runScope(options);
|
||||
return;
|
||||
}
|
||||
if (options.command === 'test-scoped') {
|
||||
runTestScopedCmd(options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.list) {
|
||||
const runner = createDefaultRunner();
|
||||
console.log('\nAvailable rules:\n');
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export type Command =
|
|||
| 'rules'
|
||||
| 'discover'
|
||||
| 'orchestrate'
|
||||
| 'affected-packages'
|
||||
| 'scope'
|
||||
| 'test-scoped'
|
||||
| 'filter-shard';
|
||||
|
||||
export interface CliOptions {
|
||||
|
|
@ -51,6 +54,12 @@ export interface CliOptions {
|
|||
shards?: number;
|
||||
shardIndex?: number;
|
||||
impact: boolean;
|
||||
// Affected-packages / scope options
|
||||
changedFiles?: string;
|
||||
runner?: 'jest' | 'vitest';
|
||||
packageDir?: string;
|
||||
/** Anything after `--` — forwarded to the test runner by `test-scoped`. */
|
||||
passthroughArgs: string[];
|
||||
// filter-shard-specific options
|
||||
url?: string;
|
||||
}
|
||||
|
|
@ -64,6 +73,9 @@ const SUBCOMMANDS: Record<string, Command> = {
|
|||
rules: 'rules',
|
||||
discover: 'discover',
|
||||
orchestrate: 'orchestrate',
|
||||
'affected-packages': 'affected-packages',
|
||||
scope: 'scope',
|
||||
'test-scoped': 'test-scoped',
|
||||
'filter-shard': 'filter-shard',
|
||||
};
|
||||
|
||||
|
|
@ -171,6 +183,19 @@ const VALUE_FLAG_HANDLERS: Record<string, (options: CliOptions, value: string) =
|
|||
'--shard-index=': (opts, value) => {
|
||||
opts.shardIndex = Number.parseInt(value, 10);
|
||||
},
|
||||
'--changed-files=': (opts, value) => {
|
||||
opts.changedFiles = value;
|
||||
},
|
||||
'--runner=': (opts, value) => {
|
||||
if (value === 'jest' || value === 'vitest') {
|
||||
opts.runner = value;
|
||||
} else {
|
||||
throw new Error(`Unknown --runner=${value}. Expected 'jest' or 'vitest'.`);
|
||||
}
|
||||
},
|
||||
'--package-dir=': (opts, value) => {
|
||||
opts.packageDir = value;
|
||||
},
|
||||
'--url=': (opts, value) => {
|
||||
opts.url = value;
|
||||
},
|
||||
|
|
@ -204,6 +229,10 @@ function createDefaultOptions(): CliOptions {
|
|||
shards: undefined,
|
||||
shardIndex: undefined,
|
||||
impact: false,
|
||||
changedFiles: undefined,
|
||||
runner: undefined,
|
||||
packageDir: undefined,
|
||||
passthroughArgs: [],
|
||||
url: undefined,
|
||||
};
|
||||
}
|
||||
|
|
@ -218,10 +247,29 @@ function parseSubcommand(args: string[]): { command: Command; startIdx: number }
|
|||
return { command: 'analyze', startIdx: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* For test-scoped, unrecognised flags must forward to the underlying runner
|
||||
* (jest/vitest) — they can't be silently dropped because consumers compose
|
||||
* extra flags via turbo + npm script chains. For all other subcommands the
|
||||
* passthrough list stays empty and unused.
|
||||
*/
|
||||
const PASSTHROUGH_COMMANDS = new Set<Command>(['test-scoped']);
|
||||
|
||||
function parseFlags(args: string[], startIdx: number, options: CliOptions): void {
|
||||
const allowPassthrough = PASSTHROUGH_COMMANDS.has(options.command);
|
||||
let passthroughActive = false;
|
||||
for (let i = startIdx; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (passthroughActive) {
|
||||
options.passthroughArgs.push(arg);
|
||||
continue;
|
||||
}
|
||||
if (arg === '--') {
|
||||
passthroughActive = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle simple flags
|
||||
const handler = FLAG_HANDLERS[arg];
|
||||
if (handler) {
|
||||
|
|
@ -230,13 +278,18 @@ function parseFlags(args: string[], startIdx: number, options: CliOptions): void
|
|||
}
|
||||
|
||||
// Handle value flags
|
||||
let matched = false;
|
||||
for (const [prefix, valueHandler] of Object.entries(VALUE_FLAG_HANDLERS)) {
|
||||
if (arg.startsWith(prefix)) {
|
||||
const value = arg.slice(prefix.length);
|
||||
valueHandler(options, value);
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched && allowPassthrough) {
|
||||
options.passthroughArgs.push(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -210,6 +210,67 @@ Examples:
|
|||
`);
|
||||
}
|
||||
|
||||
// --changed-files accepts repo-root-relative paths, newline-separated OR
|
||||
// comma-separated. Same format as ci-filter's `changed-files` output.
|
||||
// Example: --changed-files="packages/cli/src/x.ts
|
||||
// packages/workflow/src/y.ts"
|
||||
|
||||
export function showAffectedPackagesHelp(): void {
|
||||
console.log(`
|
||||
Affected Packages - List workspace packages affected by changed files
|
||||
|
||||
Walks the pnpm workspace dependency graph: directly-affected packages plus
|
||||
everything transitively downstream. Outputs one package name per line.
|
||||
|
||||
Usage:
|
||||
janitor affected-packages [--changed-files=<list>]
|
||||
|
||||
--changed-files: newline- OR comma-separated repo-root-relative paths.
|
||||
Defaults to $CHANGED_FILES env var.
|
||||
|
||||
When neither --changed-files nor $CHANGED_FILES is set, returns ALL packages
|
||||
(safe default for local dev).
|
||||
|
||||
Bailout triggers (return ALL packages): pnpm-lock.yaml, root package.json.
|
||||
`);
|
||||
}
|
||||
|
||||
export function showScopeHelp(): void {
|
||||
console.log(`
|
||||
Scope - Per-package jest/vitest scope from changed files
|
||||
|
||||
Usage:
|
||||
janitor scope --runner=<jest|vitest> [--package-dir=<dir>] [--changed-files=<list>]
|
||||
|
||||
--package-dir: defaults to cwd (matches how pnpm/turbo invoke test scripts).
|
||||
--changed-files: newline- OR comma-separated repo-root-relative paths.
|
||||
Defaults to $CHANGED_FILES env var.
|
||||
|
||||
Output (single line on stdout):
|
||||
SKIP No in-package files changed
|
||||
RUN_FULL Config file changed, OR no CHANGED_FILES signal (local dev)
|
||||
<files> Pass to jest --findRelatedTests / vitest related
|
||||
`);
|
||||
}
|
||||
|
||||
export function showTestScopedHelp(): void {
|
||||
console.log(`
|
||||
Test-Scoped - Compute scope and spawn jest/vitest with the right flags
|
||||
|
||||
Usage:
|
||||
janitor test-scoped --runner=<jest|vitest> [--package-dir=<dir>] [--changed-files=<list>] [extra runner args]
|
||||
|
||||
--package-dir: defaults to cwd (matches how pnpm/turbo invoke test scripts).
|
||||
--changed-files: newline- OR comma-separated repo-root-relative paths.
|
||||
Defaults to $CHANGED_FILES env var.
|
||||
|
||||
Local dev (no $CHANGED_FILES set): runs the full suite.
|
||||
CI: scopes via jest --findRelatedTests / vitest related --run, or skips
|
||||
if the package wasn't touched. Unrecognised flags are forwarded to the
|
||||
runner.
|
||||
`);
|
||||
}
|
||||
|
||||
export function showRulesHelp(): void {
|
||||
console.log(`
|
||||
Rules - Show detailed information about available rules
|
||||
|
|
|
|||
|
|
@ -9,4 +9,7 @@ export {
|
|||
showTcrHelp,
|
||||
showDiscoverHelp,
|
||||
showOrchestrateHelp,
|
||||
showAffectedPackagesHelp,
|
||||
showScopeHelp,
|
||||
showTestScopedHelp,
|
||||
} from './help.js';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { affectedPackages, findWorkspaceRoot } from './affected-packages-analyzer.js';
|
||||
|
||||
interface PackageSpec {
|
||||
name: string;
|
||||
deps?: string[];
|
||||
}
|
||||
|
||||
interface TurboTaskSpec {
|
||||
taskId: string;
|
||||
inputs: string[];
|
||||
}
|
||||
|
||||
function makeFixture(opts: {
|
||||
patterns: string[];
|
||||
packages: Record<string, PackageSpec>;
|
||||
turboTasks?: TurboTaskSpec[];
|
||||
}): string {
|
||||
const root = join(tmpdir(), `janitor-affected-${Math.random().toString(36).slice(2)}`);
|
||||
mkdirSync(root, { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, 'pnpm-workspace.yaml'),
|
||||
`packages:\n${opts.patterns.map((p) => ` - '${p}'`).join('\n')}\n`,
|
||||
);
|
||||
writeFileSync(join(root, 'package.json'), JSON.stringify({ name: 'monorepo-root' }));
|
||||
|
||||
for (const [dir, spec] of Object.entries(opts.packages)) {
|
||||
const pkgDir = join(root, dir);
|
||||
mkdirSync(pkgDir, { recursive: true });
|
||||
const pkg: Record<string, unknown> = { name: spec.name };
|
||||
if (spec.deps && spec.deps.length > 0) {
|
||||
pkg.dependencies = Object.fromEntries(spec.deps.map((d) => [d, 'workspace:*']));
|
||||
}
|
||||
writeFileSync(join(pkgDir, 'package.json'), JSON.stringify(pkg));
|
||||
}
|
||||
|
||||
if (opts.turboTasks) {
|
||||
writeFileSync(
|
||||
join(root, 'turbo.json'),
|
||||
JSON.stringify({
|
||||
tasks: Object.fromEntries(opts.turboTasks.map((t) => [t.taskId, { inputs: t.inputs }])),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
describe('affectedPackages', () => {
|
||||
it('returns all packages when CHANGED_FILES signal is missing', () => {
|
||||
const rootDir = makeFixture({
|
||||
patterns: ['packages/*'],
|
||||
packages: { 'packages/a': { name: 'a' }, 'packages/b': { name: 'b' } },
|
||||
});
|
||||
expect(affectedPackages({ rootDir, changedFiles: null })).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('returns empty when changed-files list is explicitly empty', () => {
|
||||
const rootDir = makeFixture({
|
||||
patterns: ['packages/*'],
|
||||
packages: { 'packages/a': { name: 'a' }, 'packages/b': { name: 'b' } },
|
||||
});
|
||||
expect(affectedPackages({ rootDir, changedFiles: [] })).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns just the directly-changed package when no deps', () => {
|
||||
const rootDir = makeFixture({
|
||||
patterns: ['packages/*'],
|
||||
packages: { 'packages/a': { name: 'a' }, 'packages/b': { name: 'b' } },
|
||||
});
|
||||
expect(affectedPackages({ rootDir, changedFiles: ['packages/a/src/index.ts'] })).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('includes transitive downstream packages', () => {
|
||||
const rootDir = makeFixture({
|
||||
patterns: ['packages/*'],
|
||||
packages: {
|
||||
'packages/workflow': { name: 'workflow' },
|
||||
'packages/core': { name: 'core', deps: ['workflow'] },
|
||||
'packages/cli': { name: 'cli', deps: ['core'] },
|
||||
'packages/unrelated': { name: 'unrelated' },
|
||||
},
|
||||
});
|
||||
expect(affectedPackages({ rootDir, changedFiles: ['packages/workflow/src/index.ts'] })).toEqual(
|
||||
['cli', 'core', 'workflow'],
|
||||
);
|
||||
});
|
||||
|
||||
it('expands all packages when pnpm-lock.yaml changes', () => {
|
||||
const rootDir = makeFixture({
|
||||
patterns: ['packages/*'],
|
||||
packages: { 'packages/a': { name: 'a' }, 'packages/b': { name: 'b' } },
|
||||
});
|
||||
expect(affectedPackages({ rootDir, changedFiles: ['pnpm-lock.yaml'] })).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('expands all packages when root package.json changes', () => {
|
||||
const rootDir = makeFixture({
|
||||
patterns: ['packages/*'],
|
||||
packages: { 'packages/a': { name: 'a' }, 'packages/b': { name: 'b' } },
|
||||
});
|
||||
expect(affectedPackages({ rootDir, changedFiles: ['package.json'] })).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('handles turbo extra-inputs pointing at another package', () => {
|
||||
const rootDir = makeFixture({
|
||||
patterns: ['packages/*'],
|
||||
packages: {
|
||||
'packages/cli': { name: 'n8n' },
|
||||
'packages/nodes-base': { name: 'n8n-nodes-base' },
|
||||
},
|
||||
turboTasks: [
|
||||
{ taskId: 'n8n-nodes-base#test', inputs: ['../cli/src/public-api/v1/**/*.yml'] },
|
||||
],
|
||||
});
|
||||
expect(
|
||||
affectedPackages({
|
||||
rootDir,
|
||||
changedFiles: ['packages/cli/src/public-api/v1/openapi.yml'],
|
||||
}),
|
||||
).toEqual(['n8n', 'n8n-nodes-base']);
|
||||
});
|
||||
|
||||
it('matches nested workspace patterns (frontend/**)', () => {
|
||||
const rootDir = makeFixture({
|
||||
patterns: ['packages/frontend/**'],
|
||||
packages: {
|
||||
'packages/frontend/editor-ui': { name: 'editor-ui' },
|
||||
'packages/frontend/@n8n/stores': { name: 'stores' },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
affectedPackages({ rootDir, changedFiles: ['packages/frontend/@n8n/stores/src/auth.ts'] }),
|
||||
).toEqual(['stores']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findWorkspaceRoot', () => {
|
||||
it('walks up to find pnpm-workspace.yaml', () => {
|
||||
const root = makeFixture({
|
||||
patterns: ['packages/*'],
|
||||
packages: { 'packages/a': { name: 'a' } },
|
||||
});
|
||||
expect(findWorkspaceRoot(join(root, 'packages', 'a'))).toBe(root);
|
||||
});
|
||||
|
||||
it('throws when no workspace root above startDir', () => {
|
||||
expect(() => findWorkspaceRoot('/')).toThrow(/Could not locate/);
|
||||
});
|
||||
});
|
||||
156
packages/testing/janitor/src/core/affected-packages-analyzer.ts
Normal file
156
packages/testing/janitor/src/core/affected-packages-analyzer.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* Given a list of changed files, returns the workspace packages affected —
|
||||
* the package containing each file, plus everything transitively downstream.
|
||||
* Pure-Node (no git); designed to be invoked once per CI run and passed to
|
||||
* test jobs as filter args.
|
||||
*/
|
||||
|
||||
import { globSync } from 'glob';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
|
||||
import { findWorkspaceRoot, toPosix } from './path-utils.js';
|
||||
|
||||
function parseJsonFile<T>(path: string): T {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, 'utf-8')) as T;
|
||||
} catch (cause) {
|
||||
throw new Error(`Failed to parse ${path}: ${(cause as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export { findWorkspaceRoot } from './path-utils.js';
|
||||
|
||||
interface WorkspacePackage {
|
||||
name: string;
|
||||
dir: string;
|
||||
workspaceDeps: string[];
|
||||
}
|
||||
|
||||
export interface AnalyzeOptions {
|
||||
rootDir?: string;
|
||||
/** Repo-root-relative, forward slashes. `null` = no signal → all packages. */
|
||||
changedFiles: string[] | null;
|
||||
}
|
||||
|
||||
const GLOBAL_TRIGGER_FILES = new Set(['pnpm-lock.yaml', 'package.json']);
|
||||
|
||||
function loadWorkspacePackages(rootDir: string): WorkspacePackage[] {
|
||||
const wsFile = join(rootDir, 'pnpm-workspace.yaml');
|
||||
if (!existsSync(wsFile)) throw new Error(`pnpm-workspace.yaml not found at ${wsFile}`);
|
||||
const patterns =
|
||||
(parseYaml(readFileSync(wsFile, 'utf-8')) as { packages?: string[] } | null)?.packages ?? [];
|
||||
|
||||
const dirs = new Set<string>();
|
||||
for (const pattern of patterns) {
|
||||
for (const pkgJson of globSync(`${pattern}/package.json`, {
|
||||
cwd: rootDir,
|
||||
ignore: '**/node_modules/**',
|
||||
})) {
|
||||
dirs.add(toPosix(pkgJson.replace(/\/package\.json$/, '')));
|
||||
}
|
||||
}
|
||||
|
||||
const known = new Set<string>();
|
||||
const pkgs: Array<{ dir: string; pkg: Record<string, unknown> }> = [];
|
||||
for (const dir of dirs) {
|
||||
const pkg = parseJsonFile<Record<string, unknown>>(join(rootDir, dir, 'package.json'));
|
||||
if (typeof pkg.name !== 'string') continue;
|
||||
known.add(pkg.name);
|
||||
pkgs.push({ dir, pkg });
|
||||
}
|
||||
|
||||
return pkgs.map(({ dir, pkg }) => ({
|
||||
name: pkg.name as string,
|
||||
dir,
|
||||
workspaceDeps: collectWorkspaceDeps(pkg, known),
|
||||
}));
|
||||
}
|
||||
|
||||
function collectWorkspaceDeps(pkg: Record<string, unknown>, known: Set<string>): string[] {
|
||||
const deps = new Set<string>();
|
||||
for (const field of ['dependencies', 'devDependencies'] as const) {
|
||||
const block = pkg[field];
|
||||
if (!block || typeof block !== 'object') continue;
|
||||
for (const name of Object.keys(block as Record<string, string>)) {
|
||||
if (known.has(name)) deps.add(name);
|
||||
}
|
||||
}
|
||||
return [...deps];
|
||||
}
|
||||
|
||||
interface TurboBinding {
|
||||
packageName: string;
|
||||
pathPrefix: string;
|
||||
}
|
||||
|
||||
function loadTurboExtraInputs(rootDir: string, packages: WorkspacePackage[]): TurboBinding[] {
|
||||
const turboFile = join(rootDir, 'turbo.json');
|
||||
if (!existsSync(turboFile)) return [];
|
||||
const parsed = parseJsonFile<{ tasks?: Record<string, { inputs?: string[] }> }>(turboFile);
|
||||
|
||||
const bindings: TurboBinding[] = [];
|
||||
for (const [taskId, task] of Object.entries(parsed.tasks ?? {})) {
|
||||
const hashIdx = taskId.indexOf('#');
|
||||
if (hashIdx === -1) continue;
|
||||
const packageName = taskId.slice(0, hashIdx);
|
||||
const ownerPkg = packages.find((p) => p.name === packageName);
|
||||
if (!ownerPkg) continue;
|
||||
for (const input of task.inputs ?? []) {
|
||||
if (!input.startsWith('../')) continue;
|
||||
const repoRelative = toPosix(relative(rootDir, join(rootDir, ownerPkg.dir, input)));
|
||||
// Strip glob suffixes to get a directory prefix we can prefix-match
|
||||
// changed files against. The two branches cover:
|
||||
// `/?**/...` — recursive globs (e.g. `../cli/src/**/*.yml`)
|
||||
// `/*foo` — single-segment globs (e.g. `../cli/src/*.ts`)
|
||||
// After stripping, e.g. `packages/cli/src/public-api/v1/**/*.yml`
|
||||
// becomes `packages/cli/src/public-api/v1`.
|
||||
const pathPrefix = repoRelative.replace(/\/?\*\*.*$|\/\*[^/]*$/g, '');
|
||||
bindings.push({ packageName, pathPrefix });
|
||||
}
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
export function affectedPackages(options: AnalyzeOptions): string[] {
|
||||
const rootDir = options.rootDir ?? findWorkspaceRoot(process.cwd());
|
||||
const packages = loadWorkspacePackages(rootDir);
|
||||
const allNames = packages.map((p) => p.name).sort();
|
||||
|
||||
// No signal (local dev, missing env) → safest default: everything.
|
||||
if (options.changedFiles === null) return allNames;
|
||||
|
||||
if (options.changedFiles.some((f) => GLOBAL_TRIGGER_FILES.has(f))) return allNames;
|
||||
|
||||
const direct = new Set<string>();
|
||||
for (const file of options.changedFiles) {
|
||||
const owner = packages.find((p) => file === p.dir || file.startsWith(`${p.dir}/`));
|
||||
if (owner) direct.add(owner.name);
|
||||
}
|
||||
|
||||
for (const { packageName, pathPrefix } of loadTurboExtraInputs(rootDir, packages)) {
|
||||
if (options.changedFiles.some((f) => f === pathPrefix || f.startsWith(`${pathPrefix}/`))) {
|
||||
direct.add(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
const dependents = new Map<string, string[]>();
|
||||
for (const pkg of packages) {
|
||||
for (const dep of pkg.workspaceDeps) {
|
||||
const list = dependents.get(dep) ?? [];
|
||||
list.push(pkg.name);
|
||||
dependents.set(dep, list);
|
||||
}
|
||||
}
|
||||
|
||||
const affected = new Set<string>();
|
||||
const queue = [...direct];
|
||||
while (queue.length > 0) {
|
||||
const name = queue.shift()!;
|
||||
if (affected.has(name)) continue;
|
||||
affected.add(name);
|
||||
queue.push(...(dependents.get(name) ?? []));
|
||||
}
|
||||
return [...affected].sort();
|
||||
}
|
||||
19
packages/testing/janitor/src/core/path-utils.ts
Normal file
19
packages/testing/janitor/src/core/path-utils.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join, resolve, sep } from 'node:path';
|
||||
|
||||
export function toPosix(p: string): string {
|
||||
return sep === '/' ? p : p.split(sep).join('/');
|
||||
}
|
||||
|
||||
/** Walk up from startDir to the directory containing pnpm-workspace.yaml. */
|
||||
export function findWorkspaceRoot(startDir: string): string {
|
||||
let dir = resolve(startDir);
|
||||
while (true) {
|
||||
if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) {
|
||||
throw new Error(`Could not locate pnpm-workspace.yaml walking up from ${startDir}`);
|
||||
}
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
146
packages/testing/janitor/src/core/scope-analyzer.test.ts
Normal file
146
packages/testing/janitor/src/core/scope-analyzer.test.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { computeScope, formatScope } from './scope-analyzer.js';
|
||||
|
||||
function makePackageDir(name: string): string {
|
||||
const root = join(tmpdir(), `janitor-scope-${Math.random().toString(36).slice(2)}`);
|
||||
const pkgDir = join(root, name);
|
||||
mkdirSync(pkgDir, { recursive: true });
|
||||
writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ name: 'some-pkg' }));
|
||||
return root;
|
||||
}
|
||||
|
||||
describe('computeScope', () => {
|
||||
it('returns full when CHANGED_FILES signal is missing (local dev)', () => {
|
||||
const rootDir = makePackageDir('packages/cli');
|
||||
const result = computeScope({
|
||||
runner: 'jest',
|
||||
packageDir: 'packages/cli',
|
||||
rootDir,
|
||||
changedFiles: null,
|
||||
});
|
||||
expect(result.kind).toBe('full');
|
||||
expect(formatScope(result)).toBe('RUN_FULL');
|
||||
});
|
||||
|
||||
it('returns skip when no in-package files changed', () => {
|
||||
const rootDir = makePackageDir('packages/cli');
|
||||
const result = computeScope({
|
||||
runner: 'jest',
|
||||
packageDir: 'packages/cli',
|
||||
rootDir,
|
||||
changedFiles: ['packages/other/src/x.ts'],
|
||||
});
|
||||
expect(result).toEqual({ kind: 'skip', reason: 'No changed files in package' });
|
||||
expect(formatScope(result)).toBe('SKIP');
|
||||
});
|
||||
|
||||
it('returns scoped list when only source files changed', () => {
|
||||
const rootDir = makePackageDir('packages/cli');
|
||||
const result = computeScope({
|
||||
runner: 'jest',
|
||||
packageDir: 'packages/cli',
|
||||
rootDir,
|
||||
changedFiles: ['packages/cli/src/a.ts', 'packages/cli/src/b.ts'],
|
||||
});
|
||||
expect(result).toEqual({
|
||||
kind: 'scoped',
|
||||
files: ['packages/cli/src/a.ts', 'packages/cli/src/b.ts'],
|
||||
});
|
||||
expect(formatScope(result)).toBe('packages/cli/src/a.ts packages/cli/src/b.ts');
|
||||
});
|
||||
|
||||
it('bails to full on jest.config change for jest runner', () => {
|
||||
const rootDir = makePackageDir('packages/cli');
|
||||
const result = computeScope({
|
||||
runner: 'jest',
|
||||
packageDir: 'packages/cli',
|
||||
rootDir,
|
||||
changedFiles: ['packages/cli/jest.config.js'],
|
||||
});
|
||||
expect(result.kind).toBe('full');
|
||||
expect(formatScope(result)).toBe('RUN_FULL');
|
||||
});
|
||||
|
||||
it('bails to full on package.json change', () => {
|
||||
const rootDir = makePackageDir('packages/cli');
|
||||
const result = computeScope({
|
||||
runner: 'vitest',
|
||||
packageDir: 'packages/cli',
|
||||
rootDir,
|
||||
changedFiles: ['packages/cli/package.json'],
|
||||
});
|
||||
expect(result.kind).toBe('full');
|
||||
});
|
||||
|
||||
it('bails to full on tsconfig change', () => {
|
||||
const rootDir = makePackageDir('packages/cli');
|
||||
const result = computeScope({
|
||||
runner: 'vitest',
|
||||
packageDir: 'packages/cli',
|
||||
rootDir,
|
||||
changedFiles: ['packages/cli/tsconfig.build.json'],
|
||||
});
|
||||
expect(result.kind).toBe('full');
|
||||
});
|
||||
|
||||
it('ignores files outside the package even when configs match', () => {
|
||||
const rootDir = makePackageDir('packages/cli');
|
||||
const result = computeScope({
|
||||
runner: 'jest',
|
||||
packageDir: 'packages/cli',
|
||||
rootDir,
|
||||
changedFiles: ['packages/other/package.json'],
|
||||
});
|
||||
expect(result.kind).toBe('skip');
|
||||
});
|
||||
|
||||
it('does NOT bail on vitest.config when runner is jest', () => {
|
||||
const rootDir = makePackageDir('packages/cli');
|
||||
const result = computeScope({
|
||||
runner: 'jest',
|
||||
packageDir: 'packages/cli',
|
||||
rootDir,
|
||||
changedFiles: ['packages/cli/vitest.config.ts'],
|
||||
});
|
||||
// vitest.config in a jest-tested package is not a runner bailout trigger.
|
||||
// (It's unusual but the bailout is runner-specific.)
|
||||
expect(result.kind).toBe('scoped');
|
||||
});
|
||||
|
||||
it('bails to full on vite.config change for vitest runner (vitest reads vite.config)', () => {
|
||||
const rootDir = makePackageDir('packages/frontend/editor-ui');
|
||||
const result = computeScope({
|
||||
runner: 'vitest',
|
||||
packageDir: 'packages/frontend/editor-ui',
|
||||
rootDir,
|
||||
changedFiles: ['packages/frontend/editor-ui/vite.config.mts'],
|
||||
});
|
||||
expect(result.kind).toBe('full');
|
||||
});
|
||||
|
||||
it('bails to full on src/__tests__/setup.ts change for vitest runner', () => {
|
||||
const rootDir = makePackageDir('packages/frontend/editor-ui');
|
||||
const result = computeScope({
|
||||
runner: 'vitest',
|
||||
packageDir: 'packages/frontend/editor-ui',
|
||||
rootDir,
|
||||
changedFiles: ['packages/frontend/editor-ui/src/__tests__/setup.ts'],
|
||||
});
|
||||
expect(result.kind).toBe('full');
|
||||
});
|
||||
|
||||
it('bails to full on src/__tests__/setup.ts change for jest runner', () => {
|
||||
const rootDir = makePackageDir('packages/nodes-base');
|
||||
const result = computeScope({
|
||||
runner: 'jest',
|
||||
packageDir: 'packages/nodes-base',
|
||||
rootDir,
|
||||
changedFiles: ['packages/nodes-base/src/__tests__/setup.ts'],
|
||||
});
|
||||
expect(result.kind).toBe('full');
|
||||
});
|
||||
});
|
||||
94
packages/testing/janitor/src/core/scope-analyzer.ts
Normal file
94
packages/testing/janitor/src/core/scope-analyzer.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Filters CHANGED_FILES to a single package and detects config-file bailouts.
|
||||
* Output is a shell sentinel (SKIP / RUN_FULL / file list). The actual
|
||||
* import-graph walk is the runner's job (jest --findRelatedTests / vitest
|
||||
* related).
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { isAbsolute, relative, resolve } from 'node:path';
|
||||
|
||||
import { toPosix } from './path-utils.js';
|
||||
|
||||
export type Runner = 'jest' | 'vitest';
|
||||
|
||||
// Bailout patterns are centralised here (vs the original DEVP-194 spec's
|
||||
// per-package `n8nTestChanged.inPackageBailouts` field) because the n8n
|
||||
// workspace shares the same vitest config helpers + setup file layout via
|
||||
// @n8n/vitest-config. If a package ever needs a custom bailout that doesn't
|
||||
// fit these patterns, switch to per-package config rather than expanding
|
||||
// these any further.
|
||||
const COMMON_BAILOUT = [
|
||||
/^package\.json$/,
|
||||
/^tsconfig(\..+)?\.json$/,
|
||||
/^\.swcrc$/,
|
||||
/^babel\.config\.[cm]?[jt]s$/,
|
||||
];
|
||||
const JEST_BAILOUT = [
|
||||
...COMMON_BAILOUT,
|
||||
/^jest\.config\.[cm]?[jt]s$/,
|
||||
/(?:^|\/)(?:jest|test)\.setup\.[cm]?[jt]s$/,
|
||||
/(?:^|\/)__tests__\/setup\.[cm]?[jt]s$/,
|
||||
];
|
||||
// Frontend packages use vite.config.* for the vitest config too (vitest reads
|
||||
// vite.config). Setup files live at src/__tests__/setup.ts per the shared
|
||||
// @n8n/vitest-config convention.
|
||||
const VITEST_BAILOUT = [
|
||||
...COMMON_BAILOUT,
|
||||
/^vite\.config\.[cm]?[jt]s$/,
|
||||
/^vitest\.config\.[cm]?[jt]s$/,
|
||||
/(?:^|\/)vitest\.setup\.[cm]?[jt]s$/,
|
||||
/(?:^|\/)__tests__\/setup\.[cm]?[jt]s$/,
|
||||
];
|
||||
|
||||
export interface ComputeScopeOptions {
|
||||
runner: Runner;
|
||||
packageDir: string;
|
||||
rootDir: string;
|
||||
/** `null` = no signal → RUN_FULL (local dev with unset env). */
|
||||
changedFiles: string[] | null;
|
||||
}
|
||||
|
||||
export type ScopeResult =
|
||||
| { kind: 'skip'; reason: string }
|
||||
| { kind: 'full'; reason: string; trigger?: string }
|
||||
| { kind: 'scoped'; files: string[] };
|
||||
|
||||
export function computeScope(options: ComputeScopeOptions): ScopeResult {
|
||||
if (options.changedFiles === null) {
|
||||
return { kind: 'full', reason: 'No CHANGED_FILES signal (local dev)' };
|
||||
}
|
||||
|
||||
const absolute = isAbsolute(options.packageDir)
|
||||
? options.packageDir
|
||||
: resolve(options.rootDir, options.packageDir);
|
||||
if (!existsSync(absolute)) throw new Error(`Package directory not found: ${absolute}`);
|
||||
const pkgPrefix = toPosix(relative(options.rootDir, absolute));
|
||||
const pkgPrefixSlash = `${pkgPrefix}/`;
|
||||
|
||||
const inPackage = options.changedFiles.filter(
|
||||
(f) => f === pkgPrefix || f.startsWith(pkgPrefixSlash),
|
||||
);
|
||||
if (inPackage.length === 0) return { kind: 'skip', reason: 'No changed files in package' };
|
||||
|
||||
const bailout = options.runner === 'jest' ? JEST_BAILOUT : VITEST_BAILOUT;
|
||||
for (const file of inPackage) {
|
||||
const relInPkg = file.slice(pkgPrefixSlash.length);
|
||||
if (bailout.some((p) => p.test(relInPkg))) {
|
||||
return { kind: 'full', reason: 'Config or package-level file changed', trigger: file };
|
||||
}
|
||||
}
|
||||
|
||||
return { kind: 'scoped', files: inPackage };
|
||||
}
|
||||
|
||||
export function formatScope(result: ScopeResult): string {
|
||||
switch (result.kind) {
|
||||
case 'skip':
|
||||
return 'SKIP';
|
||||
case 'full':
|
||||
return 'RUN_FULL';
|
||||
case 'scoped':
|
||||
return result.files.join(' ');
|
||||
}
|
||||
}
|
||||
61
packages/testing/janitor/src/core/test-scoped-runner.test.ts
Normal file
61
packages/testing/janitor/src/core/test-scoped-runner.test.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { isAbsolute } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildRunnerArgs } from './test-scoped-runner.js';
|
||||
|
||||
const rootDir = '/repo/root';
|
||||
|
||||
describe('buildRunnerArgs', () => {
|
||||
it('jest scoped: emits --findRelatedTests with absolute paths', () => {
|
||||
const args = buildRunnerArgs(
|
||||
'jest',
|
||||
{ kind: 'scoped', files: ['packages/nodes-base/nodes/Foo.node.ts'] },
|
||||
rootDir,
|
||||
['--summarize'],
|
||||
);
|
||||
expect(args[0]).toBe('--findRelatedTests');
|
||||
expect(isAbsolute(args[1])).toBe(true);
|
||||
expect(args[1]).toBe('/repo/root/packages/nodes-base/nodes/Foo.node.ts');
|
||||
expect(args[2]).toBe('--summarize');
|
||||
});
|
||||
|
||||
it('vitest scoped: emits `related` with absolute paths and `--run` to avoid watch mode', () => {
|
||||
const args = buildRunnerArgs(
|
||||
'vitest',
|
||||
{
|
||||
kind: 'scoped',
|
||||
files: ['packages/frontend/editor-ui/src/x.ts', 'packages/frontend/editor-ui/src/y.ts'],
|
||||
},
|
||||
rootDir,
|
||||
['--shard=1/2'],
|
||||
);
|
||||
expect(args[0]).toBe('related');
|
||||
expect(args.slice(1, 3).every(isAbsolute)).toBe(true);
|
||||
// `--run` is required: `vitest related` defaults to watch mode without
|
||||
// TTY-detection and would hang the CI runner forever.
|
||||
expect(args).toContain('--run');
|
||||
expect(args.at(-1)).toBe('--shard=1/2');
|
||||
});
|
||||
|
||||
it('preserves already-absolute paths', () => {
|
||||
const args = buildRunnerArgs(
|
||||
'jest',
|
||||
{ kind: 'scoped', files: ['/already/absolute/path.ts'] },
|
||||
rootDir,
|
||||
[],
|
||||
);
|
||||
expect(args).toEqual(['--findRelatedTests', '/already/absolute/path.ts']);
|
||||
});
|
||||
|
||||
it('jest full: passes through args with no related-tests flag', () => {
|
||||
expect(
|
||||
buildRunnerArgs('jest', { kind: 'full', reason: 'config change' }, rootDir, ['--summarize']),
|
||||
).toEqual(['--summarize']);
|
||||
});
|
||||
|
||||
it('vitest full: prepends `run` subcommand', () => {
|
||||
expect(
|
||||
buildRunnerArgs('vitest', { kind: 'full', reason: 'no signal' }, rootDir, ['--coverage']),
|
||||
).toEqual(['run', '--coverage']);
|
||||
});
|
||||
});
|
||||
64
packages/testing/janitor/src/core/test-scoped-runner.ts
Normal file
64
packages/testing/janitor/src/core/test-scoped-runner.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/** Compute per-package scope and dispatch to jest/vitest with the right flags. */
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { isAbsolute, resolve } from 'node:path';
|
||||
|
||||
import { computeScope, type Runner, type ScopeResult } from './scope-analyzer.js';
|
||||
|
||||
export interface TestScopedOptions {
|
||||
runner: Runner;
|
||||
packageDir: string;
|
||||
rootDir: string;
|
||||
changedFiles: string[] | null;
|
||||
passthroughArgs: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the runner argv from a scope result. Paths are resolved to absolute
|
||||
* because pnpm/turbo runs `test:changed` with cwd=packageDir, while CHANGED_FILES
|
||||
* is repo-root-relative — handing a relative path to `jest --findRelatedTests` or
|
||||
* `vitest related` from inside the package would silently match zero files and
|
||||
* exit 0 with no tests run.
|
||||
*/
|
||||
export function buildRunnerArgs(
|
||||
runner: Runner,
|
||||
scope: Extract<ScopeResult, { kind: 'scoped' | 'full' }>,
|
||||
rootDir: string,
|
||||
passthroughArgs: string[],
|
||||
): string[] {
|
||||
if (scope.kind === 'full') {
|
||||
return runner === 'vitest' ? ['run', ...passthroughArgs] : [...passthroughArgs];
|
||||
}
|
||||
const absoluteFiles = scope.files.map((f) => (isAbsolute(f) ? f : resolve(rootDir, f)));
|
||||
// `vitest related` defaults to watch mode and does NOT TTY-detect, so it
|
||||
// would hang the CI runner forever. `--run` forces a single-pass execution.
|
||||
return runner === 'jest'
|
||||
? ['--findRelatedTests', ...absoluteFiles, ...passthroughArgs]
|
||||
: ['related', ...absoluteFiles, '--run', ...passthroughArgs];
|
||||
}
|
||||
|
||||
export function runTestScoped(options: TestScopedOptions): number {
|
||||
const scope = computeScope({
|
||||
runner: options.runner,
|
||||
packageDir: options.packageDir,
|
||||
rootDir: options.rootDir,
|
||||
changedFiles: options.changedFiles,
|
||||
});
|
||||
|
||||
if (scope.kind === 'skip') {
|
||||
console.log(`[janitor:test-scoped] ${scope.reason} → skipping`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (scope.kind === 'full') {
|
||||
console.log(`[janitor:test-scoped] ${scope.reason} → full suite`);
|
||||
} else {
|
||||
console.log(`[janitor:test-scoped] scoping to ${scope.files.length} file(s)`);
|
||||
}
|
||||
|
||||
const args = buildRunnerArgs(options.runner, scope, options.rootDir, options.passthroughArgs);
|
||||
// Pass cwd explicitly so an override via --package-dir is honoured
|
||||
// (otherwise spawnSync inherits the caller's cwd and jest/vitest would
|
||||
// resolve config + tests from the wrong project).
|
||||
return spawnSync(options.runner, args, { stdio: 'inherit', cwd: options.packageDir }).status ?? 1;
|
||||
}
|
||||
|
|
@ -3425,6 +3425,9 @@ importers:
|
|||
'@n8n/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/eslint-config
|
||||
'@n8n/playwright-janitor':
|
||||
specifier: workspace:*
|
||||
version: link:../../../testing/janitor
|
||||
'@n8n/stylelint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/stylelint-config
|
||||
|
|
@ -3464,6 +3467,9 @@ importers:
|
|||
'@n8n/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/eslint-config
|
||||
'@n8n/playwright-janitor':
|
||||
specifier: workspace:*
|
||||
version: link:../../../testing/janitor
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/typescript-config
|
||||
|
|
@ -3576,6 +3582,9 @@ importers:
|
|||
'@n8n/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/eslint-config
|
||||
'@n8n/playwright-janitor':
|
||||
specifier: workspace:*
|
||||
version: link:../../../testing/janitor
|
||||
'@n8n/stylelint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/stylelint-config
|
||||
|
|
@ -3661,6 +3670,9 @@ importers:
|
|||
'@n8n/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/eslint-config
|
||||
'@n8n/playwright-janitor':
|
||||
specifier: workspace:*
|
||||
version: link:../../../testing/janitor
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/typescript-config
|
||||
|
|
@ -3737,6 +3749,9 @@ importers:
|
|||
'@n8n/i18n':
|
||||
specifier: workspace:*
|
||||
version: link:../i18n
|
||||
'@n8n/playwright-janitor':
|
||||
specifier: workspace:*
|
||||
version: link:../../../testing/janitor
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/typescript-config
|
||||
|
|
@ -3771,6 +3786,9 @@ importers:
|
|||
'@n8n/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/eslint-config
|
||||
'@n8n/playwright-janitor':
|
||||
specifier: workspace:*
|
||||
version: link:../../../testing/janitor
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/typescript-config
|
||||
|
|
@ -4202,6 +4220,9 @@ importers:
|
|||
'@n8n/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../@n8n/eslint-config
|
||||
'@n8n/playwright-janitor':
|
||||
specifier: workspace:*
|
||||
version: link:../../testing/janitor
|
||||
'@n8n/stylelint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../@n8n/stylelint-config
|
||||
|
|
@ -4579,6 +4600,9 @@ importers:
|
|||
'@n8n/eslint-plugin-community-nodes':
|
||||
specifier: workspace:*
|
||||
version: link:../@n8n/eslint-plugin-community-nodes
|
||||
'@n8n/playwright-janitor':
|
||||
specifier: workspace:*
|
||||
version: link:../testing/janitor
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../@n8n/typescript-config
|
||||
|
|
@ -4738,6 +4762,9 @@ importers:
|
|||
glob:
|
||||
specifier: 10.5.0
|
||||
version: 10.5.0
|
||||
yaml:
|
||||
specifier: 'catalog:'
|
||||
version: 2.8.3
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.17.50
|
||||
|
|
|
|||
|
|
@ -39,7 +39,12 @@
|
|||
"dependsOn": ["^build", "build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", "../cli/src/public-api/v1/**/*.yml"],
|
||||
"outputs": ["coverage/**", "*.xml"],
|
||||
"env": ["COVERAGE_ENABLED", "MERGE_BASE"]
|
||||
"env": ["COVERAGE_ENABLED", "CHANGED_FILES"]
|
||||
},
|
||||
"test:changed": {
|
||||
"dependsOn": ["^build", "build"],
|
||||
"outputs": ["coverage/**", "*.xml"],
|
||||
"env": ["COVERAGE_ENABLED", "CHANGED_FILES"]
|
||||
},
|
||||
"test:unit": {
|
||||
"dependsOn": ["^build", "build"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user