@webpieces/pr-gate 0.4.593 → 0.4.595
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/dashboard/checklist-comment-renderer.d.ts +78 -0
- package/src/dashboard/checklist-comment-renderer.js +272 -0
- package/src/dashboard/checklist-comment-renderer.js.map +1 -0
- package/src/dashboard/dashboard.d.ts +66 -52
- package/src/dashboard/dashboard.js +105 -262
- package/src/dashboard/dashboard.js.map +1 -1
- package/src/index.d.ts +3 -1
- package/src/index.js +11 -1
- package/src/index.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +53 -34
- package/src/scripts/commands/finish-upsert-pr-command.js +181 -160
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/finish-banner.js +7 -11
- package/src/scripts/workflow/finish-banner.js.map +1 -1
- package/src/scripts/workflow/pr-comment-upserter.d.ts +54 -0
- package/src/scripts/workflow/pr-comment-upserter.js +100 -0
- package/src/scripts/workflow/pr-comment-upserter.js.map +1 -0
- package/src/scripts/workflow/pr-merger.js +13 -8
- package/src/scripts/workflow/pr-merger.js.map +1 -1
- package/src/scripts/workflow/provenance-enforcer.d.ts +68 -0
- package/src/scripts/workflow/provenance-enforcer.js +169 -0
- package/src/scripts/workflow/provenance-enforcer.js.map +1 -0
|
@@ -19,19 +19,46 @@ const pr_merger_1 = require("../workflow/pr-merger");
|
|
|
19
19
|
const finish_banner_1 = require("../workflow/finish-banner");
|
|
20
20
|
const merge_body_filer_1 = require("../workflow/merge-body-filer");
|
|
21
21
|
const gated_pr_publisher_1 = require("../workflow/gated-pr-publisher");
|
|
22
|
+
const provenance_enforcer_1 = require("../workflow/provenance-enforcer");
|
|
23
|
+
const pr_comment_upserter_1 = require("../workflow/pr-comment-upserter");
|
|
22
24
|
const dashboard_1 = require("../../dashboard/dashboard");
|
|
25
|
+
const checklist_comment_renderer_1 = require("../../dashboard/checklist-comment-renderer");
|
|
23
26
|
const checklist_comment_row_1 = require("../../dashboard/checklist-comment-row");
|
|
24
27
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
25
28
|
/**
|
|
26
|
-
* The
|
|
27
|
-
*
|
|
29
|
+
* The three inputs the PR COMMENTS are rendered from, bundled so `publishAll` takes one parameter for
|
|
30
|
+
* them instead of three it only forwards. Data-only, per CLAUDE.md.
|
|
28
31
|
*/
|
|
29
|
-
class
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.
|
|
32
|
+
class PrCommentSources {
|
|
33
|
+
scan;
|
|
34
|
+
review;
|
|
35
|
+
provenance;
|
|
36
|
+
constructor(scan, review, provenance) {
|
|
37
|
+
this.scan = scan;
|
|
38
|
+
this.review = review;
|
|
39
|
+
this.provenance = provenance;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Everything the PR upsert needs. Data-only, per CLAUDE.md — it replaced a 5-positional-parameter method
|
|
44
|
+
* once `tokenSuffix` had to travel too, so the body can be re-rendered with the PR's own URL after
|
|
45
|
+
* `gh pr create` returns it (see the INVARIANT comment in upsertPr).
|
|
46
|
+
*/
|
|
47
|
+
class PrUpsertRequest {
|
|
48
|
+
repoRoot = '';
|
|
49
|
+
baseBranch = '';
|
|
50
|
+
title = '';
|
|
51
|
+
/** The description as first published — rendered with whatever URL was known at the time. */
|
|
52
|
+
body = '';
|
|
53
|
+
/**
|
|
54
|
+
* The hidden gate-token marker exactly as appended to `body`, so a re-render can reproduce the same
|
|
55
|
+
* bytes. Kept separate from `body` because Dashboard renders the human/git-log half and must not know
|
|
56
|
+
* about HMACs.
|
|
57
|
+
*/
|
|
58
|
+
tokenSuffix = '';
|
|
59
|
+
input;
|
|
60
|
+
constructor(input) {
|
|
61
|
+
this.input = input;
|
|
35
62
|
}
|
|
36
63
|
}
|
|
37
64
|
// A resolved PR's number + web URL. Both '' when the PR can't be resolved (e.g. create failed).
|
|
@@ -76,24 +103,30 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
76
103
|
prMerger;
|
|
77
104
|
publisher;
|
|
78
105
|
dashboard;
|
|
106
|
+
checklistComment;
|
|
79
107
|
checklistScanner;
|
|
80
108
|
verdictGate;
|
|
81
109
|
reviewJsonService;
|
|
82
110
|
gateTokenService;
|
|
83
|
-
|
|
84
|
-
provenanceRecord;
|
|
85
|
-
reviewerInstructions;
|
|
111
|
+
provenanceEnforcer;
|
|
86
112
|
receipts;
|
|
87
113
|
banner;
|
|
88
114
|
mergeBodyFiler;
|
|
89
|
-
|
|
115
|
+
commentUpserter;
|
|
116
|
+
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, prMerger, publisher, dashboard,
|
|
117
|
+
// The 2nd PR comment. Its own class because it is its own surface — see ChecklistCommentRenderer.
|
|
118
|
+
checklistComment, checklistScanner, verdictGate, reviewJsonService, gateTokenService,
|
|
119
|
+
// Owns the reviewer-provenance integrity check and its audit record — see ProvenanceEnforcer.
|
|
120
|
+
provenanceEnforcer,
|
|
90
121
|
// NOTE: ChecklistInstructionsService is deliberately NOT injected here any more. Its "You MUST run
|
|
91
122
|
// these N reviewer subagent(s)" block is now rendered by ReviewerVerdictGate and ONLY for checklists
|
|
92
123
|
// that genuinely never ran, so no other code path in this command can print it at a refusal.
|
|
93
124
|
receipts, banner,
|
|
94
125
|
// The MACHINE-GLOBAL home for the one artifact whose scope is bigger than this tree: the gated
|
|
95
126
|
// squash-commit body, which `wp-land-pr` must find from any tree (see MergeBodyFiler).
|
|
96
|
-
mergeBodyFiler
|
|
127
|
+
mergeBodyFiler,
|
|
128
|
+
// ONE marker-keyed upsert, shared by both PR comments (the full dashboard and the checklist).
|
|
129
|
+
commentUpserter) {
|
|
97
130
|
this.repoRootFinder = repoRootFinder;
|
|
98
131
|
this.aiBranchName = aiBranchName;
|
|
99
132
|
this.branchNaming = branchNaming;
|
|
@@ -103,16 +136,16 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
103
136
|
this.prMerger = prMerger;
|
|
104
137
|
this.publisher = publisher;
|
|
105
138
|
this.dashboard = dashboard;
|
|
139
|
+
this.checklistComment = checklistComment;
|
|
106
140
|
this.checklistScanner = checklistScanner;
|
|
107
141
|
this.verdictGate = verdictGate;
|
|
108
142
|
this.reviewJsonService = reviewJsonService;
|
|
109
143
|
this.gateTokenService = gateTokenService;
|
|
110
|
-
this.
|
|
111
|
-
this.provenanceRecord = provenanceRecord;
|
|
112
|
-
this.reviewerInstructions = reviewerInstructions;
|
|
144
|
+
this.provenanceEnforcer = provenanceEnforcer;
|
|
113
145
|
this.receipts = receipts;
|
|
114
146
|
this.banner = banner;
|
|
115
147
|
this.mergeBodyFiler = mergeBodyFiler;
|
|
148
|
+
this.commentUpserter = commentUpserter;
|
|
116
149
|
}
|
|
117
150
|
async run() {
|
|
118
151
|
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
@@ -149,7 +182,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
149
182
|
// artifacts) that such a subagent actually ran on this branch — the coding agent may not
|
|
150
183
|
// self-certify. Absent CLAUDE_CODE_SESSION_ID this skips with a warning (CI / plain terminal).
|
|
151
184
|
const currentBranch = (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
152
|
-
const provenance = this.
|
|
185
|
+
const provenance = this.provenanceEnforcer.enforce(verdicted, currentBranch, repoRoot, (0, rules_config_1.loadAndValidate)(repoRoot).prGate);
|
|
153
186
|
// 2b. The build gate validates the WORKING TREE but we push HEAD — so they MUST be identical.
|
|
154
187
|
this.gitExec.assertCleanTree(repoRoot);
|
|
155
188
|
// 3. Build gate, then post the gated body, then push (that ORDER — see GatedPrPublisher).
|
|
@@ -158,18 +191,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
158
191
|
process.stdout.write('\n' + SEP + '📋 Dashboard + PR\n' + SEP + '\n');
|
|
159
192
|
const title = this.prTitleFrom(review);
|
|
160
193
|
const input = this.computeDashboardInput(repoRoot, true, review, title, verdicted);
|
|
161
|
-
|
|
162
|
-
// GatedPrPublisher writes the body first so CI's `synchronize` read can never see a stale token. A
|
|
163
|
-
// valid token in the PR body is proof this gated flow ran + passed on this exact commit, which CI
|
|
164
|
-
// (`wp-check-pr`) recomputes. We reach here only after the build gate + every BLOCK checklist
|
|
165
|
-
// passed, so minting is legitimate. Nothing about HMAC(salt, HEAD) needs the remote to have it.
|
|
166
|
-
const gateSalt = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.gateSalt;
|
|
167
|
-
const headSha = this.gitOut(['rev-parse', 'HEAD']);
|
|
168
|
-
const body = this.dashboard.renderDashboard(input) + this.gateTokenBody(gateSalt, headSha);
|
|
169
|
-
const result = this.upsertPr(repoRoot, base, body, title, input);
|
|
170
|
-
// Publish each reviewer's full output as ONE combined PR comment (idempotent, opt-out-aware). Never
|
|
171
|
-
// fatal — the PR is already up by now, so a comment failure only warns.
|
|
172
|
-
this.postChecklistComment(repoRoot, result.prNumber, scan, review, provenance);
|
|
194
|
+
const result = this.publishAll(repoRoot, base, input, new PrCommentSources(scan, review, provenance));
|
|
173
195
|
this.archiveConsumedReview(repoRoot, featureName, result);
|
|
174
196
|
// The closing recap + the clickable-link directive, BOTH derived from the real merge outcome.
|
|
175
197
|
// Nothing here may hard-code success: a stranded PR under a green checkmark is how PRs got
|
|
@@ -283,7 +305,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
283
305
|
process.stdout.write(` archived this run's review.json → ${archived} (audit only) ✓\n`);
|
|
284
306
|
// Beside it, so an archived review keeps the transcript links belonging to the round that
|
|
285
307
|
// produced it — a review whose provenance was overwritten by the NEXT round audits nothing.
|
|
286
|
-
this.
|
|
308
|
+
this.provenanceEnforcer.archiveRecord((0, rules_config_1.prDirFor)(repoRoot, featureName));
|
|
287
309
|
}
|
|
288
310
|
catch (err) {
|
|
289
311
|
const error = (0, rules_config_1.toError)(err);
|
|
@@ -313,7 +335,10 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
313
335
|
const gateResults = this.dashboard.computeGateResults(config.gates, changedFiles);
|
|
314
336
|
const disables = this.dashboard.countAddedDisables(patch);
|
|
315
337
|
const rows = this.checklistRows(required, review);
|
|
316
|
-
|
|
338
|
+
// buildCommand travels into the dashboard so the PR-body footer can NAME the command that vouched
|
|
339
|
+
// for this commit. The footer used to assert "build ran via nx affected" on every repo, which was
|
|
340
|
+
// simply false wherever buildCommand is not nx.
|
|
341
|
+
return new dashboard_1.DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows, config.buildCommand);
|
|
317
342
|
}
|
|
318
343
|
/**
|
|
319
344
|
* The applicable checklists MINUS the optional ones nobody ran.
|
|
@@ -373,100 +398,6 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
373
398
|
const marker = this.gateTokenService.gateTokenMarker(gateSalt, headSha);
|
|
374
399
|
return marker === '' ? '' : `\n\n${marker}\n`;
|
|
375
400
|
}
|
|
376
|
-
// Enforce that EACH matched checklist was reviewed by its OWN named subagent, as a DISTINCT run —
|
|
377
|
-
// the coding agent may not self-certify, and one reviewer may not stand in for several. A verified set
|
|
378
|
-
// passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.
|
|
379
|
-
// eslint-disable-next-line @typescript-eslint/max-params
|
|
380
|
-
enforceProvenance(required, branch, repoRoot, config) {
|
|
381
|
-
const errors = [];
|
|
382
|
-
const report = new ProvenanceReport(true, []); // no reviewers to verify ⇒ vacuously verified
|
|
383
|
-
const subagents = required.map((r) => r.subagent.trim()).filter((s) => s !== '');
|
|
384
|
-
// verifyDistinct short-circuits to OK on an empty set, so this runs unconditionally: a repo with no
|
|
385
|
-
// checklists still gets a provenance record naming the session and the main agent's own transcript.
|
|
386
|
-
const result = this.provenance.verifyDistinct(subagents, branch);
|
|
387
|
-
report.verified = result.status === rules_config_1.PROVENANCE_OK;
|
|
388
|
-
if (result.status === rules_config_1.PROVENANCE_MISSING) {
|
|
389
|
-
errors.push(result.detail);
|
|
390
|
-
}
|
|
391
|
-
else if (result.status === rules_config_1.PROVENANCE_SKIPPED) {
|
|
392
|
-
process.stderr.write(`⚠️ ${result.detail}\n`);
|
|
393
|
-
}
|
|
394
|
-
report.evidence = this.gatherEvidence(repoRoot, required, result, branch);
|
|
395
|
-
errors.push(...this.evidenceErrors(report.evidence, config));
|
|
396
|
-
// BEFORE the throw below, deliberately. A refused round is the one most worth auditing, and a record
|
|
397
|
-
// that only ever appeared on success could not answer what the reviewers did the time it was refused.
|
|
398
|
-
this.writeProvenanceRecord(repoRoot, required, result, report.evidence);
|
|
399
|
-
if (errors.length > 0) {
|
|
400
|
-
throw new rules_config_1.InformAiError(`${errors.length} checklist(s) require an independent reviewer subagent that did not run — fix, then re-run pnpm wp-finish-upsert-pr:\n\n` +
|
|
401
|
-
errors.map((e) => ` • ${e}`).join('\n') +
|
|
402
|
-
`\n\nSpawn the named reviewer subagent to review the checklist on THIS branch, then re-run.`);
|
|
403
|
-
}
|
|
404
|
-
return report;
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* What each credited reviewer actually read. Purely observational here — {@link evidenceErrors} decides
|
|
408
|
-
* whether any of it blocks, and by default none of it does.
|
|
409
|
-
*/
|
|
410
|
-
// eslint-disable-next-line @typescript-eslint/max-params
|
|
411
|
-
gatherEvidence(repoRoot, required, result, branch) {
|
|
412
|
-
const docPaths = {};
|
|
413
|
-
for (const req of required) {
|
|
414
|
-
if (req.subagent.trim() !== '')
|
|
415
|
-
docPaths[req.subagent] = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);
|
|
416
|
-
}
|
|
417
|
-
const diffDir = path.join((0, rules_config_1.prDirFor)(repoRoot, this.aiBranchName.getFeatureName()), 'diff');
|
|
418
|
-
return this.provenance.evidenceFor(new rules_config_1.EvidenceRequest(branch, result.agentIds, diffDir, docPaths));
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Write this round's audit record: `.webpieces/pr-review/<branch>/provenance.json`, linking each verdict
|
|
422
|
-
* to the transcript of the subagent that produced it.
|
|
423
|
-
*
|
|
424
|
-
* A SEPARATE file rather than a field inside review.json / review-<id>.json, for two reasons. The
|
|
425
|
-
* reviewer cannot supply this itself — a subagent's environment exposes the PARENT session id and no
|
|
426
|
-
* agent id, so a self-reported transcript link would be invented — and keeping the AI-authored files
|
|
427
|
-
* byte-untouched means nothing in the record can be mistaken for something a reviewer claimed about
|
|
428
|
-
* itself. Every path here is derived by the tooling from the harness's own artifacts.
|
|
429
|
-
*
|
|
430
|
-
* Never fatal: an unwritable record is a lost audit trail, not a reason to refuse a PR.
|
|
431
|
-
*/
|
|
432
|
-
// eslint-disable-next-line @typescript-eslint/max-params
|
|
433
|
-
writeProvenanceRecord(repoRoot, required, result, evidence) {
|
|
434
|
-
const featureName = this.aiBranchName.getFeatureName();
|
|
435
|
-
const prDir = (0, rules_config_1.prDirFor)(repoRoot, featureName);
|
|
436
|
-
const request = new rules_config_1.ProvenanceWriteRequest(prDir, featureName, this.gitOut(['rev-parse', 'HEAD']), result.status);
|
|
437
|
-
request.offered = new rules_config_1.OfferedContext(path.join(prDir, 'diff'), this.reviewerInstructions.instructionsDirFor(repoRoot, featureName));
|
|
438
|
-
request.reviewers = evidence.map((e) => new rules_config_1.ReviewerTranscript(e, this.reviewerPathsFor(repoRoot, featureName, required, e.agentType)));
|
|
439
|
-
const written = this.provenanceRecord.write(request);
|
|
440
|
-
if (written !== '')
|
|
441
|
-
process.stdout.write(` transcript provenance → ${written}\n`);
|
|
442
|
-
}
|
|
443
|
-
// Where ONE reviewer's verdict, instructions and checklist doc live. Keyed by agentType, which IS the
|
|
444
|
-
// checklist id: ChecklistDefinition sets `id = subagent`, so the two never diverge.
|
|
445
|
-
// eslint-disable-next-line @typescript-eslint/max-params
|
|
446
|
-
reviewerPathsFor(repoRoot, featureName, required, agentType) {
|
|
447
|
-
const req = required.find((r) => r.subagent.trim() === agentType);
|
|
448
|
-
const doc = req !== undefined && req.doc.trim() !== '' ? path.resolve(repoRoot, req.doc) : '';
|
|
449
|
-
return new rules_config_1.ReviewerPaths(this.reviewJsonService.checklistResultPath((0, rules_config_1.reviewJsonPath)(repoRoot, featureName), req?.id ?? agentType), this.reviewerInstructions.pathFor(repoRoot, featureName, agentType), doc);
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.
|
|
453
|
-
*
|
|
454
|
-
* Default-warn because the signal is derived from undocumented Claude Code transcript internals: if the
|
|
455
|
-
* format shifts, a blocking check wedges every PR in every consumer repo with no self-service recovery.
|
|
456
|
-
* `requireDiffEvidence` lets a repo that has watched the warning promote it deliberately.
|
|
457
|
-
*/
|
|
458
|
-
evidenceErrors(evidence, config) {
|
|
459
|
-
const blind = evidence.filter((e) => !e.readDiff);
|
|
460
|
-
if (blind.length === 0)
|
|
461
|
-
return [];
|
|
462
|
-
const names = blind.map((e) => e.agentType).join(', ');
|
|
463
|
-
if (!config.requireDiffEvidence) {
|
|
464
|
-
process.stderr.write(`\n⚠️ ${blind.length} reviewer(s) wrote a verdict with no record of opening the extracted diff: ${names}\n` +
|
|
465
|
-
' Published on the PR as a note. Not blocking — set pr-gate.requireDiffEvidence:true to make it one.\n');
|
|
466
|
-
return [];
|
|
467
|
-
}
|
|
468
|
-
return [`these reviewers wrote a verdict without opening the diff (pr-gate.requireDiffEvidence is on): ${names}`];
|
|
469
|
-
}
|
|
470
401
|
/**
|
|
471
402
|
* Publish the roster + every reviewer's full `output` as ONE combined PR comment, idempotently (find the
|
|
472
403
|
* marker comment → PATCH it, else POST). Never fatal: by here the PR is already created/updated, so a
|
|
@@ -475,51 +406,36 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
475
406
|
* Posted on EVERY run of a repo that defines checklists — including one where nothing matched, because
|
|
476
407
|
* "all five were evaluated and none applied" is the good news the old comment could not deliver. The
|
|
477
408
|
* guard is `scan.defined.length`, deliberately NOT the number of rows that ran: a repo with no
|
|
478
|
-
* checklists configured must still see no comment at all (see
|
|
409
|
+
* checklists configured must still see no comment at all (see ChecklistCommentRenderer).
|
|
479
410
|
*/
|
|
480
411
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
481
|
-
// eslint-disable-next-line @typescript-eslint/max-params
|
|
482
412
|
postChecklistComment(repoRoot, prNumber, scan, review, provenance) {
|
|
483
413
|
if (prNumber === '' || scan.defined.length === 0)
|
|
484
414
|
return;
|
|
485
415
|
if (!(0, rules_config_1.loadAndValidate)(repoRoot).prGate.checklistComments)
|
|
486
416
|
return;
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
: ['api', '--method', 'POST', `repos/{owner}/{repo}/issues/${prNumber}/comments`, '--input', payload];
|
|
496
|
-
const res = (0, child_process_1.spawnSync)('gh', args, { encoding: 'utf8' });
|
|
497
|
-
if (res.status !== 0) {
|
|
498
|
-
process.stderr.write('⚠️ Could not post the checklist review comment (non-fatal — the PR is already up).\n');
|
|
499
|
-
}
|
|
500
|
-
else {
|
|
501
|
-
process.stdout.write(` ${commentId !== '' ? 'updated' : 'posted'} the checklist review comment ✓\n`);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
// The id of THIS tool's existing checklist comment on the PR (by the hidden marker), or '' if none.
|
|
505
|
-
findChecklistCommentId(prNumber) {
|
|
506
|
-
const res = (0, child_process_1.spawnSync)('gh', [
|
|
507
|
-
'api', '--paginate', `repos/{owner}/{repo}/issues/${prNumber}/comments`,
|
|
508
|
-
'--jq', `.[] | select(.body | contains("${dashboard_1.CHECKLIST_COMMENT_MARKER}")) | .id`,
|
|
509
|
-
], { encoding: 'utf8' });
|
|
510
|
-
if (res.status !== 0)
|
|
511
|
-
return '';
|
|
512
|
-
return (res.stdout ?? '').trim().split('\n')[0] ?? '';
|
|
417
|
+
const request = new pr_comment_upserter_1.PrCommentRequest();
|
|
418
|
+
request.prNumber = prNumber;
|
|
419
|
+
request.marker = checklist_comment_renderer_1.CHECKLIST_COMMENT_MARKER;
|
|
420
|
+
request.body = this.checklistComment.render(this.commentRows(scan, review, provenance), provenance.verified, scan.roster.baseResolved);
|
|
421
|
+
request.payloadDir = (0, rules_config_1.prDirFor)(repoRoot, this.aiBranchName.getFeatureName());
|
|
422
|
+
request.payloadName = 'checklist-comment.json';
|
|
423
|
+
request.label = 'checklist review comment';
|
|
424
|
+
this.commentUpserter.upsert(request);
|
|
513
425
|
}
|
|
514
426
|
// The PR, the remote branch, and the local branch all share the one stable feature name. Look up /
|
|
515
427
|
// create / merge against `baseBranch` (baseBranchName tolerates a leftover `…wpN` mid-transition).
|
|
516
428
|
// GatedPrPublisher owns the edit/push/create half and its ORDERING — the gated body goes up before
|
|
517
429
|
// the push, so CI's `synchronize` read cannot see the previous run's token.
|
|
518
|
-
upsertPr(
|
|
430
|
+
upsertPr(request) {
|
|
431
|
+
const repoRoot = request.repoRoot;
|
|
432
|
+
const baseBranch = request.baseBranch;
|
|
433
|
+
const title = request.title;
|
|
434
|
+
const input = request.input;
|
|
519
435
|
const prDir = (0, rules_config_1.prDirFor)(repoRoot, this.aiBranchName.getFeatureName());
|
|
520
436
|
fs.mkdirSync(prDir, { recursive: true });
|
|
521
437
|
const bodyFile = path.join(prDir, 'pr-body.md');
|
|
522
|
-
fs.writeFileSync(bodyFile, body + '\n');
|
|
438
|
+
fs.writeFileSync(bodyFile, request.body + '\n');
|
|
523
439
|
const published = this.publisher.publish(baseBranch, title, bodyFile);
|
|
524
440
|
if (published.createFailed) {
|
|
525
441
|
process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\n ' + bodyFile + '\n');
|
|
@@ -532,6 +448,24 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
532
448
|
// subject GitHub would inherit from the single squash commit on the branch.
|
|
533
449
|
const ref = this.prRef(baseBranch);
|
|
534
450
|
const subject = ref.number !== '' ? `${title} (#${ref.number})` : title;
|
|
451
|
+
/*
|
|
452
|
+
* THE INVARIANT: the merge body IS the PR description, byte for byte.
|
|
453
|
+
*
|
|
454
|
+
* That is what makes every landing route agree — the GitHub Merge button and a bare `gh pr merge`
|
|
455
|
+
* copy the description via `squash_merge_commit_message: PR_BODY`, while `wp-land-pr` and finish's
|
|
456
|
+
* own auto-merge pass these bytes with `--body-file`. If the two strings could differ, which route
|
|
457
|
+
* landed the commit would change what history says, which is precisely the bug this whole change
|
|
458
|
+
* exists to remove. `pr-body-is-merge-body.spec.ts` pins it.
|
|
459
|
+
*
|
|
460
|
+
* The one wrinkle is the self-link. A brand-new PR has no URL until `gh pr create` returns, so the
|
|
461
|
+
* body published a moment ago was rendered with `prUrl: ''`. Re-render now that the URL is known
|
|
462
|
+
* and push the corrected description back, so the two strings match and the commit carries its own
|
|
463
|
+
* link. Only the CREATE path pays that extra edit: on every later run the URL was already known
|
|
464
|
+
* before publishing, `finalBody` matches what went up, and nothing is re-sent.
|
|
465
|
+
*/
|
|
466
|
+
const finalBody = this.dashboard.renderPrBody(input, ref.url) + request.tokenSuffix;
|
|
467
|
+
if (finalBody !== request.body)
|
|
468
|
+
this.backfillPrBody(ref.number, bodyFile, finalBody);
|
|
535
469
|
// MACHINE-GLOBAL, keyed by the PR's identity, so `wp-land-pr` finds it from ANY tree — see
|
|
536
470
|
// MergeBodyFiler for the incident that moved it out of this worktree's pr-review/ dir.
|
|
537
471
|
const bodyRequest = new merge_body_filer_1.MergeBodyRequest();
|
|
@@ -540,7 +474,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
540
474
|
bodyRequest.feature = this.aiBranchName.getFeatureName();
|
|
541
475
|
bodyRequest.prNumber = ref.number;
|
|
542
476
|
bodyRequest.prUrl = ref.url;
|
|
543
|
-
bodyRequest.body =
|
|
477
|
+
bodyRequest.body = finalBody + '\n';
|
|
544
478
|
const mergeBodyFile = this.mergeBodyFiler.file(bodyRequest);
|
|
545
479
|
// PrMerger owns the direct-merge / auto-merge-fallback decision AND checks every gh status, so a
|
|
546
480
|
// merge that did not happen is reported as such instead of being swallowed (see pr-merger.ts).
|
|
@@ -551,6 +485,93 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
551
485
|
const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, new pr_merger_1.MergeIntent(mergeMode, false));
|
|
552
486
|
return new UpsertResult(ref.number !== '' ? ref.number : num, ref.url, outcome);
|
|
553
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* Everything the gated flow PUBLISHES, in the one order that is safe — the three surfaces plus the
|
|
490
|
+
* merge, as a single unit.
|
|
491
|
+
*
|
|
492
|
+
* 1. the PR DESCRIPTION = the compact git-log body (+ the hidden gate token), written before the
|
|
493
|
+
* push so CI's `synchronize` read can never see the previous run's token,
|
|
494
|
+
* 2. the 1st comment = the full dashboard,
|
|
495
|
+
* 3. the 2nd comment = each reviewer's output.
|
|
496
|
+
*
|
|
497
|
+
* The token is bound to the LOCAL HEAD sha and minted here because we only reach this line after the
|
|
498
|
+
* build gate and every BLOCK checklist passed, so minting is legitimate. Nothing about
|
|
499
|
+
* HMAC(salt, HEAD) needs the remote to have the commit yet.
|
|
500
|
+
*
|
|
501
|
+
* Extracted from `run` when the two comments made it 82 lines against a hard 80-line rule — and it
|
|
502
|
+
* earns its own name: these four steps have a REQUIRED order, and the comment explaining that order
|
|
503
|
+
* belongs with them rather than inside a method that also loads config and runs a build gate.
|
|
504
|
+
*/
|
|
505
|
+
publishAll(repoRoot, base, input, sources) {
|
|
506
|
+
const gateSalt = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.gateSalt;
|
|
507
|
+
const headSha = this.gitOut(['rev-parse', 'HEAD']);
|
|
508
|
+
const upsert = new PrUpsertRequest(input);
|
|
509
|
+
upsert.repoRoot = repoRoot;
|
|
510
|
+
upsert.baseBranch = base;
|
|
511
|
+
upsert.title = input.title;
|
|
512
|
+
upsert.tokenSuffix = this.gateTokenBody(gateSalt, headSha);
|
|
513
|
+
// `existingPrUrl` is '' only for a brand-new PR — there is no URL to self-link to until
|
|
514
|
+
// `gh pr create` returns one, and upsertPr back-fills it the moment it does.
|
|
515
|
+
upsert.body = this.dashboard.renderPrBody(input, this.existingPrUrl(base)) + upsert.tokenSuffix;
|
|
516
|
+
const result = this.upsertPr(upsert);
|
|
517
|
+
// In the order the PR body's pointer promises: full dashboard 1st, reviewer output 2nd. Both are
|
|
518
|
+
// idempotent by hidden marker and both non-fatal — the PR is up by now, so a `gh` hiccup on a
|
|
519
|
+
// comment must not turn a finished run into a failed one.
|
|
520
|
+
this.postDetailComment(repoRoot, result.prNumber, input);
|
|
521
|
+
this.postChecklistComment(repoRoot, result.prNumber, sources.scan, sources.review, sources.provenance);
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Re-publish the description now that the PR's own URL is known (create path only).
|
|
526
|
+
*
|
|
527
|
+
* Non-fatal on purpose. The PR exists, its body already carries a valid gate token for the pushed
|
|
528
|
+
* head, and the code is up — the ONLY thing missing is the self-link on the first line. Aborting a
|
|
529
|
+
* finished run over a cosmetic back-link would be a worse outcome than the missing link, and the very
|
|
530
|
+
* next `wp-finish-upsert-pr` writes it (by then the URL is known before publishing, so it goes up with
|
|
531
|
+
* the body). The merge body still gets the linked version regardless: it is filed from `finalBody`.
|
|
532
|
+
*/
|
|
533
|
+
backfillPrBody(prNumber, bodyFile, finalBody) {
|
|
534
|
+
if (prNumber === '')
|
|
535
|
+
return;
|
|
536
|
+
fs.writeFileSync(bodyFile, finalBody + '\n');
|
|
537
|
+
const res = (0, child_process_1.spawnSync)('gh', ['pr', 'edit', prNumber, '--body-file', bodyFile], { encoding: 'utf8' });
|
|
538
|
+
if (res.status !== 0) {
|
|
539
|
+
process.stderr.write('⚠️ Could not add the PR\'s own link to its description (non-fatal — the PR and the gate\n' +
|
|
540
|
+
' token are already up). The next wp-finish-upsert-pr writes it.\n');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
process.stdout.write(' back-filled the PR description with its own link ✓\n');
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* The PR's web URL if one is already open for this branch, else '' — read BEFORE publishing so the
|
|
547
|
+
* description can carry its own link on the first render. Only a brand-new PR gets '' (and then
|
|
548
|
+
* {@link backfillPrBody} fixes it up after `gh pr create` returns a URL).
|
|
549
|
+
*/
|
|
550
|
+
existingPrUrl(baseBranch) {
|
|
551
|
+
return this.prRef(baseBranch).url;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The 1st PR comment: the FULL dashboard — every row including the green ones, the whole summary, the
|
|
555
|
+
* 3-point hash points. This is what the PR description used to be, and moving it here is the entire
|
|
556
|
+
* point of the change: the description is now the git-log body, and a squash commit must not carry a
|
|
557
|
+
* risk table.
|
|
558
|
+
*
|
|
559
|
+
* Posted unconditionally (any repo, checklists or not), because the description's last bullet PROMISES
|
|
560
|
+
* a 1st comment holding the detail. A pointer to a comment that does not exist is worse than no
|
|
561
|
+
* pointer. Non-fatal — the PR is already up, and the description alone is a complete, readable record.
|
|
562
|
+
*/
|
|
563
|
+
postDetailComment(repoRoot, prNumber, input) {
|
|
564
|
+
if (prNumber === '')
|
|
565
|
+
return;
|
|
566
|
+
const request = new pr_comment_upserter_1.PrCommentRequest();
|
|
567
|
+
request.prNumber = prNumber;
|
|
568
|
+
request.marker = dashboard_1.DETAIL_COMMENT_MARKER;
|
|
569
|
+
request.body = dashboard_1.DETAIL_COMMENT_MARKER + '\n' + this.dashboard.renderDetailComment(input);
|
|
570
|
+
request.payloadDir = (0, rules_config_1.prDirFor)(repoRoot, this.aiBranchName.getFeatureName());
|
|
571
|
+
request.payloadName = 'detail-comment.json';
|
|
572
|
+
request.label = 'full dashboard comment';
|
|
573
|
+
this.commentUpserter.upsert(request);
|
|
574
|
+
}
|
|
554
575
|
// The PR's number + web URL (for the merge subject `(#N)` and the commit-body back-link). Both ''
|
|
555
576
|
// if it can't be resolved. Rendered via jq into one tab-separated line so no JSON parsing is needed.
|
|
556
577
|
prRef(baseBranch) {
|
|
@@ -574,15 +595,15 @@ exports.FinishUpsertPrCommand = FinishUpsertPrCommand = tslib_1.__decorate([
|
|
|
574
595
|
pr_merger_1.PrMerger,
|
|
575
596
|
gated_pr_publisher_1.GatedPrPublisher,
|
|
576
597
|
dashboard_1.Dashboard,
|
|
598
|
+
checklist_comment_renderer_1.ChecklistCommentRenderer,
|
|
577
599
|
checklist_scanner_1.ChecklistScanner,
|
|
578
600
|
reviewer_verdict_gate_1.ReviewerVerdictGate,
|
|
579
601
|
rules_config_1.ReviewJsonService,
|
|
580
602
|
rules_config_1.GateTokenService,
|
|
581
|
-
|
|
582
|
-
rules_config_1.ReviewProvenanceService,
|
|
583
|
-
rules_config_1.ReviewerInstructionsService,
|
|
603
|
+
provenance_enforcer_1.ProvenanceEnforcer,
|
|
584
604
|
review_stage_receipt_1.ReviewStageReceiptService,
|
|
585
605
|
finish_banner_1.FinishBanner,
|
|
586
|
-
merge_body_filer_1.MergeBodyFiler
|
|
606
|
+
merge_body_filer_1.MergeBodyFiler,
|
|
607
|
+
pr_comment_upserter_1.PrCommentUpserter])
|
|
587
608
|
], FinishUpsertPrCommand);
|
|
588
609
|
//# sourceMappingURL=finish-upsert-pr-command.js.map
|