@webpieces/pr-gate 0.4.686 → 0.4.688
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/index.js +7 -2
- package/src/index.js.map +1 -1
- package/src/scripts/commands/build-command.d.ts +34 -3
- package/src/scripts/commands/build-command.js +80 -5
- package/src/scripts/commands/build-command.js.map +1 -1
- package/src/scripts/pr-gate-app.d.ts +2 -2
- package/src/scripts/pr-gate-app.js +2 -2
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/build-affected.d.ts +25 -10
- package/src/scripts/workflow/build-affected.js +42 -16
- package/src/scripts/workflow/build-affected.js.map +1 -1
- package/src/scripts/wp-build.js +22 -5
- package/src/scripts/wp-build.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.688",
|
|
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",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"directory": "packages/tooling/pr-gate"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@webpieces/rules-config": "0.4.
|
|
18
|
+
"@webpieces/rules-config": "0.4.688",
|
|
19
19
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
20
20
|
"inversify": "7.10.4",
|
|
21
21
|
"reflect-metadata": "0.2.2"
|
package/src/index.js
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
// Public surface is intentionally small for now. The package mainly ships bin
|
|
6
6
|
// commands (wp-start-update, wp-finish-update, wp-finish-upsert-pr, wp-start-upsert-pr).
|
|
7
7
|
// The build gate and 3-point merge-info gathering are internal `workflow/` library functions
|
|
8
|
-
// (
|
|
9
|
-
// etc.) back the dashboard those commands render.
|
|
8
|
+
// (BuildAffected.runBuildGate, gatherInfo), not bins. Shared library exports (dashboard gate
|
|
9
|
+
// computation, etc.) back the dashboard those commands render.
|
|
10
|
+
//
|
|
11
|
+
// `runBuildGate` is the ONLY way to run a build — the `runBuildAffected` / `runConfiguredBuildGate`
|
|
12
|
+
// side doors it used to sit beside are gone, folded into one private spawn, because each of them
|
|
13
|
+
// executed `buildCommand` with no caller identity and no `~/.webpieces/builds.log` row. One spelling,
|
|
14
|
+
// and an unlogged build no longer compiles.
|
|
10
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
16
|
exports.PrGateApp = exports.TriggeredChecklist = exports.ChecklistDetector = exports.SQUASH_MESSAGE_REQUIRED = exports.SQUASH_TITLE_REQUIRED = exports.SquashSettings = exports.SquashSettingsEnforcer = exports.PrCommentResult = exports.PrCommentRequest = exports.PrCommentUpserter = exports.CHECKLIST_COMMENT_MARKER = exports.ChecklistCommentRenderer = exports.DETAIL_COMMENT_MARKER = exports.Dashboard = exports.DashboardInput = exports.ChecklistRow = exports.DisableCounts = exports.GateResult = void 0;
|
|
12
17
|
var dashboard_1 = require("./dashboard/dashboard");
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/pr-gate/src/index.ts"],"names":[],"mappings":";AAAA,wCAAwC;AACxC,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,yFAAyF;AACzF,6FAA6F;AAC7F,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/pr-gate/src/index.ts"],"names":[],"mappings":";AAAA,wCAAwC;AACxC,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,yFAAyF;AACzF,6FAA6F;AAC7F,6FAA6F;AAC7F,+DAA+D;AAC/D,EAAE;AACF,oGAAoG;AACpG,iGAAiG;AACjG,sGAAsG;AACtG,4CAA4C;;;AAE5C,mDAO+B;AAN3B,uGAAA,UAAU,OAAA;AACV,0GAAA,aAAa,OAAA;AACb,yGAAA,YAAY,OAAA;AACZ,2GAAA,cAAc,OAAA;AACd,sGAAA,SAAS,OAAA;AACT,kHAAA,qBAAqB,OAAA;AAEzB,uGAAuG;AACvG,kFAAkF;AAClF,qFAGgD;AAF5C,sIAAA,wBAAwB,OAAA;AACxB,sIAAA,wBAAwB,OAAA;AAE5B,8EAA8G;AAArG,wHAAA,iBAAiB,OAAA;AAAE,uHAAA,gBAAgB,OAAA;AAAE,sHAAA,eAAe,OAAA;AAC7D,wGAAwG;AACxG,kGAAkG;AAClG,wFAEqD;AADjD,kIAAA,sBAAsB,OAAA;AAAE,0HAAA,cAAc,OAAA;AAAE,iIAAA,qBAAqB,OAAA;AAAE,mIAAA,uBAAuB,OAAA;AAE1F,4EAA8F;AAArF,uHAAA,iBAAiB,OAAA;AAAE,wHAAA,kBAAkB,OAAA;AAC9C,qDAAkD;AAAzC,wGAAA,SAAS,OAAA","sourcesContent":["// @webpieces/pr-gate — gated PR system.\n//\n// Now published to npm via the release workflow (OIDC trusted publishing).\n// Public surface is intentionally small for now. The package mainly ships bin\n// commands (wp-start-update, wp-finish-update, wp-finish-upsert-pr, wp-start-upsert-pr).\n// The build gate and 3-point merge-info gathering are internal `workflow/` library functions\n// (BuildAffected.runBuildGate, gatherInfo), not bins. Shared library exports (dashboard gate\n// computation, etc.) back the dashboard those commands render.\n//\n// `runBuildGate` is the ONLY way to run a build — the `runBuildAffected` / `runConfiguredBuildGate`\n// side doors it used to sit beside are gone, folded into one private spawn, because each of them\n// executed `buildCommand` with no caller identity and no `~/.webpieces/builds.log` row. One spelling,\n// and an unlogged build no longer compiles.\n\nexport {\n GateResult,\n DisableCounts,\n ChecklistRow,\n DashboardInput,\n Dashboard,\n DETAIL_COMMENT_MARKER,\n} from './dashboard/dashboard';\n// The 2nd PR comment's renderer — its own surface, its own class. A barrel is the surface too, so this\n// is exported alongside Dashboard rather than left reachable only by deep import.\nexport {\n ChecklistCommentRenderer,\n CHECKLIST_COMMENT_MARKER,\n} from './dashboard/checklist-comment-renderer';\nexport { PrCommentUpserter, PrCommentRequest, PrCommentResult } from './scripts/workflow/pr-comment-upserter';\n// Pins the two server-side GitHub settings the git-log body depends on. Exported because a consumer may\n// want to assert them in its own CI — they are the one part of this design no config can express.\nexport {\n SquashSettingsEnforcer, SquashSettings, SQUASH_TITLE_REQUIRED, SQUASH_MESSAGE_REQUIRED,\n} from './scripts/workflow/squash-settings-enforcer';\nexport { ChecklistDetector, TriggeredChecklist } from './scripts/workflow/checklist-detector';\nexport { PrGateApp } from './scripts/pr-gate-app';\n"]}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import { RepoRootFinder } from '@webpieces/rules-config';
|
|
1
|
+
import { BuildsLog, HomeConfigService, RepoRootFinder } from '@webpieces/rules-config';
|
|
2
2
|
import { BuildAffected } from '../workflow/build-affected';
|
|
3
|
+
/** The rule name the refusal is reported under, so it is greppable from a transcript. */
|
|
4
|
+
export declare const TOO_MANY_CONCURRENT_BUILDS = "too-many-concurrent-builds";
|
|
5
|
+
/**
|
|
6
|
+
* What `wp-build` was asked to do. Data-only (a class, per CLAUDE.md), with exactly one field so far.
|
|
7
|
+
*
|
|
8
|
+
* `force` SKIPS THE CONCURRENCY CHECK AND NOTHING ELSE. It does not touch the command, and it must never
|
|
9
|
+
* grow a field that does: `wp-build` runs `commands.pr-gate.buildCommand` verbatim, and a flag that
|
|
10
|
+
* changed the command would make it a different build from the one the PR gate runs — which is the exact
|
|
11
|
+
* drift this bin exists to delete.
|
|
12
|
+
*/
|
|
13
|
+
export declare class BuildOptions {
|
|
14
|
+
force: boolean;
|
|
15
|
+
constructor(force: boolean);
|
|
16
|
+
}
|
|
3
17
|
/**
|
|
4
18
|
* `wp-build` — run THE project's build, and nothing else.
|
|
5
19
|
*
|
|
@@ -39,6 +53,23 @@ import { BuildAffected } from '../workflow/build-affected';
|
|
|
39
53
|
export declare class BuildCommand {
|
|
40
54
|
private readonly buildAffected;
|
|
41
55
|
private readonly repoRootFinder;
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
private readonly buildsLog;
|
|
57
|
+
private readonly homeConfig;
|
|
58
|
+
constructor(buildAffected: BuildAffected, repoRootFinder: RepoRootFinder, buildsLog: BuildsLog, homeConfig: HomeConfigService);
|
|
59
|
+
run(opts: BuildOptions): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Refuse when this MACHINE is already running `maxConcurrentBuilds` builds.
|
|
62
|
+
*
|
|
63
|
+
* ─── WHY ONLY HERE, AND NEVER ON THE GATE STAGES ─────────────────────────────────────────────────
|
|
64
|
+
* `wp-review-upsert-pr` and `wp-finish-upsert-pr` are the SANCTIONED path, and a refusal there wedges
|
|
65
|
+
* a PR that has nowhere else to go. So they always run — and their builds still write ledger rows, so
|
|
66
|
+
* they count toward what refuses an ad-hoc `wp-build`. The asymmetry is the whole design: the thing
|
|
67
|
+
* that gets throttled is the extra build, not the one the process needs.
|
|
68
|
+
*
|
|
69
|
+
* The cures are `Option`s handed to `RuleFailError`, which `runMain` renders through
|
|
70
|
+
* `formatFixOptions`. They are never numbered by hand in the message — the framework owns
|
|
71
|
+
* "Fix Option N:", and a hand-numbered list in a string literal is an automatic review reject.
|
|
72
|
+
*/
|
|
73
|
+
private assertMachineHasRoom;
|
|
74
|
+
private describe;
|
|
44
75
|
}
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BuildCommand = void 0;
|
|
3
|
+
exports.BuildCommand = exports.BuildOptions = exports.TOO_MANY_CONCURRENT_BUILDS = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
6
6
|
const inversify_1 = require("inversify");
|
|
7
7
|
const build_affected_1 = require("../workflow/build-affected");
|
|
8
8
|
const build_gate_log_1 = require("../workflow/build-gate-log");
|
|
9
|
+
/** The rule name the refusal is reported under, so it is greppable from a transcript. */
|
|
10
|
+
exports.TOO_MANY_CONCURRENT_BUILDS = 'too-many-concurrent-builds';
|
|
11
|
+
/**
|
|
12
|
+
* What `wp-build` was asked to do. Data-only (a class, per CLAUDE.md), with exactly one field so far.
|
|
13
|
+
*
|
|
14
|
+
* `force` SKIPS THE CONCURRENCY CHECK AND NOTHING ELSE. It does not touch the command, and it must never
|
|
15
|
+
* grow a field that does: `wp-build` runs `commands.pr-gate.buildCommand` verbatim, and a flag that
|
|
16
|
+
* changed the command would make it a different build from the one the PR gate runs — which is the exact
|
|
17
|
+
* drift this bin exists to delete.
|
|
18
|
+
*/
|
|
19
|
+
class BuildOptions {
|
|
20
|
+
force;
|
|
21
|
+
// REQUIRED, no default. `new BuildOptions()` and `new BuildOptions(false)` would be two spellings of
|
|
22
|
+
// one decision, and the bare form would keep compiling unchanged if this class ever grew a second
|
|
23
|
+
// field — silently meaning "not forced" at a call site that was never revisited. That is the same
|
|
24
|
+
// argument HomeConfig's constructor makes about its own four required parameters; a defaulted
|
|
25
|
+
// parameter is exactly the shim this repo does not ship.
|
|
26
|
+
constructor(force) {
|
|
27
|
+
this.force = force;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.BuildOptions = BuildOptions;
|
|
9
31
|
/**
|
|
10
32
|
* `wp-build` — run THE project's build, and nothing else.
|
|
11
33
|
*
|
|
@@ -45,21 +67,74 @@ const build_gate_log_1 = require("../workflow/build-gate-log");
|
|
|
45
67
|
let BuildCommand = class BuildCommand {
|
|
46
68
|
buildAffected;
|
|
47
69
|
repoRootFinder;
|
|
48
|
-
|
|
70
|
+
buildsLog;
|
|
71
|
+
homeConfig;
|
|
72
|
+
constructor(buildAffected, repoRootFinder, buildsLog, homeConfig) {
|
|
49
73
|
this.buildAffected = buildAffected;
|
|
50
74
|
this.repoRootFinder = repoRootFinder;
|
|
75
|
+
this.buildsLog = buildsLog;
|
|
76
|
+
this.homeConfig = homeConfig;
|
|
51
77
|
}
|
|
52
|
-
run() {
|
|
78
|
+
run(opts) {
|
|
53
79
|
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
80
|
+
if (!opts.force)
|
|
81
|
+
this.assertMachineHasRoom();
|
|
54
82
|
// runBuildGate announces the resolved command, runs it, and throws CliExitError on failure so
|
|
55
|
-
// runMain owns the exit — the same three things it does for stage ② and stage ③.
|
|
83
|
+
// runMain owns the exit — the same three things it does for stage ② and stage ③. It also writes
|
|
84
|
+
// this build's START/DONE pair to the ledger the check above just read.
|
|
56
85
|
return this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ wp-build', 'pnpm wp-build', 'Build failed.', build_gate_log_1.BUILD_STAGE, true));
|
|
57
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Refuse when this MACHINE is already running `maxConcurrentBuilds` builds.
|
|
89
|
+
*
|
|
90
|
+
* ─── WHY ONLY HERE, AND NEVER ON THE GATE STAGES ─────────────────────────────────────────────────
|
|
91
|
+
* `wp-review-upsert-pr` and `wp-finish-upsert-pr` are the SANCTIONED path, and a refusal there wedges
|
|
92
|
+
* a PR that has nowhere else to go. So they always run — and their builds still write ledger rows, so
|
|
93
|
+
* they count toward what refuses an ad-hoc `wp-build`. The asymmetry is the whole design: the thing
|
|
94
|
+
* that gets throttled is the extra build, not the one the process needs.
|
|
95
|
+
*
|
|
96
|
+
* The cures are `Option`s handed to `RuleFailError`, which `runMain` renders through
|
|
97
|
+
* `formatFixOptions`. They are never numbered by hand in the message — the framework owns
|
|
98
|
+
* "Fix Option N:", and a hand-numbered list in a string literal is an automatic review reject.
|
|
99
|
+
*/
|
|
100
|
+
assertMachineHasRoom() {
|
|
101
|
+
const live = this.buildsLog.running();
|
|
102
|
+
const max = this.homeConfig.load().maxConcurrentBuilds;
|
|
103
|
+
if (live.length < max)
|
|
104
|
+
return;
|
|
105
|
+
throw new rules_config_1.RuleFailError(exports.TOO_MANY_CONCURRENT_BUILDS, `This machine is already running ${String(live.length)} build(s), and the limit is ` +
|
|
106
|
+
`${String(max)}. Starting another makes all of them slower — CPU contention between agents ` +
|
|
107
|
+
`building at once was measured at ~3.2x total test time.\n\n${this.describe(live)}`, undefined, undefined, [
|
|
108
|
+
new rules_config_1.Option('Re-use the gate you should be running anyway: `pnpm wp-start-upsert-pr` then\n'
|
|
109
|
+
+ '`pnpm wp-review-upsert-pr`. Stage ② runs the SAME `commands.pr-gate.buildCommand`\n'
|
|
110
|
+
+ 'this would have run, so its green is the same evidence — and it posts the PR.', true),
|
|
111
|
+
new rules_config_1.Option('Wait for one of the builds above to finish and run `pnpm wp-build` again. A build\n'
|
|
112
|
+
+ 'that has already died leaves no row: the count only holds live processes.'),
|
|
113
|
+
new rules_config_1.Option('Verify only what you are editing instead of the whole affected set:\n'
|
|
114
|
+
+ '`pnpm exec vitest run <one spec file>`, or `pnpm nx run <project>:test`.'),
|
|
115
|
+
new rules_config_1.Option('Raise the limit for THIS machine, if it genuinely has the cores: put\n'
|
|
116
|
+
+ '`{"experimental": {"maxConcurrentBuilds": <n>}}` in `~/.webpieces/config.json`.'),
|
|
117
|
+
new rules_config_1.Option('If you are really stuck and cannot use a gate and really really need wp-build\n'
|
|
118
|
+
+ 'then use `pnpm wp-build --force`.'),
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
// One line per live build: which repo, which worktree, on what branch, and how long it has been
|
|
122
|
+
// going. Age is what tells a reader whether to wait thirty seconds or go and look at a stuck agent.
|
|
123
|
+
describe(live) {
|
|
124
|
+
return live.map((build) => {
|
|
125
|
+
const age = Math.max(0, Math.round((Date.now() - build.startedMs) / 1000));
|
|
126
|
+
return ` • ${build.repo} [tree=${build.tree || 'primary'}] `
|
|
127
|
+
+ `branch=${build.branch || '?'} by=${build.by} pid=${String(build.pid)} `
|
|
128
|
+
+ `running for ${String(age)}s`;
|
|
129
|
+
}).join('\n');
|
|
130
|
+
}
|
|
58
131
|
};
|
|
59
132
|
exports.BuildCommand = BuildCommand;
|
|
60
133
|
exports.BuildCommand = BuildCommand = tslib_1.__decorate([
|
|
61
134
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
62
135
|
tslib_1.__metadata("design:paramtypes", [build_affected_1.BuildAffected,
|
|
63
|
-
rules_config_1.RepoRootFinder
|
|
136
|
+
rules_config_1.RepoRootFinder,
|
|
137
|
+
rules_config_1.BuildsLog,
|
|
138
|
+
rules_config_1.HomeConfigService])
|
|
64
139
|
], BuildCommand);
|
|
65
140
|
//# sourceMappingURL=build-command.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/build-command.ts"],"names":[],"mappings":";;;;AAAA,0DAAyD;AACzD,yCAA2D;AAE3D,+DAA6E;AAC7E,+DAAyD;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEI,IAAM,YAAY,GAAlB,MAAM,YAAY;IAEA;IACA;IAFrB,YACqB,aAA4B,EAC5B,cAA8B;QAD9B,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;IAChD,CAAC;IAEJ,GAAG;QACC,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,8FAA8F;QAC9F,iFAAiF;QACjF,OAAO,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,iCAAgB,CACjE,eAAe,EACf,eAAe,EACf,eAAe,EACf,4BAAW,EACX,IAAI,CACP,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AAlBY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGD,8BAAa;QACZ,6BAAc;GAH1C,YAAY,CAkBxB","sourcesContent":["import { RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { BUILD_STAGE } from '../workflow/build-gate-log';\n\n/**\n * `wp-build` — run THE project's build, and nothing else.\n *\n * ─── Why this bin exists ───────────────────────────────────────────────────────────────────────────\n * The correct verification command has lived in `commands.pr-gate.buildCommand` all along, and NOTHING\n * SURFACED IT. So every repo hand-composed its own verify chain, and those chains drifted: one sibling\n * repo's `ci:local` was `prettier --check .` (whole repo) + `wp-ci` (whole monorepo) +\n * `nx affected -t test` with NO `--base` (whole repo again) — three whole-world passes on an inner\n * loop, none of them the command the gate runs. `wp-build` closes that by making the configured value\n * the only thing anyone has to type.\n *\n * ─── It composes NOTHING, deliberately ─────────────────────────────────────────────────────────────\n * This command runs `commands.pr-gate.buildCommand` VERBATIM and adds no leg of its own — no format\n * check, no lint pass, no extra test run. Composition is exactly what drifted. Anything that must run\n * on every build belongs INSIDE `buildCommand`, where the PR gate runs it too; a leg added here would\n * be a leg the gate never sees, which is the same defect one level in.\n *\n * ─── ONE resolver, and ONE gate ────────────────────────────────────────────────────────────────────\n * `BuildAffected.runBuildGate` is the single code path that resolves the command\n * (`resolveBuildCommand`), announces it, runs it, honours `buildGateLogCapture`, and renders the\n * failure. `wp-build` calls THAT — it does not re-read the config key and does not hand-write a second\n * failure string. A second reader is a second thing that can drift, and a second failure message is a\n * second thing that can teach a command the gate does not run; drift is the entire reason this bin\n * exists, so reproducing it one level in would be self-defeating.\n *\n * Only `BuildGateOptions` differs from stage ② and stage ③ — the label, the command to re-run, the\n * headline, the log-file stage id, and `alwaysCapture`.\n *\n * ─── The output goes to a FILE, unconditionally ────────────────────────────────────────────────────\n * `alwaysCapture: true` is the one behaviour wp-build does not share with the PR-flow stages, and it is\n * the reason this bin is worth running: the build's full stdout+stderr land in `.webpieces/build.log`\n * (previous run kept as `.webpieces/build.log.bak`), and the console gets a heartbeat plus a pointer at\n * that file. A measured session burned ~19 minutes re-running `nx affected` five times with no code\n * change in between, purely to see a different slice of output that had scrolled past. Reading a\n * different slice must cost a `grep`, not a build — see BuildGateLog.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildCommand {\n constructor(\n private readonly buildAffected: BuildAffected,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // runBuildGate announces the resolved command, runs it, and throws CliExitError on failure so\n // runMain owns the exit — the same three things it does for stage ② and stage ③.\n return this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ wp-build',\n 'pnpm wp-build',\n 'Build failed.',\n BUILD_STAGE,\n true,\n ));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"build-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/build-command.ts"],"names":[],"mappings":";;;;AAAA,0DAEiC;AACjC,yCAA2D;AAE3D,+DAA6E;AAC7E,+DAAyD;AAEzD,yFAAyF;AAC5E,QAAA,0BAA0B,GAAG,4BAA4B,CAAC;AAEvE;;;;;;;GAOG;AACH,MAAa,YAAY;IACrB,KAAK,CAAU;IAEf,qGAAqG;IACrG,kGAAkG;IAClG,kGAAkG;IAClG,8FAA8F;IAC9F,yDAAyD;IACzD,YAAY,KAAc;QACtB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AAXD,oCAWC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEI,IAAM,YAAY,GAAlB,MAAM,YAAY;IAEA;IACA;IACA;IACA;IAJrB,YACqB,aAA4B,EAC5B,cAA8B,EAC9B,SAAoB,EACpB,UAA6B;QAH7B,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,cAAS,GAAT,SAAS,CAAW;QACpB,eAAU,GAAV,UAAU,CAAmB;IAC/C,CAAC;IAEJ,GAAG,CAAC,IAAkB;QAClB,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC7C,8FAA8F;QAC9F,gGAAgG;QAChG,wEAAwE;QACxE,OAAO,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,iCAAgB,CACjE,eAAe,EACf,eAAe,EACf,eAAe,EACf,4BAAW,EACX,IAAI,CACP,CAAC,CAAC;IACP,CAAC;IAED;;;;;;;;;;;;OAYG;IACK,oBAAoB;QACxB,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,mBAAmB,CAAC;QACvD,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG;YAAE,OAAO;QAC9B,MAAM,IAAI,4BAAa,CACnB,kCAA0B,EAC1B,mCAAmC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,8BAA8B;YACpF,GAAG,MAAM,CAAC,GAAG,CAAC,8EAA8E;YAC5F,8DAA8D,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EACnF,SAAS,EACT,SAAS,EACT;YACI,IAAI,qBAAM,CACN,gFAAgF;kBAC9E,qFAAqF;kBACrF,+EAA+E,EACjF,IAAI,CAAC;YACT,IAAI,qBAAM,CACN,qFAAqF;kBACnF,2EAA2E,CAAC;YAClF,IAAI,qBAAM,CACN,uEAAuE;kBACrE,0EAA0E,CAAC;YACjF,IAAI,qBAAM,CACN,wEAAwE;kBACtE,iFAAiF,CAAC;YACxF,IAAI,qBAAM,CACN,iFAAiF;kBAC/E,mCAAmC,CAAC;SAC7C,CACJ,CAAC;IACN,CAAC;IAED,gGAAgG;IAChG,oGAAoG;IAC5F,QAAQ,CAAC,IAA6B;QAC1C,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAmB,EAAU,EAAE;YAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;YAC3E,OAAO,OAAO,KAAK,CAAC,IAAI,UAAU,KAAK,CAAC,IAAI,IAAI,SAAS,IAAI;kBACvD,UAAU,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC,EAAE,QAAQ,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG;kBACxE,eAAe,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;QACxC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC;CACJ,CAAA;AA/EY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGD,8BAAa;QACZ,6BAAc;QACnB,wBAAS;QACR,gCAAiB;GALzC,YAAY,CA+ExB","sourcesContent":["import {\n BuildsLog, HomeConfigService, Option, RepoRootFinder, RuleFailError, RunningBuild,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { BUILD_STAGE } from '../workflow/build-gate-log';\n\n/** The rule name the refusal is reported under, so it is greppable from a transcript. */\nexport const TOO_MANY_CONCURRENT_BUILDS = 'too-many-concurrent-builds';\n\n/**\n * What `wp-build` was asked to do. Data-only (a class, per CLAUDE.md), with exactly one field so far.\n *\n * `force` SKIPS THE CONCURRENCY CHECK AND NOTHING ELSE. It does not touch the command, and it must never\n * grow a field that does: `wp-build` runs `commands.pr-gate.buildCommand` verbatim, and a flag that\n * changed the command would make it a different build from the one the PR gate runs — which is the exact\n * drift this bin exists to delete.\n */\nexport class BuildOptions {\n force: boolean;\n\n // REQUIRED, no default. `new BuildOptions()` and `new BuildOptions(false)` would be two spellings of\n // one decision, and the bare form would keep compiling unchanged if this class ever grew a second\n // field — silently meaning \"not forced\" at a call site that was never revisited. That is the same\n // argument HomeConfig's constructor makes about its own four required parameters; a defaulted\n // parameter is exactly the shim this repo does not ship.\n constructor(force: boolean) {\n this.force = force;\n }\n}\n\n/**\n * `wp-build` — run THE project's build, and nothing else.\n *\n * ─── Why this bin exists ───────────────────────────────────────────────────────────────────────────\n * The correct verification command has lived in `commands.pr-gate.buildCommand` all along, and NOTHING\n * SURFACED IT. So every repo hand-composed its own verify chain, and those chains drifted: one sibling\n * repo's `ci:local` was `prettier --check .` (whole repo) + `wp-ci` (whole monorepo) +\n * `nx affected -t test` with NO `--base` (whole repo again) — three whole-world passes on an inner\n * loop, none of them the command the gate runs. `wp-build` closes that by making the configured value\n * the only thing anyone has to type.\n *\n * ─── It composes NOTHING, deliberately ─────────────────────────────────────────────────────────────\n * This command runs `commands.pr-gate.buildCommand` VERBATIM and adds no leg of its own — no format\n * check, no lint pass, no extra test run. Composition is exactly what drifted. Anything that must run\n * on every build belongs INSIDE `buildCommand`, where the PR gate runs it too; a leg added here would\n * be a leg the gate never sees, which is the same defect one level in.\n *\n * ─── ONE resolver, and ONE gate ────────────────────────────────────────────────────────────────────\n * `BuildAffected.runBuildGate` is the single code path that resolves the command\n * (`resolveBuildCommand`), announces it, runs it, honours `buildGateLogCapture`, and renders the\n * failure. `wp-build` calls THAT — it does not re-read the config key and does not hand-write a second\n * failure string. A second reader is a second thing that can drift, and a second failure message is a\n * second thing that can teach a command the gate does not run; drift is the entire reason this bin\n * exists, so reproducing it one level in would be self-defeating.\n *\n * Only `BuildGateOptions` differs from stage ② and stage ③ — the label, the command to re-run, the\n * headline, the log-file stage id, and `alwaysCapture`.\n *\n * ─── The output goes to a FILE, unconditionally ────────────────────────────────────────────────────\n * `alwaysCapture: true` is the one behaviour wp-build does not share with the PR-flow stages, and it is\n * the reason this bin is worth running: the build's full stdout+stderr land in `.webpieces/build.log`\n * (previous run kept as `.webpieces/build.log.bak`), and the console gets a heartbeat plus a pointer at\n * that file. A measured session burned ~19 minutes re-running `nx affected` five times with no code\n * change in between, purely to see a different slice of output that had scrolled past. Reading a\n * different slice must cost a `grep`, not a build — see BuildGateLog.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildCommand {\n constructor(\n private readonly buildAffected: BuildAffected,\n private readonly repoRootFinder: RepoRootFinder,\n private readonly buildsLog: BuildsLog,\n private readonly homeConfig: HomeConfigService,\n ) {}\n\n run(opts: BuildOptions): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n if (!opts.force) this.assertMachineHasRoom();\n // runBuildGate announces the resolved command, runs it, and throws CliExitError on failure so\n // runMain owns the exit — the same three things it does for stage ② and stage ③. It also writes\n // this build's START/DONE pair to the ledger the check above just read.\n return this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ wp-build',\n 'pnpm wp-build',\n 'Build failed.',\n BUILD_STAGE,\n true,\n ));\n }\n\n /**\n * Refuse when this MACHINE is already running `maxConcurrentBuilds` builds.\n *\n * ─── WHY ONLY HERE, AND NEVER ON THE GATE STAGES ─────────────────────────────────────────────────\n * `wp-review-upsert-pr` and `wp-finish-upsert-pr` are the SANCTIONED path, and a refusal there wedges\n * a PR that has nowhere else to go. So they always run — and their builds still write ledger rows, so\n * they count toward what refuses an ad-hoc `wp-build`. The asymmetry is the whole design: the thing\n * that gets throttled is the extra build, not the one the process needs.\n *\n * The cures are `Option`s handed to `RuleFailError`, which `runMain` renders through\n * `formatFixOptions`. They are never numbered by hand in the message — the framework owns\n * \"Fix Option N:\", and a hand-numbered list in a string literal is an automatic review reject.\n */\n private assertMachineHasRoom(): void {\n const live = this.buildsLog.running();\n const max = this.homeConfig.load().maxConcurrentBuilds;\n if (live.length < max) return;\n throw new RuleFailError(\n TOO_MANY_CONCURRENT_BUILDS,\n `This machine is already running ${String(live.length)} build(s), and the limit is ` +\n `${String(max)}. Starting another makes all of them slower — CPU contention between agents ` +\n `building at once was measured at ~3.2x total test time.\\n\\n${this.describe(live)}`,\n undefined,\n undefined,\n [\n new Option(\n 'Re-use the gate you should be running anyway: `pnpm wp-start-upsert-pr` then\\n'\n + '`pnpm wp-review-upsert-pr`. Stage ② runs the SAME `commands.pr-gate.buildCommand`\\n'\n + 'this would have run, so its green is the same evidence — and it posts the PR.',\n true),\n new Option(\n 'Wait for one of the builds above to finish and run `pnpm wp-build` again. A build\\n'\n + 'that has already died leaves no row: the count only holds live processes.'),\n new Option(\n 'Verify only what you are editing instead of the whole affected set:\\n'\n + '`pnpm exec vitest run <one spec file>`, or `pnpm nx run <project>:test`.'),\n new Option(\n 'Raise the limit for THIS machine, if it genuinely has the cores: put\\n'\n + '`{\"experimental\": {\"maxConcurrentBuilds\": <n>}}` in `~/.webpieces/config.json`.'),\n new Option(\n 'If you are really stuck and cannot use a gate and really really need wp-build\\n'\n + 'then use `pnpm wp-build --force`.'),\n ],\n );\n }\n\n // One line per live build: which repo, which worktree, on what branch, and how long it has been\n // going. Age is what tells a reader whether to wait thirty seconds or go and look at a stuck agent.\n private describe(live: readonly RunningBuild[]): string {\n return live.map((build: RunningBuild): string => {\n const age = Math.max(0, Math.round((Date.now() - build.startedMs) / 1000));\n return ` • ${build.repo} [tree=${build.tree || 'primary'}] `\n + `branch=${build.branch || '?'} by=${build.by} pid=${String(build.pid)} `\n + `running for ${String(age)}s`;\n }).join('\\n');\n }\n}\n"]}
|
|
@@ -8,7 +8,7 @@ import { LandPrCommand } from './commands/land-pr-command';
|
|
|
8
8
|
import { CheckPrCommand } from './commands/check-pr-command';
|
|
9
9
|
import { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';
|
|
10
10
|
import { ReapWorktreeCommand } from './commands/reap-worktree-command';
|
|
11
|
-
import { BuildCommand } from './commands/build-command';
|
|
11
|
+
import { BuildCommand, BuildOptions } from './commands/build-command';
|
|
12
12
|
import { PushDevCommand, PushDevOptions } from './commands/push-dev-command';
|
|
13
13
|
import { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';
|
|
14
14
|
import { PushDevStateStore } from './workflow/push-dev-state';
|
|
@@ -60,7 +60,7 @@ export declare class PrGateApp {
|
|
|
60
60
|
* command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a
|
|
61
61
|
* `wp-push-dev --resolve`: it mutates nothing and reads no branch state.
|
|
62
62
|
*/
|
|
63
|
-
build(): Promise<void>;
|
|
63
|
+
build(opts: BuildOptions): Promise<void>;
|
|
64
64
|
/** `wp-start-update`: 3-point squash-update from main (no PR). */
|
|
65
65
|
startUpdate(): Promise<void>;
|
|
66
66
|
/** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */
|
|
@@ -92,8 +92,8 @@ let PrGateApp = class PrGateApp {
|
|
|
92
92
|
* command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a
|
|
93
93
|
* `wp-push-dev --resolve`: it mutates nothing and reads no branch state.
|
|
94
94
|
*/
|
|
95
|
-
build() {
|
|
96
|
-
return this.buildCommand.run();
|
|
95
|
+
build(opts) {
|
|
96
|
+
return this.buildCommand.run(opts);
|
|
97
97
|
}
|
|
98
98
|
/** `wp-start-update`: 3-point squash-update from main (no PR). */
|
|
99
99
|
startUpdate() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';\nimport { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { BuildCommand } from './commands/build-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly checkoutCleanMainCommand: CheckoutCleanMainCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly buildCommand: BuildCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /**\n * `wp-build`: run this repo's ONE configured build (`commands.pr-gate.buildCommand`) — the same\n * command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a\n * `wp-push-dev --resolve`: it mutates nothing and reads no branch state.\n */\n build(): Promise<void> {\n return this.buildCommand.run();\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /** `wp-cleanup`: reap what is provably merged, and ASK about everything that merely looks dead. */\n cleanup(): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run();\n }\n\n /**\n * `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep\n * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull\n * origin main` outright — see CheckoutCleanMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n checkoutCleanMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-checkout-clean-main');\n return this.checkoutCleanMainCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run();\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';\nimport { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { BuildCommand, BuildOptions } from './commands/build-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly checkoutCleanMainCommand: CheckoutCleanMainCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly buildCommand: BuildCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /**\n * `wp-build`: run this repo's ONE configured build (`commands.pr-gate.buildCommand`) — the same\n * command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a\n * `wp-push-dev --resolve`: it mutates nothing and reads no branch state.\n */\n build(opts: BuildOptions): Promise<void> {\n return this.buildCommand.run(opts);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /** `wp-cleanup`: reap what is provably merged, and ASK about everything that merely looks dead. */\n cleanup(): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run();\n }\n\n /**\n * `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep\n * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull\n * origin main` outright — see CheckoutCleanMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n checkoutCleanMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-checkout-clean-main');\n return this.checkoutCleanMainCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run();\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HomeConfigService } from '@webpieces/rules-config';
|
|
1
|
+
import { HomeConfigService, BuildsLog } from '@webpieces/rules-config';
|
|
2
2
|
import { BuildGateLog } from './build-gate-log';
|
|
3
3
|
/**
|
|
4
4
|
* The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept
|
|
@@ -17,7 +17,8 @@ export declare class BuildGateOptions {
|
|
|
17
17
|
export declare class BuildAffected {
|
|
18
18
|
private readonly homeConfig;
|
|
19
19
|
private readonly buildLog;
|
|
20
|
-
|
|
20
|
+
private readonly buildsLog;
|
|
21
|
+
constructor(homeConfig: HomeConfigService, buildLog: BuildGateLog, buildsLog: BuildsLog);
|
|
21
22
|
/**
|
|
22
23
|
* EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.
|
|
23
24
|
*
|
|
@@ -33,18 +34,32 @@ export declare class BuildAffected {
|
|
|
33
34
|
*/
|
|
34
35
|
resolveBuildCommand(repoRoot: string): string;
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
+
* Spawn the resolved build command, streamed to the terminal. Returns the exit code (0 = pass).
|
|
37
38
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
39
|
+
* PRIVATE, and that is the point. This and `runConfiguredBuildGate` used to be PUBLIC side doors that
|
|
40
|
+
* spawned `buildCommand` with no caller identity and no ledger row — so "run the build" had three
|
|
41
|
+
* spellings, two of which were invisible to `~/.webpieces/builds.log`. They are folded into this one
|
|
42
|
+
* private method, reachable only through `runBuildGate`, which means an unlogged build no longer
|
|
43
|
+
* COMPILES. That is the one-spelling rule from CLAUDE.md § "NO webpieces surface is released
|
|
44
|
+
* backwards-compatible" applied to a behaviour rather than a type.
|
|
45
|
+
*
|
|
46
|
+
* Prints NOTHING itself — `runBuildGate` announces the command in one line.
|
|
40
47
|
*/
|
|
41
|
-
|
|
42
|
-
/** Run the build gate using the project's configured command (PrGateConfig.buildCommand). */
|
|
43
|
-
runConfiguredBuildGate(repoRoot: string): number;
|
|
48
|
+
private spawnBuild;
|
|
44
49
|
/**
|
|
45
50
|
* Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on
|
|
46
|
-
* failure so the bin's main()/runMain owns the exit.
|
|
47
|
-
* wp-finish-upsert-pr
|
|
51
|
+
* failure so the bin's main()/runMain owns the exit. THE single source of truth for running a build:
|
|
52
|
+
* wp-build, wp-review-upsert-pr and wp-finish-upsert-pr all call THIS (only the BuildGateOptions
|
|
53
|
+
* differ), and there is no other way to reach the spawn.
|
|
54
|
+
*
|
|
55
|
+
* ─── EVERY BUILD ON THIS MACHINE IS LEDGERED FROM HERE ───────────────────────────────────────────
|
|
56
|
+
* A START row goes into `~/.webpieces/builds.log` before the spawn and a DONE row after it, in a
|
|
57
|
+
* `finally` so a throw on the failure path still closes the pair. `opts.stage` — `build` | `review` |
|
|
58
|
+
* `finish` — IS the caller id the row records; there is deliberately no second `caller` field, since
|
|
59
|
+
* two spellings of one fact is the shim CLAUDE.md rejects.
|
|
60
|
+
*
|
|
61
|
+
* The ledger is best-effort by construction (see BuildsLog): it can never throw, so no build can die
|
|
62
|
+
* because a log file was busy.
|
|
48
63
|
*/
|
|
49
64
|
runBuildGate(repoRoot: string, opts: BuildGateOptions): Promise<void>;
|
|
50
65
|
/**
|
|
@@ -43,9 +43,11 @@ exports.BuildGateOptions = BuildGateOptions;
|
|
|
43
43
|
let BuildAffected = class BuildAffected {
|
|
44
44
|
homeConfig;
|
|
45
45
|
buildLog;
|
|
46
|
-
|
|
46
|
+
buildsLog;
|
|
47
|
+
constructor(homeConfig, buildLog, buildsLog) {
|
|
47
48
|
this.homeConfig = homeConfig;
|
|
48
49
|
this.buildLog = buildLog;
|
|
50
|
+
this.buildsLog = buildsLog;
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.
|
|
@@ -67,24 +69,37 @@ let BuildAffected = class BuildAffected {
|
|
|
67
69
|
return configured !== undefined && configured.trim() !== '' ? configured : rules_config_1.DEFAULT_BUILD_COMMAND;
|
|
68
70
|
}
|
|
69
71
|
/**
|
|
70
|
-
*
|
|
72
|
+
* Spawn the resolved build command, streamed to the terminal. Returns the exit code (0 = pass).
|
|
71
73
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
+
* PRIVATE, and that is the point. This and `runConfiguredBuildGate` used to be PUBLIC side doors that
|
|
75
|
+
* spawned `buildCommand` with no caller identity and no ledger row — so "run the build" had three
|
|
76
|
+
* spellings, two of which were invisible to `~/.webpieces/builds.log`. They are folded into this one
|
|
77
|
+
* private method, reachable only through `runBuildGate`, which means an unlogged build no longer
|
|
78
|
+
* COMPILES. That is the one-spelling rule from CLAUDE.md § "NO webpieces surface is released
|
|
79
|
+
* backwards-compatible" applied to a behaviour rather than a type.
|
|
80
|
+
*
|
|
81
|
+
* Prints NOTHING itself — `runBuildGate` announces the command in one line.
|
|
74
82
|
*/
|
|
75
|
-
|
|
76
|
-
const
|
|
83
|
+
spawnBuild(repoRoot) {
|
|
84
|
+
const configured = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.buildCommand;
|
|
85
|
+
const cmd = configured !== undefined && configured.trim() !== '' ? configured : rules_config_1.DEFAULT_BUILD_COMMAND;
|
|
77
86
|
const result = (0, child_process_1.spawnSync)(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });
|
|
78
87
|
return result.status ?? 1;
|
|
79
88
|
}
|
|
80
|
-
/** Run the build gate using the project's configured command (PrGateConfig.buildCommand). */
|
|
81
|
-
runConfiguredBuildGate(repoRoot) {
|
|
82
|
-
return this.runBuildAffected(repoRoot, (0, rules_config_1.loadAndValidate)(repoRoot).prGate.buildCommand);
|
|
83
|
-
}
|
|
84
89
|
/**
|
|
85
90
|
* Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on
|
|
86
|
-
* failure so the bin's main()/runMain owns the exit.
|
|
87
|
-
* wp-finish-upsert-pr
|
|
91
|
+
* failure so the bin's main()/runMain owns the exit. THE single source of truth for running a build:
|
|
92
|
+
* wp-build, wp-review-upsert-pr and wp-finish-upsert-pr all call THIS (only the BuildGateOptions
|
|
93
|
+
* differ), and there is no other way to reach the spawn.
|
|
94
|
+
*
|
|
95
|
+
* ─── EVERY BUILD ON THIS MACHINE IS LEDGERED FROM HERE ───────────────────────────────────────────
|
|
96
|
+
* A START row goes into `~/.webpieces/builds.log` before the spawn and a DONE row after it, in a
|
|
97
|
+
* `finally` so a throw on the failure path still closes the pair. `opts.stage` — `build` | `review` |
|
|
98
|
+
* `finish` — IS the caller id the row records; there is deliberately no second `caller` field, since
|
|
99
|
+
* two spellings of one fact is the shim CLAUDE.md rejects.
|
|
100
|
+
*
|
|
101
|
+
* The ledger is best-effort by construction (see BuildsLog): it can never throw, so no build can die
|
|
102
|
+
* because a log file was busy.
|
|
88
103
|
*/
|
|
89
104
|
async runBuildGate(repoRoot, opts) {
|
|
90
105
|
const buildCommand = this.resolveBuildCommand(repoRoot);
|
|
@@ -96,9 +111,19 @@ let BuildAffected = class BuildAffected {
|
|
|
96
111
|
// `~/.webpieces/config.json`. Everything below then runs exactly the code it ran before capture
|
|
97
112
|
// existed — same spawn, streamed to the terminal, same message, no extra file.
|
|
98
113
|
const logPath = opts.alwaysCapture || this.isCaptureEnabled() ? this.buildLog.pathFor(repoRoot, opts.stage) : '';
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
const ticket = this.buildsLog.start(opts.stage, repoRoot);
|
|
115
|
+
let buildCode = 1;
|
|
116
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: the DONE row must be written whether the
|
|
117
|
+
// build passed, failed, or the spawn itself blew up; the throw is re-raised untouched below
|
|
118
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
119
|
+
try {
|
|
120
|
+
buildCode = logPath === ''
|
|
121
|
+
? this.spawnBuild(repoRoot)
|
|
122
|
+
: await this.buildLog.run(repoRoot, buildCommand, logPath);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
this.buildsLog.finish(ticket, buildCode);
|
|
126
|
+
}
|
|
102
127
|
if (buildCode !== 0)
|
|
103
128
|
throw new rules_config_1.CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));
|
|
104
129
|
process.stdout.write(logPath === '' ? '\n✅ Build passed.\n' : this.buildLog.successMessage(logPath));
|
|
@@ -120,6 +145,7 @@ exports.BuildAffected = BuildAffected;
|
|
|
120
145
|
exports.BuildAffected = BuildAffected = tslib_1.__decorate([
|
|
121
146
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
122
147
|
tslib_1.__metadata("design:paramtypes", [rules_config_1.HomeConfigService,
|
|
123
|
-
build_gate_log_1.BuildGateLog
|
|
148
|
+
build_gate_log_1.BuildGateLog,
|
|
149
|
+
rules_config_1.BuildsLog])
|
|
124
150
|
], BuildAffected);
|
|
125
151
|
//# sourceMappingURL=build-affected.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,0DAAkH;AAClH,yCAA2D;AAC3D,qDAAgD;AAEhD,iGAAiG;AACjG,8FAA8F;AAC9F,iGAAiG;AACjG,oFAAoF;AACpF,oGAAoG;AACpG,iEAAiE;AAEjE,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;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;IACd,uGAAuG;IACvG,uGAAuG;IACvG,uGAAuG;IACvG,yDAAyD;IACzD,aAAa,CAAU;IAEvB,YAAY,KAAa,EAAE,YAAoB,EAAE,eAAuB,EAAE,KAAa,EAAE,aAAsB;QAC3G,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;QACnB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACvC,CAAC;CACJ;AApBD,4CAoBC;AAED,qGAAqG;AAE9F,IAAM,aAAa,GAAnB,MAAM,aAAa;IAED;IACA;IAFrB,YACqB,UAA6B,EAC7B,QAAsB;QADtB,eAAU,GAAV,UAAU,CAAmB;QAC7B,aAAQ,GAAR,QAAQ,CAAc;IACxC,CAAC;IAEJ;;;;;;;OAOG;IACH,gBAAgB;QACZ,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,mBAAmB,CAAC;IACtD,CAAC;IAED;;;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;;;;;OAKG;IACH,gBAAgB,CAAC,QAAgB,EAAE,YAAqB;QACpD,MAAM,GAAG,GAAG,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,oCAAqB,CAAC;QAC5G,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAChF,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,6FAA6F;IAC7F,sBAAsB,CAAC,QAAgB;QACnC,OAAO,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1F,CAAC;IAED;;;;OAIG;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,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,KAAK,KAAK,YAAY,IAAI,CAAC,CAAC;QAC3D,yFAAyF;QACzF,gGAAgG;QAChG,+EAA+E;QAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACjH,MAAM,SAAS,GAAG,OAAO,KAAK,EAAE;YAC5B,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,QAAQ,CAAC;YACvC,CAAC,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;QAC/D,IAAI,SAAS,KAAK,CAAC;YAAE,MAAM,IAAI,2BAAY,CAAC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC;QACtG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IACzG,CAAC;IAED;;;;OAIG;IACK,WAAW,CAAC,IAAsB,EAAE,YAAoB,EAAE,OAAe;QAC7E,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAC/E,OAAO,OAAO,IAAI,CAAC,eAAe,MAAM;YACpC,uEAAuE,IAAI,CAAC,YAAY,OAAO;YAC/F,OAAO,YAAY,IAAI,CAAC;IAChC,CAAC;CACJ,CAAA;AA7EY,sCAAa;wBAAb,aAAa;IADzB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGJ,gCAAiB;QACnB,6BAAY;GAHlC,aAAa,CA6EzB","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
|
+
{"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 {\n loadAndValidate, CliExitError, HomeConfigService, DEFAULT_BUILD_COMMAND, BuildsLog,\n} 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 private readonly buildsLog: BuildsLog,\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 * Spawn the resolved build command, streamed to the terminal. Returns the exit code (0 = pass).\n *\n * PRIVATE, and that is the point. This and `runConfiguredBuildGate` used to be PUBLIC side doors that\n * spawned `buildCommand` with no caller identity and no ledger row — so \"run the build\" had three\n * spellings, two of which were invisible to `~/.webpieces/builds.log`. They are folded into this one\n * private method, reachable only through `runBuildGate`, which means an unlogged build no longer\n * COMPILES. That is the one-spelling rule from CLAUDE.md § \"NO webpieces surface is released\n * backwards-compatible\" applied to a behaviour rather than a type.\n *\n * Prints NOTHING itself — `runBuildGate` announces the command in one line.\n */\n private spawnBuild(repoRoot: string): number {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n const cmd = configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n const result = spawnSync(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });\n return result.status ?? 1;\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.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 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 ticket = this.buildsLog.start(opts.stage, repoRoot);\n let buildCode = 1;\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 buildCode = logPath === ''\n ? this.spawnBuild(repoRoot)\n : await this.buildLog.run(repoRoot, buildCommand, logPath);\n } finally {\n this.buildsLog.finish(ticket, buildCode);\n }\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"]}
|
package/src/scripts/wp-build.js
CHANGED
|
@@ -5,16 +5,33 @@ require("reflect-metadata");
|
|
|
5
5
|
const inversify_1 = require("inversify");
|
|
6
6
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
7
|
const pr_gate_app_1 = require("./pr-gate-app");
|
|
8
|
+
const build_command_1 = require("./commands/build-command");
|
|
9
|
+
const FORCE = '--force';
|
|
8
10
|
// Composition root for `wp-build`: run this repo's ONE configured build command
|
|
9
11
|
// (`commands.pr-gate.buildCommand`) and nothing else. See BuildCommand for why it composes no other
|
|
10
12
|
// leg — a verify chain assembled per repo is what drifted into building the world three times over.
|
|
11
13
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
14
|
+
// ─── THE INVARIANT, AND WHAT `--force` IS AND IS NOT ──────────────────────────────────────────────────
|
|
15
|
+
// `wp-build` runs `buildCommand` VERBATIM: no legs, no knobs. That is the promise, and it is why a green
|
|
16
|
+
// result here is evidence about the PR gate — the two must be the SAME command. This bin used to state
|
|
17
|
+
// that as "takes NO arguments", which conflated the promise with its enforcement: the thing that must
|
|
18
|
+
// never exist is a flag that changes WHAT IS RUN, not a flag as such.
|
|
19
|
+
//
|
|
20
|
+
// `--force` changes nothing about the command. It skips ONE precondition — the machine-wide concurrency
|
|
21
|
+
// refusal that reads `~/.webpieces/builds.log` — and then runs byte-for-byte the same build. The gate
|
|
22
|
+
// stages have no equivalent and never refuse, so there is nothing here for the two to differ about.
|
|
23
|
+
//
|
|
24
|
+
// Narrower loops still have their own commands (`nx run <project>:ci`, `vitest run <path>`) and do not
|
|
25
|
+
// go through this bin.
|
|
15
26
|
(0, rules_config_1.runMain)(async () => {
|
|
16
27
|
const container = new inversify_1.Container({ autobind: true });
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
// Reject `--help`/bogus flags BEFORE anything runs — a mistyped `--forc` must never be silently
|
|
29
|
+
// dropped and then turn a deliberate override back into a refusal.
|
|
30
|
+
const args = container.get(rules_config_1.CliArgs).parse(new rules_config_1.CliUsage('wp-build', 'Run this repo`s configured build (commands.pr-gate.buildCommand) — the same command the PR gate runs.', [
|
|
31
|
+
new rules_config_1.CliFlag(FORCE, 'Build even though this machine is already at its concurrent-build limit. Skips\n'
|
|
32
|
+
+ ' that ONE check and nothing else — the command run is identical. Reach for\n'
|
|
33
|
+
+ ' the gate (`pnpm wp-review-upsert-pr`) first; it runs the same build.'),
|
|
34
|
+
]));
|
|
35
|
+
await container.get(pr_gate_app_1.PrGateApp).build(new build_command_1.BuildOptions(args.has(FORCE)));
|
|
19
36
|
});
|
|
20
37
|
//# sourceMappingURL=wp-build.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wp-build.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-build.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,
|
|
1
|
+
{"version":3,"file":"wp-build.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-build.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,0DAA8E;AAC9E,+CAA0C;AAC1C,4DAAwD;AAExD,MAAM,KAAK,GAAG,SAAS,CAAC;AAExB,gFAAgF;AAChF,oGAAoG;AACpG,oGAAoG;AACpG,EAAE;AACF,yGAAyG;AACzG,yGAAyG;AACzG,uGAAuG;AACvG,sGAAsG;AACtG,sEAAsE;AACtE,EAAE;AACF,wGAAwG;AACxG,sGAAsG;AACtG,oGAAoG;AACpG,EAAE;AACF,uGAAuG;AACvG,uBAAuB;AACvB,IAAA,sBAAO,EAAC,KAAK,IAAmB,EAAE;IAC9B,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,gGAAgG;IAChG,mEAAmE;IACnE,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,sBAAO,CAAC,CAAC,KAAK,CAAC,IAAI,uBAAQ,CAAC,UAAU,EAC7D,uGAAuG,EACvG;QACI,IAAI,sBAAO,CAAC,KAAK,EACb,kFAAkF;cAChF,6FAA6F;cAC7F,sFAAsF,CAAC;KAChG,CAAC,CAAC,CAAC;IACR,MAAM,SAAS,CAAC,GAAG,CAAC,uBAAS,CAAC,CAAC,KAAK,CAAC,IAAI,4BAAY,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC5E,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\nimport { runMain, CliArgs, CliFlag, CliUsage } from '@webpieces/rules-config';\nimport { PrGateApp } from './pr-gate-app';\nimport { BuildOptions } from './commands/build-command';\n\nconst FORCE = '--force';\n\n// Composition root for `wp-build`: run this repo's ONE configured build command\n// (`commands.pr-gate.buildCommand`) and nothing else. See BuildCommand for why it composes no other\n// leg — a verify chain assembled per repo is what drifted into building the world three times over.\n//\n// ─── THE INVARIANT, AND WHAT `--force` IS AND IS NOT ──────────────────────────────────────────────────\n// `wp-build` runs `buildCommand` VERBATIM: no legs, no knobs. That is the promise, and it is why a green\n// result here is evidence about the PR gate — the two must be the SAME command. This bin used to state\n// that as \"takes NO arguments\", which conflated the promise with its enforcement: the thing that must\n// never exist is a flag that changes WHAT IS RUN, not a flag as such.\n//\n// `--force` changes nothing about the command. It skips ONE precondition — the machine-wide concurrency\n// refusal that reads `~/.webpieces/builds.log` — and then runs byte-for-byte the same build. The gate\n// stages have no equivalent and never refuse, so there is nothing here for the two to differ about.\n//\n// Narrower loops still have their own commands (`nx run <project>:ci`, `vitest run <path>`) and do not\n// go through this bin.\nrunMain(async (): Promise<void> => {\n const container = new Container({ autobind: true });\n // Reject `--help`/bogus flags BEFORE anything runs — a mistyped `--forc` must never be silently\n // dropped and then turn a deliberate override back into a refusal.\n const args = container.get(CliArgs).parse(new CliUsage('wp-build',\n 'Run this repo`s configured build (commands.pr-gate.buildCommand) — the same command the PR gate runs.',\n [\n new CliFlag(FORCE,\n 'Build even though this machine is already at its concurrent-build limit. Skips\\n'\n + ' that ONE check and nothing else — the command run is identical. Reach for\\n'\n + ' the gate (`pnpm wp-review-upsert-pr`) first; it runs the same build.'),\n ]));\n await container.get(PrGateApp).build(new BuildOptions(args.has(FORCE)));\n});\n"]}
|