@webpieces/pr-gate 0.4.477 → 0.4.478
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/scripts/commands/check-pr-command.d.ts +6 -1
- package/src/scripts/commands/check-pr-command.js +12 -21
- package/src/scripts/commands/check-pr-command.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +3 -2
- package/src/scripts/commands/finish-upsert-pr-command.js +20 -47
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/gated-pr-publisher.d.ts +46 -0
- package/src/scripts/workflow/gated-pr-publisher.js +111 -0
- package/src/scripts/workflow/gated-pr-publisher.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.478",
|
|
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",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"directory": "packages/tooling/pr-gate"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@webpieces/rules-config": "0.4.
|
|
27
|
+
"@webpieces/rules-config": "0.4.478",
|
|
28
28
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
29
29
|
"inversify": "7.10.4",
|
|
30
30
|
"reflect-metadata": "0.2.2"
|
|
@@ -9,6 +9,12 @@ import { RepoRootFinder, GateTokenService } from '@webpieces/rules-config';
|
|
|
9
9
|
* This is what catches a PR opened OUTSIDE the gated flow — an unhooked teammate who `git push`ed and
|
|
10
10
|
* clicked "Create pull request" in the web UI carries no valid token for its head sha and fails here.
|
|
11
11
|
*
|
|
12
|
+
* This job is THE required check — the ONE thing a consumer marks required in branch protection. It used
|
|
13
|
+
* to also post a `webpieces/pr-gate` commit status, because `wp-finish-upsert-pr` pushed BEFORE writing
|
|
14
|
+
* the PR body and a status (newest post wins for a context) could supersede the failure that race caused.
|
|
15
|
+
* GatedPrPublisher now writes the body first, so there is no race to recover from and no second entry to
|
|
16
|
+
* confuse anyone about which one to require. This job's exit code is the whole verdict.
|
|
17
|
+
*
|
|
12
18
|
* A repo with no `gateSalt` configured has not opted in → this is a no-op success (exit 0), so it is safe
|
|
13
19
|
* to add the workflow before turning enforcement on.
|
|
14
20
|
*
|
|
@@ -21,7 +27,6 @@ export declare class CheckPrCommand {
|
|
|
21
27
|
run(): Promise<void>;
|
|
22
28
|
private verifyWithRetry;
|
|
23
29
|
private delay;
|
|
24
|
-
private postStatus;
|
|
25
30
|
private failureMessage;
|
|
26
31
|
private resolvePr;
|
|
27
32
|
private prNumber;
|
|
@@ -26,6 +26,12 @@ class PrUnderCheck {
|
|
|
26
26
|
* This is what catches a PR opened OUTSIDE the gated flow — an unhooked teammate who `git push`ed and
|
|
27
27
|
* clicked "Create pull request" in the web UI carries no valid token for its head sha and fails here.
|
|
28
28
|
*
|
|
29
|
+
* This job is THE required check — the ONE thing a consumer marks required in branch protection. It used
|
|
30
|
+
* to also post a `webpieces/pr-gate` commit status, because `wp-finish-upsert-pr` pushed BEFORE writing
|
|
31
|
+
* the PR body and a status (newest post wins for a context) could supersede the failure that race caused.
|
|
32
|
+
* GatedPrPublisher now writes the body first, so there is no race to recover from and no second entry to
|
|
33
|
+
* confuse anyone about which one to require. This job's exit code is the whole verdict.
|
|
34
|
+
*
|
|
29
35
|
* A repo with no `gateSalt` configured has not opted in → this is a no-op success (exit 0), so it is safe
|
|
30
36
|
* to add the workflow before turning enforcement on.
|
|
31
37
|
*
|
|
@@ -52,19 +58,17 @@ let CheckPrCommand = class CheckPrCommand {
|
|
|
52
58
|
}
|
|
53
59
|
pr = await this.verifyWithRetry(pr, gateSalt);
|
|
54
60
|
if (this.gateTokenService.verifyGateToken(pr.body, gateSalt, pr.headSha)) {
|
|
55
|
-
this.postStatus(pr.headSha, 'success', 'gated flow verified');
|
|
56
61
|
process.stdout.write(`✅ wp-check-pr: valid webpieces gate token for PR #${pr.number} @ ${pr.headSha.slice(0, 12)} — created through the gated flow.\n`);
|
|
57
62
|
return;
|
|
58
63
|
}
|
|
59
|
-
// An UNHOOKED push never reaches wp-finish-upsert-pr, so no
|
|
60
|
-
//
|
|
61
|
-
this.postStatus(pr.headSha, 'failure', 'Not created through the webpieces gated flow — run pnpm wp-start-upsert-pr');
|
|
64
|
+
// An UNHOOKED push never reaches wp-finish-upsert-pr, so it carries no valid token — fail the job.
|
|
65
|
+
// THIS EXIT CODE IS THE WHOLE SIGNAL: there is no companion commit status any more (see the JSDoc).
|
|
62
66
|
throw new rules_config_1.CliExitError(1, this.failureMessage(pr));
|
|
63
67
|
}
|
|
64
|
-
// Re-read the PR ONCE after a short delay if the token looks stale.
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
+
// Re-read the PR ONCE after a short delay if the token looks stale. The push-before-body-edit RACE is
|
|
69
|
+
// gone (GatedPrPublisher writes the body first), so this no longer backstops a known ordering bug —
|
|
70
|
+
// it absorbs GitHub's own event/read replication jitter, which matters more now that this job is the
|
|
71
|
+
// single required check. It costs nothing on the success path. Returns the freshest PR.
|
|
68
72
|
async verifyWithRetry(pr, gateSalt) {
|
|
69
73
|
if (this.gateTokenService.verifyGateToken(pr.body, gateSalt, pr.headSha))
|
|
70
74
|
return pr;
|
|
@@ -78,19 +82,6 @@ let CheckPrCommand = class CheckPrCommand {
|
|
|
78
82
|
setTimeout(resolve, ms);
|
|
79
83
|
});
|
|
80
84
|
}
|
|
81
|
-
// Post the webpieces/pr-gate commit status on `sha`. Best-effort: a missing statuses:write scope is a
|
|
82
|
-
// warning, not a hard failure (the job's own exit code still reflects pass/fail).
|
|
83
|
-
postStatus(sha, state, description) {
|
|
84
|
-
const res = (0, child_process_1.spawnSync)('gh', [
|
|
85
|
-
'api', '--method', 'POST', `repos/{owner}/{repo}/statuses/${sha}`,
|
|
86
|
-
'-f', `state=${state}`,
|
|
87
|
-
'-f', 'context=webpieces/pr-gate',
|
|
88
|
-
'-f', `description=${description}`,
|
|
89
|
-
], { encoding: 'utf8' });
|
|
90
|
-
if (res.status !== 0) {
|
|
91
|
-
process.stderr.write('⚠️ wp-check-pr could not post the webpieces/pr-gate commit status (needs statuses:write).\n');
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
85
|
// The actionable red-check message: this PR did not come through the gated flow (or hooks are missing).
|
|
95
86
|
failureMessage(pr) {
|
|
96
87
|
return (`❌ wp-check-pr: PR #${pr.number} (head ${pr.headSha.slice(0, 12)}) has no valid webpieces gate token.\n\n` +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/check-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport {\n loadAndValidate, RepoRootFinder, GateTokenService, CliExitError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// The head sha + body of the PR under check, resolved from `gh`. Data-only.\nclass PrUnderCheck {\n number: string;\n headSha: string;\n body: string;\n\n constructor(number: string, headSha: string, body: string) {\n this.number = number;\n this.headSha = headSha;\n this.body = body;\n }\n}\n\n/**\n * `wp-check-pr` — the SERVER-SIDE half of the gate, meant to run as a required CI check. It is READ-ONLY:\n * it never touches git state, never pushes, never calls `gh pr create`. It recomputes\n * `HMAC(prGate.gateSalt, PR_head_sha)` from the committed salt and verifies the PR body carries that\n * token. Because `wp-finish-upsert-pr` refuses to mint the token unless the build gate + every BLOCK\n * checklist passed, a valid token IS proof the gated flow ran and passed on this exact commit.\n *\n * This is what catches a PR opened OUTSIDE the gated flow — an unhooked teammate who `git push`ed and\n * clicked \"Create pull request\" in the web UI carries no valid token for its head sha and fails here.\n *\n * A repo with no `gateSalt` configured has not opted in → this is a no-op success (exit 0), so it is safe\n * to add the workflow before turning enforcement on.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and resolved by PrGateApp.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CheckPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly gateTokenService: GateTokenService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n if (gateSalt.trim() === '') {\n throw new CliExitError(0,\n 'ℹ️ wp-check-pr: no prGate.gateSalt configured — server-side gate token enforcement is disabled. ' +\n 'Add a committed \"gateSalt\" under the pr-gate section of webpieces.config.json to enable it.');\n }\n\n let pr = this.resolvePr();\n if (pr.headSha === '') {\n throw new CliExitError(1,\n '❌ wp-check-pr: could not resolve the PR head sha via `gh`. Ensure the workflow runs on a pull_request ' +\n 'event with `gh` authenticated (GH_TOKEN) and the PR number available (WP_PR_NUMBER or GITHUB_REF).');\n }\n\n pr = await this.verifyWithRetry(pr, gateSalt);\n if (this.gateTokenService.verifyGateToken(pr.body, gateSalt, pr.headSha)) {\n this.postStatus(pr.headSha, 'success', 'gated flow verified');\n process.stdout.write(`✅ wp-check-pr: valid webpieces gate token for PR #${pr.number} @ ${pr.headSha.slice(0, 12)} — created through the gated flow.\\n`);\n return;\n }\n\n // An UNHOOKED push never reaches wp-finish-upsert-pr, so no status ever appears — mark it failed\n // with an actionable status (attached to this sha) and fail the job so the PR shows red with a reason.\n this.postStatus(pr.headSha, 'failure', 'Not created through the webpieces gated flow — run pnpm wp-start-upsert-pr');\n throw new CliExitError(1, this.failureMessage(pr));\n }\n\n // Re-read the PR ONCE after a short delay if the token looks stale. `wp-finish-upsert-pr` posts the\n // authoritative commit status directly, but this workflow can be triggered by the `synchronize` push a\n // beat BEFORE the body edit lands — so a first stale read is re-checked rather than red-flagging a\n // correctly-gated PR on a timing coin-flip (see the gate-token-race bug). Returns the freshest PR.\n private async verifyWithRetry(pr: PrUnderCheck, gateSalt: string): Promise<PrUnderCheck> {\n if (this.gateTokenService.verifyGateToken(pr.body, gateSalt, pr.headSha)) return pr;\n process.stdout.write('… no valid token yet — waiting for the PR body edit to land, then re-checking once…\\n');\n await this.delay(15000);\n const refreshed = this.resolvePr();\n return refreshed.headSha !== '' ? refreshed : pr;\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve: () => void): void => {\n setTimeout(resolve, ms);\n });\n }\n\n // Post the webpieces/pr-gate commit status on `sha`. Best-effort: a missing statuses:write scope is a\n // warning, not a hard failure (the job's own exit code still reflects pass/fail).\n private postStatus(sha: string, state: string, description: string): void {\n const res = spawnSync('gh', [\n 'api', '--method', 'POST', `repos/{owner}/{repo}/statuses/${sha}`,\n '-f', `state=${state}`,\n '-f', 'context=webpieces/pr-gate',\n '-f', `description=${description}`,\n ], { encoding: 'utf8' });\n if (res.status !== 0) {\n process.stderr.write('⚠️ wp-check-pr could not post the webpieces/pr-gate commit status (needs statuses:write).\\n');\n }\n }\n\n // The actionable red-check message: this PR did not come through the gated flow (or hooks are missing).\n private failureMessage(pr: PrUnderCheck): string {\n return (\n `❌ wp-check-pr: PR #${pr.number} (head ${pr.headSha.slice(0, 12)}) has no valid webpieces gate token.\\n\\n` +\n `This PR was NOT created through the webpieces gated flow — or was pushed after finishing without re-running it.\\n` +\n `Every commit that lands here must go through it, so:\\n\\n` +\n ` 1. Install the webpieces hooks if you don't have them.\\n` +\n ` 2. Recreate/update this PR by running: pnpm wp-start-upsert-pr → write review.json → pnpm wp-finish-upsert-pr\\n\\n` +\n `That re-stamps the PR title, body, and the gate token for the current head commit, and this check goes green.`\n );\n }\n\n // Resolve the PR (number, head sha, body) from `gh`. Prefers an explicit WP_PR_NUMBER, then the\n // pull_request number in GITHUB_REF (refs/pull/<n>/merge), then `gh`'s current-branch detection.\n private resolvePr(): PrUnderCheck {\n const num = this.prNumber();\n const args = num !== ''\n ? ['pr', 'view', num, '--json', 'number,headRefOid,body', '--jq', '\"\\\\(.number)\\\\t\\\\(.headRefOid)\\\\t\\\\(.body)\"']\n : ['pr', 'view', '--json', 'number,headRefOid,body', '--jq', '\"\\\\(.number)\\\\t\\\\(.headRefOid)\\\\t\\\\(.body)\"'];\n const result = spawnSync('gh', args, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 16 });\n if (result.status !== 0) return new PrUnderCheck(num, '', '');\n // jq joins body (which may contain tabs/newlines) last, so split on the FIRST two tabs only.\n const out = (result.stdout ?? '').trim();\n const firstTab = out.indexOf('\\t');\n const secondTab = firstTab >= 0 ? out.indexOf('\\t', firstTab + 1) : -1;\n if (firstTab < 0 || secondTab < 0) return new PrUnderCheck(num, '', '');\n const number = out.slice(0, firstTab);\n const headSha = out.slice(firstTab + 1, secondTab);\n const body = out.slice(secondTab + 1);\n return new PrUnderCheck(number, headSha, body);\n }\n\n private prNumber(): string {\n const explicit = (process.env['WP_PR_NUMBER'] ?? '').trim();\n if (explicit !== '') return explicit;\n // GitHub Actions pull_request: GITHUB_REF = refs/pull/<n>/merge\n const ref = process.env['GITHUB_REF'] ?? '';\n const m = /refs\\/pull\\/(\\d+)\\//.exec(ref);\n return m ? m[1] : '';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"check-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/check-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport {\n loadAndValidate, RepoRootFinder, GateTokenService, CliExitError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// The head sha + body of the PR under check, resolved from `gh`. Data-only.\nclass PrUnderCheck {\n number: string;\n headSha: string;\n body: string;\n\n constructor(number: string, headSha: string, body: string) {\n this.number = number;\n this.headSha = headSha;\n this.body = body;\n }\n}\n\n/**\n * `wp-check-pr` — the SERVER-SIDE half of the gate, meant to run as a required CI check. It is READ-ONLY:\n * it never touches git state, never pushes, never calls `gh pr create`. It recomputes\n * `HMAC(prGate.gateSalt, PR_head_sha)` from the committed salt and verifies the PR body carries that\n * token. Because `wp-finish-upsert-pr` refuses to mint the token unless the build gate + every BLOCK\n * checklist passed, a valid token IS proof the gated flow ran and passed on this exact commit.\n *\n * This is what catches a PR opened OUTSIDE the gated flow — an unhooked teammate who `git push`ed and\n * clicked \"Create pull request\" in the web UI carries no valid token for its head sha and fails here.\n *\n * This job is THE required check — the ONE thing a consumer marks required in branch protection. It used\n * to also post a `webpieces/pr-gate` commit status, because `wp-finish-upsert-pr` pushed BEFORE writing\n * the PR body and a status (newest post wins for a context) could supersede the failure that race caused.\n * GatedPrPublisher now writes the body first, so there is no race to recover from and no second entry to\n * confuse anyone about which one to require. This job's exit code is the whole verdict.\n *\n * A repo with no `gateSalt` configured has not opted in → this is a no-op success (exit 0), so it is safe\n * to add the workflow before turning enforcement on.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and resolved by PrGateApp.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CheckPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly gateTokenService: GateTokenService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n if (gateSalt.trim() === '') {\n throw new CliExitError(0,\n 'ℹ️ wp-check-pr: no prGate.gateSalt configured — server-side gate token enforcement is disabled. ' +\n 'Add a committed \"gateSalt\" under the pr-gate section of webpieces.config.json to enable it.');\n }\n\n let pr = this.resolvePr();\n if (pr.headSha === '') {\n throw new CliExitError(1,\n '❌ wp-check-pr: could not resolve the PR head sha via `gh`. Ensure the workflow runs on a pull_request ' +\n 'event with `gh` authenticated (GH_TOKEN) and the PR number available (WP_PR_NUMBER or GITHUB_REF).');\n }\n\n pr = await this.verifyWithRetry(pr, gateSalt);\n if (this.gateTokenService.verifyGateToken(pr.body, gateSalt, pr.headSha)) {\n process.stdout.write(`✅ wp-check-pr: valid webpieces gate token for PR #${pr.number} @ ${pr.headSha.slice(0, 12)} — created through the gated flow.\\n`);\n return;\n }\n\n // An UNHOOKED push never reaches wp-finish-upsert-pr, so it carries no valid token — fail the job.\n // THIS EXIT CODE IS THE WHOLE SIGNAL: there is no companion commit status any more (see the JSDoc).\n throw new CliExitError(1, this.failureMessage(pr));\n }\n\n // Re-read the PR ONCE after a short delay if the token looks stale. The push-before-body-edit RACE is\n // gone (GatedPrPublisher writes the body first), so this no longer backstops a known ordering bug —\n // it absorbs GitHub's own event/read replication jitter, which matters more now that this job is the\n // single required check. It costs nothing on the success path. Returns the freshest PR.\n private async verifyWithRetry(pr: PrUnderCheck, gateSalt: string): Promise<PrUnderCheck> {\n if (this.gateTokenService.verifyGateToken(pr.body, gateSalt, pr.headSha)) return pr;\n process.stdout.write('… no valid token yet — waiting for the PR body edit to land, then re-checking once…\\n');\n await this.delay(15000);\n const refreshed = this.resolvePr();\n return refreshed.headSha !== '' ? refreshed : pr;\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve: () => void): void => {\n setTimeout(resolve, ms);\n });\n }\n\n // The actionable red-check message: this PR did not come through the gated flow (or hooks are missing).\n private failureMessage(pr: PrUnderCheck): string {\n return (\n `❌ wp-check-pr: PR #${pr.number} (head ${pr.headSha.slice(0, 12)}) has no valid webpieces gate token.\\n\\n` +\n `This PR was NOT created through the webpieces gated flow — or was pushed after finishing without re-running it.\\n` +\n `Every commit that lands here must go through it, so:\\n\\n` +\n ` 1. Install the webpieces hooks if you don't have them.\\n` +\n ` 2. Recreate/update this PR by running: pnpm wp-start-upsert-pr → write review.json → pnpm wp-finish-upsert-pr\\n\\n` +\n `That re-stamps the PR title, body, and the gate token for the current head commit, and this check goes green.`\n );\n }\n\n // Resolve the PR (number, head sha, body) from `gh`. Prefers an explicit WP_PR_NUMBER, then the\n // pull_request number in GITHUB_REF (refs/pull/<n>/merge), then `gh`'s current-branch detection.\n private resolvePr(): PrUnderCheck {\n const num = this.prNumber();\n const args = num !== ''\n ? ['pr', 'view', num, '--json', 'number,headRefOid,body', '--jq', '\"\\\\(.number)\\\\t\\\\(.headRefOid)\\\\t\\\\(.body)\"']\n : ['pr', 'view', '--json', 'number,headRefOid,body', '--jq', '\"\\\\(.number)\\\\t\\\\(.headRefOid)\\\\t\\\\(.body)\"'];\n const result = spawnSync('gh', args, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 16 });\n if (result.status !== 0) return new PrUnderCheck(num, '', '');\n // jq joins body (which may contain tabs/newlines) last, so split on the FIRST two tabs only.\n const out = (result.stdout ?? '').trim();\n const firstTab = out.indexOf('\\t');\n const secondTab = firstTab >= 0 ? out.indexOf('\\t', firstTab + 1) : -1;\n if (firstTab < 0 || secondTab < 0) return new PrUnderCheck(num, '', '');\n const number = out.slice(0, firstTab);\n const headSha = out.slice(firstTab + 1, secondTab);\n const body = out.slice(secondTab + 1);\n return new PrUnderCheck(number, headSha, body);\n }\n\n private prNumber(): string {\n const explicit = (process.env['WP_PR_NUMBER'] ?? '').trim();\n if (explicit !== '') return explicit;\n // GitHub Actions pull_request: GITHUB_REF = refs/pull/<n>/merge\n const ref = process.env['GITHUB_REF'] ?? '';\n const m = /refs\\/pull\\/(\\d+)\\//.exec(ref);\n return m ? m[1] : '';\n }\n}\n"]}
|
|
@@ -7,6 +7,7 @@ import { BuildAffected } from '../workflow/build-affected';
|
|
|
7
7
|
import { MergeState } from '../workflow/merge-state';
|
|
8
8
|
import { MergeEnd } from '../workflow/merge-end';
|
|
9
9
|
import { PrMerger } from '../workflow/pr-merger';
|
|
10
|
+
import { GatedPrPublisher } from '../workflow/gated-pr-publisher';
|
|
10
11
|
import { Dashboard } from '../../dashboard/dashboard';
|
|
11
12
|
export declare class FinishUpsertPrCommand {
|
|
12
13
|
private readonly repoRootFinder;
|
|
@@ -17,20 +18,20 @@ export declare class FinishUpsertPrCommand {
|
|
|
17
18
|
private readonly mergeState;
|
|
18
19
|
private readonly mergeEnd;
|
|
19
20
|
private readonly prMerger;
|
|
21
|
+
private readonly publisher;
|
|
20
22
|
private readonly dashboard;
|
|
21
23
|
private readonly checklistDetector;
|
|
22
24
|
private readonly manifestService;
|
|
23
25
|
private readonly reviewJsonService;
|
|
24
26
|
private readonly gateTokenService;
|
|
25
27
|
private readonly provenance;
|
|
26
|
-
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, mergeEnd: MergeEnd, prMerger: PrMerger, dashboard: Dashboard, checklistDetector: ChecklistDetector, manifestService: ChecklistManifestService, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenance: SubagentProvenanceService);
|
|
28
|
+
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, mergeEnd: MergeEnd, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard, checklistDetector: ChecklistDetector, manifestService: ChecklistManifestService, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenance: SubagentProvenanceService);
|
|
27
29
|
run(): Promise<void>;
|
|
28
30
|
private gitOut;
|
|
29
31
|
private prTitleFrom;
|
|
30
32
|
private computeDashboardInput;
|
|
31
33
|
private checklistRows;
|
|
32
34
|
private gateTokenBody;
|
|
33
|
-
private postGateStatus;
|
|
34
35
|
private enforceProvenance;
|
|
35
36
|
private postChecklistComment;
|
|
36
37
|
private findChecklistCommentId;
|
|
@@ -16,6 +16,7 @@ const merge_state_1 = require("../workflow/merge-state");
|
|
|
16
16
|
const merge_end_1 = require("../workflow/merge-end");
|
|
17
17
|
const merge_start_1 = require("../workflow/merge-start");
|
|
18
18
|
const pr_merger_1 = require("../workflow/pr-merger");
|
|
19
|
+
const gated_pr_publisher_1 = require("../workflow/gated-pr-publisher");
|
|
19
20
|
const dashboard_1 = require("../../dashboard/dashboard");
|
|
20
21
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
21
22
|
// A resolved PR's number + web URL. Both '' when the PR can't be resolved (e.g. create failed).
|
|
@@ -50,13 +51,14 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
50
51
|
mergeState;
|
|
51
52
|
mergeEnd;
|
|
52
53
|
prMerger;
|
|
54
|
+
publisher;
|
|
53
55
|
dashboard;
|
|
54
56
|
checklistDetector;
|
|
55
57
|
manifestService;
|
|
56
58
|
reviewJsonService;
|
|
57
59
|
gateTokenService;
|
|
58
60
|
provenance;
|
|
59
|
-
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, mergeEnd, prMerger, dashboard, checklistDetector, manifestService, reviewJsonService, gateTokenService, provenance) {
|
|
61
|
+
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, mergeEnd, prMerger, publisher, dashboard, checklistDetector, manifestService, reviewJsonService, gateTokenService, provenance) {
|
|
60
62
|
this.repoRootFinder = repoRootFinder;
|
|
61
63
|
this.aiBranchName = aiBranchName;
|
|
62
64
|
this.branchNaming = branchNaming;
|
|
@@ -65,6 +67,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
65
67
|
this.mergeState = mergeState;
|
|
66
68
|
this.mergeEnd = mergeEnd;
|
|
67
69
|
this.prMerger = prMerger;
|
|
70
|
+
this.publisher = publisher;
|
|
68
71
|
this.dashboard = dashboard;
|
|
69
72
|
this.checklistDetector = checklistDetector;
|
|
70
73
|
this.manifestService = manifestService;
|
|
@@ -100,30 +103,29 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
100
103
|
const provenanceVerified = this.enforceProvenance(required, currentBranch);
|
|
101
104
|
// 2b. The build gate validates the WORKING TREE but we push HEAD — so they MUST be identical.
|
|
102
105
|
this.gitExec.assertCleanTree(repoRoot);
|
|
103
|
-
// 3. Authoritative build gate, then
|
|
106
|
+
// 3. Authoritative build gate, then post the gated body, then push (that ORDER — see GatedPrPublisher).
|
|
104
107
|
this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.'));
|
|
105
108
|
const base = this.branchNaming.baseBranchName((0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim());
|
|
106
|
-
this.gitExec.ensurePushed(base);
|
|
107
109
|
process.stdout.write('\n' + SEP + '📋 Dashboard + PR\n' + SEP + '\n');
|
|
108
110
|
const title = this.prTitleFrom(review);
|
|
109
111
|
const input = this.computeDashboardInput(repoRoot, true, review, title, required);
|
|
110
|
-
// Append the hidden HMAC gate token bound to the
|
|
111
|
-
//
|
|
112
|
-
//
|
|
112
|
+
// Append the hidden HMAC gate token bound to the LOCAL HEAD sha — computed BEFORE the push, because
|
|
113
|
+
// GatedPrPublisher writes the body first so CI's `synchronize` read can never see a stale token. A
|
|
114
|
+
// valid token in the PR body is proof this gated flow ran + passed on this exact commit, which CI
|
|
115
|
+
// (`wp-check-pr`) recomputes. We reach here only after the build gate + every BLOCK checklist
|
|
116
|
+
// passed, so minting is legitimate. Nothing about HMAC(salt, HEAD) needs the remote to have it.
|
|
113
117
|
const gateSalt = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.gateSalt;
|
|
114
118
|
const headSha = this.gitOut(['rev-parse', 'HEAD']);
|
|
115
119
|
const body = this.dashboard.renderDashboard(input) + this.gateTokenBody(gateSalt, headSha);
|
|
116
120
|
const result = this.upsertPr(repoRoot, base, body, title, input);
|
|
117
|
-
// Race-free required check: post the commit status on the head sha AFTER the body edit (see method).
|
|
118
|
-
this.postGateStatus(headSha, gateSalt);
|
|
119
121
|
// Publish each reviewer's full output as ONE combined PR comment (idempotent, opt-out-aware). Never
|
|
120
122
|
// fatal — the PR is already up by now, so a comment failure only warns.
|
|
121
123
|
this.postChecklistComment(repoRoot, result.prNumber, input.checklists, provenanceVerified);
|
|
122
124
|
const prNum = result.prNumber;
|
|
123
125
|
process.stdout.write('\n' + SEP + '✅ PR finished — here is exactly what I did\n' + SEP + '\n' +
|
|
124
126
|
` 1. validated the build gate (authoritative)\n` +
|
|
125
|
-
` 2.
|
|
126
|
-
` 3.
|
|
127
|
+
` 2. ${prNum ? `wrote the gated body to PR #${prNum}` : 'composed the gated PR body'} titled: "${title}"\n` +
|
|
128
|
+
` 3. force-pushed your work to origin/${base} (after the body, so CI reads the right token)\n` +
|
|
127
129
|
` 4. ${result.merge.message}\n` +
|
|
128
130
|
` You are on ${base} — same name as the remote branch and the PR head.\n\n`);
|
|
129
131
|
}
|
|
@@ -166,27 +168,6 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
166
168
|
const marker = this.gateTokenService.gateTokenMarker(gateSalt, headSha);
|
|
167
169
|
return marker === '' ? '' : `\n\n${marker}\n`;
|
|
168
170
|
}
|
|
169
|
-
// Post `webpieces/pr-gate = success` as a commit status on the head sha. This is the authoritative,
|
|
170
|
-
// race-free required check: it is attached to the sha, so unlike the PR body it cannot be read before
|
|
171
|
-
// it exists. No-op when the repo sets no gateSalt. A failure to post (missing statuses:write) is only
|
|
172
|
-
// a warning — the CI wp-check-pr workflow still enforces the gate.
|
|
173
|
-
postGateStatus(headSha, gateSalt) {
|
|
174
|
-
if (gateSalt.trim() === '' || headSha === '')
|
|
175
|
-
return;
|
|
176
|
-
const res = (0, child_process_1.spawnSync)('gh', [
|
|
177
|
-
'api', '--method', 'POST', `repos/{owner}/{repo}/statuses/${headSha}`,
|
|
178
|
-
'-f', 'state=success',
|
|
179
|
-
'-f', 'context=webpieces/pr-gate',
|
|
180
|
-
'-f', 'description=gated flow ran and passed',
|
|
181
|
-
], { encoding: 'utf8' });
|
|
182
|
-
if (res.status !== 0) {
|
|
183
|
-
process.stderr.write('⚠️ Could not post the webpieces/pr-gate commit status (needs a token with statuses:write). ' +
|
|
184
|
-
'The CI wp-check-pr workflow still enforces the gate.\n');
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
process.stdout.write(` posted webpieces/pr-gate ✓ status on ${headSha.slice(0, 12)}\n`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
171
|
// Enforce that EACH matched checklist was reviewed by its OWN named subagent, as a DISTINCT run —
|
|
191
172
|
// the coding agent may not self-certify, and one reviewer may not stand in for several. A verified set
|
|
192
173
|
// passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.
|
|
@@ -248,28 +229,19 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
248
229
|
}
|
|
249
230
|
// The PR, the remote branch, and the local branch all share the one stable feature name. Look up /
|
|
250
231
|
// create / merge against `baseBranch` (baseBranchName tolerates a leftover `…wpN` mid-transition).
|
|
232
|
+
// GatedPrPublisher owns the edit/push/create half and its ORDERING — the gated body goes up before
|
|
233
|
+
// the push, so CI's `synchronize` read cannot see the previous run's token.
|
|
251
234
|
upsertPr(repoRoot, baseBranch, body, title, input) {
|
|
252
235
|
const prDir = (0, rules_config_1.prDirFor)(repoRoot, this.aiBranchName.getFeatureName());
|
|
253
236
|
fs.mkdirSync(prDir, { recursive: true });
|
|
254
237
|
const bodyFile = path.join(prDir, 'pr-body.md');
|
|
255
238
|
fs.writeFileSync(bodyFile, body + '\n');
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const create = (0, child_process_1.spawnSync)('gh', ['pr', 'create', '--head', baseBranch, '--base', 'main', '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });
|
|
261
|
-
if (create.status !== 0) {
|
|
262
|
-
process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\n ' + bodyFile + '\n');
|
|
263
|
-
return new UpsertResult('', new pr_merger_1.MergeOutcome(false, false, '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)'));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
process.stdout.write(`Updating PR #${num}...\n`);
|
|
268
|
-
const edit = (0, child_process_1.spawnSync)('gh', ['pr', 'edit', num, '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });
|
|
269
|
-
if (edit.status !== 0) {
|
|
270
|
-
process.stderr.write(`⚠️ gh pr edit failed — PR #${num} still shows its OLD title/body. The new body is in:\n ` + bodyFile + '\n');
|
|
271
|
-
}
|
|
239
|
+
const published = this.publisher.publish(baseBranch, title, bodyFile);
|
|
240
|
+
if (published.createFailed) {
|
|
241
|
+
process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\n ' + bodyFile + '\n');
|
|
242
|
+
return new UpsertResult('', new pr_merger_1.MergeOutcome(false, false, '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)'));
|
|
272
243
|
}
|
|
244
|
+
const num = published.number;
|
|
273
245
|
// Set the squash-merge SUBJECT to the PR title (+ the `(#N)` GitHub normally appends, which an
|
|
274
246
|
// explicit --subject would otherwise drop) and the BODY to the compact commit summary, so main's
|
|
275
247
|
// history carries the PR title + risk/flags/link — NOT the internal `Squash merge of <branch>`
|
|
@@ -308,6 +280,7 @@ exports.FinishUpsertPrCommand = FinishUpsertPrCommand = tslib_1.__decorate([
|
|
|
308
280
|
merge_state_1.MergeState,
|
|
309
281
|
merge_end_1.MergeEnd,
|
|
310
282
|
pr_merger_1.PrMerger,
|
|
283
|
+
gated_pr_publisher_1.GatedPrPublisher,
|
|
311
284
|
dashboard_1.Dashboard,
|
|
312
285
|
checklist_detector_1.ChecklistDetector,
|
|
313
286
|
rules_config_1.ChecklistManifestService,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finish-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/finish-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n loadAndValidate, prDirFor, reviewJsonPath, ReviewJson, RequiredChecklist,\n writeTemplate, RepoRootFinder, ReviewJsonService, ChecklistManifestService,\n GateTokenService, SubagentProvenanceService, PROVENANCE_OK, PROVENANCE_MISSING, PROVENANCE_SKIPPED,\n InformAiError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { ChecklistDetector } from '../workflow/checklist-detector';\nimport { GitExec } from '../workflow/git-exec';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { MergeState } from '../workflow/merge-state';\nimport { MergeEnd } from '../workflow/merge-end';\nimport { MergeContext } from '../workflow/merge-start';\nimport { PrMerger, MergeOutcome } from '../workflow/pr-merger';\nimport { Dashboard, DashboardInput, ChecklistRow, CHECKLIST_COMMENT_MARKER } from '../../dashboard/dashboard';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// A resolved PR's number + web URL. Both '' when the PR can't be resolved (e.g. create failed).\nclass PrRef {\n number: string;\n url: string;\n\n constructor(number: string, url: string) {\n this.number = number;\n this.url = url;\n }\n}\n\n// The outcome of the whole upsert: the PR number ('' when it could not be resolved) plus what actually\n// happened to the merge, so the final summary reports the REAL result rather than assuming success.\nclass UpsertResult {\n prNumber: string;\n merge: MergeOutcome;\n\n constructor(prNumber: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.merge = merge;\n }\n}\n\n// FINISH of the AI-first PR flow. Runs after the AI wrote review.json. In order: (1) if a 3-point merge\n// was in progress, validate + commit + FINALIZE via merge-END; (2) REQUIRE review.json; (3) run the\n// authoritative build gate; (4) render the dashboard; (5) create/update the PR via `gh`. The ONLY\n// command that posts PRs.\n@injectable(bindingScopeValues.Singleton)\nexport class FinishUpsertPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly prMerger: PrMerger,\n private readonly dashboard: Dashboard,\n private readonly checklistDetector: ChecklistDetector,\n private readonly manifestService: ChecklistManifestService,\n private readonly reviewJsonService: ReviewJsonService,\n private readonly gateTokenService: GateTokenService,\n private readonly provenance: SubagentProvenanceService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n\n // 1. Finish any in-progress conflict resolution: validate + commit + finalize the branch swap.\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (activeDir && marker && !marker.validated) {\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-finish-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n marker.conflictedFiles,\n );\n }\n\n // 2. REQUIRE the AI-authored review.json (throws InformAiError with the schema if missing/invalid).\n // Compute the consumer checklists this diff triggered FIRST so an unacknowledged BLOCK throws\n // here — BEFORE any `gh pr create` — matching the guarantee buildCommand already provides.\n const defs = this.manifestService.load(repoRoot, loadAndValidate(repoRoot).prGate.checklistDoc);\n const required = this.checklistDetector.toRequired(this.checklistDetector.detectForRepo(repoRoot, defs));\n // review-<id>.json files persist locally between runs, so a re-run after a push re-validates the\n // EXISTING verdicts against the (possibly changed) triggered set for free: an unchanged checklist\n // needs no re-review, a newly-triggered one refuses until its file is written. That is the\n // \"full review only when the checklist surface changes\" behavior — no special-casing here.\n const review = this.reviewJsonService.loadReviewJson(reviewJsonPath(repoRoot, this.aiBranchName.getFeatureName()), required);\n\n // 2c. For any BLOCK checklist that names a reviewer `subagent`, VERIFY (from the harness's own\n // artifacts) that such a subagent actually ran on this branch — the coding agent may not\n // self-certify. Absent CLAUDE_CODE_SESSION_ID this skips with a warning (CI / plain terminal).\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n const provenanceVerified = this.enforceProvenance(required, currentBranch);\n\n // 2b. The build gate validates the WORKING TREE but we push HEAD — so they MUST be identical.\n this.gitExec.assertCleanTree(repoRoot);\n\n // 3. Authoritative build gate, then push, then post.\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.',\n ));\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n this.gitExec.ensurePushed(base);\n\n process.stdout.write('\\n' + SEP + '📋 Dashboard + PR\\n' + SEP + '\\n');\n const title = this.prTitleFrom(review);\n const input = this.computeDashboardInput(repoRoot, true, review, title, required);\n // Append the hidden HMAC gate token bound to the pushed HEAD sha. A valid token in the PR body is\n // proof this gated flow ran + passed on this exact commit — CI (`wp-check-pr`) recomputes it. We\n // reach here only after the build gate + every BLOCK checklist passed, so minting is legitimate.\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n const headSha = this.gitOut(['rev-parse', 'HEAD']);\n const body = this.dashboard.renderDashboard(input) + this.gateTokenBody(gateSalt, headSha);\n const result = this.upsertPr(repoRoot, base, body, title, input);\n // Race-free required check: post the commit status on the head sha AFTER the body edit (see method).\n this.postGateStatus(headSha, gateSalt);\n // Publish each reviewer's full output as ONE combined PR comment (idempotent, opt-out-aware). Never\n // fatal — the PR is already up by now, so a comment failure only warns.\n this.postChecklistComment(repoRoot, result.prNumber, input.checklists, provenanceVerified);\n const prNum = result.prNumber;\n\n process.stdout.write(\n '\\n' + SEP + '✅ PR finished — here is exactly what I did\\n' + SEP + '\\n' +\n ` 1. validated the build gate (authoritative)\\n` +\n ` 2. force-pushed your work to origin/${base}\\n` +\n ` 3. ${prNum ? `updated/created PR #${prNum}` : 'created the PR'} titled: \"${title}\"\\n` +\n ` 4. ${result.merge.message}\\n` +\n ` You are on ${base} — same name as the remote branch and the PR head.\\n\\n`,\n );\n }\n\n private gitOut(args: string[]): string {\n const result = spawnSync('git', args, { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The user-facing PR title: the AI-authored review.title, or — if omitted — a readable fallback\n // derived from the stable feature name (NEVER the internal `Squash merge of <branch>` commit subject).\n private prTitleFrom(review: ReviewJson): string {\n if (review.title !== '') return review.title;\n return this.aiBranchName.getFeatureName().replace(/[-/]+/g, ' ').trim();\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private computeDashboardInput(repoRoot: string, buildPassed: boolean, review: ReviewJson, title: string, required: readonly RequiredChecklist[]): DashboardInput {\n const config = loadAndValidate(repoRoot).prGate;\n const forkPoint = this.gitOut(['merge-base', 'origin/main', 'HEAD']);\n const featureHead = this.gitOut(['rev-parse', 'HEAD']);\n const mainHead = this.gitOut(['rev-parse', 'origin/main']);\n const range = `${forkPoint}..${featureHead}`;\n const changedFiles = this.gitOut(['diff', range, '--name-only']).split('\\n').filter((f: string): boolean => f.trim() !== '');\n const patch = this.gitOut(['diff', range]);\n\n const gateResults = this.dashboard.computeGateResults(config.gates, changedFiles);\n const disables = this.dashboard.countAddedDisables(patch);\n const rows = this.checklistRows(required, review);\n return new DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows);\n }\n\n // Pair each matched checklist with its resolved verdict for the dashboard. (A checklist reaching this\n // point is always PASS/OVERRIDDEN — loadReviewJson already threw on FAIL/MISSING.)\n private checklistRows(required: readonly RequiredChecklist[], review: ReviewJson): ChecklistRow[] {\n return required.map((req: RequiredChecklist): ChecklistRow => {\n const verdict = this.reviewJsonService.resolveVerdict(req, review.results);\n return new ChecklistRow(req.id, verdict.status, verdict.detail);\n });\n }\n\n // Hidden HMAC gate-token marker (with a leading blank line) to append to the PR body, or '' when the\n // repo sets no gateSalt (byte-identical body to before this feature). Bound to the pushed HEAD sha.\n private gateTokenBody(gateSalt: string, headSha: string): string {\n const marker = this.gateTokenService.gateTokenMarker(gateSalt, headSha);\n return marker === '' ? '' : `\\n\\n${marker}\\n`;\n }\n\n // Post `webpieces/pr-gate = success` as a commit status on the head sha. This is the authoritative,\n // race-free required check: it is attached to the sha, so unlike the PR body it cannot be read before\n // it exists. No-op when the repo sets no gateSalt. A failure to post (missing statuses:write) is only\n // a warning — the CI wp-check-pr workflow still enforces the gate.\n private postGateStatus(headSha: string, gateSalt: string): void {\n if (gateSalt.trim() === '' || headSha === '') return;\n const res = spawnSync('gh', [\n 'api', '--method', 'POST', `repos/{owner}/{repo}/statuses/${headSha}`,\n '-f', 'state=success',\n '-f', 'context=webpieces/pr-gate',\n '-f', 'description=gated flow ran and passed',\n ], { encoding: 'utf8' });\n if (res.status !== 0) {\n process.stderr.write(\n '⚠️ Could not post the webpieces/pr-gate commit status (needs a token with statuses:write). ' +\n 'The CI wp-check-pr workflow still enforces the gate.\\n',\n );\n } else {\n process.stdout.write(` posted webpieces/pr-gate ✓ status on ${headSha.slice(0, 12)}\\n`);\n }\n }\n\n // Enforce that EACH matched checklist was reviewed by its OWN named subagent, as a DISTINCT run —\n // the coding agent may not self-certify, and one reviewer may not stand in for several. A verified set\n // passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.\n private enforceProvenance(required: readonly RequiredChecklist[], branch: string): boolean {\n const errors: string[] = [];\n let verified = true; // no reviewers to verify ⇒ vacuously true\n const subagents = required.map((r: RequiredChecklist): string => r.subagent.trim()).filter((s: string): boolean => s !== '');\n if (subagents.length > 0) {\n const result = this.provenance.verifyDistinct(subagents, branch);\n verified = result.status === PROVENANCE_OK;\n if (result.status === PROVENANCE_MISSING) {\n errors.push(result.detail);\n } else if (result.status === PROVENANCE_SKIPPED) {\n process.stderr.write(`⚠️ ${result.detail}\\n`);\n }\n }\n if (errors.length > 0) {\n throw new InformAiError(\n `${errors.length} checklist(s) require an independent reviewer subagent that did not run — fix, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\nSpawn the named reviewer subagent to review the checklist on THIS branch, then re-run.`,\n );\n }\n return verified;\n }\n\n // Publish every reviewer's full `output` as ONE combined PR comment, idempotently (find the marker\n // comment → PATCH it, else POST). No-op when there is no PR number or no matched checklists. Never\n // fatal: by here the PR is already created/updated, so a `gh` failure only warns.\n private postChecklistComment(repoRoot: string, prNumber: string, rows: readonly ChecklistRow[], provenanceVerified: boolean): void {\n if (prNumber === '' || rows.length === 0) return;\n if (!loadAndValidate(repoRoot).prGate.checklistComments) return;\n const body = this.dashboard.renderChecklistComment(rows, provenanceVerified);\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const payload = path.join(prDir, 'checklist-comment.json');\n fs.writeFileSync(payload, JSON.stringify({ body }));\n const commentId = this.findChecklistCommentId(prNumber);\n const args = commentId !== ''\n ? ['api', '--method', 'PATCH', `repos/{owner}/{repo}/issues/comments/${commentId}`, '--input', payload]\n : ['api', '--method', 'POST', `repos/{owner}/{repo}/issues/${prNumber}/comments`, '--input', payload];\n const res = spawnSync('gh', args, { encoding: 'utf8' });\n if (res.status !== 0) {\n process.stderr.write('⚠️ Could not post the checklist review comment (non-fatal — the PR is already up).\\n');\n } else {\n process.stdout.write(` ${commentId !== '' ? 'updated' : 'posted'} the checklist review comment ✓\\n`);\n }\n }\n\n // The id of THIS tool's existing checklist comment on the PR (by the hidden marker), or '' if none.\n private findChecklistCommentId(prNumber: string): string {\n const res = spawnSync('gh', [\n 'api', '--paginate', `repos/{owner}/{repo}/issues/${prNumber}/comments`,\n '--jq', `.[] | select(.body | contains(\"${CHECKLIST_COMMENT_MARKER}\")) | .id`,\n ], { encoding: 'utf8' });\n if (res.status !== 0) return '';\n return (res.stdout ?? '').trim().split('\\n')[0] ?? '';\n }\n\n // The PR, the remote branch, and the local branch all share the one stable feature name. Look up /\n // create / merge against `baseBranch` (baseBranchName tolerates a leftover `…wpN` mid-transition).\n private upsertPr(repoRoot: string, baseBranch: string, body: string, title: string, input: DashboardInput): UpsertResult {\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const bodyFile = path.join(prDir, 'pr-body.md');\n fs.writeFileSync(bodyFile, body + '\\n');\n\n const prNumber = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n const num = prNumber.status === 0 ? (prNumber.stdout ?? '').trim() : '';\n\n if (num === '') {\n process.stdout.write('Creating PR...\\n');\n const create = spawnSync('gh', ['pr', 'create', '--head', baseBranch, '--base', 'main', '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });\n if (create.status !== 0) {\n process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\\n ' + bodyFile + '\\n');\n return new UpsertResult('', new MergeOutcome(false, false,\n '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)'));\n }\n } else {\n process.stdout.write(`Updating PR #${num}...\\n`);\n const edit = spawnSync('gh', ['pr', 'edit', num, '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });\n if (edit.status !== 0) {\n process.stderr.write(`⚠️ gh pr edit failed — PR #${num} still shows its OLD title/body. The new body is in:\\n ` + bodyFile + '\\n');\n }\n }\n\n // Set the squash-merge SUBJECT to the PR title (+ the `(#N)` GitHub normally appends, which an\n // explicit --subject would otherwise drop) and the BODY to the compact commit summary, so main's\n // history carries the PR title + risk/flags/link — NOT the internal `Squash merge of <branch>`\n // subject GitHub would inherit from the single squash commit on the branch.\n const ref = this.prRef(baseBranch);\n const subject = ref.number !== '' ? `${title} (#${ref.number})` : title;\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n fs.writeFileSync(mergeBodyFile, this.dashboard.renderCommitBody(input, ref.url) + '\\n');\n // PrMerger owns the direct-merge / auto-merge-fallback decision AND checks every gh status, so a\n // merge that did not happen is reported as such instead of being swallowed (see pr-merger.ts).\n // REQUIRED config — no default here on purpose. A missing value (an older published\n // rules-config that has no such field) reaches PrMerger as '' and is treated as \"do not merge\".\n const mergeMode = loadAndValidate(repoRoot).prGate.mergeMode ?? '';\n const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, mergeMode);\n return new UpsertResult(ref.number !== '' ? ref.number : num, outcome);\n }\n\n // The PR's number + web URL (for the merge subject `(#N)` and the commit-body back-link). Both ''\n // if it can't be resolved. Rendered via jq into one tab-separated line so no JSON parsing is needed.\n private prRef(baseBranch: string): PrRef {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,url', '--jq', '\"\\\\(.number)\\\\t\\\\(.url)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) {\n return new PrRef('', '');\n }\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrRef(parts[0] ?? '', parts[1] ?? '');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"finish-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/finish-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n loadAndValidate, prDirFor, reviewJsonPath, ReviewJson, RequiredChecklist,\n writeTemplate, RepoRootFinder, ReviewJsonService, ChecklistManifestService,\n GateTokenService, SubagentProvenanceService, PROVENANCE_OK, PROVENANCE_MISSING, PROVENANCE_SKIPPED,\n InformAiError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { ChecklistDetector } from '../workflow/checklist-detector';\nimport { GitExec } from '../workflow/git-exec';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { MergeState } from '../workflow/merge-state';\nimport { MergeEnd } from '../workflow/merge-end';\nimport { MergeContext } from '../workflow/merge-start';\nimport { PrMerger, MergeOutcome } from '../workflow/pr-merger';\nimport { GatedPrPublisher } from '../workflow/gated-pr-publisher';\nimport { Dashboard, DashboardInput, ChecklistRow, CHECKLIST_COMMENT_MARKER } from '../../dashboard/dashboard';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// A resolved PR's number + web URL. Both '' when the PR can't be resolved (e.g. create failed).\nclass PrRef {\n number: string;\n url: string;\n\n constructor(number: string, url: string) {\n this.number = number;\n this.url = url;\n }\n}\n\n// The outcome of the whole upsert: the PR number ('' when it could not be resolved) plus what actually\n// happened to the merge, so the final summary reports the REAL result rather than assuming success.\nclass UpsertResult {\n prNumber: string;\n merge: MergeOutcome;\n\n constructor(prNumber: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.merge = merge;\n }\n}\n\n// FINISH of the AI-first PR flow. Runs after the AI wrote review.json. In order: (1) if a 3-point merge\n// was in progress, validate + commit + FINALIZE via merge-END; (2) REQUIRE review.json; (3) run the\n// authoritative build gate; (4) render the dashboard; (5) create/update the PR via `gh`. The ONLY\n// command that posts PRs.\n@injectable(bindingScopeValues.Singleton)\nexport class FinishUpsertPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly prMerger: PrMerger,\n private readonly publisher: GatedPrPublisher,\n private readonly dashboard: Dashboard,\n private readonly checklistDetector: ChecklistDetector,\n private readonly manifestService: ChecklistManifestService,\n private readonly reviewJsonService: ReviewJsonService,\n private readonly gateTokenService: GateTokenService,\n private readonly provenance: SubagentProvenanceService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n\n // 1. Finish any in-progress conflict resolution: validate + commit + finalize the branch swap.\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (activeDir && marker && !marker.validated) {\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-finish-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n marker.conflictedFiles,\n );\n }\n\n // 2. REQUIRE the AI-authored review.json (throws InformAiError with the schema if missing/invalid).\n // Compute the consumer checklists this diff triggered FIRST so an unacknowledged BLOCK throws\n // here — BEFORE any `gh pr create` — matching the guarantee buildCommand already provides.\n const defs = this.manifestService.load(repoRoot, loadAndValidate(repoRoot).prGate.checklistDoc);\n const required = this.checklistDetector.toRequired(this.checklistDetector.detectForRepo(repoRoot, defs));\n // review-<id>.json files persist locally between runs, so a re-run after a push re-validates the\n // EXISTING verdicts against the (possibly changed) triggered set for free: an unchanged checklist\n // needs no re-review, a newly-triggered one refuses until its file is written. That is the\n // \"full review only when the checklist surface changes\" behavior — no special-casing here.\n const review = this.reviewJsonService.loadReviewJson(reviewJsonPath(repoRoot, this.aiBranchName.getFeatureName()), required);\n\n // 2c. For any BLOCK checklist that names a reviewer `subagent`, VERIFY (from the harness's own\n // artifacts) that such a subagent actually ran on this branch — the coding agent may not\n // self-certify. Absent CLAUDE_CODE_SESSION_ID this skips with a warning (CI / plain terminal).\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n const provenanceVerified = this.enforceProvenance(required, currentBranch);\n\n // 2b. The build gate validates the WORKING TREE but we push HEAD — so they MUST be identical.\n this.gitExec.assertCleanTree(repoRoot);\n\n // 3. Authoritative build gate, then post the gated body, then push (that ORDER — see GatedPrPublisher).\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.',\n ));\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n\n process.stdout.write('\\n' + SEP + '📋 Dashboard + PR\\n' + SEP + '\\n');\n const title = this.prTitleFrom(review);\n const input = this.computeDashboardInput(repoRoot, true, review, title, required);\n // Append the hidden HMAC gate token bound to the LOCAL HEAD sha — computed BEFORE the push, because\n // GatedPrPublisher writes the body first so CI's `synchronize` read can never see a stale token. A\n // valid token in the PR body is proof this gated flow ran + passed on this exact commit, which CI\n // (`wp-check-pr`) recomputes. We reach here only after the build gate + every BLOCK checklist\n // passed, so minting is legitimate. Nothing about HMAC(salt, HEAD) needs the remote to have it.\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n const headSha = this.gitOut(['rev-parse', 'HEAD']);\n const body = this.dashboard.renderDashboard(input) + this.gateTokenBody(gateSalt, headSha);\n const result = this.upsertPr(repoRoot, base, body, title, input);\n // Publish each reviewer's full output as ONE combined PR comment (idempotent, opt-out-aware). Never\n // fatal — the PR is already up by now, so a comment failure only warns.\n this.postChecklistComment(repoRoot, result.prNumber, input.checklists, provenanceVerified);\n const prNum = result.prNumber;\n\n process.stdout.write(\n '\\n' + SEP + '✅ PR finished — here is exactly what I did\\n' + SEP + '\\n' +\n ` 1. validated the build gate (authoritative)\\n` +\n ` 2. ${prNum ? `wrote the gated body to PR #${prNum}` : 'composed the gated PR body'} titled: \"${title}\"\\n` +\n ` 3. force-pushed your work to origin/${base} (after the body, so CI reads the right token)\\n` +\n ` 4. ${result.merge.message}\\n` +\n ` You are on ${base} — same name as the remote branch and the PR head.\\n\\n`,\n );\n }\n\n private gitOut(args: string[]): string {\n const result = spawnSync('git', args, { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The user-facing PR title: the AI-authored review.title, or — if omitted — a readable fallback\n // derived from the stable feature name (NEVER the internal `Squash merge of <branch>` commit subject).\n private prTitleFrom(review: ReviewJson): string {\n if (review.title !== '') return review.title;\n return this.aiBranchName.getFeatureName().replace(/[-/]+/g, ' ').trim();\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private computeDashboardInput(repoRoot: string, buildPassed: boolean, review: ReviewJson, title: string, required: readonly RequiredChecklist[]): DashboardInput {\n const config = loadAndValidate(repoRoot).prGate;\n const forkPoint = this.gitOut(['merge-base', 'origin/main', 'HEAD']);\n const featureHead = this.gitOut(['rev-parse', 'HEAD']);\n const mainHead = this.gitOut(['rev-parse', 'origin/main']);\n const range = `${forkPoint}..${featureHead}`;\n const changedFiles = this.gitOut(['diff', range, '--name-only']).split('\\n').filter((f: string): boolean => f.trim() !== '');\n const patch = this.gitOut(['diff', range]);\n\n const gateResults = this.dashboard.computeGateResults(config.gates, changedFiles);\n const disables = this.dashboard.countAddedDisables(patch);\n const rows = this.checklistRows(required, review);\n return new DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows);\n }\n\n // Pair each matched checklist with its resolved verdict for the dashboard. (A checklist reaching this\n // point is always PASS/OVERRIDDEN — loadReviewJson already threw on FAIL/MISSING.)\n private checklistRows(required: readonly RequiredChecklist[], review: ReviewJson): ChecklistRow[] {\n return required.map((req: RequiredChecklist): ChecklistRow => {\n const verdict = this.reviewJsonService.resolveVerdict(req, review.results);\n return new ChecklistRow(req.id, verdict.status, verdict.detail);\n });\n }\n\n // Hidden HMAC gate-token marker (with a leading blank line) to append to the PR body, or '' when the\n // repo sets no gateSalt (byte-identical body to before this feature). Bound to the pushed HEAD sha.\n private gateTokenBody(gateSalt: string, headSha: string): string {\n const marker = this.gateTokenService.gateTokenMarker(gateSalt, headSha);\n return marker === '' ? '' : `\\n\\n${marker}\\n`;\n }\n\n // Enforce that EACH matched checklist was reviewed by its OWN named subagent, as a DISTINCT run —\n // the coding agent may not self-certify, and one reviewer may not stand in for several. A verified set\n // passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.\n private enforceProvenance(required: readonly RequiredChecklist[], branch: string): boolean {\n const errors: string[] = [];\n let verified = true; // no reviewers to verify ⇒ vacuously true\n const subagents = required.map((r: RequiredChecklist): string => r.subagent.trim()).filter((s: string): boolean => s !== '');\n if (subagents.length > 0) {\n const result = this.provenance.verifyDistinct(subagents, branch);\n verified = result.status === PROVENANCE_OK;\n if (result.status === PROVENANCE_MISSING) {\n errors.push(result.detail);\n } else if (result.status === PROVENANCE_SKIPPED) {\n process.stderr.write(`⚠️ ${result.detail}\\n`);\n }\n }\n if (errors.length > 0) {\n throw new InformAiError(\n `${errors.length} checklist(s) require an independent reviewer subagent that did not run — fix, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\nSpawn the named reviewer subagent to review the checklist on THIS branch, then re-run.`,\n );\n }\n return verified;\n }\n\n // Publish every reviewer's full `output` as ONE combined PR comment, idempotently (find the marker\n // comment → PATCH it, else POST). No-op when there is no PR number or no matched checklists. Never\n // fatal: by here the PR is already created/updated, so a `gh` failure only warns.\n private postChecklistComment(repoRoot: string, prNumber: string, rows: readonly ChecklistRow[], provenanceVerified: boolean): void {\n if (prNumber === '' || rows.length === 0) return;\n if (!loadAndValidate(repoRoot).prGate.checklistComments) return;\n const body = this.dashboard.renderChecklistComment(rows, provenanceVerified);\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const payload = path.join(prDir, 'checklist-comment.json');\n fs.writeFileSync(payload, JSON.stringify({ body }));\n const commentId = this.findChecklistCommentId(prNumber);\n const args = commentId !== ''\n ? ['api', '--method', 'PATCH', `repos/{owner}/{repo}/issues/comments/${commentId}`, '--input', payload]\n : ['api', '--method', 'POST', `repos/{owner}/{repo}/issues/${prNumber}/comments`, '--input', payload];\n const res = spawnSync('gh', args, { encoding: 'utf8' });\n if (res.status !== 0) {\n process.stderr.write('⚠️ Could not post the checklist review comment (non-fatal — the PR is already up).\\n');\n } else {\n process.stdout.write(` ${commentId !== '' ? 'updated' : 'posted'} the checklist review comment ✓\\n`);\n }\n }\n\n // The id of THIS tool's existing checklist comment on the PR (by the hidden marker), or '' if none.\n private findChecklistCommentId(prNumber: string): string {\n const res = spawnSync('gh', [\n 'api', '--paginate', `repos/{owner}/{repo}/issues/${prNumber}/comments`,\n '--jq', `.[] | select(.body | contains(\"${CHECKLIST_COMMENT_MARKER}\")) | .id`,\n ], { encoding: 'utf8' });\n if (res.status !== 0) return '';\n return (res.stdout ?? '').trim().split('\\n')[0] ?? '';\n }\n\n // The PR, the remote branch, and the local branch all share the one stable feature name. Look up /\n // create / merge against `baseBranch` (baseBranchName tolerates a leftover `…wpN` mid-transition).\n // GatedPrPublisher owns the edit/push/create half and its ORDERING — the gated body goes up before\n // the push, so CI's `synchronize` read cannot see the previous run's token.\n private upsertPr(repoRoot: string, baseBranch: string, body: string, title: string, input: DashboardInput): UpsertResult {\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const bodyFile = path.join(prDir, 'pr-body.md');\n fs.writeFileSync(bodyFile, body + '\\n');\n\n const published = this.publisher.publish(baseBranch, title, bodyFile);\n if (published.createFailed) {\n process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\\n ' + bodyFile + '\\n');\n return new UpsertResult('', new MergeOutcome(false, false,\n '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)'));\n }\n const num = published.number;\n\n // Set the squash-merge SUBJECT to the PR title (+ the `(#N)` GitHub normally appends, which an\n // explicit --subject would otherwise drop) and the BODY to the compact commit summary, so main's\n // history carries the PR title + risk/flags/link — NOT the internal `Squash merge of <branch>`\n // subject GitHub would inherit from the single squash commit on the branch.\n const ref = this.prRef(baseBranch);\n const subject = ref.number !== '' ? `${title} (#${ref.number})` : title;\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n fs.writeFileSync(mergeBodyFile, this.dashboard.renderCommitBody(input, ref.url) + '\\n');\n // PrMerger owns the direct-merge / auto-merge-fallback decision AND checks every gh status, so a\n // merge that did not happen is reported as such instead of being swallowed (see pr-merger.ts).\n // REQUIRED config — no default here on purpose. A missing value (an older published\n // rules-config that has no such field) reaches PrMerger as '' and is treated as \"do not merge\".\n const mergeMode = loadAndValidate(repoRoot).prGate.mergeMode ?? '';\n const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, mergeMode);\n return new UpsertResult(ref.number !== '' ? ref.number : num, outcome);\n }\n\n // The PR's number + web URL (for the merge subject `(#N)` and the commit-body back-link). Both ''\n // if it can't be resolved. Rendered via jq into one tab-separated line so no JSON parsing is needed.\n private prRef(baseBranch: string): PrRef {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,url', '--jq', '\"\\\\(.number)\\\\t\\\\(.url)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) {\n return new PrRef('', '');\n }\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrRef(parts[0] ?? '', parts[1] ?? '');\n }\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { GitExec } from './git-exec';
|
|
2
|
+
export declare class PublishedPr {
|
|
3
|
+
number: string;
|
|
4
|
+
createFailed: boolean;
|
|
5
|
+
constructor(number: string, createFailed: boolean);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Owns the ONE ordering constraint that lets the webpieces gate be a single required check: the PR body
|
|
9
|
+
* — which carries `HMAC(gateSalt, HEAD_sha)` — is written BEFORE the push.
|
|
10
|
+
*
|
|
11
|
+
* WHY the order matters. Pushing first fires `pull_request:synchronize`, and CI (`wp-check-pr`) reads the
|
|
12
|
+
* PR body it finds at that instant. If the body edit has not landed yet, that read sees the PREVIOUS
|
|
13
|
+
* run's token, which is bound to the parent sha, and the check goes red on a timing coin-flip. #485
|
|
14
|
+
* papered over this by also posting a `webpieces/pr-gate` commit status, whose newest post supersedes an
|
|
15
|
+
* older one — recovery rather than prevention, and it left consumers with two entries on every PR and no
|
|
16
|
+
* way to tell which one to mark required.
|
|
17
|
+
*
|
|
18
|
+
* The sha is already known locally: nothing about minting `HMAC(gateSalt, git rev-parse HEAD)` needs the
|
|
19
|
+
* remote to have the commit. So write the body first and the `synchronize` read can only ever see the
|
|
20
|
+
* right token. A brand-new PR is unaffected either way — `gh pr create` composes the body after the push,
|
|
21
|
+
* so the `opened` event already sees the final body.
|
|
22
|
+
*
|
|
23
|
+
* NOTE this is not the whole story: `wp-start-upsert-pr` also pushes, so on iteration 2+ the red usually
|
|
24
|
+
* comes from THAT push, and by the time finish runs its own `ensurePushed` is a no-op firing no event.
|
|
25
|
+
* What clears it is the `edited` event from the body write, which is why `edited` must stay in the
|
|
26
|
+
* scaffolded workflow's trigger list. The ordering here covers the remaining case where finish's push
|
|
27
|
+
* really does move the remote head (its 3-point merge finalize rewrote the branch, or the developer
|
|
28
|
+
* committed between start and finish).
|
|
29
|
+
*
|
|
30
|
+
* `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
|
|
31
|
+
*/
|
|
32
|
+
export declare class GatedPrPublisher {
|
|
33
|
+
private readonly gitExec;
|
|
34
|
+
constructor(gitExec: GitExec);
|
|
35
|
+
/**
|
|
36
|
+
* Land `bodyFile` on the PR for `baseBranch` and push, in the gate-safe order.
|
|
37
|
+
*
|
|
38
|
+
* @param bodyFile the rendered dashboard ALREADY carrying the gate token for the LOCAL head sha
|
|
39
|
+
*/
|
|
40
|
+
publish(baseBranch: string, title: string, bodyFile: string): PublishedPr;
|
|
41
|
+
private editPrOrAbort;
|
|
42
|
+
protected findOpenPr(baseBranch: string): string;
|
|
43
|
+
protected editPr(prNumber: string, title: string, bodyFile: string): boolean;
|
|
44
|
+
protected createPr(baseBranch: string, title: string, bodyFile: string): boolean;
|
|
45
|
+
protected push(baseBranch: string): void;
|
|
46
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GatedPrPublisher = exports.PublishedPr = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
|
+
const inversify_1 = require("inversify");
|
|
8
|
+
const git_exec_1 = require("./git-exec");
|
|
9
|
+
// Which PR the gated body landed on. Data-only.
|
|
10
|
+
class PublishedPr {
|
|
11
|
+
// The PR that already existed and was edited; '' when this run had to create one.
|
|
12
|
+
number;
|
|
13
|
+
// True only on the create path, when `gh pr create` itself failed — there is no PR to merge.
|
|
14
|
+
createFailed;
|
|
15
|
+
constructor(number, createFailed) {
|
|
16
|
+
this.number = number;
|
|
17
|
+
this.createFailed = createFailed;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.PublishedPr = PublishedPr;
|
|
21
|
+
/**
|
|
22
|
+
* Owns the ONE ordering constraint that lets the webpieces gate be a single required check: the PR body
|
|
23
|
+
* — which carries `HMAC(gateSalt, HEAD_sha)` — is written BEFORE the push.
|
|
24
|
+
*
|
|
25
|
+
* WHY the order matters. Pushing first fires `pull_request:synchronize`, and CI (`wp-check-pr`) reads the
|
|
26
|
+
* PR body it finds at that instant. If the body edit has not landed yet, that read sees the PREVIOUS
|
|
27
|
+
* run's token, which is bound to the parent sha, and the check goes red on a timing coin-flip. #485
|
|
28
|
+
* papered over this by also posting a `webpieces/pr-gate` commit status, whose newest post supersedes an
|
|
29
|
+
* older one — recovery rather than prevention, and it left consumers with two entries on every PR and no
|
|
30
|
+
* way to tell which one to mark required.
|
|
31
|
+
*
|
|
32
|
+
* The sha is already known locally: nothing about minting `HMAC(gateSalt, git rev-parse HEAD)` needs the
|
|
33
|
+
* remote to have the commit. So write the body first and the `synchronize` read can only ever see the
|
|
34
|
+
* right token. A brand-new PR is unaffected either way — `gh pr create` composes the body after the push,
|
|
35
|
+
* so the `opened` event already sees the final body.
|
|
36
|
+
*
|
|
37
|
+
* NOTE this is not the whole story: `wp-start-upsert-pr` also pushes, so on iteration 2+ the red usually
|
|
38
|
+
* comes from THAT push, and by the time finish runs its own `ensurePushed` is a no-op firing no event.
|
|
39
|
+
* What clears it is the `edited` event from the body write, which is why `edited` must stay in the
|
|
40
|
+
* scaffolded workflow's trigger list. The ordering here covers the remaining case where finish's push
|
|
41
|
+
* really does move the remote head (its 3-point merge finalize rewrote the branch, or the developer
|
|
42
|
+
* committed between start and finish).
|
|
43
|
+
*
|
|
44
|
+
* `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
|
|
45
|
+
*/
|
|
46
|
+
let GatedPrPublisher = class GatedPrPublisher {
|
|
47
|
+
gitExec;
|
|
48
|
+
constructor(gitExec) {
|
|
49
|
+
this.gitExec = gitExec;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Land `bodyFile` on the PR for `baseBranch` and push, in the gate-safe order.
|
|
53
|
+
*
|
|
54
|
+
* @param bodyFile the rendered dashboard ALREADY carrying the gate token for the LOCAL head sha
|
|
55
|
+
*/
|
|
56
|
+
publish(baseBranch, title, bodyFile) {
|
|
57
|
+
const existing = this.findOpenPr(baseBranch);
|
|
58
|
+
// 1. Body FIRST, so a `synchronize` read arriving the instant after the push finds the right token.
|
|
59
|
+
if (existing !== '') {
|
|
60
|
+
process.stdout.write(`Updating PR #${existing} (body before push, so CI cannot read a stale token)...\n`);
|
|
61
|
+
this.editPrOrAbort(existing, title, bodyFile);
|
|
62
|
+
// Say the consequence BEFORE the push can fail: from here the PR advertises a token for the
|
|
63
|
+
// local HEAD, so a failed push leaves the PR pointing at the old commit and the gate check goes
|
|
64
|
+
// RED. That is the deliberate fail-CLOSED direction, and it self-corrects on the next good run.
|
|
65
|
+
process.stdout.write(' body updated. Pushing now — if this push FAILS, the PR advertises a token for a commit\n' +
|
|
66
|
+
' the remote does not have, so the webpieces gate check goes RED (never falsely green)\n' +
|
|
67
|
+
' until you fix the push and re-run pnpm wp-finish-upsert-pr.\n');
|
|
68
|
+
}
|
|
69
|
+
// 2. THEN push. Nothing was pushed above, so an edit failure left the remote wholly untouched.
|
|
70
|
+
this.push(baseBranch);
|
|
71
|
+
// 3. Only a brand-new PR is created, and only after the push — `gh pr create` needs the remote ref.
|
|
72
|
+
if (existing !== '')
|
|
73
|
+
return new PublishedPr(existing, false);
|
|
74
|
+
process.stdout.write('Creating PR...\n');
|
|
75
|
+
return new PublishedPr('', !this.createPr(baseBranch, title, bodyFile));
|
|
76
|
+
}
|
|
77
|
+
// Edit the PR body/title, aborting BEFORE the push if `gh` failed. Warning-and-continuing here would
|
|
78
|
+
// push code that the PR body does not vouch for; aborting leaves the PR on its previous body, whose
|
|
79
|
+
// token is still valid for the sha the remote still has. Nothing is half-done either way.
|
|
80
|
+
editPrOrAbort(prNumber, title, bodyFile) {
|
|
81
|
+
if (this.editPr(prNumber, title, bodyFile))
|
|
82
|
+
return;
|
|
83
|
+
throw new rules_config_1.CliExitError(1, `❌ gh pr edit failed on PR #${prNumber} — NOTHING was pushed, so the PR and the remote branch are\n` +
|
|
84
|
+
` both unchanged and still consistent. Fix gh (auth / network / permissions) and re-run\n` +
|
|
85
|
+
` pnpm wp-finish-upsert-pr. The new body is in:\n ${bodyFile}`);
|
|
86
|
+
}
|
|
87
|
+
// The `gh`/push seams, protected so the spec can drive the ordering with no gh, no network, no repo.
|
|
88
|
+
// The open PR whose head is `baseBranch`, or '' when there is none (or `gh` failed — treated the same,
|
|
89
|
+
// so the create path then fails loudly rather than silently editing the wrong PR).
|
|
90
|
+
findOpenPr(baseBranch) {
|
|
91
|
+
const result = (0, child_process_1.spawnSync)('gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'], { encoding: 'utf8' });
|
|
92
|
+
return result.status === 0 ? (result.stdout ?? '').trim() : '';
|
|
93
|
+
}
|
|
94
|
+
editPr(prNumber, title, bodyFile) {
|
|
95
|
+
return (0, child_process_1.spawnSync)('gh', ['pr', 'edit', prNumber, '--title', title, '--body-file', bodyFile], { stdio: 'inherit' }).status === 0;
|
|
96
|
+
}
|
|
97
|
+
createPr(baseBranch, title, bodyFile) {
|
|
98
|
+
return (0, child_process_1.spawnSync)('gh', ['pr', 'create', '--head', baseBranch, '--base', 'main', '--title', title, '--body-file', bodyFile], { stdio: 'inherit' }).status === 0;
|
|
99
|
+
}
|
|
100
|
+
// Throws (CliExitError, via runGitChecked) when the push fails — deliberately NOT caught here: the
|
|
101
|
+
// consequence is already printed above, and swallowing it would report a PR that was never updated.
|
|
102
|
+
push(baseBranch) {
|
|
103
|
+
this.gitExec.ensurePushed(baseBranch);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
exports.GatedPrPublisher = GatedPrPublisher;
|
|
107
|
+
exports.GatedPrPublisher = GatedPrPublisher = tslib_1.__decorate([
|
|
108
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
109
|
+
tslib_1.__metadata("design:paramtypes", [git_exec_1.GitExec])
|
|
110
|
+
], GatedPrPublisher);
|
|
111
|
+
//# sourceMappingURL=gated-pr-publisher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gated-pr-publisher.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/gated-pr-publisher.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { CliExitError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GitExec } from './git-exec';\n\n// Which PR the gated body landed on. Data-only.\nexport class PublishedPr {\n // The PR that already existed and was edited; '' when this run had to create one.\n number: string;\n // True only on the create path, when `gh pr create` itself failed — there is no PR to merge.\n createFailed: boolean;\n\n constructor(number: string, createFailed: boolean) {\n this.number = number;\n this.createFailed = createFailed;\n }\n}\n\n/**\n * Owns the ONE ordering constraint that lets the webpieces gate be a single required check: the PR body\n * — which carries `HMAC(gateSalt, HEAD_sha)` — is written BEFORE the push.\n *\n * WHY the order matters. Pushing first fires `pull_request:synchronize`, and CI (`wp-check-pr`) reads the\n * PR body it finds at that instant. If the body edit has not landed yet, that read sees the PREVIOUS\n * run's token, which is bound to the parent sha, and the check goes red on a timing coin-flip. #485\n * papered over this by also posting a `webpieces/pr-gate` commit status, whose newest post supersedes an\n * older one — recovery rather than prevention, and it left consumers with two entries on every PR and no\n * way to tell which one to mark required.\n *\n * The sha is already known locally: nothing about minting `HMAC(gateSalt, git rev-parse HEAD)` needs the\n * remote to have the commit. So write the body first and the `synchronize` read can only ever see the\n * right token. A brand-new PR is unaffected either way — `gh pr create` composes the body after the push,\n * so the `opened` event already sees the final body.\n *\n * NOTE this is not the whole story: `wp-start-upsert-pr` also pushes, so on iteration 2+ the red usually\n * comes from THAT push, and by the time finish runs its own `ensurePushed` is a no-op firing no event.\n * What clears it is the `edited` event from the body write, which is why `edited` must stay in the\n * scaffolded workflow's trigger list. The ordering here covers the remaining case where finish's push\n * really does move the remote head (its 3-point merge finalize rewrote the branch, or the developer\n * committed between start and finish).\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class GatedPrPublisher {\n constructor(private readonly gitExec: GitExec) {}\n\n /**\n * Land `bodyFile` on the PR for `baseBranch` and push, in the gate-safe order.\n *\n * @param bodyFile the rendered dashboard ALREADY carrying the gate token for the LOCAL head sha\n */\n publish(baseBranch: string, title: string, bodyFile: string): PublishedPr {\n const existing = this.findOpenPr(baseBranch);\n\n // 1. Body FIRST, so a `synchronize` read arriving the instant after the push finds the right token.\n if (existing !== '') {\n process.stdout.write(`Updating PR #${existing} (body before push, so CI cannot read a stale token)...\\n`);\n this.editPrOrAbort(existing, title, bodyFile);\n // Say the consequence BEFORE the push can fail: from here the PR advertises a token for the\n // local HEAD, so a failed push leaves the PR pointing at the old commit and the gate check goes\n // RED. That is the deliberate fail-CLOSED direction, and it self-corrects on the next good run.\n process.stdout.write(\n ' body updated. Pushing now — if this push FAILS, the PR advertises a token for a commit\\n' +\n ' the remote does not have, so the webpieces gate check goes RED (never falsely green)\\n' +\n ' until you fix the push and re-run pnpm wp-finish-upsert-pr.\\n',\n );\n }\n\n // 2. THEN push. Nothing was pushed above, so an edit failure left the remote wholly untouched.\n this.push(baseBranch);\n\n // 3. Only a brand-new PR is created, and only after the push — `gh pr create` needs the remote ref.\n if (existing !== '') return new PublishedPr(existing, false);\n process.stdout.write('Creating PR...\\n');\n return new PublishedPr('', !this.createPr(baseBranch, title, bodyFile));\n }\n\n // Edit the PR body/title, aborting BEFORE the push if `gh` failed. Warning-and-continuing here would\n // push code that the PR body does not vouch for; aborting leaves the PR on its previous body, whose\n // token is still valid for the sha the remote still has. Nothing is half-done either way.\n private editPrOrAbort(prNumber: string, title: string, bodyFile: string): void {\n if (this.editPr(prNumber, title, bodyFile)) return;\n throw new CliExitError(1,\n `❌ gh pr edit failed on PR #${prNumber} — NOTHING was pushed, so the PR and the remote branch are\\n` +\n ` both unchanged and still consistent. Fix gh (auth / network / permissions) and re-run\\n` +\n ` pnpm wp-finish-upsert-pr. The new body is in:\\n ${bodyFile}`);\n }\n\n // The `gh`/push seams, protected so the spec can drive the ordering with no gh, no network, no repo.\n\n // The open PR whose head is `baseBranch`, or '' when there is none (or `gh` failed — treated the same,\n // so the create path then fails loudly rather than silently editing the wrong PR).\n protected findOpenPr(baseBranch: string): string {\n const result = spawnSync('gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n protected editPr(prNumber: string, title: string, bodyFile: string): boolean {\n return spawnSync('gh', ['pr', 'edit', prNumber, '--title', title, '--body-file', bodyFile], { stdio: 'inherit' }).status === 0;\n }\n\n protected createPr(baseBranch: string, title: string, bodyFile: string): boolean {\n return spawnSync('gh', ['pr', 'create', '--head', baseBranch, '--base', 'main', '--title', title, '--body-file', bodyFile], { stdio: 'inherit' }).status === 0;\n }\n\n // Throws (CliExitError, via runGitChecked) when the push fails — deliberately NOT caught here: the\n // consequence is already printed above, and swallowing it would report a PR that was never updated.\n protected push(baseBranch: string): void {\n this.gitExec.ensurePushed(baseBranch);\n }\n}\n"]}
|