@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.
- package/package.json +2 -2
- package/src/scripts/commands/build-command.d.ts +9 -1
- package/src/scripts/commands/build-command.js +10 -3
- package/src/scripts/commands/build-command.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.js +3 -3
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/review-upsert-pr-command.js +3 -3
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/build-affected.d.ts +3 -2
- package/src/scripts/workflow/build-affected.js +16 -10
- package/src/scripts/workflow/build-affected.js.map +1 -1
- package/src/scripts/workflow/build-gate-log.d.ts +79 -38
- package/src/scripts/workflow/build-gate-log.js +195 -88
- package/src/scripts/workflow/build-gate-log.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
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 { spawnSync } from 'child_process';\nimport { loadAndValidate, CliExitError, HomeConfigService, DEFAULT_BUILD_COMMAND } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { BuildGateLog } from './build-gate-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\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\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 or FINISH_STAGE. Required, with no default: it is part of\n // the captured log's filename, and a default would silently make two stages share one log 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 homeConfig: HomeConfigService,\n private readonly buildLog: BuildGateLog,\n ) {}\n\n /**\n * EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.\n *\n * That file does not exist for essentially anyone, and its absence is not an error, a warning or a\n * behaviour change of any kind — `HomeConfigService.load` returns all-defaults silently. False here is\n * therefore the state every consumer is in, and false means runBuildGate executes exactly the code it\n * executed before this feature existed.\n */\n isCaptureEnabled(): boolean {\n return this.homeConfig.load().buildGateLogCapture;\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 build gate. Returns the process exit code (0 = pass).\n *\n * Prints NOTHING itself — `runBuildGate` announces the command in one line. This used to print its own\n * `▶ Build gate: <cmd>` banner on top of that, so the command appeared twice in a row.\n */\n runBuildAffected(repoRoot: string, buildCommand?: string): number {\n const cmd = buildCommand !== undefined && buildCommand.trim() !== '' ? buildCommand : DEFAULT_BUILD_COMMAND;\n const result = spawnSync(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });\n return result.status ?? 1;\n }\n\n /** Run the build gate using the project's configured command (PrGateConfig.buildCommand). */\n runConfiguredBuildGate(repoRoot: string): number {\n return this.runBuildAffected(repoRoot, loadAndValidate(repoRoot).prGate.buildCommand);\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. Single source of truth: wp-start-upsert-pr and\n * wp-finish-upsert-pr both call THIS (only the BuildGateOptions differ).\n */\n runBuildGate(repoRoot: string, opts: BuildGateOptions): 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 process.stdout.write(`\\n${opts.label}: ${buildCommand}\\n`);\n // '' means NOT capturing, which is the case for every user who has not created the OPTIONAL\n // `~/.webpieces/config.json`. Everything below then runs exactly the code it ran before this\n // feature existed — same spawn, same message, no extra file, no extra line of output.\n const logPath = this.isCaptureEnabled() ? this.buildLog.pathFor(repoRoot, opts.stage) : '';\n const buildCode = logPath === ''\n ? this.runConfiguredBuildGate(repoRoot)\n : this.buildLog.run(repoRoot, buildCommand, logPath);\n if (buildCode !== 0) throw new CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));\n process.stdout.write('\\n✅ Build passed.\\n');\n }\n\n /**\n * On failure, WITHOUT capture: the pre-existing text, which tells the AI to re-run the build itself.\n * WITH capture: a deliberately tiny pointer at the log the gate already wrote — the whole point of the\n * feature is that the agent reads one file instead of rebuilding the repo and eating the transcript.\n */\n private failureText(opts: BuildGateOptions, buildCommand: string, logPath: string): string {\n if (logPath !== '') return this.buildLog.failureMessage(buildCommand, logPath);\n return `\\n❌ ${opts.failureHeadline}\\n\\n` +\n `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\\n\\n` +\n ` ${buildCommand}\\n`;\n }\n}\n"]}
|
|
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 { spawnSync } from 'child_process';\nimport { loadAndValidate, CliExitError, HomeConfigService, DEFAULT_BUILD_COMMAND } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { BuildGateLog } from './build-gate-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\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\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 // Capture regardless of the EXPERIMENTAL `~/.webpieces/config.json` opt-in. True for `wp-build`, whose\n // entire contract IS the log file — its console output is a heartbeat and a pointer at that file, so a\n // wp-build that did not capture would have nothing to point at. The PR-flow stages pass false and stay\n // on the opt-in until the experiment lands for them too.\n alwaysCapture: boolean;\n\n constructor(label: string, rerunCommand: string, failureHeadline: string, stage: string, alwaysCapture: boolean) {\n this.label = label;\n this.rerunCommand = rerunCommand;\n this.failureHeadline = failureHeadline;\n this.stage = stage;\n this.alwaysCapture = alwaysCapture;\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 homeConfig: HomeConfigService,\n private readonly buildLog: BuildGateLog,\n ) {}\n\n /**\n * EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.\n *\n * That file does not exist for essentially anyone, and its absence is not an error, a warning or a\n * behaviour change of any kind — `HomeConfigService.load` returns all-defaults silently. False here is\n * therefore the state every consumer is in, and false means runBuildGate executes exactly the code it\n * executed before this feature existed.\n */\n isCaptureEnabled(): boolean {\n return this.homeConfig.load().buildGateLogCapture;\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 build gate. Returns the process exit code (0 = pass).\n *\n * Prints NOTHING itself — `runBuildGate` announces the command in one line. This used to print its own\n * `▶ Build gate: <cmd>` banner on top of that, so the command appeared twice in a row.\n */\n runBuildAffected(repoRoot: string, buildCommand?: string): number {\n const cmd = buildCommand !== undefined && buildCommand.trim() !== '' ? buildCommand : DEFAULT_BUILD_COMMAND;\n const result = spawnSync(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });\n return result.status ?? 1;\n }\n\n /** Run the build gate using the project's configured command (PrGateConfig.buildCommand). */\n runConfiguredBuildGate(repoRoot: string): number {\n return this.runBuildAffected(repoRoot, loadAndValidate(repoRoot).prGate.buildCommand);\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. Single source of truth: wp-start-upsert-pr and\n * wp-finish-upsert-pr both call THIS (only the BuildGateOptions differ).\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 process.stdout.write(`\\n${opts.label}: ${buildCommand}\\n`);\n // '' means NOT capturing: a PR-flow stage on a machine that has not created the OPTIONAL\n // `~/.webpieces/config.json`. Everything below then runs exactly the code it ran before capture\n // existed — same spawn, streamed to the terminal, same message, no extra file.\n const logPath = opts.alwaysCapture || this.isCaptureEnabled() ? this.buildLog.pathFor(repoRoot, opts.stage) : '';\n const buildCode = logPath === ''\n ? this.runConfiguredBuildGate(repoRoot)\n : await this.buildLog.run(repoRoot, buildCommand, logPath);\n if (buildCode !== 0) throw new CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));\n process.stdout.write(logPath === '' ? '\\n✅ Build passed.\\n' : this.buildLog.successMessage(logPath));\n }\n\n /**\n * On failure, WITHOUT capture: the pre-existing text, which tells the AI to re-run the build itself.\n * WITH capture: a deliberately tiny pointer at the log the gate already wrote — the whole point of the\n * feature is that the agent reads one file instead of rebuilding the repo and eating the transcript.\n */\n private failureText(opts: BuildGateOptions, buildCommand: string, logPath: string): string {\n if (logPath !== '') return this.buildLog.failureMessage(buildCommand, logPath);\n return `\\n❌ ${opts.failureHeadline}\\n\\n` +\n `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\\n\\n` +\n ` ${buildCommand}\\n`;\n }\n}\n"]}
|
|
@@ -1,64 +1,105 @@
|
|
|
1
|
+
/** How often the heartbeat reports the log's size. Hardcoded: a knob here would be a knob the PR gate's
|
|
2
|
+
* own build never receives, and the two must stay the same command. */
|
|
3
|
+
export declare const HEARTBEAT_MS = 10000;
|
|
4
|
+
/** How many trailing log lines the failure message echoes, so the immediate cause is visible without a
|
|
5
|
+
* second command. Small on purpose — the FULL log is one grep away and the message must not become the
|
|
6
|
+
* transcript it exists to replace. */
|
|
7
|
+
export declare const FAILURE_TAIL_LINES = 20;
|
|
1
8
|
/**
|
|
2
|
-
* Which stage's gate is being captured. The value
|
|
3
|
-
* write the same file even at the same commit on the same branch.
|
|
9
|
+
* Which stage's gate is being captured. The value decides the log FILENAME.
|
|
4
10
|
*/
|
|
5
11
|
export declare const REVIEW_STAGE = "review";
|
|
6
12
|
export declare const FINISH_STAGE = "finish";
|
|
7
13
|
export declare const BUILD_STAGE = "build";
|
|
14
|
+
/** The one fixed log name — see BuildGateLog.fileNameFor for why only `wp-build` gets one. */
|
|
15
|
+
export declare const BUILD_LOG_NAME = "build.log";
|
|
8
16
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
17
|
+
* The heartbeat's state: the line count reported on the PREVIOUS tick, so a tick that has not moved can
|
|
18
|
+
* say so. Stateful per RUN, which is why it is constructed per run rather than injected.
|
|
11
19
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
*
|
|
20
|
+
* `still` is the load-bearing word. A build that is linking, or waiting on a cold nx cache, produces no
|
|
21
|
+
* output for minutes; without `still` the caller sees the same number twice and cannot tell a stalled
|
|
22
|
+
* BUILD from a stalled REPORTER.
|
|
23
|
+
*/
|
|
24
|
+
export declare class BuildLogHeartbeat {
|
|
25
|
+
private readonly logPath;
|
|
26
|
+
private readonly displayPath;
|
|
27
|
+
private previous;
|
|
28
|
+
constructor(logPath: string, displayPath: string);
|
|
29
|
+
/** One heartbeat line — `<path> size <n> lines`, plus ` still` when <n> has not moved. */
|
|
30
|
+
tick(): string;
|
|
31
|
+
private lineCount;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Captures the build gate's full output to a file, reports progress while it runs, and renders the
|
|
35
|
+
* pointer the caller is handed instead of a rebuild instruction.
|
|
36
|
+
*
|
|
37
|
+
* ─── Two naming schemes, one rule each ─────────────────────────────────────────────────────────────────
|
|
38
|
+
* • `wp-build` (BUILD_STAGE) writes ONE fixed path, `.webpieces/build.log`. It is fixed because a HUMAN
|
|
39
|
+
* OR AN AGENT TYPES IT — `grep -n error .webpieces/build.log` has to be writable from memory, and a
|
|
40
|
+
* name carrying a branch and a sha is not. History comes from the rotation below instead.
|
|
41
|
+
* • stage ② and stage ③ write `logs/build-gate-<stage>-<branch>-<shortSha>.log`, because those two
|
|
42
|
+
* gates CAN run against one commit and a failure message from one must not be pointing at a file the
|
|
43
|
+
* other overwrote. Nobody types those names; the failure message prints them.
|
|
44
|
+
*
|
|
45
|
+
* ─── Rotation, everywhere ──────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
* Every run moves an existing log to `<log>.bak` before writing, so the last TWO runs are always on disk.
|
|
47
|
+
* One rule for every stage: no branch, and the previous run of a re-run at the same commit survives
|
|
48
|
+
* instead of being truncated away.
|
|
49
|
+
*
|
|
50
|
+
* ─── Concurrency ───────────────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
* The DIRECTORY is `dotWebpieces.local()`-scoped — `<primary>/.webpieces/worktrees/<git worktree name>/`
|
|
52
|
+
* in a linked worktree — so "N agents in N worktrees" is safe by construction rather than by naming. The
|
|
53
|
+
* residual case is two builds in the SAME worktree at once, which is already unsupported: both would be
|
|
54
|
+
* driving one git index.
|
|
28
55
|
*/
|
|
29
56
|
export declare class BuildGateLog {
|
|
30
|
-
/** Absolute path of the log for `stage` at the current HEAD, creating
|
|
57
|
+
/** Absolute path of the log for `stage` at the current HEAD, creating its directory. */
|
|
31
58
|
pathFor(repoRoot: string, stage: string): string;
|
|
32
59
|
/** The same path WITHOUT creating anything, and '' when no such log exists. Used by finish's skip path. */
|
|
33
60
|
existingLogFor(repoRoot: string, stage: string): string;
|
|
34
|
-
/** `build-gate-<stage>-<branch>-<shortSha>.log`
|
|
61
|
+
/** `build.log` for `wp-build`; `build-gate-<stage>-<branch>-<shortSha>.log` for the PR-flow stages. */
|
|
35
62
|
fileNameFor(repoRoot: string, stage: string): string;
|
|
63
|
+
/** Where the PREVIOUS run of `logPath` is kept — always `<logPath>.bak`. */
|
|
64
|
+
backupPathFor(logPath: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Move an existing log aside to `<log>.bak`, overwriting any previous backup. A missing log is the
|
|
67
|
+
* normal first-run state and is not an error.
|
|
68
|
+
*/
|
|
69
|
+
rotate(logPath: string): void;
|
|
70
|
+
/**
|
|
71
|
+
* Run `buildCommand` with its stdout AND stderr redirected in full to `logPath`, printing a heartbeat
|
|
72
|
+
* to the console every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's exit code.
|
|
73
|
+
* Nothing is truncated and nothing is streamed.
|
|
74
|
+
*/
|
|
75
|
+
run(repoRoot: string, buildCommand: string, logPath: string): Promise<number>;
|
|
36
76
|
/**
|
|
37
|
-
*
|
|
38
|
-
* `logPath`. Returns the BUILD's exit code (not tee's). Nothing is truncated.
|
|
77
|
+
* The success summary: the caller is told WHERE the full output is, not handed the output.
|
|
39
78
|
*/
|
|
40
|
-
|
|
79
|
+
successMessage(logPath: string): string;
|
|
41
80
|
/**
|
|
42
|
-
* The ENTIRE message the
|
|
43
|
-
*
|
|
81
|
+
* The ENTIRE message the caller gets on a failed build. It names the log, echoes the last
|
|
82
|
+
* FAILURE_TAIL_LINES lines so the immediate cause needs no second command, and forbids the rebuild
|
|
83
|
+
* this whole file exists to prevent.
|
|
44
84
|
*
|
|
45
85
|
* The last sentence is not filler. If the log holds no visible failure then something upstream is wrong
|
|
46
|
-
* (a runner that died without printing, a truncated
|
|
47
|
-
* guessing or rebuilding — so it is told to surface the contradiction to the human and stop.
|
|
86
|
+
* (a runner that died without printing, a truncated redirect), and the worst possible response is an
|
|
87
|
+
* agent guessing or rebuilding — so it is told to surface the contradiction to the human and stop.
|
|
48
88
|
*/
|
|
49
89
|
failureMessage(buildCommand: string, logPath: string): string;
|
|
90
|
+
private logPointer;
|
|
50
91
|
/**
|
|
51
|
-
*
|
|
52
|
-
* ending in a `#` comment cannot swallow what follows it.
|
|
92
|
+
* The log's last FAILURE_TAIL_LINES lines, or a plain statement of why there are none.
|
|
53
93
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
94
|
+
* A read that fails is REPORTED, never allowed to throw: this renders the message for a build that has
|
|
95
|
+
* ALREADY failed, so an I/O error escaping here would replace the real failure with the renderer's own
|
|
96
|
+
* — the caller would lose the build error and be handed a filesystem error instead. The full log is
|
|
97
|
+
* still named on the line above, so nothing is hidden by degrading to one line.
|
|
58
98
|
*/
|
|
59
|
-
private
|
|
60
|
-
private
|
|
61
|
-
private
|
|
99
|
+
private tail;
|
|
100
|
+
private awaitExit;
|
|
101
|
+
private displayPath;
|
|
102
|
+
private resolvePath;
|
|
62
103
|
private slug;
|
|
63
104
|
private git;
|
|
64
105
|
}
|
|
@@ -1,138 +1,245 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BuildGateLog = exports.BUILD_STAGE = exports.FINISH_STAGE = exports.REVIEW_STAGE = void 0;
|
|
3
|
+
exports.BuildGateLog = exports.BuildLogHeartbeat = exports.BUILD_LOG_NAME = exports.BUILD_STAGE = exports.FINISH_STAGE = exports.REVIEW_STAGE = exports.FAILURE_TAIL_LINES = exports.HEARTBEAT_MS = void 0;
|
|
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
7
|
const path = tslib_1.__importStar(require("path"));
|
|
8
8
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
9
9
|
const inversify_1 = require("inversify");
|
|
10
|
-
// EXPERIMENTAL (opt-in via `~/.webpieces/config.json` → experimental.buildGateLogCapture). Nothing in this
|
|
11
|
-
// file runs for a user who has not created that OPTIONAL file — see `HomeConfigService`
|
|
12
|
-
// (`@webpieces/rules-config`), which returns all-defaults silently when it is absent.
|
|
13
|
-
//
|
|
14
|
-
// EVERY key in that file is optional, so the smallest document enabling THIS feature is just:
|
|
15
|
-
// { "experimental": { "buildGateLogCapture": true } }
|
|
16
|
-
// Optional is not a convenience: the file is MACHINE-GLOBAL and the repos on one machine pin different
|
|
17
|
-
// webpieces releases, so a REQUIRED key there has no satisfiable value — omitting it fails the new
|
|
18
|
-
// release, adding it fails every older one. `HomeConfigService` owns that rule.
|
|
19
|
-
//
|
|
20
10
|
// ─── Why ───────────────────────────────────────────────────────────────────────────────────────────────
|
|
21
|
-
// The build gate already builds everything. When
|
|
22
|
-
//
|
|
23
|
-
//
|
|
11
|
+
// The build gate already builds everything. When the output only ever went to the CONSOLE, an agent that
|
|
12
|
+
// wanted a different slice of it re-ran the WHOLE BUILD to get it: one measured session spent 23.9 minutes
|
|
13
|
+
// across nine `nx affected` runs, five of them with NO code change in between — `| tail -50`, then
|
|
14
|
+
// `> /tmp/file`, then `| grep`, then `| sed -n '1100,1230p'`. ~19 minutes spent re-reading a log.
|
|
15
|
+
//
|
|
16
|
+
// So the build's output is not streamed; it is REDIRECTED, in full, to a file whose path the caller is
|
|
17
|
+
// handed on completion. Reading a different slice is then a `grep` of a FILE, not a second build.
|
|
24
18
|
//
|
|
25
|
-
// ─── Why a
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
19
|
+
// ─── Why a redirect and not `tee` ──────────────────────────────────────────────────────────────────────
|
|
20
|
+
// An earlier cut of this used `cmd 2>&1 | tee log`, to keep the terminal byte-identical. That is what
|
|
21
|
+
// makes the transcript expensive in the first place — an AI caller carries every line of it in context.
|
|
22
|
+
// The redirect keeps the console to a handful of lines (a heartbeat, then a pointer at the file), which is
|
|
23
|
+
// the entire productivity claim. Losing `tee` also deletes the `$?`-into-a-side-file dance it needed: in a
|
|
24
|
+
// pipeline the shell reports TEE's status, which is 0 whether the build passed or failed, so the status
|
|
25
|
+
// had to be smuggled out through a side file. With no pipe, the child's own exit code IS the answer.
|
|
29
26
|
//
|
|
30
|
-
// ─── Why
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
// ─── Why async (`spawn`, not `spawnSync`) ──────────────────────────────────────────────────────────────
|
|
28
|
+
// `spawnSync` blocks the event loop for the length of the build, so NOTHING can print while it runs — and
|
|
29
|
+
// a silent terminal for 3–7 minutes is indistinguishable from a hang. The heartbeat is the reason this is
|
|
30
|
+
// async, and it is why `run` returns a Promise and `BuildAffected.runBuildGate` is async with it.
|
|
31
|
+
/** How often the heartbeat reports the log's size. Hardcoded: a knob here would be a knob the PR gate's
|
|
32
|
+
* own build never receives, and the two must stay the same command. */
|
|
33
|
+
exports.HEARTBEAT_MS = 10_000;
|
|
34
|
+
/** How many trailing log lines the failure message echoes, so the immediate cause is visible without a
|
|
35
|
+
* second command. Small on purpose — the FULL log is one grep away and the message must not become the
|
|
36
|
+
* transcript it exists to replace. */
|
|
37
|
+
exports.FAILURE_TAIL_LINES = 20;
|
|
36
38
|
/**
|
|
37
|
-
* Which stage's gate is being captured. The value
|
|
38
|
-
* write the same file even at the same commit on the same branch.
|
|
39
|
+
* Which stage's gate is being captured. The value decides the log FILENAME.
|
|
39
40
|
*/
|
|
40
41
|
exports.REVIEW_STAGE = 'review';
|
|
41
42
|
exports.FINISH_STAGE = 'finish';
|
|
42
43
|
// `wp-build`, which is not a stage of the PR flow but runs the SAME gate (BuildAffected.runBuildGate).
|
|
43
|
-
// Its own id so a developer's inner-loop build never overwrites the log stage ② or ③ is holding.
|
|
44
44
|
exports.BUILD_STAGE = 'build';
|
|
45
|
+
/** The one fixed log name — see BuildGateLog.fileNameFor for why only `wp-build` gets one. */
|
|
46
|
+
exports.BUILD_LOG_NAME = 'build.log';
|
|
47
|
+
const BACKUP_SUFFIX = '.bak';
|
|
45
48
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
49
|
+
* The heartbeat's state: the line count reported on the PREVIOUS tick, so a tick that has not moved can
|
|
50
|
+
* say so. Stateful per RUN, which is why it is constructed per run rather than injected.
|
|
51
|
+
*
|
|
52
|
+
* `still` is the load-bearing word. A build that is linking, or waiting on a cold nx cache, produces no
|
|
53
|
+
* output for minutes; without `still` the caller sees the same number twice and cannot tell a stalled
|
|
54
|
+
* BUILD from a stalled REPORTER.
|
|
55
|
+
*/
|
|
56
|
+
class BuildLogHeartbeat {
|
|
57
|
+
logPath;
|
|
58
|
+
displayPath;
|
|
59
|
+
previous = null;
|
|
60
|
+
constructor(logPath, displayPath) {
|
|
61
|
+
this.logPath = logPath;
|
|
62
|
+
this.displayPath = displayPath;
|
|
63
|
+
}
|
|
64
|
+
/** One heartbeat line — `<path> size <n> lines`, plus ` still` when <n> has not moved. */
|
|
65
|
+
tick() {
|
|
66
|
+
const count = this.lineCount();
|
|
67
|
+
const still = this.previous !== null && count === this.previous ? ' still' : '';
|
|
68
|
+
this.previous = count;
|
|
69
|
+
return `${this.displayPath} size ${count} lines${still}`;
|
|
70
|
+
}
|
|
71
|
+
// Lines currently in the log. A log that does not exist yet is zero lines, not an error: the build may
|
|
72
|
+
// simply not have written its first byte, and a heartbeat may never be the reason a build stops.
|
|
73
|
+
lineCount() {
|
|
74
|
+
if (!fs.existsSync(this.logPath))
|
|
75
|
+
return 0;
|
|
76
|
+
const body = fs.readFileSync(this.logPath, 'utf8');
|
|
77
|
+
if (body === '')
|
|
78
|
+
return 0;
|
|
79
|
+
return body.split('\n').length - (body.endsWith('\n') ? 1 : 0);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.BuildLogHeartbeat = BuildLogHeartbeat;
|
|
83
|
+
/**
|
|
84
|
+
* Captures the build gate's full output to a file, reports progress while it runs, and renders the
|
|
85
|
+
* pointer the caller is handed instead of a rebuild instruction.
|
|
86
|
+
*
|
|
87
|
+
* ─── Two naming schemes, one rule each ─────────────────────────────────────────────────────────────────
|
|
88
|
+
* • `wp-build` (BUILD_STAGE) writes ONE fixed path, `.webpieces/build.log`. It is fixed because a HUMAN
|
|
89
|
+
* OR AN AGENT TYPES IT — `grep -n error .webpieces/build.log` has to be writable from memory, and a
|
|
90
|
+
* name carrying a branch and a sha is not. History comes from the rotation below instead.
|
|
91
|
+
* • stage ② and stage ③ write `logs/build-gate-<stage>-<branch>-<shortSha>.log`, because those two
|
|
92
|
+
* gates CAN run against one commit and a failure message from one must not be pointing at a file the
|
|
93
|
+
* other overwrote. Nobody types those names; the failure message prints them.
|
|
94
|
+
*
|
|
95
|
+
* ─── Rotation, everywhere ──────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
* Every run moves an existing log to `<log>.bak` before writing, so the last TWO runs are always on disk.
|
|
97
|
+
* One rule for every stage: no branch, and the previous run of a re-run at the same commit survives
|
|
98
|
+
* instead of being truncated away.
|
|
48
99
|
*
|
|
49
|
-
* ───
|
|
50
|
-
* The
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* is what makes "N agents in N worktrees" safe by construction rather than by naming.
|
|
55
|
-
* • `<stage>` separates review from finish, the two gates that CAN run against one commit.
|
|
56
|
-
* • `<branch>` — a branch can be checked out in at most one worktree (git enforces it), so within one
|
|
57
|
-
* log directory the branch is effectively constant; it is in the name so a human reading the directory
|
|
58
|
-
* can tell whose log is whose, and so switching branches never appends to a stale file.
|
|
59
|
-
* • `<shortSha>` — a re-run at a NEW commit gets a new file, so the log a failure message points at is
|
|
60
|
-
* always the build for the code that failed. A re-run at the SAME commit deliberately TRUNCATES: it is
|
|
61
|
-
* the same build of the same tree, and the fresh one is the one worth reading.
|
|
62
|
-
* The residual case is two agents running the same stage, at the same commit, in the SAME worktree,
|
|
63
|
-
* simultaneously. That is already unsupported — both would be driving one git index and one merge state —
|
|
64
|
-
* and it is the only case this scheme does not separate.
|
|
100
|
+
* ─── Concurrency ───────────────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
* The DIRECTORY is `dotWebpieces.local()`-scoped — `<primary>/.webpieces/worktrees/<git worktree name>/`
|
|
102
|
+
* in a linked worktree — so "N agents in N worktrees" is safe by construction rather than by naming. The
|
|
103
|
+
* residual case is two builds in the SAME worktree at once, which is already unsupported: both would be
|
|
104
|
+
* driving one git index.
|
|
65
105
|
*/
|
|
66
106
|
let BuildGateLog = class BuildGateLog {
|
|
67
|
-
/** Absolute path of the log for `stage` at the current HEAD, creating
|
|
107
|
+
/** Absolute path of the log for `stage` at the current HEAD, creating its directory. */
|
|
68
108
|
pathFor(repoRoot, stage) {
|
|
69
|
-
const file =
|
|
109
|
+
const file = this.resolvePath(repoRoot, stage);
|
|
70
110
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
71
111
|
return file;
|
|
72
112
|
}
|
|
73
113
|
/** The same path WITHOUT creating anything, and '' when no such log exists. Used by finish's skip path. */
|
|
74
114
|
existingLogFor(repoRoot, stage) {
|
|
75
|
-
const file =
|
|
115
|
+
const file = this.resolvePath(repoRoot, stage);
|
|
76
116
|
return fs.existsSync(file) ? file : '';
|
|
77
117
|
}
|
|
78
|
-
/** `build-gate-<stage>-<branch>-<shortSha>.log`
|
|
118
|
+
/** `build.log` for `wp-build`; `build-gate-<stage>-<branch>-<shortSha>.log` for the PR-flow stages. */
|
|
79
119
|
fileNameFor(repoRoot, stage) {
|
|
120
|
+
if (stage === exports.BUILD_STAGE)
|
|
121
|
+
return exports.BUILD_LOG_NAME;
|
|
80
122
|
const branch = this.slug(this.git(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']));
|
|
81
123
|
const sha = this.slug(this.git(repoRoot, ['rev-parse', '--short', 'HEAD']));
|
|
82
124
|
return `build-gate-${this.slug(stage)}-${branch === '' ? 'nobranch' : branch}-${sha === '' ? 'nosha' : sha}.log`;
|
|
83
125
|
}
|
|
126
|
+
/** Where the PREVIOUS run of `logPath` is kept — always `<logPath>.bak`. */
|
|
127
|
+
backupPathFor(logPath) {
|
|
128
|
+
return `${logPath}${BACKUP_SUFFIX}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Move an existing log aside to `<log>.bak`, overwriting any previous backup. A missing log is the
|
|
132
|
+
* normal first-run state and is not an error.
|
|
133
|
+
*/
|
|
134
|
+
rotate(logPath) {
|
|
135
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
136
|
+
if (!fs.existsSync(logPath))
|
|
137
|
+
return;
|
|
138
|
+
fs.rmSync(this.backupPathFor(logPath), { force: true });
|
|
139
|
+
fs.renameSync(logPath, this.backupPathFor(logPath));
|
|
140
|
+
}
|
|
84
141
|
/**
|
|
85
|
-
* Run `buildCommand` with its
|
|
86
|
-
*
|
|
142
|
+
* Run `buildCommand` with its stdout AND stderr redirected in full to `logPath`, printing a heartbeat
|
|
143
|
+
* to the console every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's exit code.
|
|
144
|
+
* Nothing is truncated and nothing is streamed.
|
|
87
145
|
*/
|
|
88
|
-
run(repoRoot, buildCommand, logPath) {
|
|
89
|
-
|
|
90
|
-
fs.
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
146
|
+
async run(repoRoot, buildCommand, logPath) {
|
|
147
|
+
this.rotate(logPath);
|
|
148
|
+
const fd = fs.openSync(logPath, 'w');
|
|
149
|
+
const heartbeat = new BuildLogHeartbeat(logPath, this.displayPath(repoRoot, logPath));
|
|
150
|
+
const timer = setInterval(() => { process.stdout.write(`${heartbeat.tick()}\n`); }, exports.HEARTBEAT_MS);
|
|
151
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: the timer and the fd MUST be released
|
|
152
|
+
// whatever the child does, and the exit code is returned rather than thrown so runBuildGate owns
|
|
153
|
+
// the one CliExitError.
|
|
154
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
155
|
+
try {
|
|
156
|
+
return await this.awaitExit((0, child_process_1.spawn)(buildCommand, { cwd: repoRoot, shell: true, stdio: ['ignore', fd, fd] }), fd);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
clearInterval(timer);
|
|
160
|
+
fs.closeSync(fd);
|
|
161
|
+
}
|
|
99
162
|
}
|
|
100
163
|
/**
|
|
101
|
-
* The
|
|
102
|
-
|
|
164
|
+
* The success summary: the caller is told WHERE the full output is, not handed the output.
|
|
165
|
+
*/
|
|
166
|
+
successMessage(logPath) {
|
|
167
|
+
return `\nBuild success\n${this.logPointer(logPath)}`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The ENTIRE message the caller gets on a failed build. It names the log, echoes the last
|
|
171
|
+
* FAILURE_TAIL_LINES lines so the immediate cause needs no second command, and forbids the rebuild
|
|
172
|
+
* this whole file exists to prevent.
|
|
103
173
|
*
|
|
104
174
|
* The last sentence is not filler. If the log holds no visible failure then something upstream is wrong
|
|
105
|
-
* (a runner that died without printing, a truncated
|
|
106
|
-
* guessing or rebuilding — so it is told to surface the contradiction to the human and stop.
|
|
175
|
+
* (a runner that died without printing, a truncated redirect), and the worst possible response is an
|
|
176
|
+
* agent guessing or rebuilding — so it is told to surface the contradiction to the human and stop.
|
|
107
177
|
*/
|
|
108
178
|
failureMessage(buildCommand, logPath) {
|
|
109
|
-
return `\
|
|
110
|
-
`
|
|
111
|
-
`
|
|
179
|
+
return `\nBuild Failed: ${buildCommand}\n${this.logPointer(logPath)}\n` +
|
|
180
|
+
`Last ${exports.FAILURE_TAIL_LINES} lines of that log:\n${this.tail(logPath)}\n` +
|
|
181
|
+
`Read that FILE for the failures. Do NOT re-run the build to see them.\n` +
|
|
112
182
|
`If you do not see failures in that log, report that to the user and stop.\n`;
|
|
113
183
|
}
|
|
184
|
+
// The two lines that name the log, identical on success and failure so there is one thing to recognise.
|
|
185
|
+
// The backup line says what is TRUE RIGHT NOW: on the very first build in a tree there is no `.bak`
|
|
186
|
+
// yet, and pointing a reader at a file that does not exist is the small lie that costs a wasted `cat`.
|
|
187
|
+
logPointer(logPath) {
|
|
188
|
+
const name = path.basename(logPath);
|
|
189
|
+
const backedUp = fs.existsSync(this.backupPathFor(logPath))
|
|
190
|
+
? `(${name} is backed up to ${name}${BACKUP_SUFFIX} every run so you have the last 2 builds of logs)`
|
|
191
|
+
: `(the previous ${name} is kept as ${name}${BACKUP_SUFFIX} on every run — this is the first, so there is none yet)`;
|
|
192
|
+
return `FullLog : ${logPath}\n${backedUp}\n`;
|
|
193
|
+
}
|
|
114
194
|
/**
|
|
115
|
-
*
|
|
116
|
-
* ending in a `#` comment cannot swallow what follows it.
|
|
195
|
+
* The log's last FAILURE_TAIL_LINES lines, or a plain statement of why there are none.
|
|
117
196
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
197
|
+
* A read that fails is REPORTED, never allowed to throw: this renders the message for a build that has
|
|
198
|
+
* ALREADY failed, so an I/O error escaping here would replace the real failure with the renderer's own
|
|
199
|
+
* — the caller would lose the build error and be handed a filesystem error instead. The full log is
|
|
200
|
+
* still named on the line above, so nothing is hidden by degrading to one line.
|
|
122
201
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
202
|
+
tail(logPath) {
|
|
203
|
+
if (!fs.existsSync(logPath))
|
|
204
|
+
return ` (no log file at ${logPath})\n`;
|
|
205
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: see above, the failure renderer may not
|
|
206
|
+
// replace the build's failure with its own.
|
|
207
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
208
|
+
try {
|
|
209
|
+
const lines = fs.readFileSync(logPath, 'utf8').split('\n').filter((l) => l !== '');
|
|
210
|
+
if (lines.length === 0)
|
|
211
|
+
return ' (the log is empty)\n';
|
|
212
|
+
return lines.slice(-exports.FAILURE_TAIL_LINES).map((l) => ` ${l}\n`).join('');
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
const error = (0, rules_config_1.toError)(err);
|
|
216
|
+
return ` (could not read ${logPath}: ${error.message})\n`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Resolve, and wait for, the child's exit code. A spawn that never starts (a shell that is missing, a
|
|
220
|
+
// cwd that vanished) fails CLOSED to 1 — calling a build that never ran green is the one outcome that
|
|
221
|
+
// must be impossible — and the reason is APPENDED TO THE LOG, so the failure message's pointer still
|
|
222
|
+
// leads to it rather than to an empty file.
|
|
223
|
+
awaitExit(child, fd) {
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
child.on('error', (err) => {
|
|
226
|
+
fs.writeSync(fd, `\nThe build command could not be started: ${err.message}\n`);
|
|
227
|
+
resolve(1);
|
|
228
|
+
});
|
|
229
|
+
child.on('close', (code) => { resolve(code ?? 1); });
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// The path as the heartbeat shows it: relative to the repo when it sits inside it (a linked worktree's
|
|
233
|
+
// state lives under the PRIMARY clone, so it often does not), absolute otherwise.
|
|
234
|
+
displayPath(repoRoot, logPath) {
|
|
235
|
+
const relative = path.relative(repoRoot, logPath);
|
|
236
|
+
return relative === '' || relative.startsWith('..') || path.isAbsolute(relative) ? logPath : relative;
|
|
237
|
+
}
|
|
238
|
+
resolvePath(repoRoot, stage) {
|
|
239
|
+
const name = this.fileNameFor(repoRoot, stage);
|
|
240
|
+
// `wp-build`'s log sits at the ROOT of the state dir, not under `logs/`, because it is the one log
|
|
241
|
+
// path a person types from memory. Everything else keeps the per-commit names in `logs/`.
|
|
242
|
+
return stage === exports.BUILD_STAGE ? rules_config_1.dotWebpieces.localFile(repoRoot, name) : rules_config_1.dotWebpieces.logsFile(repoRoot, name);
|
|
136
243
|
}
|
|
137
244
|
// Anything that is not a filename-safe character becomes '-', so `dean/feat` cannot create directories.
|
|
138
245
|
slug(value) {
|