@webpieces/pr-gate 0.4.666 → 0.4.668

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"build-gate-log.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-gate-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { dotWebpieces } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// EXPERIMENTAL (opt-in via `~/.webpieces/config.json` → experimental.buildGateLogCapture). Nothing in this\n// file runs for a user who has not created that OPTIONAL file — see `HomeConfigService`\n// (`@webpieces/rules-config`), which returns all-defaults silently when it is absent.\n//\n// EVERY key in that file is optional, so the smallest document enabling THIS feature is just:\n// { \"experimental\": { \"buildGateLogCapture\": true } }\n// Optional is not a convenience: the file is MACHINE-GLOBAL and the repos on one machine pin different\n// webpieces releases, so a REQUIRED key there has no satisfiable value — omitting it fails the new\n// release, adding it fails every older one. `HomeConfigService` owns that rule.\n//\n// ─── Why ───────────────────────────────────────────────────────────────────────────────────────────────\n// The build gate already builds everything. When it fails, the pre-existing message says \"run THIS exact\n// command to reproduce\", and an AI agent obeys it — so the repo is built a SECOND time purely to see the\n// errors that scrolled past the first time. This captures the first build's output instead.\n//\n// ─── Why a shell `tee` and not spawnSync's pipes ───────────────────────────────────────────────────────\n// spawnSync BUFFERS a piped stream and hands it over only after the child exits, so the human would watch\n// a silent terminal for the length of a full build. The output has to be split at the OS level, which is\n// what `tee` is. The gate keeps `stdio: 'inherit'`, so the terminal is byte-identical to today.\n//\n// ─── Why an exit-status side file and not `pipefail` ───────────────────────────────────────────────────\n// In a pipeline the shell reports the LAST command's status, i.e. tee's, which is 0 whether the build\n// passed or failed. `set -o pipefail` and `${PIPESTATUS[0]}` are bash/zsh, and spawnSync's `shell: true`\n// gives `/bin/sh` — dash on Debian. Writing `$?` to a side file inside the group is plain POSIX and works\n// under every one of them. The redirect keeps it out of the pipe, so it never lands in the log.\nconst STATUS_SUFFIX = '.status';\n\n/**\n * Which stage's gate is being captured. The value is part of the log FILENAME, so review and finish never\n * write the same file even at the same commit on the same branch.\n */\nexport const REVIEW_STAGE = 'review';\nexport const FINISH_STAGE = 'finish';\n// `wp-build`, which is not a stage of the PR flow but runs the SAME gate (BuildAffected.runBuildGate).\n// Its own id so a developer's inner-loop build never overwrites the log stage ② or ③ is holding.\nexport const BUILD_STAGE = 'build';\n\n/**\n * Captures the build gate's full output to `.webpieces/logs/`, and renders the small pointer the AI is\n * handed instead of a rebuild instruction.\n *\n * ─── Why the filename cannot collide ───────────────────────────────────────────────────────────────────\n * The path is `dotWebpieces.logsFile(repoRoot, 'build-gate-<stage>-<branch>-<shortSha>.log')`, and each\n * component rules out one class of concurrent writer:\n * • the DIRECTORY is `dotWebpieces.local()`-scoped — `<primary>/.webpieces/worktrees/<git worktree\n * name>/logs/` in a linked worktree. Every worktree therefore has its own log directory already, which\n * is what makes \"N agents in N worktrees\" safe by construction rather than by naming.\n * • `<stage>` separates review from finish, the two gates that CAN run against one commit.\n * • `<branch>` — a branch can be checked out in at most one worktree (git enforces it), so within one\n * log directory the branch is effectively constant; it is in the name so a human reading the directory\n * can tell whose log is whose, and so switching branches never appends to a stale file.\n * • `<shortSha>` — a re-run at a NEW commit gets a new file, so the log a failure message points at is\n * always the build for the code that failed. A re-run at the SAME commit deliberately TRUNCATES: it is\n * the same build of the same tree, and the fresh one is the one worth reading.\n * The residual case is two agents running the same stage, at the same commit, in the SAME worktree,\n * simultaneously. That is already unsupported — both would be driving one git index and one merge state —\n * and it is the only case this scheme does not separate.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildGateLog {\n /** Absolute path of the log for `stage` at the current HEAD, creating the log directory. */\n pathFor(repoRoot: string, stage: string): string {\n const file = dotWebpieces.logsFile(repoRoot, this.fileNameFor(repoRoot, stage));\n fs.mkdirSync(path.dirname(file), { recursive: true });\n return file;\n }\n\n /** The same path WITHOUT creating anything, and '' when no such log exists. Used by finish's skip path. */\n existingLogFor(repoRoot: string, stage: string): string {\n const file = dotWebpieces.logsFile(repoRoot, this.fileNameFor(repoRoot, stage));\n return fs.existsSync(file) ? file : '';\n }\n\n /** `build-gate-<stage>-<branch>-<shortSha>.log` — see the class docstring for why this cannot collide. */\n fileNameFor(repoRoot: string, stage: string): string {\n const branch = this.slug(this.git(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']));\n const sha = this.slug(this.git(repoRoot, ['rev-parse', '--short', 'HEAD']));\n return `build-gate-${this.slug(stage)}-${branch === '' ? 'nobranch' : branch}-${sha === '' ? 'nosha' : sha}.log`;\n }\n\n /**\n * Run `buildCommand` with its combined stdout+stderr streaming to the terminal AND appended in full to\n * `logPath`. Returns the BUILD's exit code (not tee's). Nothing is truncated.\n */\n run(repoRoot: string, buildCommand: string, logPath: string): number {\n const statusFile = `${logPath}${STATUS_SUFFIX}`;\n fs.rmSync(statusFile, { force: true });\n const result = spawnSync(this.wrap(buildCommand, logPath, statusFile),\n { stdio: 'inherit', cwd: repoRoot, shell: true });\n const fromFile = this.readStatus(statusFile);\n fs.rmSync(statusFile, { force: true });\n if (fromFile !== null) return fromFile;\n // The side file never appeared ⇒ the shell died before the echo. The PIPELINE's status is tee's,\n // which is 0, and reporting 0 there would call a dead build green — so fail CLOSED instead.\n return result.status !== null && result.status !== 0 ? result.status : 1;\n }\n\n /**\n * The ENTIRE message the AI gets on a captured failure. Deliberately tiny: the whole point is that the\n * agent reads ONE file rather than carrying a build transcript in its context.\n *\n * The last sentence is not filler. If the log holds no visible failure then something upstream is wrong\n * (a runner that died without printing, a truncated pipe), and the worst possible response is an agent\n * guessing or rebuilding — so it is told to surface the contradiction to the human and stop.\n */\n failureMessage(buildCommand: string, logPath: string): string {\n return `\\n❌ The CI build failed. We ran\\n\\n` +\n ` ${buildCommand} > ${logPath}\\n\\n` +\n `and it failed, so read that file for the failures. Do NOT re-run the build to see them.\\n` +\n `If you do not see failures in that log, report that to the user and stop.\\n`;\n }\n\n /**\n * `{ ( <build> ) ; echo $? > <status> ; } 2>&1 | tee <log>` — written across LINES so a build command\n * ending in a `#` comment cannot swallow what follows it.\n *\n * The inner `( … )` is load-bearing, not decoration. The left side of a pipeline is already a subshell,\n * so a build command containing a plain `exit 7` (or any script that calls `exit`) would terminate that\n * subshell OUTRIGHT — `echo $?` never runs, the side file never appears, and the only status left is\n * tee's 0. The nested subshell absorbs the `exit`, so `$?` is the build's code every time.\n */\n private wrap(buildCommand: string, logPath: string, statusFile: string): string {\n return `{\\n(\\n${buildCommand}\\n)\\necho $? > ${this.quote(statusFile)}\\n} 2>&1 | tee ${this.quote(logPath)}`;\n }\n\n // The build's own exit code, or null when the side file never appeared (the shell died before the echo).\n private readStatus(statusFile: string): number | null {\n if (!fs.existsSync(statusFile)) return null;\n const parsed = Number.parseInt(fs.readFileSync(statusFile, 'utf8').trim(), 10);\n return Number.isNaN(parsed) ? null : parsed;\n }\n\n // POSIX single-quoting: everything is literal inside '…', and a literal ' is spelled '\\''.\n private quote(value: string): string {\n return `'${value.split(\"'\").join(`'\\\\''`)}'`;\n }\n\n // Anything that is not a filename-safe character becomes '-', so `dean/feat` cannot create directories.\n private slug(value: string): string {\n return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');\n }\n\n // One local git read. Fails SOFT to '' — a missing branch/sha degrades the FILENAME, and degrading a\n // filename may never be the reason a build gate does not run.\n private git(repoRoot: string, args: string[]): string {\n const result = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8' });\n if (result.status !== 0) return '';\n return (result.stdout ?? '').trim();\n }\n}\n"]}
1
+ {"version":3,"file":"build-gate-log.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-gate-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, spawnSync, ChildProcess } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { dotWebpieces, toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// ─── Why ───────────────────────────────────────────────────────────────────────────────────────────────\n// The build gate already builds everything. When the output only ever went to the CONSOLE, an agent that\n// wanted a different slice of it re-ran the WHOLE BUILD to get it: one measured session spent 23.9 minutes\n// across nine `nx affected` runs, five of them with NO code change in between — `| tail -50`, then\n// `> /tmp/file`, then `| grep`, then `| sed -n '1100,1230p'`. ~19 minutes spent re-reading a log.\n//\n// So the build's output is not streamed; it is REDIRECTED, in full, to a file whose path the caller is\n// handed on completion. Reading a different slice is then a `grep` of a FILE, not a second build.\n//\n// ─── Why a redirect and not `tee` ──────────────────────────────────────────────────────────────────────\n// An earlier cut of this used `cmd 2>&1 | tee log`, to keep the terminal byte-identical. That is what\n// makes the transcript expensive in the first place — an AI caller carries every line of it in context.\n// The redirect keeps the console to a handful of lines (a heartbeat, then a pointer at the file), which is\n// the entire productivity claim. Losing `tee` also deletes the `$?`-into-a-side-file dance it needed: in a\n// pipeline the shell reports TEE's status, which is 0 whether the build passed or failed, so the status\n// had to be smuggled out through a side file. With no pipe, the child's own exit code IS the answer.\n//\n// ─── Why async (`spawn`, not `spawnSync`) ──────────────────────────────────────────────────────────────\n// `spawnSync` blocks the event loop for the length of the build, so NOTHING can print while it runs — and\n// a silent terminal for 3–7 minutes is indistinguishable from a hang. The heartbeat is the reason this is\n// async, and it is why `run` returns a Promise and `BuildAffected.runBuildGate` is async with it.\n\n/** How often the heartbeat reports the log's size. Hardcoded: a knob here would be a knob the PR gate's\n * own build never receives, and the two must stay the same command. */\nexport const HEARTBEAT_MS = 10_000;\n\n/** How many trailing log lines the failure message echoes, so the immediate cause is visible without a\n * second command. Small on purpose — the FULL log is one grep away and the message must not become the\n * transcript it exists to replace. */\nexport const FAILURE_TAIL_LINES = 20;\n\n/**\n * Which stage's gate is being captured. The value decides the log FILENAME.\n */\nexport const REVIEW_STAGE = 'review';\nexport const FINISH_STAGE = 'finish';\n// `wp-build`, which is not a stage of the PR flow but runs the SAME gate (BuildAffected.runBuildGate).\nexport const BUILD_STAGE = 'build';\n\n/** The one fixed log name — see BuildGateLog.fileNameFor for why only `wp-build` gets one. */\nexport const BUILD_LOG_NAME = 'build.log';\nconst BACKUP_SUFFIX = '.bak';\n\n/**\n * The heartbeat's state: the line count reported on the PREVIOUS tick, so a tick that has not moved can\n * say so. Stateful per RUN, which is why it is constructed per run rather than injected.\n *\n * `still` is the load-bearing word. A build that is linking, or waiting on a cold nx cache, produces no\n * output for minutes; without `still` the caller sees the same number twice and cannot tell a stalled\n * BUILD from a stalled REPORTER.\n */\nexport class BuildLogHeartbeat {\n private previous: number | null = null;\n\n constructor(private readonly logPath: string, private readonly displayPath: string) {}\n\n /** One heartbeat line — `<path> size <n> lines`, plus ` still` when <n> has not moved. */\n tick(): string {\n const count = this.lineCount();\n const still = this.previous !== null && count === this.previous ? ' still' : '';\n this.previous = count;\n return `${this.displayPath} size ${count} lines${still}`;\n }\n\n // Lines currently in the log. A log that does not exist yet is zero lines, not an error: the build may\n // simply not have written its first byte, and a heartbeat may never be the reason a build stops.\n private lineCount(): number {\n if (!fs.existsSync(this.logPath)) return 0;\n const body = fs.readFileSync(this.logPath, 'utf8');\n if (body === '') return 0;\n return body.split('\\n').length - (body.endsWith('\\n') ? 1 : 0);\n }\n}\n\n/**\n * Captures the build gate's full output to a file, reports progress while it runs, and renders the\n * pointer the caller is handed instead of a rebuild instruction.\n *\n * ─── Two naming schemes, one rule each ─────────────────────────────────────────────────────────────────\n * • `wp-build` (BUILD_STAGE) writes ONE fixed path, `.webpieces/build.log`. It is fixed because a HUMAN\n * OR AN AGENT TYPES IT — `grep -n error .webpieces/build.log` has to be writable from memory, and a\n * name carrying a branch and a sha is not. History comes from the rotation below instead.\n * • stage ② and stage ③ write `logs/build-gate-<stage>-<branch>-<shortSha>.log`, because those two\n * gates CAN run against one commit and a failure message from one must not be pointing at a file the\n * other overwrote. Nobody types those names; the failure message prints them.\n *\n * ─── Rotation, everywhere ──────────────────────────────────────────────────────────────────────────────\n * Every run moves an existing log to `<log>.bak` before writing, so the last TWO runs are always on disk.\n * One rule for every stage: no branch, and the previous run of a re-run at the same commit survives\n * instead of being truncated away.\n *\n * ─── Concurrency ───────────────────────────────────────────────────────────────────────────────────────\n * The DIRECTORY is `dotWebpieces.local()`-scoped — `<primary>/.webpieces/worktrees/<git worktree name>/`\n * in a linked worktree — so \"N agents in N worktrees\" is safe by construction rather than by naming. The\n * residual case is two builds in the SAME worktree at once, which is already unsupported: both would be\n * driving one git index.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildGateLog {\n /** Absolute path of the log for `stage` at the current HEAD, creating its directory. */\n pathFor(repoRoot: string, stage: string): string {\n const file = this.resolvePath(repoRoot, stage);\n fs.mkdirSync(path.dirname(file), { recursive: true });\n return file;\n }\n\n /** The same path WITHOUT creating anything, and '' when no such log exists. Used by finish's skip path. */\n existingLogFor(repoRoot: string, stage: string): string {\n const file = this.resolvePath(repoRoot, stage);\n return fs.existsSync(file) ? file : '';\n }\n\n /** `build.log` for `wp-build`; `build-gate-<stage>-<branch>-<shortSha>.log` for the PR-flow stages. */\n fileNameFor(repoRoot: string, stage: string): string {\n if (stage === BUILD_STAGE) return BUILD_LOG_NAME;\n const branch = this.slug(this.git(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']));\n const sha = this.slug(this.git(repoRoot, ['rev-parse', '--short', 'HEAD']));\n return `build-gate-${this.slug(stage)}-${branch === '' ? 'nobranch' : branch}-${sha === '' ? 'nosha' : sha}.log`;\n }\n\n /** Where the PREVIOUS run of `logPath` is kept — always `<logPath>.bak`. */\n backupPathFor(logPath: string): string {\n return `${logPath}${BACKUP_SUFFIX}`;\n }\n\n /**\n * Move an existing log aside to `<log>.bak`, overwriting any previous backup. A missing log is the\n * normal first-run state and is not an error.\n */\n rotate(logPath: string): void {\n fs.mkdirSync(path.dirname(logPath), { recursive: true });\n if (!fs.existsSync(logPath)) return;\n fs.rmSync(this.backupPathFor(logPath), { force: true });\n fs.renameSync(logPath, this.backupPathFor(logPath));\n }\n\n /**\n * Run `buildCommand` with its stdout AND stderr redirected in full to `logPath`, printing a heartbeat\n * to the console every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's exit code.\n * Nothing is truncated and nothing is streamed.\n */\n async run(repoRoot: string, buildCommand: string, logPath: string): Promise<number> {\n this.rotate(logPath);\n const fd = fs.openSync(logPath, 'w');\n const heartbeat = new BuildLogHeartbeat(logPath, this.displayPath(repoRoot, logPath));\n const timer = setInterval((): void => { process.stdout.write(`${heartbeat.tick()}\\n`); }, HEARTBEAT_MS);\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the timer and the fd MUST be released\n // whatever the child does, and the exit code is returned rather than thrown so runBuildGate owns\n // the one CliExitError.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await this.awaitExit(spawn(buildCommand, { cwd: repoRoot, shell: true, stdio: ['ignore', fd, fd] }), fd);\n } finally {\n clearInterval(timer);\n fs.closeSync(fd);\n }\n }\n\n /**\n * The success summary: the caller is told WHERE the full output is, not handed the output.\n */\n successMessage(logPath: string): string {\n return `\\nBuild success\\n${this.logPointer(logPath)}`;\n }\n\n /**\n * The ENTIRE message the caller gets on a failed build. It names the log, echoes the last\n * FAILURE_TAIL_LINES lines so the immediate cause needs no second command, and forbids the rebuild\n * this whole file exists to prevent.\n *\n * The last sentence is not filler. If the log holds no visible failure then something upstream is wrong\n * (a runner that died without printing, a truncated redirect), and the worst possible response is an\n * agent guessing or rebuilding — so it is told to surface the contradiction to the human and stop.\n */\n failureMessage(buildCommand: string, logPath: string): string {\n return `\\nBuild Failed: ${buildCommand}\\n${this.logPointer(logPath)}\\n` +\n `Last ${FAILURE_TAIL_LINES} lines of that log:\\n${this.tail(logPath)}\\n` +\n `Read that FILE for the failures. Do NOT re-run the build to see them.\\n` +\n `If you do not see failures in that log, report that to the user and stop.\\n`;\n }\n\n // The two lines that name the log, identical on success and failure so there is one thing to recognise.\n // The backup line says what is TRUE RIGHT NOW: on the very first build in a tree there is no `.bak`\n // yet, and pointing a reader at a file that does not exist is the small lie that costs a wasted `cat`.\n private logPointer(logPath: string): string {\n const name = path.basename(logPath);\n const backedUp = fs.existsSync(this.backupPathFor(logPath))\n ? `(${name} is backed up to ${name}${BACKUP_SUFFIX} every run so you have the last 2 builds of logs)`\n : `(the previous ${name} is kept as ${name}${BACKUP_SUFFIX} on every run — this is the first, so there is none yet)`;\n return `FullLog : ${logPath}\\n${backedUp}\\n`;\n }\n\n /**\n * The log's last FAILURE_TAIL_LINES lines, or a plain statement of why there are none.\n *\n * A read that fails is REPORTED, never allowed to throw: this renders the message for a build that has\n * ALREADY failed, so an I/O error escaping here would replace the real failure with the renderer's own\n * — the caller would lose the build error and be handed a filesystem error instead. The full log is\n * still named on the line above, so nothing is hidden by degrading to one line.\n */\n private tail(logPath: string): string {\n if (!fs.existsSync(logPath)) return ` (no log file at ${logPath})\\n`;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: see above, the failure renderer may not\n // replace the build's failure with its own.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const lines = fs.readFileSync(logPath, 'utf8').split('\\n').filter((l: string): boolean => l !== '');\n if (lines.length === 0) return ' (the log is empty)\\n';\n return lines.slice(-FAILURE_TAIL_LINES).map((l: string): string => ` ${l}\\n`).join('');\n } catch (err: unknown) {\n const error = toError(err);\n return ` (could not read ${logPath}: ${error.message})\\n`;\n }\n }\n\n // Resolve, and wait for, the child's exit code. A spawn that never starts (a shell that is missing, a\n // cwd that vanished) fails CLOSED to 1 — calling a build that never ran green is the one outcome that\n // must be impossible — and the reason is APPENDED TO THE LOG, so the failure message's pointer still\n // leads to it rather than to an empty file.\n private awaitExit(child: ChildProcess, fd: number): Promise<number> {\n return new Promise<number>((resolve: (code: number) => void): void => {\n child.on('error', (err: Error): void => {\n fs.writeSync(fd, `\\nThe build command could not be started: ${err.message}\\n`);\n resolve(1);\n });\n child.on('close', (code: number | null): void => { resolve(code ?? 1); });\n });\n }\n\n // The path as the heartbeat shows it: relative to the repo when it sits inside it (a linked worktree's\n // state lives under the PRIMARY clone, so it often does not), absolute otherwise.\n private displayPath(repoRoot: string, logPath: string): string {\n const relative = path.relative(repoRoot, logPath);\n return relative === '' || relative.startsWith('..') || path.isAbsolute(relative) ? logPath : relative;\n }\n\n private resolvePath(repoRoot: string, stage: string): string {\n const name = this.fileNameFor(repoRoot, stage);\n // `wp-build`'s log sits at the ROOT of the state dir, not under `logs/`, because it is the one log\n // path a person types from memory. Everything else keeps the per-commit names in `logs/`.\n return stage === BUILD_STAGE ? dotWebpieces.localFile(repoRoot, name) : dotWebpieces.logsFile(repoRoot, name);\n }\n\n // Anything that is not a filename-safe character becomes '-', so `dean/feat` cannot create directories.\n private slug(value: string): string {\n return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');\n }\n\n // One local git read. Fails SOFT to '' — a missing branch/sha degrades the FILENAME, and degrading a\n // filename may never be the reason a build gate does not run.\n private git(repoRoot: string, args: string[]): string {\n const result = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8' });\n if (result.status !== 0) return '';\n return (result.stdout ?? '').trim();\n }\n}\n"]}