@webpieces/pr-gate 0.4.754 → 0.4.755
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.
- package/package.json +3 -3
- package/src/scripts/workflow/build-affected.js +9 -7
- package/src/scripts/workflow/build-affected.js.map +1 -1
- package/src/scripts/workflow/build-gate-log.d.ts +7 -4
- package/src/scripts/workflow/build-gate-log.js +29 -9
- package/src/scripts/workflow/build-gate-log.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.755",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
19
|
-
"@webpieces/ai-hook-rules": "0.4.
|
|
20
|
-
"@webpieces/rules-config": "0.4.
|
|
19
|
+
"@webpieces/ai-hook-rules": "0.4.755",
|
|
20
|
+
"@webpieces/rules-config": "0.4.755",
|
|
21
21
|
"inversify": "7.10.4",
|
|
22
22
|
"reflect-metadata": "0.2.2"
|
|
23
23
|
},
|
|
@@ -77,18 +77,20 @@ let BuildAffected = class BuildAffected {
|
|
|
77
77
|
this.stageConsole.say(`\n${opts.label}: ${buildCommand}\n`);
|
|
78
78
|
const logPath = this.buildLog.pathFor(repoRoot, opts.stage);
|
|
79
79
|
const ticket = this.buildsLog.start(opts.stage, repoRoot);
|
|
80
|
-
let
|
|
80
|
+
let termination = new rules_config_1.BuildTermination(1, null);
|
|
81
81
|
// webpieces-disable no-unmanaged-exceptions -- chokepoint: the DONE row must be written whether the
|
|
82
82
|
// build passed, failed, or the spawn itself blew up; the throw is re-raised untouched below
|
|
83
83
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
84
84
|
try {
|
|
85
|
-
|
|
85
|
+
termination = await this.buildLog.run(repoRoot, buildCommand, logPath);
|
|
86
86
|
}
|
|
87
87
|
finally {
|
|
88
|
-
this.buildsLog.finish(ticket,
|
|
88
|
+
this.buildsLog.finish(ticket, termination);
|
|
89
|
+
}
|
|
90
|
+
const buildCode = termination.code === null || termination.signal !== null ? 1 : termination.code;
|
|
91
|
+
if (buildCode !== 0) {
|
|
92
|
+
throw new rules_config_1.CliExitError(buildCode, this.failureText(opts, buildCommand, logPath, termination));
|
|
89
93
|
}
|
|
90
|
-
if (buildCode !== 0)
|
|
91
|
-
throw new rules_config_1.CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));
|
|
92
94
|
this.stageConsole.say(this.buildLog.successMessage(logPath));
|
|
93
95
|
}
|
|
94
96
|
/**
|
|
@@ -99,9 +101,9 @@ let BuildAffected = class BuildAffected {
|
|
|
99
101
|
* and the un-captured branch's advice was "run the build again yourself", which is the single most
|
|
100
102
|
* expensive thing an agent can be told. Reading a FILE is now the only answer this gate gives.
|
|
101
103
|
*/
|
|
102
|
-
failureText(opts, buildCommand, logPath) {
|
|
104
|
+
failureText(opts, buildCommand, logPath, termination) {
|
|
103
105
|
return `\n❌ ${opts.failureHeadline}\n` +
|
|
104
|
-
this.buildLog.failureMessage(buildCommand, logPath) +
|
|
106
|
+
this.buildLog.failureMessage(buildCommand, logPath, termination) +
|
|
105
107
|
`Fix what that log shows, then re-run ${opts.rerunCommand}.\n`;
|
|
106
108
|
}
|
|
107
109
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":";;;;AAAA,0DAEiC;AACjC,yCAA2D;AAC3D,qDAAgD;AAChD,yDAAoD;AAEpD,iGAAiG;AACjG,8FAA8F;AAC9F,iGAAiG;AACjG,oFAAoF;AACpF,oGAAoG;AACpG,iEAAiE;AAGjE;;;;GAIG;AACH,MAAa,gBAAgB;IACzB,KAAK,CAAS,CAAY,sCAAsC;IAChE,YAAY,CAAS,CAAK,gDAAgD;IAC1E,eAAe,CAAS,CAAE,gCAAgC;IAC1D,qGAAqG;IACrG,uGAAuG;IACvG,KAAK,CAAS;IAEd,YAAY,KAAa,EAAE,YAAoB,EAAE,eAAuB,EAAE,KAAa;QACnF,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AAdD,4CAcC;AAED,qGAAqG;AAE9F,IAAM,aAAa,GAAnB,MAAM,aAAa;IAED;IACA;IACA;IAHrB,YACqB,QAAsB,EACtB,SAAoB,EACpB,YAA4B;QAF5B,aAAQ,GAAR,QAAQ,CAAc;QACtB,cAAS,GAAT,SAAS,CAAW;QACpB,iBAAY,GAAZ,YAAY,CAAgB;IAC9C,CAAC;IAEJ;;;OAGG;IACH,mBAAmB,CAAC,QAAgB;QAChC,MAAM,UAAU,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;QACjE,OAAO,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,oCAAqB,CAAC;IACrG,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,YAAY,CAAC,QAAgB,EAAE,IAAsB;QACvD,MAAM,YAAY,GAAG,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QACxD,mGAAmG;QACnG,mGAAmG;QACnG,sFAAsF;QACtF,EAAE;QACF,4FAA4F;QAC5F,iGAAiG;QACjG,qBAAqB;QACrB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,KAAK,KAAK,YAAY,IAAI,CAAC,CAAC;QAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAC1D,IAAI,
|
|
1
|
+
{"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n loadAndValidate, CliExitError, DEFAULT_BUILD_COMMAND, BuildsLog, BuildTermination,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { BuildGateLog } from './build-gate-log';\nimport { StageOutputLog } from './stage-output-log';\n\n// Single source of truth for RUNNING the build gate. `wp-start-upsert-pr` runs NO build; stage ②\n// (`wp-review-upsert-pr`) runs it authoritatively before any reviewer is spawned, and stage ③\n// (`wp-finish-upsert-pr`) re-runs it only when HEAD has moved since. nx `affected` only rebuilds\n// changed projects. The fallback command itself lives in @webpieces/rules-config as\n// DEFAULT_BUILD_COMMAND — whole-repo-build-guard prints the same string, and one definition is what\n// keeps the refusal message naming the build that actually runs.\n\n\n/**\n * The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept\n * as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)\n * per the codebase's data-structure convention.\n */\nexport class BuildGateOptions {\n label: string; // section header shown above the gate\n rerunCommand: string; // command the AI re-runs after fixing the build\n failureHeadline: string; // first line printed on failure\n // WHICH stage's gate this is — REVIEW_STAGE, FINISH_STAGE or BUILD_STAGE. Required, with no default:\n // it decides the captured log's filename, and a default would silently make two stages share one file.\n stage: string;\n\n constructor(label: string, rerunCommand: string, failureHeadline: string, stage: string) {\n this.label = label;\n this.rerunCommand = rerunCommand;\n this.failureHeadline = failureHeadline;\n this.stage = stage;\n }\n}\n\n/** Runs the authoritative nx-affected build gate for wp-review-upsert-pr and wp-finish-upsert-pr. */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildAffected {\n constructor(\n private readonly buildLog: BuildGateLog,\n private readonly buildsLog: BuildsLog,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n /**\n * Resolve the exact build command this gate will run: the project's configured\n * PrGateConfig.buildCommand, or the default affected-ci command when none is set.\n */\n resolveBuildCommand(repoRoot: string): string {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n return configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n }\n\n /**\n * Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on\n * failure so the bin's main()/runMain owns the exit. THE single source of truth for running a build:\n * wp-build, wp-review-upsert-pr and wp-finish-upsert-pr all call THIS (only the BuildGateOptions\n * differ), and there is no other way to reach the spawn.\n *\n * ─── EVERY BUILD ON THIS MACHINE IS LEDGERED FROM HERE ───────────────────────────────────────────\n * A START row goes into `~/.webpieces/builds.log` before the spawn and a DONE row after it, in a\n * `finally` so a throw on the failure path still closes the pair. `opts.stage` — `build` | `review` |\n * `finish` — IS the caller id the row records; there is deliberately no second `caller` field, since\n * two spellings of one fact is the shim `.claude/rules/no-backwards-compat.md` rejects.\n *\n * The ledger is best-effort by construction (see BuildsLog): it can never throw, so no build can die\n * because a log file was busy.\n */\n async runBuildGate(repoRoot: string, opts: BuildGateOptions): Promise<void> {\n const buildCommand = this.resolveBuildCommand(repoRoot);\n // TWO lines on the happy path — the command, then the result. The old framing spent a banner and a\n // paragraph explaining how to reproduce a build that was about to pass anyway; that explanation is\n // only useful when the build FAILS, so it now lives solely on the failure path below.\n //\n // `say`, so it survives a stage that is capturing its own console (see StageOutputLog): the\n // command being run and the result of running it are exactly what a caller watching a long build\n // needs to see live.\n this.stageConsole.say(`\\n${opts.label}: ${buildCommand}\\n`);\n const logPath = this.buildLog.pathFor(repoRoot, opts.stage);\n const ticket = this.buildsLog.start(opts.stage, repoRoot);\n let termination = new BuildTermination(1, null);\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the DONE row must be written whether the\n // build passed, failed, or the spawn itself blew up; the throw is re-raised untouched below\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n termination = await this.buildLog.run(repoRoot, buildCommand, logPath);\n } finally {\n this.buildsLog.finish(ticket, termination);\n }\n const buildCode = termination.code === null || termination.signal !== null ? 1 : termination.code;\n if (buildCode !== 0) {\n throw new CliExitError(buildCode, this.failureText(opts, buildCommand, logPath, termination));\n }\n this.stageConsole.say(this.buildLog.successMessage(logPath));\n }\n\n /**\n * The failure text: this gate's own headline, then the pointer at the log the build already wrote.\n *\n * There is no second, streamed variant any more. Capturing used to be behind an EXPERIMENTAL\n * machine-local opt-in, which meant the refusal an agent read depended on a file almost nobody had —\n * and the un-captured branch's advice was \"run the build again yourself\", which is the single most\n * expensive thing an agent can be told. Reading a FILE is now the only answer this gate gives.\n */\n private failureText(\n opts: BuildGateOptions, buildCommand: string, logPath: string, termination: BuildTermination,\n ): string {\n return `\\n❌ ${opts.failureHeadline}\\n` +\n this.buildLog.failureMessage(buildCommand, logPath, termination) +\n `Fix what that log shows, then re-run ${opts.rerunCommand}.\\n`;\n }\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BuildTermination } from '@webpieces/rules-config';
|
|
1
2
|
import { GateLogFile } from './gate-log-file';
|
|
2
3
|
import { StageOutputLog } from './stage-output-log';
|
|
3
4
|
/** How often the heartbeat reports the log's size. Hardcoded: a knob here would be a knob the PR gate's
|
|
@@ -65,10 +66,10 @@ export declare class BuildGateLog {
|
|
|
65
66
|
fileNameFor(repoRoot: string, stage: string): string;
|
|
66
67
|
/**
|
|
67
68
|
* Run `buildCommand` with its stdout AND stderr redirected in full to `logPath`, printing a heartbeat
|
|
68
|
-
* every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's
|
|
69
|
-
* truncated and nothing is streamed.
|
|
69
|
+
* every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's raw termination facts.
|
|
70
|
+
* Nothing is truncated and nothing is streamed.
|
|
70
71
|
*/
|
|
71
|
-
run(repoRoot: string, buildCommand: string, logPath: string): Promise<
|
|
72
|
+
run(repoRoot: string, buildCommand: string, logPath: string): Promise<BuildTermination>;
|
|
72
73
|
/**
|
|
73
74
|
* The success summary: the caller is told WHERE the full output is, not handed the output.
|
|
74
75
|
*/
|
|
@@ -82,8 +83,10 @@ export declare class BuildGateLog {
|
|
|
82
83
|
* (a runner that died without printing, a truncated redirect), and the worst possible response is an
|
|
83
84
|
* agent guessing or rebuilding — so it is told to surface the contradiction to the human and stop.
|
|
84
85
|
*/
|
|
85
|
-
failureMessage(buildCommand: string, logPath: string): string;
|
|
86
|
+
failureMessage(buildCommand: string, logPath: string, termination: BuildTermination): string;
|
|
86
87
|
private awaitExit;
|
|
88
|
+
/** One stable, human-readable spelling shared by the FullLog and the immediate failure output. */
|
|
89
|
+
private terminationRecord;
|
|
87
90
|
private resolvePath;
|
|
88
91
|
private slug;
|
|
89
92
|
private git;
|
|
@@ -4,6 +4,7 @@ exports.BuildGateLog = exports.BuildLogHeartbeat = exports.BUILD_LOG_NAME = expo
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
6
|
const fs = tslib_1.__importStar(require("fs"));
|
|
7
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
8
|
const inversify_1 = require("inversify");
|
|
8
9
|
const gate_log_file_1 = require("./gate-log-file");
|
|
9
10
|
const stage_output_log_1 = require("./stage-output-log");
|
|
@@ -122,8 +123,8 @@ let BuildGateLog = class BuildGateLog {
|
|
|
122
123
|
}
|
|
123
124
|
/**
|
|
124
125
|
* Run `buildCommand` with its stdout AND stderr redirected in full to `logPath`, printing a heartbeat
|
|
125
|
-
* every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's
|
|
126
|
-
* truncated and nothing is streamed.
|
|
126
|
+
* every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's raw termination facts.
|
|
127
|
+
* Nothing is truncated and nothing is streamed.
|
|
127
128
|
*/
|
|
128
129
|
async run(repoRoot, buildCommand, logPath) {
|
|
129
130
|
this.files.rotate(logPath);
|
|
@@ -157,25 +158,44 @@ let BuildGateLog = class BuildGateLog {
|
|
|
157
158
|
* (a runner that died without printing, a truncated redirect), and the worst possible response is an
|
|
158
159
|
* agent guessing or rebuilding — so it is told to surface the contradiction to the human and stop.
|
|
159
160
|
*/
|
|
160
|
-
failureMessage(buildCommand, logPath) {
|
|
161
|
+
failureMessage(buildCommand, logPath, termination) {
|
|
161
162
|
return `\nBuild Failed: ${buildCommand}\n${this.files.pointer(logPath)}\n` +
|
|
163
|
+
`${this.terminationRecord(termination)}\n` +
|
|
162
164
|
`Last ${exports.FAILURE_TAIL_LINES} lines of that log:\n${this.files.tail(logPath, exports.FAILURE_TAIL_LINES)}\n` +
|
|
163
165
|
`Read that FILE for the failures. Do NOT re-run the build to see them.\n` +
|
|
164
166
|
`If you do not see failures in that log, report that to the user and stop.\n`;
|
|
165
167
|
}
|
|
166
|
-
// Resolve, and wait for, the child's
|
|
167
|
-
// cwd that vanished) fails CLOSED to 1 — calling a build that never ran green is the one
|
|
168
|
-
// must be impossible — and the reason is APPENDED TO THE LOG, so the failure message's
|
|
169
|
-
// leads to it rather than to an empty file.
|
|
168
|
+
// Resolve, and wait for, the child's close code AND signal. A spawn that never starts (a shell that is
|
|
169
|
+
// missing, a cwd that vanished) fails CLOSED to 1 — calling a build that never ran green is the one
|
|
170
|
+
// outcome that must be impossible — and the reason is APPENDED TO THE LOG, so the failure message's
|
|
171
|
+
// pointer still leads to it rather than to an empty file.
|
|
170
172
|
awaitExit(child, fd) {
|
|
171
173
|
return new Promise((resolve) => {
|
|
174
|
+
let settled = false;
|
|
172
175
|
child.on('error', (err) => {
|
|
176
|
+
if (settled)
|
|
177
|
+
return;
|
|
178
|
+
settled = true;
|
|
173
179
|
fs.writeSync(fd, `\nThe build command could not be started: ${err.message}\n`);
|
|
174
|
-
resolve(1);
|
|
180
|
+
resolve(new rules_config_1.BuildTermination(1, null));
|
|
181
|
+
});
|
|
182
|
+
child.on('close', (code, signal) => {
|
|
183
|
+
if (settled)
|
|
184
|
+
return;
|
|
185
|
+
settled = true;
|
|
186
|
+
const termination = new rules_config_1.BuildTermination(code, signal);
|
|
187
|
+
if (code === null || signal !== null)
|
|
188
|
+
fs.writeSync(fd, `\n${this.terminationRecord(termination)}\n`);
|
|
189
|
+
resolve(termination);
|
|
175
190
|
});
|
|
176
|
-
child.on('close', (code) => { resolve(code ?? 1); });
|
|
177
191
|
});
|
|
178
192
|
}
|
|
193
|
+
/** One stable, human-readable spelling shared by the FullLog and the immediate failure output. */
|
|
194
|
+
terminationRecord(termination) {
|
|
195
|
+
const code = termination.code === null ? 'null' : String(termination.code);
|
|
196
|
+
const signal = termination.signal === null ? 'none' : termination.signal;
|
|
197
|
+
return `Build process termination: exit code ${code}; signal ${signal}.`;
|
|
198
|
+
}
|
|
179
199
|
resolvePath(repoRoot, stage) {
|
|
180
200
|
const name = this.fileNameFor(repoRoot, stage);
|
|
181
201
|
// `wp-build`'s log sits at the ROOT of the state dir, not under `logs/`, because it is the one log
|
|
@@ -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 { spawn, spawnSync, ChildProcess } from 'child_process';\nimport * as fs from 'fs';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GateLogFile } from './gate-log-file';\nimport { StageOutputLog } from './stage-output-log';\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// It is also the reason the heartbeat goes through `StageOutputLog.say` rather than `process.stdout` —\n// stage ② and stage ③ capture their own console output to a file, and a heartbeat captured into a file is\n// a heartbeat nobody can see. The 600-second watchdog that kills a silent command does not read files.\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';\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(\n private readonly files: GateLogFile,\n private readonly logPath: string,\n private readonly displayPath: string,\n ) {}\n\n /** One heartbeat line — `<path> size <n> lines`, plus ` still` when <n> has not moved. */\n tick(): string {\n const count = this.files.lineCount(this.logPath);\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\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 * WHERE the file goes, how the previous run is kept, and what the pointer reads like are `GateLogFile`'s\n * — one mechanism, shared with the stage console log, so there is a single answer to \"where is it?\".\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 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 * ─── 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 constructor(\n private readonly files: GateLogFile,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n /** Absolute path of the log for `stage` at the current HEAD, creating its directory. */\n pathFor(repoRoot: string, stage: string): string {\n return this.resolvePath(repoRoot, stage);\n }\n\n /** The same path, and '' when no log has been WRITTEN there yet. 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 /**\n * Run `buildCommand` with its stdout AND stderr redirected in full to `logPath`, printing a heartbeat\n * every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's exit code. Nothing is\n * truncated and nothing is streamed.\n */\n async run(repoRoot: string, buildCommand: string, logPath: string): Promise<number> {\n this.files.rotate(logPath);\n const fd = fs.openSync(logPath, 'w');\n const heartbeat = new BuildLogHeartbeat(this.files, logPath, this.files.displayPath(repoRoot, logPath));\n const timer = setInterval((): void => { this.stageConsole.say(`${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.files.pointer(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.files.pointer(logPath)}\\n` +\n `Last ${FAILURE_TAIL_LINES} lines of that log:\\n${this.files.tail(logPath, FAILURE_TAIL_LINES)}\\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 // 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 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 ? this.files.localPath(repoRoot, name) : this.files.logsPath(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"]}
|
|
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 { BuildTermination } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GateLogFile } from './gate-log-file';\nimport { StageOutputLog } from './stage-output-log';\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// It is also the reason the heartbeat goes through `StageOutputLog.say` rather than `process.stdout` —\n// stage ② and stage ③ capture their own console output to a file, and a heartbeat captured into a file is\n// a heartbeat nobody can see. The 600-second watchdog that kills a silent command does not read files.\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';\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(\n private readonly files: GateLogFile,\n private readonly logPath: string,\n private readonly displayPath: string,\n ) {}\n\n /** One heartbeat line — `<path> size <n> lines`, plus ` still` when <n> has not moved. */\n tick(): string {\n const count = this.files.lineCount(this.logPath);\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\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 * WHERE the file goes, how the previous run is kept, and what the pointer reads like are `GateLogFile`'s\n * — one mechanism, shared with the stage console log, so there is a single answer to \"where is it?\".\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 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 * ─── 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 constructor(\n private readonly files: GateLogFile,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n /** Absolute path of the log for `stage` at the current HEAD, creating its directory. */\n pathFor(repoRoot: string, stage: string): string {\n return this.resolvePath(repoRoot, stage);\n }\n\n /** The same path, and '' when no log has been WRITTEN there yet. 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 /**\n * Run `buildCommand` with its stdout AND stderr redirected in full to `logPath`, printing a heartbeat\n * every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's raw termination facts.\n * Nothing is truncated and nothing is streamed.\n */\n async run(repoRoot: string, buildCommand: string, logPath: string): Promise<BuildTermination> {\n this.files.rotate(logPath);\n const fd = fs.openSync(logPath, 'w');\n const heartbeat = new BuildLogHeartbeat(this.files, logPath, this.files.displayPath(repoRoot, logPath));\n const timer = setInterval((): void => { this.stageConsole.say(`${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.files.pointer(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, termination: BuildTermination): string {\n return `\\nBuild Failed: ${buildCommand}\\n${this.files.pointer(logPath)}\\n` +\n `${this.terminationRecord(termination)}\\n` +\n `Last ${FAILURE_TAIL_LINES} lines of that log:\\n${this.files.tail(logPath, FAILURE_TAIL_LINES)}\\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 // Resolve, and wait for, the child's close code AND signal. A spawn that never starts (a shell that is\n // missing, a cwd that vanished) fails CLOSED to 1 — calling a build that never ran green is the one\n // outcome that must be impossible — and the reason is APPENDED TO THE LOG, so the failure message's\n // pointer still leads to it rather than to an empty file.\n private awaitExit(child: ChildProcess, fd: number): Promise<BuildTermination> {\n return new Promise<BuildTermination>((resolve: (termination: BuildTermination) => void): void => {\n let settled = false;\n child.on('error', (err: Error): void => {\n if (settled) return;\n settled = true;\n fs.writeSync(fd, `\\nThe build command could not be started: ${err.message}\\n`);\n resolve(new BuildTermination(1, null));\n });\n child.on('close', (code: number | null, signal: NodeJS.Signals | null): void => {\n if (settled) return;\n settled = true;\n const termination = new BuildTermination(code, signal);\n if (code === null || signal !== null) fs.writeSync(fd, `\\n${this.terminationRecord(termination)}\\n`);\n resolve(termination);\n });\n });\n }\n\n /** One stable, human-readable spelling shared by the FullLog and the immediate failure output. */\n private terminationRecord(termination: BuildTermination): string {\n const code = termination.code === null ? 'null' : String(termination.code);\n const signal = termination.signal === null ? 'none' : termination.signal;\n return `Build process termination: exit code ${code}; signal ${signal}.`;\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 ? this.files.localPath(repoRoot, name) : this.files.logsPath(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"]}
|