test(core): Guard against private packages in the published runtime graph (#34889)

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Charlie Kolb
2026-07-27 07:01:43 +00:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 4e43644bba
commit 2c4805cf4c
3 changed files with 97 additions and 1 deletions
+6
View File
@@ -32,6 +32,12 @@ pre-commit:
skip:
- merge
- rebase
workspace_private_deps_check:
glob: '**/package.json'
run: node scripts/check-workspace-private-deps.mjs
skip:
- merge
- rebase
zod_peer_deps_check:
glob: 'packages/{@n8n/api-types,workflow,core,@n8n/agents}/package.json'
run: node scripts/check-zod-peer-deps.mjs
+2 -1
View File
@@ -42,13 +42,14 @@
"lint:styles:fix": "turbo run lint:styles:fix",
"lint:affected": "turbo run lint --affected",
"lint:fix": "turbo run lint:fix",
"lint:ci": "turbo run lint lint:styles && pnpm boundaries:check",
"lint:ci": "turbo run lint lint:styles && pnpm boundaries:check && pnpm check:workspace-private-deps",
"boundaries": "turbo boundaries",
"boundaries:check": "node scripts/check-boundaries.mjs",
"boundaries:baseline": "node scripts/check-boundaries.mjs --write",
"sync:skill-links": "node scripts/sync-agent-skill-links.mjs",
"check:skill-links": "node scripts/sync-agent-skill-links.mjs --check",
"check:zod-peer-deps": "node scripts/check-zod-peer-deps.mjs",
"check:workspace-private-deps": "node scripts/check-workspace-private-deps.mjs",
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
"setup-backend-module": "node scripts/ensure-zx.mjs && zx scripts/backend-module/setup.mjs",
"start": "node scripts/os-normalize.mjs --dir packages/cli/bin n8n",
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env node
/**
* Guards against a broken `npm install n8n`: a publishable (non-private)
* workspace package must not depend at runtime on a `private` one.
*
* The `n8n` cli depends on `n8n-editor-ui` (published), which depended on
* `@n8n/frontend-constants`, `@n8n/frontend-utils` and `@n8n/telemetry` — all
* marked `"private": true`. Publishing `n8n` then produces an install graph
* pointing at packages that were never published. Marking any package private
* (or adding a private package as a runtime dependency of a published one)
* silently reintroduces this.
*
* We only need to check *direct* runtime deps: if every published package's
* direct workspace deps are also published, the whole runtime closure is clean
* by induction. `devDependencies` don't ship, so they're ignored.
*/
import { globSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
/**
* Pure, testable core. Given the workspace manifests, return violation strings
* for every non-private package that has a private workspace runtime dep.
* @param {Array<{name: string, private?: boolean, dependencies?: Record<string,string>}>} manifests
*/
export function findViolations(manifests) {
const byName = new Map(manifests.map((m) => [m.name, m]));
const violations = [];
for (const pkg of manifests) {
if (pkg.private) continue; // a private package may depend on anything
for (const [dep, range] of Object.entries(pkg.dependencies ?? {})) {
if (typeof range !== 'string' || !range.startsWith('workspace:')) continue;
if (byName.get(dep)?.private) {
violations.push(`${pkg.name} → ${dep} (${dep} is "private": true)`);
}
}
}
return violations;
}
function loadManifests() {
const files = globSync('packages/**/package.json', {
cwd: root,
exclude: (p) => p.includes('node_modules'),
});
return files.map((rel) => JSON.parse(readFileSync(resolve(root, rel), 'utf8'))).filter((m) => m.name);
}
function main() {
const violations = findViolations(loadManifests());
if (violations.length > 0) {
console.error('');
console.error('ERROR: publishable packages must not depend on "private" packages at runtime.');
console.error('');
console.error('These packages are published to npm, so a private runtime dependency leaves the');
console.error('published install graph pointing at packages that were never published, breaking');
console.error('`npm install n8n`.');
console.error('');
for (const v of violations) console.error(` - ${v}`);
console.error('');
console.error('Either drop "private": true from the dependency, or move it to devDependencies.');
console.error('');
process.exit(1);
}
console.log('OK: no publishable package depends on a private package at runtime.');
}
// `--self-test` exercises the core logic without touching the workspace.
if (process.argv.includes('--self-test')) {
const manifests = [
{ name: 'pub', dependencies: { priv: 'workspace:*', ok: 'workspace:*' } },
{ name: 'priv', private: true },
{ name: 'ok' },
{ name: 'priv-dep-of-priv', private: true, dependencies: { priv: 'workspace:*' } },
{ name: 'ext', dependencies: { lodash: '^4' } },
];
const v = findViolations(manifests);
const pass = v.length === 1 && v[0].startsWith('pub → priv');
console.log(pass ? 'self-test passed' : `self-test FAILED: ${JSON.stringify(v)}`);
process.exit(pass ? 0 : 1);
}
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}