mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
chore(turbo): lint against recursive \turbo run\ calls in child packages
CLAUDE.md flagged this as "CRITICAL" — a child package.json defining e.g. \`"build": "turbo run build"\` causes a 10+ minute CI hang with thousands of duplicate task spawns. The rule was documented but never enforced, so it re-emerged every couple of months as someone copied a parent script pattern. - \`scripts/validate-no-recursive-turbo.mjs\` walks every tracked package.json (via \`git ls-files\`, so node_modules is auto-skipped) and fails if any non-root package has build/type-check/lint/test/ test:coverage/check scripts containing \`turbo run\`. \`dev\` stays allowed — delegating it from a parent is the intended ergonomic. - Wired as \`pnpm run validate:turbo\` + a new CI step in the validate job (before type-check — fails fast). - CLAUDE.md §Turborepo updated to point at the enforcer and call out the full task list (test/test:coverage/check were missing from the original prose). Verified: 138 non-root package.json files scan clean. Drift simulation (injecting \`"build": "turbo run build"\` into apps/mana/apps/web) fails with a clear message pointing at the offending file + script + fix. This closes audit item #32 from the architecture review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c7af693c6d
commit
1eda3f5395
4 changed files with 98 additions and 1 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
|
@ -440,6 +440,9 @@ jobs:
|
||||||
- name: Validate monorepo best practices
|
- name: Validate monorepo best practices
|
||||||
run: pnpm run validate:monorepo
|
run: pnpm run validate:monorepo
|
||||||
|
|
||||||
|
- name: Validate no recursive turbo calls
|
||||||
|
run: pnpm run validate:turbo
|
||||||
|
|
||||||
- name: Audit crypto registry (Dexie ↔ registry ↔ allowlist)
|
- name: Audit crypto registry (Dexie ↔ registry ↔ allowlist)
|
||||||
run: pnpm run check:crypto
|
run: pnpm run check:crypto
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,9 @@ MinIO (Docker, S3-compatible) in both local and prod. Console: http://localhost:
|
||||||
|
|
||||||
### Turborepo: avoid recursive turbo calls
|
### Turborepo: avoid recursive turbo calls
|
||||||
|
|
||||||
**CRITICAL**: Parent workspace packages (e.g. `apps/chat/package.json`) must NEVER define `type-check`, `build`, or `lint` scripts that call `turbo run <task>`. Root turbo already orchestrates those — defining them in children causes infinite recursion (10+ minute hangs, thousands of duplicate tasks). Only `dev` is OK to delegate to turbo from a parent package, since it's persistent and typically scoped.
|
**CRITICAL**: Parent workspace packages (e.g. `apps/chat/package.json`) must NEVER define `type-check`, `build`, `lint`, `test`, `test:coverage`, or `check` scripts that call `turbo run <task>`. Root turbo already orchestrates those — defining them in children causes infinite recursion (10+ minute hangs, thousands of duplicate tasks). Only `dev` is OK to delegate to turbo from a parent package, since it's persistent and typically scoped.
|
||||||
|
|
||||||
|
Enforced by `pnpm run validate:turbo` (`scripts/validate-no-recursive-turbo.mjs`), wired into the CI `validate` job — a new `turbo run build` inside a non-root package.json now fails the PR.
|
||||||
|
|
||||||
## Shared Packages (`packages/`)
|
## Shared Packages (`packages/`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"format:check": "prettier --config .prettierrc.json --check \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"",
|
"format:check": "prettier --config .prettierrc.json --check \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"",
|
||||||
"check:status": "bash scripts/check-status.sh",
|
"check:status": "bash scripts/check-status.sh",
|
||||||
"validate:dockerfiles": "node scripts/validate-dockerfiles.mjs",
|
"validate:dockerfiles": "node scripts/validate-dockerfiles.mjs",
|
||||||
|
"validate:turbo": "node scripts/validate-no-recursive-turbo.mjs",
|
||||||
"check:crypto": "node scripts/audit-crypto-registry.mjs",
|
"check:crypto": "node scripts/audit-crypto-registry.mjs",
|
||||||
"check:crypto:seed": "node scripts/audit-crypto-registry.mjs --seed",
|
"check:crypto:seed": "node scripts/audit-crypto-registry.mjs --seed",
|
||||||
"audit:deps": "node scripts/audit-workspace-deps.mjs",
|
"audit:deps": "node scripts/audit-workspace-deps.mjs",
|
||||||
|
|
|
||||||
91
scripts/validate-no-recursive-turbo.mjs
Executable file
91
scripts/validate-no-recursive-turbo.mjs
Executable file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Validate that no non-root package.json calls `turbo run` in a script
|
||||||
|
* that turbo itself orchestrates. Those recursive calls are a 10+ minute
|
||||||
|
* build hang waiting to happen — see the root CLAUDE.md §Turborepo for
|
||||||
|
* the incident that prompted this rule.
|
||||||
|
*
|
||||||
|
* Rule: inside any package.json EXCEPT the repo root, the scripts
|
||||||
|
* { build, type-check, lint, test, test:coverage }
|
||||||
|
* must NOT contain `turbo run`. Turbo drives them from the root; if a
|
||||||
|
* child also calls turbo, you get N² task spawns, duplicate work, and
|
||||||
|
* eventually a hung CI job.
|
||||||
|
*
|
||||||
|
* `dev` is explicitly allowed: it's persistent, usually scoped via
|
||||||
|
* --filter, and delegating it from a parent package is the intended
|
||||||
|
* ergonomic shortcut (e.g. `pnpm mana:dev` → turbo run dev --filter=mana...).
|
||||||
|
*
|
||||||
|
* Zero deps — runs as plain Node ESM. Uses `git ls-files` so it
|
||||||
|
* automatically respects .gitignore (no node_modules traversal).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const REPO_ROOT = join(__dirname, '..');
|
||||||
|
|
||||||
|
/** Scripts that turbo orchestrates from the root. Recursive turbo calls
|
||||||
|
* in these scripts are what cause the build hang. */
|
||||||
|
const FORBIDDEN_TASKS = new Set(['build', 'type-check', 'lint', 'test', 'test:coverage', 'check']);
|
||||||
|
|
||||||
|
function listPackageJsons() {
|
||||||
|
const out = execSync('git ls-files "package.json" "**/package.json"', {
|
||||||
|
cwd: REPO_ROOT,
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
return out
|
||||||
|
.split('\n')
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
const paths = listPackageJsons();
|
||||||
|
const rootRel = 'package.json';
|
||||||
|
const violations = [];
|
||||||
|
let scanned = 0;
|
||||||
|
|
||||||
|
for (const rel of paths) {
|
||||||
|
if (rel === rootRel) continue; // root is ALLOWED to orchestrate turbo
|
||||||
|
scanned++;
|
||||||
|
const abs = join(REPO_ROOT, rel);
|
||||||
|
let pkg;
|
||||||
|
try {
|
||||||
|
pkg = JSON.parse(readFileSync(abs, 'utf8'));
|
||||||
|
} catch (err) {
|
||||||
|
violations.push(`${rel}: invalid JSON (${err.message})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const scripts = pkg.scripts ?? {};
|
||||||
|
for (const [task, cmd] of Object.entries(scripts)) {
|
||||||
|
if (!FORBIDDEN_TASKS.has(task)) continue;
|
||||||
|
if (typeof cmd !== 'string') continue;
|
||||||
|
// Match `turbo run` as a whole word — don't trip on strings like
|
||||||
|
// "turbot run" (hypothetical) or unrelated tooling.
|
||||||
|
if (/\bturbo\s+run\b/.test(cmd)) {
|
||||||
|
violations.push(
|
||||||
|
`${rel}: script '${task}' calls \`turbo run\` — this causes recursion when ` +
|
||||||
|
`the root turbo also runs '${task}'. Replace with the direct tool ` +
|
||||||
|
`(e.g. \`tsc --noEmit\`, \`vite build\`) or remove the script entirely.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.error(`\n✗ Recursive-turbo check FAILED (${violations.length} violation(s)):\n`);
|
||||||
|
for (const v of violations) console.error(` • ${v}`);
|
||||||
|
console.error(
|
||||||
|
`\nSee CLAUDE.md §Turborepo for context. Allowed: \`dev\` scripts may call ` +
|
||||||
|
`\`turbo run dev --filter=…\` (persistent + scoped).\n`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ No recursive turbo calls: scanned ${scanned} non-root package.json files.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate();
|
||||||
Loading…
Add table
Add a link
Reference in a new issue