mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-05 02:59:27 +02:00
build: Add pnpm agent:setup for fresh-checkout install + build + test (#31756)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ff3657ded7
commit
91182ed02b
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -31,6 +31,7 @@ CHANGELOG-*.md
|
|||
!packages/frontend/@n8n/design-system/**/*.mdx
|
||||
build-storybook.log
|
||||
build.log
|
||||
.agent-setup/
|
||||
sbom-source.cdx.json
|
||||
*.junit.xml
|
||||
junit.xml
|
||||
|
|
|
|||
17
AGENTS.md
17
AGENTS.md
|
|
@ -29,6 +29,23 @@ See [plugin README](.claude/plugins/n8n/README.md) for structure and details.
|
|||
|
||||
## Essential Commands
|
||||
|
||||
### Fresh checkout / agent setup
|
||||
|
||||
For a fresh checkout (cat-bot, a new hire, any agent verifying the repo
|
||||
builds), prefer `pnpm agent:setup` over running install + build + tests by
|
||||
hand. It chains them in one process, caps per-process memory and turbo
|
||||
concurrency so a 6GB box doesn't OOM, streams all output to
|
||||
`.agent-setup/<step>.log` (gitignored), and surfaces only a one-line summary
|
||||
per step plus the tail of the failing log. A machine-readable
|
||||
`.agent-setup/summary.json` is always written so a backgrounded run is
|
||||
readable in a single shot — no polling, no scrolling logs.
|
||||
|
||||
```bash
|
||||
pnpm agent:setup # install → build → test (full suite)
|
||||
pnpm agent:setup install # one step at a time
|
||||
pnpm agent:setup --json # JSON summary on stdout (for scripts/agents)
|
||||
```
|
||||
|
||||
### Building
|
||||
Use `pnpm build` to build all packages. ALWAYS redirect the output of the
|
||||
build command to a file:
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
"reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs",
|
||||
"format": "turbo run format && node scripts/format.mjs",
|
||||
"grind": "node scripts/grind.mjs",
|
||||
"agent:setup": "node scripts/agent-setup.mjs",
|
||||
"format:check": "turbo run format:check",
|
||||
"lint": "turbo run lint",
|
||||
"lint:styles": "turbo run lint:styles",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"lint": "eslint . --quiet",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest"
|
||||
"test": "jest --passWithNoTests"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
"build:unchecked": "pnpm run build",
|
||||
"create-json-schema": "tsx scripts/create-json-schema.ts",
|
||||
"preview": "vite preview",
|
||||
"test": "jest"
|
||||
"test": "jest --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "catalog:frontend",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
"lint:fix": "eslint src --fix",
|
||||
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b",
|
||||
"watch": "tsc --watch",
|
||||
"test": "jest"
|
||||
"test": "jest --passWithNoTests"
|
||||
},
|
||||
"bin": {
|
||||
"n8n-node-dev": "./bin/n8n-node-dev"
|
||||
|
|
|
|||
220
scripts/agent-setup.mjs
Normal file
220
scripts/agent-setup.mjs
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Token- and memory-friendly fresh-checkout setup/verify for agents.
|
||||
*
|
||||
* Runs install → build → test in one process and surfaces only a compact
|
||||
* summary, so a fresh agent (cat-bot, Claude Code, a new hire) can verify a
|
||||
* checkout without burning context tokens on tens of thousands of lines of
|
||||
* pnpm/turbo/vitest output. Every spawned Node process is capped via
|
||||
* NODE_OPTIONS=--max-old-space-size and turbo concurrency is capped so total
|
||||
* resident memory stays bounded on a 6GB box.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm agent:setup [all|install|build|test] [flags]
|
||||
* node scripts/agent-setup.mjs all --mem 6144 --concurrency 4
|
||||
*
|
||||
* Exit codes: 0 = all steps pass, 1 = a step failed, 2 = invalid arguments.
|
||||
*
|
||||
* See DEVP-367 for design notes.
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdirSync, openSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
const REPO_ROOT = resolve(import.meta.dirname, '..');
|
||||
const VALID_STEPS = ['all', 'install', 'build', 'test'];
|
||||
|
||||
const USAGE = `Usage: pnpm agent:setup [all|install|build|test] [flags]
|
||||
|
||||
Steps:
|
||||
all install → build → test (default; stops at first failure)
|
||||
install pnpm install --frozen-lockfile
|
||||
build turbo run build
|
||||
test turbo run test (full suite)
|
||||
|
||||
Flags:
|
||||
--mem <MB> per-process old-space cap (default 6144, matches CI)
|
||||
--concurrency <n> turbo concurrency for build/test (default 4)
|
||||
--tail <n> lines of the failing log to print on failure (default 60)
|
||||
--json emit only the JSON summary to stdout
|
||||
--log-dir <path> write logs and summary.json here (default .agent-setup/)
|
||||
-h, --help show this help
|
||||
|
||||
All step output streams to <log-dir>/<step>.log. A machine-readable
|
||||
<log-dir>/summary.json is always written.
|
||||
`;
|
||||
|
||||
function fail(msg) {
|
||||
process.stderr.write(`agent-setup: ${msg}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
let values;
|
||||
let positionals;
|
||||
try {
|
||||
({ values, positionals } = parseArgs({
|
||||
options: {
|
||||
mem: { type: 'string', default: '6144' },
|
||||
concurrency: { type: 'string', default: '4' },
|
||||
tail: { type: 'string', default: '60' },
|
||||
json: { type: 'boolean', default: false },
|
||||
'log-dir': { type: 'string' },
|
||||
help: { type: 'boolean', default: false, short: 'h' },
|
||||
},
|
||||
allowPositionals: true,
|
||||
strict: true,
|
||||
}));
|
||||
} catch (err) {
|
||||
fail(err.message);
|
||||
}
|
||||
|
||||
if (values.help) {
|
||||
process.stdout.write(USAGE);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (positionals.length > 1) {
|
||||
fail(`unexpected extra arguments: ${positionals.slice(1).join(' ')}`);
|
||||
}
|
||||
const step = positionals[0] ?? 'all';
|
||||
if (!VALID_STEPS.includes(step)) {
|
||||
fail(`unknown step "${step}" — must be one of: ${VALID_STEPS.join(', ')}`);
|
||||
}
|
||||
|
||||
const mem = Number(values.mem);
|
||||
const concurrency = Number(values.concurrency);
|
||||
const tailLines = Number(values.tail);
|
||||
if (!Number.isInteger(mem) || mem <= 0) fail('--mem must be a positive integer (MB)');
|
||||
if (!Number.isInteger(concurrency) || concurrency <= 0) {
|
||||
fail('--concurrency must be a positive integer');
|
||||
}
|
||||
if (!Number.isInteger(tailLines) || tailLines < 0) {
|
||||
fail('--tail must be a non-negative integer');
|
||||
}
|
||||
|
||||
const logDir = values['log-dir']
|
||||
? resolve(process.cwd(), values['log-dir'])
|
||||
: resolve(REPO_ROOT, '.agent-setup');
|
||||
mkdirSync(logDir, { recursive: true });
|
||||
|
||||
// Call turbo directly via `pnpm exec` rather than the wrapper scripts so a
|
||||
// user-supplied `--concurrency` isn't silently fought by a flag baked into a
|
||||
// wrapper.
|
||||
const PLAN = {
|
||||
install: { cmd: 'pnpm', args: ['install', '--frozen-lockfile'] },
|
||||
build: {
|
||||
cmd: 'pnpm',
|
||||
args: ['exec', 'turbo', 'run', 'build', `--concurrency=${concurrency}`],
|
||||
},
|
||||
test: {
|
||||
cmd: 'pnpm',
|
||||
args: ['exec', 'turbo', 'run', 'test', `--concurrency=${concurrency}`],
|
||||
},
|
||||
};
|
||||
|
||||
const stepsToRun = step === 'all' ? ['install', 'build', 'test'] : [step];
|
||||
|
||||
const NODE_OPTS = `--max-old-space-size=${mem}`;
|
||||
const childEnv = {
|
||||
...process.env,
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS
|
||||
? `${process.env.NODE_OPTIONS} ${NODE_OPTS}`
|
||||
: NODE_OPTS,
|
||||
FORCE_COLOR: '0',
|
||||
};
|
||||
|
||||
function elapsed(start) {
|
||||
return Math.max(1, Math.round((Date.now() - start) / 1000));
|
||||
}
|
||||
|
||||
function runStep(name) {
|
||||
return new Promise((res) => {
|
||||
const { cmd, args } = PLAN[name];
|
||||
const logPath = resolve(logDir, `${name}.log`);
|
||||
const start = Date.now();
|
||||
const logFd = openSync(logPath, 'w');
|
||||
|
||||
if (!values.json) process.stdout.write(`▶ ${name.padEnd(7)} `);
|
||||
|
||||
const child = spawn(cmd, args, {
|
||||
cwd: REPO_ROOT,
|
||||
env: childEnv,
|
||||
stdio: ['ignore', logFd, logFd],
|
||||
});
|
||||
|
||||
child.once('error', (err) => {
|
||||
const seconds = elapsed(start);
|
||||
if (!values.json) process.stdout.write(`✗ failed to spawn (${err.message})\n`);
|
||||
res({ name, ok: false, seconds, log: logPath, error: err.message });
|
||||
});
|
||||
|
||||
child.once('exit', (code, signal) => {
|
||||
const seconds = elapsed(start);
|
||||
const ok = code === 0;
|
||||
if (!values.json) {
|
||||
const reason = signal ?? `exit ${code}`;
|
||||
process.stdout.write(ok ? `✓ ${seconds}s\n` : `✗ ${seconds}s (${reason})\n`);
|
||||
}
|
||||
res({ name, ok, seconds, log: logPath, exitCode: code, signal });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function tailFile(path, n) {
|
||||
if (n <= 0) return '';
|
||||
const content = await readFile(path, 'utf8');
|
||||
const lines = content.split(/\r?\n/);
|
||||
while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
||||
return lines.slice(-n).join('\n');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const results = [];
|
||||
for (const name of stepsToRun) {
|
||||
const r = await runStep(name);
|
||||
try {
|
||||
r.logKb = Math.round(statSync(r.log).size / 1024);
|
||||
} catch {
|
||||
r.logKb = 0;
|
||||
}
|
||||
results.push(r);
|
||||
if (!r.ok) break;
|
||||
}
|
||||
|
||||
const failed = results.find((r) => !r.ok);
|
||||
const failTail = failed ? await tailFile(failed.log, tailLines) : undefined;
|
||||
|
||||
const summary = {
|
||||
ok: !failed,
|
||||
steps: results.map(({ name, ok, seconds, log, logKb }) => ({
|
||||
name,
|
||||
ok,
|
||||
seconds,
|
||||
log,
|
||||
logKb,
|
||||
})),
|
||||
...(failTail !== undefined && { failTail }),
|
||||
};
|
||||
const summaryPath = resolve(logDir, 'summary.json');
|
||||
writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
|
||||
|
||||
if (values.json) {
|
||||
process.stdout.write(JSON.stringify(summary) + '\n');
|
||||
} else if (failed) {
|
||||
process.stdout.write(`\n--- last ${tailLines} lines of ${failed.log} ---\n`);
|
||||
if (failTail) process.stdout.write(failTail + '\n');
|
||||
process.stdout.write(`(full log: ${failed.log})\n`);
|
||||
process.stdout.write(`(summary: ${summaryPath})\n`);
|
||||
} else {
|
||||
process.stdout.write(`(summary: ${summaryPath})\n`);
|
||||
}
|
||||
|
||||
process.exit(summary.ok ? 0 : 1);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`agent-setup: ${err.stack ?? err.message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user