@webpieces/pr-gate 0.4.533 → 0.4.535
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/workflow/finish-banner.d.ts +19 -0
- package/src/scripts/workflow/finish-banner.js +102 -17
- package/src/scripts/workflow/finish-banner.js.map +1 -1
- package/src/scripts/workflow/pr-merger.d.ts +41 -0
- package/src/scripts/workflow/pr-merger.js +65 -8
- package/src/scripts/workflow/pr-merger.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.535",
|
|
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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"directory": "packages/tooling/pr-gate"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@webpieces/rules-config": "0.4.
|
|
29
|
+
"@webpieces/rules-config": "0.4.535",
|
|
30
30
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
31
31
|
"inversify": "7.10.4",
|
|
32
32
|
"reflect-metadata": "0.2.2"
|
|
@@ -25,8 +25,27 @@ export declare class FinishBanner {
|
|
|
25
25
|
render(input: FinishBannerInput): string;
|
|
26
26
|
isDone(merge: MergeOutcome): boolean;
|
|
27
27
|
private header;
|
|
28
|
+
private behindHeader;
|
|
28
29
|
private whatIsOwed;
|
|
30
|
+
/**
|
|
31
|
+
* The BEHIND follow-up. Two rules govern every word here.
|
|
32
|
+
*
|
|
33
|
+
* FIRST: the remedy is the FULL ①②③, never a shortcut. `gh pr update-branch` looks like the obvious
|
|
34
|
+
* one-command fix and is a trap — it rewrites the REMOTE branch while every fork-point consumer
|
|
35
|
+
* (`ForkPoint.resolveForkPoint`, `nx affected --base=$(git merge-base ...)`, the review diff) computes
|
|
36
|
+
* against the LOCAL HEAD. That splits reality in two: stage ③ force-pushes local over the remote and
|
|
37
|
+
* silently reverts it, the recorded hash points describe a tree that is no longer the PR head, and the
|
|
38
|
+
* rebased tree never passes a build gate. Only ① moves the fork point AND records it; only ② rebuilds
|
|
39
|
+
* and re-receipts against the new one.
|
|
40
|
+
*
|
|
41
|
+
* SECOND: it ASKS, it does not order. An imperative command list is what turns an agent into a loop —
|
|
42
|
+
* it complies, main moves again, it complies again. Asking forces a stop at a human, which is the only
|
|
43
|
+
* thing that reliably terminates a race we cannot win by retrying.
|
|
44
|
+
*/
|
|
29
45
|
private behindRemedy;
|
|
46
|
+
private behindSituation;
|
|
47
|
+
private behindAsk;
|
|
48
|
+
private behindAskClose;
|
|
30
49
|
private doneNote;
|
|
31
50
|
/**
|
|
32
51
|
* The closing AI directive: the resolved PR's number/title/URL and an instruction to end the
|
|
@@ -67,37 +67,109 @@ let FinishBanner = class FinishBanner {
|
|
|
67
67
|
if (merge.result === pr_merger_1.MERGE_RESULT_LEFT_TO_HUMAN) {
|
|
68
68
|
return '✅ PR finished — posted for a human to merge (that is this repo\'s policy)\n';
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// NOT "PR NOT FINISHED". Everything this command owns SUCCEEDED — the branch is pushed, the body
|
|
71
|
+
// is written, the build gate is green. What happened is that another author landed on main
|
|
72
|
+
// between our fetch and our push. Reporting that as the author's failure sent agents off
|
|
73
|
+
// re-auditing their own diff and hunting build flakes for a race they did not cause.
|
|
74
|
+
if (merge.isBehind())
|
|
75
|
+
return this.behindHeader(merge);
|
|
73
76
|
return '⚠️ PR NOT FINISHED — the PR is up, but the merge did NOT happen\n';
|
|
74
77
|
}
|
|
78
|
+
behindHeader(merge) {
|
|
79
|
+
if (merge.result === pr_merger_1.MERGE_RESULT_BEHIND_CONFLICTING) {
|
|
80
|
+
return '⏸️ PR IS UP AND GREEN — someone landed on main first, and it CONFLICTS with your work\n';
|
|
81
|
+
}
|
|
82
|
+
if (merge.result === pr_merger_1.MERGE_RESULT_BEHIND_UNKNOWN) {
|
|
83
|
+
return '⏸️ PR IS UP AND GREEN — GitHub has not finished computing mergeability yet\n';
|
|
84
|
+
}
|
|
85
|
+
return '⏸️ PR IS UP AND GREEN — someone landed on main first (no conflicts with your work)\n';
|
|
86
|
+
}
|
|
75
87
|
// The follow-up block. '' for a done outcome — a finished run should not invent chores.
|
|
76
88
|
whatIsOwed(input) {
|
|
77
89
|
if (this.isDone(input.merge))
|
|
78
90
|
return this.doneNote(input.merge);
|
|
79
|
-
if (input.merge.
|
|
91
|
+
if (input.merge.isBehind())
|
|
80
92
|
return this.behindRemedy(input);
|
|
81
93
|
return '\n' + SEP +
|
|
82
94
|
' ⚠️ DO NOT report this PR as done. The merge failed for the reason in step 4 above.\n' +
|
|
83
95
|
' Fix that, then re-run: pnpm wp-finish-upsert-pr\n' +
|
|
84
96
|
` Confirm for yourself: gh pr view ${input.prNumber === '' ? '<n>' : input.prNumber} --json mergeable,mergeStateStatus,state\n`;
|
|
85
97
|
}
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The BEHIND follow-up. Two rules govern every word here.
|
|
100
|
+
*
|
|
101
|
+
* FIRST: the remedy is the FULL ①②③, never a shortcut. `gh pr update-branch` looks like the obvious
|
|
102
|
+
* one-command fix and is a trap — it rewrites the REMOTE branch while every fork-point consumer
|
|
103
|
+
* (`ForkPoint.resolveForkPoint`, `nx affected --base=$(git merge-base ...)`, the review diff) computes
|
|
104
|
+
* against the LOCAL HEAD. That splits reality in two: stage ③ force-pushes local over the remote and
|
|
105
|
+
* silently reverts it, the recorded hash points describe a tree that is no longer the PR head, and the
|
|
106
|
+
* rebased tree never passes a build gate. Only ① moves the fork point AND records it; only ② rebuilds
|
|
107
|
+
* and re-receipts against the new one.
|
|
108
|
+
*
|
|
109
|
+
* SECOND: it ASKS, it does not order. An imperative command list is what turns an agent into a loop —
|
|
110
|
+
* it complies, main moves again, it complies again. Asking forces a stop at a human, which is the only
|
|
111
|
+
* thing that reliably terminates a race we cannot win by retrying.
|
|
112
|
+
*/
|
|
88
113
|
behindRemedy(input) {
|
|
89
114
|
const num = input.prNumber === '' ? '<n>' : input.prNumber;
|
|
90
|
-
return '\n' + SEP +
|
|
91
|
-
'
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
'
|
|
97
|
-
'
|
|
115
|
+
return '\n' + SEP + this.behindSituation(input.merge) + '\n' +
|
|
116
|
+
' ⚠️ STOP HERE AND ASK THE HUMAN. Do NOT run these yourself — if main keeps moving,\n' +
|
|
117
|
+
' running them on your own is an infinite loop with a full build inside it.\n\n' +
|
|
118
|
+
' Ask, in your own words:\n' +
|
|
119
|
+
` "${this.behindAsk(input.merge)}\n` +
|
|
120
|
+
` ${this.behindAskClose(input.merge)}"\n\n` +
|
|
121
|
+
' Only once they say yes:\n\n' +
|
|
122
|
+
' pnpm wp-start-upsert-pr # 3-point merge from main — re-forks onto the new main\n' +
|
|
123
|
+
' pnpm wp-review-upsert-pr # re-validates the merge + REBUILDS on the new fork point\n' +
|
|
124
|
+
' pnpm wp-finish-upsert-pr # gated body + merge, now up to date\n\n' +
|
|
125
|
+
' Do NOT skip ②. It is the only stage that validates the new merge and rebuilds against\n' +
|
|
126
|
+
' the new fork point; skipping it publishes a PR whose tree was never gated.\n' +
|
|
127
|
+
' Do NOT reach for `gh pr update-branch`. It rewrites the REMOTE branch only, which stage\n' +
|
|
128
|
+
' ③ then force-pushes over — and it lands a tree no build gate ever saw.\n' +
|
|
98
129
|
` Verify independently, do not trust this banner:\n` +
|
|
99
130
|
` gh pr view ${num} --json mergeable,mergeStateStatus,state\n`;
|
|
100
131
|
}
|
|
132
|
+
// What is actually true right now, per kind. The CLEAN case gets the caveat that it may not even
|
|
133
|
+
// matter: "out of date" only blocks a merge on repos that require branches be up to date.
|
|
134
|
+
behindSituation(merge) {
|
|
135
|
+
if (merge.result === pr_merger_1.MERGE_RESULT_BEHIND_CONFLICTING) {
|
|
136
|
+
return ' Your PR is pushed, its body is written, and the build gate passed. Then someone\n' +
|
|
137
|
+
' else landed on main, and their change CONFLICTS with yours — same lines. Real\n' +
|
|
138
|
+
' resolution is owed, and if people keep landing ahead of you it genuinely repeats.\n' +
|
|
139
|
+
' That is inherent to concurrent editing, not a bug and not something you did wrong.\n';
|
|
140
|
+
}
|
|
141
|
+
if (merge.result === pr_merger_1.MERGE_RESULT_BEHIND_UNKNOWN) {
|
|
142
|
+
return ' Your PR is pushed, its body is written, and the build gate passed. GitHub has not\n' +
|
|
143
|
+
' finished computing mergeability yet (it is asynchronous, and we asked seconds after\n' +
|
|
144
|
+
' the push), so we do NOT know whether this conflicts. Re-check before doing anything:\n' +
|
|
145
|
+
' a few seconds later the answer is usually CLEAN and no work is owed at all.\n';
|
|
146
|
+
}
|
|
147
|
+
return ' Your PR is pushed, its body is written, and the build gate passed. Someone else simply\n' +
|
|
148
|
+
' landed on main first. There are NO conflicts — nobody touched your lines.\n' +
|
|
149
|
+
' This may not even matter: "out of date" blocks a merge only on repos that REQUIRE\n' +
|
|
150
|
+
' branches be up to date. If yours does not, this PR can merge as-is.\n';
|
|
151
|
+
}
|
|
152
|
+
// The one sentence to put to the human, in their terms.
|
|
153
|
+
behindAsk(merge) {
|
|
154
|
+
if (merge.result === pr_merger_1.MERGE_RESULT_BEHIND_CONFLICTING) {
|
|
155
|
+
return 'Someone beat me to landing on main and there are conflicts. We MUST run a\n' +
|
|
156
|
+
' 3-point merge so I can resolve them properly.';
|
|
157
|
+
}
|
|
158
|
+
if (merge.result === pr_merger_1.MERGE_RESULT_BEHIND_UNKNOWN) {
|
|
159
|
+
return 'Someone beat me to landing on main. GitHub has not said yet whether it\n' +
|
|
160
|
+
' conflicts, so I have not touched anything.';
|
|
161
|
+
}
|
|
162
|
+
return 'Someone beat me to landing on main. There are no conflicts, so this is just\n' +
|
|
163
|
+
' a re-sync — but it costs a full rebuild.';
|
|
164
|
+
}
|
|
165
|
+
// The actual question. UNKNOWN gets a DIFFERENT one: with mergeability still uncomputed, proposing a
|
|
166
|
+
// full re-run is proposing work we cannot yet show is needed — a re-check is free and often ends it.
|
|
167
|
+
behindAskClose(merge) {
|
|
168
|
+
if (merge.result === pr_merger_1.MERGE_RESULT_BEHIND_UNKNOWN) {
|
|
169
|
+
return 'Shall I re-check in a moment, or start the wp-*-upsert-pr process over?';
|
|
170
|
+
}
|
|
171
|
+
return 'May I start the wp-*-upsert-pr process over again?';
|
|
172
|
+
}
|
|
101
173
|
// A short, positive "you are free to stop" line for the two non-merged-but-fine outcomes, so an AI
|
|
102
174
|
// reading a queued PR does not go hunting for work that is not owed.
|
|
103
175
|
doneNote(merge) {
|
|
@@ -125,15 +197,28 @@ let FinishBanner = class FinishBanner {
|
|
|
125
197
|
const label = this.isDone(input.merge) ? input.title : `${input.title} — ${this.notDoneTag(input.merge)}`;
|
|
126
198
|
const preamble = this.isDone(input.merge)
|
|
127
199
|
? ''
|
|
128
|
-
: `AI — this PR is NOT done (${this.notDoneTag(input.merge)}). Your summary MUST say so plainly
|
|
129
|
-
|
|
200
|
+
: `AI — this PR is NOT done (${this.notDoneTag(input.merge)}). Your summary MUST say so plainly.\n` +
|
|
201
|
+
(input.merge.isBehind()
|
|
202
|
+
? `MUST END BY ASKING the human for permission to re-run the flow, and MUST NOT run any of\n` +
|
|
203
|
+
`the commands above until they answer. Say plainly that nothing they did caused this —\n` +
|
|
204
|
+
`another author landed on main first — and that the PR itself is pushed and gate-green.\n\n`
|
|
205
|
+
: `MUST include the remedy commands printed above. Do not describe this run as complete.\n\n`);
|
|
130
206
|
return SEP + `🔗 PR #${input.prNumber}: ${input.title}\n ${input.prUrl}\n\n` + SEP + preamble +
|
|
131
207
|
`AI — the LAST line of your summary to the user MUST be EXACTLY this clickable link, with\n` +
|
|
132
208
|
`NOTHING printed after it (no sign-off, no next steps, no blank commentary):\n\n` +
|
|
133
209
|
` [#${input.prNumber} ${label}](${input.prUrl})\n\n`;
|
|
134
210
|
}
|
|
211
|
+
// The tag rides INSIDE the clickable link, which the directive above forces to be the last line the
|
|
212
|
+
// AI prints — so it is the one piece of wording guaranteed to reach the user. It says who is waiting
|
|
213
|
+
// on whom: for every BEHIND flavour the answer is "a human", not "more automation".
|
|
135
214
|
notDoneTag(merge) {
|
|
136
|
-
|
|
215
|
+
if (merge.result === pr_merger_1.MERGE_RESULT_BEHIND_CONFLICTING)
|
|
216
|
+
return 'NOT MERGED — main moved and it conflicts, needs your OK to re-sync';
|
|
217
|
+
if (merge.result === pr_merger_1.MERGE_RESULT_BEHIND_UNKNOWN)
|
|
218
|
+
return 'NOT MERGED — main moved, GitHub still computing mergeability';
|
|
219
|
+
if (merge.isBehind())
|
|
220
|
+
return 'NOT MERGED — main moved (no conflicts), needs your OK to re-sync';
|
|
221
|
+
return 'NOT MERGED';
|
|
137
222
|
}
|
|
138
223
|
};
|
|
139
224
|
exports.FinishBanner = FinishBanner;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finish-banner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/finish-banner.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n MergeOutcome, MERGE_RESULT_MERGED, MERGE_RESULT_AUTO_QUEUED, MERGE_RESULT_LEFT_TO_HUMAN,\n MERGE_RESULT_BEHIND,\n} from './pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Everything the closing block of wp-finish-upsert-pr needs to describe what actually happened. Both\n// `prNumber` and `prUrl` are '' when the PR could not be resolved (e.g. `gh pr create` failed).\nexport class FinishBannerInput {\n prNumber: string;\n prUrl: string;\n title: string;\n base: string;\n merge: MergeOutcome;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(prNumber: string, prUrl: string, title: string, base: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.prUrl = prUrl;\n this.title = title;\n this.base = base;\n this.merge = merge;\n }\n}\n\n/**\n * Renders the closing block of `wp-finish-upsert-pr` — and it is the FRAME, not the merge message, that\n * this class exists to get right.\n *\n * PrMerger has long been honest in its `message`. The banner around it was not: it printed a hard-coded\n * `✅ PR finished` on every path, so a run whose merge failed still looked like a completed one at a\n * glance. The worst case is `mergeStateStatus: BEHIND` — unlike BLOCKED (waiting on checks), which\n * auto-merge resolves on its own, a BEHIND branch NEVER lands. Under a green checkmark, agents walked\n * away from stranded PRs. Three of them independently reported the output as untrustworthy.\n *\n * So the header is derived from `MergeOutcome.result`, and a not-done outcome is loud, distinct, and\n * carries the exact commands that fix it — including in the clickable-link directive, whose whole job\n * is to be the last thing the AI says.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class FinishBanner {\n // The full closing recap: header keyed to the real outcome, the four things this command did, and —\n // when the PR is not done — what must happen next.\n render(input: FinishBannerInput): string {\n const prNum = input.prNumber;\n return '\\n' + SEP + this.header(input.merge) + 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: \"${input.title}\"\\n` +\n ` 3. force-pushed your work to origin/${input.base} (after the body, so CI reads the right token)\\n` +\n ` 4. ${input.merge.message}\\n` +\n ` You are on ${input.base} — same name as the remote branch and the PR head.\\n` +\n this.whatIsOwed(input) + '\\n';\n }\n\n // TRUE only for the outcomes where nothing further is owed: merged, queued behind green-able checks,\n // or deliberately left for a human. Everything else — BEHIND, config mismatch, gh failure, no PR —\n // is unfinished work, and the banner must not decorate it with a checkmark.\n isDone(merge: MergeOutcome): boolean {\n return merge.result === MERGE_RESULT_MERGED\n || merge.result === MERGE_RESULT_AUTO_QUEUED\n || merge.result === MERGE_RESULT_LEFT_TO_HUMAN;\n }\n\n private header(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_MERGED) return '✅ PR finished AND MERGED — here is exactly what I did\\n';\n if (merge.result === MERGE_RESULT_AUTO_QUEUED) {\n return '✅ PR finished — auto-merge is ON; it lands itself when the checks pass\\n';\n }\n if (merge.result === MERGE_RESULT_LEFT_TO_HUMAN) {\n return '✅ PR finished — posted for a human to merge (that is this repo\\'s policy)\\n';\n }\n if (merge.result === MERGE_RESULT_BEHIND) {\n return '⛔ PR NOT FINISHED — the branch is BEHIND main and will NEVER merge on its own\\n';\n }\n return '⚠️ PR NOT FINISHED — the PR is up, but the merge did NOT happen\\n';\n }\n\n // The follow-up block. '' for a done outcome — a finished run should not invent chores.\n private whatIsOwed(input: FinishBannerInput): string {\n if (this.isDone(input.merge)) return this.doneNote(input.merge);\n if (input.merge.result === MERGE_RESULT_BEHIND) return this.behindRemedy(input);\n return '\\n' + SEP +\n ' ⚠️ DO NOT report this PR as done. The merge failed for the reason in step 4 above.\\n' +\n ' Fix that, then re-run: pnpm wp-finish-upsert-pr\\n' +\n ` Confirm for yourself: gh pr view ${input.prNumber === '' ? '<n>' : input.prNumber} --json mergeable,mergeStateStatus,state\\n`;\n }\n\n // The ONE remedy that actually clears BEHIND. wp-start-upsert-pr is what re-syncs from main (3-point\n // merge); finishing again re-runs the gate and re-attempts the merge on an up-to-date branch.\n private behindRemedy(input: FinishBannerInput): string {\n const num = input.prNumber === '' ? '<n>' : input.prNumber;\n return '\\n' + SEP +\n ' ⛔ DO NOT WALK AWAY — this PR is STRANDED, not pending.\\n\\n' +\n ' BEHIND means the head branch is out of date with main. Auto-merge does NOT update\\n' +\n ' branches, so unlike a BLOCKED (checks-running) PR this one never resolves itself. Main\\n' +\n ' moved while you worked; re-sync and finish again, from THIS worktree:\\n\\n' +\n ' pnpm wp-start-upsert-pr # 3-point merge from main — this is what clears BEHIND\\n' +\n ' pnpm wp-finish-upsert-pr # build gate + gated body + merge, now up to date\\n\\n' +\n ' (If the re-sync conflicts, wp-start-upsert-pr walks you through resolving it.)\\n' +\n ` Verify independently, do not trust this banner:\\n` +\n ` gh pr view ${num} --json mergeable,mergeStateStatus,state\\n`;\n }\n\n // A short, positive \"you are free to stop\" line for the two non-merged-but-fine outcomes, so an AI\n // reading a queued PR does not go hunting for work that is not owed.\n private doneNote(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_AUTO_QUEUED) {\n return ' Nothing else is owed: GitHub lands it when the checks go green. You can stop here.\\n';\n }\n if (merge.result === MERGE_RESULT_LEFT_TO_HUMAN) {\n return ' Nothing else is owed by the tooling — a person merges it. You can stop here.\\n';\n }\n return '';\n }\n\n /**\n * The closing AI directive: the resolved PR's number/title/URL and an instruction to end the\n * user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it.\n *\n * The link text CARRIES the outcome. Ending on a bare cheerful link is exactly how a stranded PR got\n * reported as finished, and the directive says the link must be last — so the truth has to live\n * inside the link itself, not merely above it.\n *\n * '' when the PR could not be resolved: better to say nothing than to point at a link that is not real.\n */\n linkDirective(input: FinishBannerInput): string {\n if (input.prNumber === '' || input.prUrl === '') return '';\n const label = this.isDone(input.merge) ? input.title : `${input.title} — ${this.notDoneTag(input.merge)}`;\n const preamble = this.isDone(input.merge)\n ? ''\n : `AI — this PR is NOT done (${this.notDoneTag(input.merge)}). Your summary MUST say so plainly and\\n` +\n `MUST include the remedy commands printed above. Do not describe this run as complete.\\n\\n`;\n return SEP + `🔗 PR #${input.prNumber}: ${input.title}\\n ${input.prUrl}\\n\\n` + SEP + preamble +\n `AI — the LAST line of your summary to the user MUST be EXACTLY this clickable link, with\\n` +\n `NOTHING printed after it (no sign-off, no next steps, no blank commentary):\\n\\n` +\n ` [#${input.prNumber} ${label}](${input.prUrl})\\n\\n`;\n }\n\n private notDoneTag(merge: MergeOutcome): string {\n return merge.result === MERGE_RESULT_BEHIND ? 'NOT MERGED — BEHIND main, needs a re-sync' : 'NOT MERGED';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"finish-banner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/finish-banner.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n MergeOutcome, MERGE_RESULT_MERGED, MERGE_RESULT_AUTO_QUEUED, MERGE_RESULT_LEFT_TO_HUMAN,\n MERGE_RESULT_BEHIND_CONFLICTING, MERGE_RESULT_BEHIND_UNKNOWN,\n} from './pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Everything the closing block of wp-finish-upsert-pr needs to describe what actually happened. Both\n// `prNumber` and `prUrl` are '' when the PR could not be resolved (e.g. `gh pr create` failed).\nexport class FinishBannerInput {\n prNumber: string;\n prUrl: string;\n title: string;\n base: string;\n merge: MergeOutcome;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(prNumber: string, prUrl: string, title: string, base: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.prUrl = prUrl;\n this.title = title;\n this.base = base;\n this.merge = merge;\n }\n}\n\n/**\n * Renders the closing block of `wp-finish-upsert-pr` — and it is the FRAME, not the merge message, that\n * this class exists to get right.\n *\n * PrMerger has long been honest in its `message`. The banner around it was not: it printed a hard-coded\n * `✅ PR finished` on every path, so a run whose merge failed still looked like a completed one at a\n * glance. The worst case is `mergeStateStatus: BEHIND` — unlike BLOCKED (waiting on checks), which\n * auto-merge resolves on its own, a BEHIND branch NEVER lands. Under a green checkmark, agents walked\n * away from stranded PRs. Three of them independently reported the output as untrustworthy.\n *\n * So the header is derived from `MergeOutcome.result`, and a not-done outcome is loud, distinct, and\n * carries the exact commands that fix it — including in the clickable-link directive, whose whole job\n * is to be the last thing the AI says.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class FinishBanner {\n // The full closing recap: header keyed to the real outcome, the four things this command did, and —\n // when the PR is not done — what must happen next.\n render(input: FinishBannerInput): string {\n const prNum = input.prNumber;\n return '\\n' + SEP + this.header(input.merge) + 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: \"${input.title}\"\\n` +\n ` 3. force-pushed your work to origin/${input.base} (after the body, so CI reads the right token)\\n` +\n ` 4. ${input.merge.message}\\n` +\n ` You are on ${input.base} — same name as the remote branch and the PR head.\\n` +\n this.whatIsOwed(input) + '\\n';\n }\n\n // TRUE only for the outcomes where nothing further is owed: merged, queued behind green-able checks,\n // or deliberately left for a human. Everything else — BEHIND, config mismatch, gh failure, no PR —\n // is unfinished work, and the banner must not decorate it with a checkmark.\n isDone(merge: MergeOutcome): boolean {\n return merge.result === MERGE_RESULT_MERGED\n || merge.result === MERGE_RESULT_AUTO_QUEUED\n || merge.result === MERGE_RESULT_LEFT_TO_HUMAN;\n }\n\n private header(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_MERGED) return '✅ PR finished AND MERGED — here is exactly what I did\\n';\n if (merge.result === MERGE_RESULT_AUTO_QUEUED) {\n return '✅ PR finished — auto-merge is ON; it lands itself when the checks pass\\n';\n }\n if (merge.result === MERGE_RESULT_LEFT_TO_HUMAN) {\n return '✅ PR finished — posted for a human to merge (that is this repo\\'s policy)\\n';\n }\n // NOT \"PR NOT FINISHED\". Everything this command owns SUCCEEDED — the branch is pushed, the body\n // is written, the build gate is green. What happened is that another author landed on main\n // between our fetch and our push. Reporting that as the author's failure sent agents off\n // re-auditing their own diff and hunting build flakes for a race they did not cause.\n if (merge.isBehind()) return this.behindHeader(merge);\n return '⚠️ PR NOT FINISHED — the PR is up, but the merge did NOT happen\\n';\n }\n\n private behindHeader(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) {\n return '⏸️ PR IS UP AND GREEN — someone landed on main first, and it CONFLICTS with your work\\n';\n }\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return '⏸️ PR IS UP AND GREEN — GitHub has not finished computing mergeability yet\\n';\n }\n return '⏸️ PR IS UP AND GREEN — someone landed on main first (no conflicts with your work)\\n';\n }\n\n // The follow-up block. '' for a done outcome — a finished run should not invent chores.\n private whatIsOwed(input: FinishBannerInput): string {\n if (this.isDone(input.merge)) return this.doneNote(input.merge);\n if (input.merge.isBehind()) return this.behindRemedy(input);\n return '\\n' + SEP +\n ' ⚠️ DO NOT report this PR as done. The merge failed for the reason in step 4 above.\\n' +\n ' Fix that, then re-run: pnpm wp-finish-upsert-pr\\n' +\n ` Confirm for yourself: gh pr view ${input.prNumber === '' ? '<n>' : input.prNumber} --json mergeable,mergeStateStatus,state\\n`;\n }\n\n /**\n * The BEHIND follow-up. Two rules govern every word here.\n *\n * FIRST: the remedy is the FULL ①②③, never a shortcut. `gh pr update-branch` looks like the obvious\n * one-command fix and is a trap — it rewrites the REMOTE branch while every fork-point consumer\n * (`ForkPoint.resolveForkPoint`, `nx affected --base=$(git merge-base ...)`, the review diff) computes\n * against the LOCAL HEAD. That splits reality in two: stage ③ force-pushes local over the remote and\n * silently reverts it, the recorded hash points describe a tree that is no longer the PR head, and the\n * rebased tree never passes a build gate. Only ① moves the fork point AND records it; only ② rebuilds\n * and re-receipts against the new one.\n *\n * SECOND: it ASKS, it does not order. An imperative command list is what turns an agent into a loop —\n * it complies, main moves again, it complies again. Asking forces a stop at a human, which is the only\n * thing that reliably terminates a race we cannot win by retrying.\n */\n private behindRemedy(input: FinishBannerInput): string {\n const num = input.prNumber === '' ? '<n>' : input.prNumber;\n return '\\n' + SEP + this.behindSituation(input.merge) + '\\n' +\n ' ⚠️ STOP HERE AND ASK THE HUMAN. Do NOT run these yourself — if main keeps moving,\\n' +\n ' running them on your own is an infinite loop with a full build inside it.\\n\\n' +\n ' Ask, in your own words:\\n' +\n ` \"${this.behindAsk(input.merge)}\\n` +\n ` ${this.behindAskClose(input.merge)}\"\\n\\n` +\n ' Only once they say yes:\\n\\n' +\n ' pnpm wp-start-upsert-pr # 3-point merge from main — re-forks onto the new main\\n' +\n ' pnpm wp-review-upsert-pr # re-validates the merge + REBUILDS on the new fork point\\n' +\n ' pnpm wp-finish-upsert-pr # gated body + merge, now up to date\\n\\n' +\n ' Do NOT skip ②. It is the only stage that validates the new merge and rebuilds against\\n' +\n ' the new fork point; skipping it publishes a PR whose tree was never gated.\\n' +\n ' Do NOT reach for `gh pr update-branch`. It rewrites the REMOTE branch only, which stage\\n' +\n ' ③ then force-pushes over — and it lands a tree no build gate ever saw.\\n' +\n ` Verify independently, do not trust this banner:\\n` +\n ` gh pr view ${num} --json mergeable,mergeStateStatus,state\\n`;\n }\n\n // What is actually true right now, per kind. The CLEAN case gets the caveat that it may not even\n // matter: \"out of date\" only blocks a merge on repos that require branches be up to date.\n private behindSituation(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) {\n return ' Your PR is pushed, its body is written, and the build gate passed. Then someone\\n' +\n ' else landed on main, and their change CONFLICTS with yours — same lines. Real\\n' +\n ' resolution is owed, and if people keep landing ahead of you it genuinely repeats.\\n' +\n ' That is inherent to concurrent editing, not a bug and not something you did wrong.\\n';\n }\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return ' Your PR is pushed, its body is written, and the build gate passed. GitHub has not\\n' +\n ' finished computing mergeability yet (it is asynchronous, and we asked seconds after\\n' +\n ' the push), so we do NOT know whether this conflicts. Re-check before doing anything:\\n' +\n ' a few seconds later the answer is usually CLEAN and no work is owed at all.\\n';\n }\n return ' Your PR is pushed, its body is written, and the build gate passed. Someone else simply\\n' +\n ' landed on main first. There are NO conflicts — nobody touched your lines.\\n' +\n ' This may not even matter: \"out of date\" blocks a merge only on repos that REQUIRE\\n' +\n ' branches be up to date. If yours does not, this PR can merge as-is.\\n';\n }\n\n // The one sentence to put to the human, in their terms.\n private behindAsk(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) {\n return 'Someone beat me to landing on main and there are conflicts. We MUST run a\\n' +\n ' 3-point merge so I can resolve them properly.';\n }\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return 'Someone beat me to landing on main. GitHub has not said yet whether it\\n' +\n ' conflicts, so I have not touched anything.';\n }\n return 'Someone beat me to landing on main. There are no conflicts, so this is just\\n' +\n ' a re-sync — but it costs a full rebuild.';\n }\n\n // The actual question. UNKNOWN gets a DIFFERENT one: with mergeability still uncomputed, proposing a\n // full re-run is proposing work we cannot yet show is needed — a re-check is free and often ends it.\n private behindAskClose(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return 'Shall I re-check in a moment, or start the wp-*-upsert-pr process over?';\n }\n return 'May I start the wp-*-upsert-pr process over again?';\n }\n\n // A short, positive \"you are free to stop\" line for the two non-merged-but-fine outcomes, so an AI\n // reading a queued PR does not go hunting for work that is not owed.\n private doneNote(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_AUTO_QUEUED) {\n return ' Nothing else is owed: GitHub lands it when the checks go green. You can stop here.\\n';\n }\n if (merge.result === MERGE_RESULT_LEFT_TO_HUMAN) {\n return ' Nothing else is owed by the tooling — a person merges it. You can stop here.\\n';\n }\n return '';\n }\n\n /**\n * The closing AI directive: the resolved PR's number/title/URL and an instruction to end the\n * user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it.\n *\n * The link text CARRIES the outcome. Ending on a bare cheerful link is exactly how a stranded PR got\n * reported as finished, and the directive says the link must be last — so the truth has to live\n * inside the link itself, not merely above it.\n *\n * '' when the PR could not be resolved: better to say nothing than to point at a link that is not real.\n */\n linkDirective(input: FinishBannerInput): string {\n if (input.prNumber === '' || input.prUrl === '') return '';\n const label = this.isDone(input.merge) ? input.title : `${input.title} — ${this.notDoneTag(input.merge)}`;\n const preamble = this.isDone(input.merge)\n ? ''\n : `AI — this PR is NOT done (${this.notDoneTag(input.merge)}). Your summary MUST say so plainly.\\n` +\n (input.merge.isBehind()\n ? `MUST END BY ASKING the human for permission to re-run the flow, and MUST NOT run any of\\n` +\n `the commands above until they answer. Say plainly that nothing they did caused this —\\n` +\n `another author landed on main first — and that the PR itself is pushed and gate-green.\\n\\n`\n : `MUST include the remedy commands printed above. Do not describe this run as complete.\\n\\n`);\n return SEP + `🔗 PR #${input.prNumber}: ${input.title}\\n ${input.prUrl}\\n\\n` + SEP + preamble +\n `AI — the LAST line of your summary to the user MUST be EXACTLY this clickable link, with\\n` +\n `NOTHING printed after it (no sign-off, no next steps, no blank commentary):\\n\\n` +\n ` [#${input.prNumber} ${label}](${input.prUrl})\\n\\n`;\n }\n\n // The tag rides INSIDE the clickable link, which the directive above forces to be the last line the\n // AI prints — so it is the one piece of wording guaranteed to reach the user. It says who is waiting\n // on whom: for every BEHIND flavour the answer is \"a human\", not \"more automation\".\n private notDoneTag(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) return 'NOT MERGED — main moved and it conflicts, needs your OK to re-sync';\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) return 'NOT MERGED — main moved, GitHub still computing mergeability';\n if (merge.isBehind()) return 'NOT MERGED — main moved (no conflicts), needs your OK to re-sync';\n return 'NOT MERGED';\n }\n}\n"]}
|
|
@@ -15,6 +15,25 @@ export declare const MERGE_RESULT_LEFT_TO_HUMAN = "LEFT_TO_HUMAN";
|
|
|
15
15
|
* green "✅ PR finished" and get abandoned.
|
|
16
16
|
*/
|
|
17
17
|
export declare const MERGE_RESULT_BEHIND = "BEHIND";
|
|
18
|
+
/**
|
|
19
|
+
* BEHIND *and* GitHub says the tree merges cleanly (`mergeable: MERGEABLE`). Nobody touched the same
|
|
20
|
+
* lines — somebody simply landed on main first. One clean re-run of ①②③ converges. Split out from the
|
|
21
|
+
* conflicting case because the two need completely different sentences: this one is not the author's
|
|
22
|
+
* problem to solve, it is a queue collision, and telling them "resolve the conflict" is a lie.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MERGE_RESULT_BEHIND_CLEAN = "BEHIND_CLEAN";
|
|
25
|
+
/**
|
|
26
|
+
* BEHIND *and* `mergeable: CONFLICTING` — the landed work and this branch touch the same lines. Real
|
|
27
|
+
* human/AI judgement is owed, and if others keep landing first it genuinely repeats. That is inherent to
|
|
28
|
+
* concurrent editing, not a defect, and the banner says so rather than pretending a re-run is free.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MERGE_RESULT_BEHIND_CONFLICTING = "BEHIND_CONFLICTING";
|
|
31
|
+
/**
|
|
32
|
+
* BEHIND but `mergeable: UNKNOWN` (or unreadable). GitHub computes mergeability ASYNCHRONOUSLY and we ask
|
|
33
|
+
* moments after a force-push, so UNKNOWN is the EXPECTED answer, not an error. Diagnosing from it would be
|
|
34
|
+
* guessing, so this outcome asks for a re-check instead of prescribing a remedy.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MERGE_RESULT_BEHIND_UNKNOWN = "BEHIND_UNKNOWN";
|
|
18
37
|
/** Not merged for some other reason (config mismatch, gh error, no PR at all). Read `message`. */
|
|
19
38
|
export declare const MERGE_RESULT_FAILED = "FAILED";
|
|
20
39
|
export declare class MergeOutcome {
|
|
@@ -23,6 +42,7 @@ export declare class MergeOutcome {
|
|
|
23
42
|
message: string;
|
|
24
43
|
result: string;
|
|
25
44
|
constructor(merged: boolean, autoMergeEnabled: boolean, message: string, result: string);
|
|
45
|
+
isBehind(): boolean;
|
|
26
46
|
}
|
|
27
47
|
export declare class PrMergeState {
|
|
28
48
|
mergeable: string;
|
|
@@ -30,6 +50,19 @@ export declare class PrMergeState {
|
|
|
30
50
|
state: string;
|
|
31
51
|
constructor(mergeable: string, mergeStateStatus: string, state: string);
|
|
32
52
|
isBehind(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* WHICH kind of BEHIND this is, from the `mergeable` field we have always fetched and never read.
|
|
55
|
+
*
|
|
56
|
+
* This is the whole point of the split. "Out of date" and "conflicting" are different situations with
|
|
57
|
+
* different costs, and collapsing them told every author the expensive story: a clean queue collision
|
|
58
|
+
* — nobody touched your lines, somebody just landed first — got reported in the same alarming words as
|
|
59
|
+
* a genuine textual conflict.
|
|
60
|
+
*
|
|
61
|
+
* UNKNOWN is its own answer, never folded into CLEAN. GitHub computes mergeability asynchronously and
|
|
62
|
+
* we ask seconds after a force-push, so UNKNOWN is the ordinary reply in exactly our situation.
|
|
63
|
+
* Treating it as "no conflicts" would promise a clean re-run we cannot see.
|
|
64
|
+
*/
|
|
65
|
+
behindKind(): string;
|
|
33
66
|
describe(): string;
|
|
34
67
|
}
|
|
35
68
|
/**
|
|
@@ -65,6 +98,14 @@ export declare class PrMerger {
|
|
|
65
98
|
* is NOT success either, and reporting it as such is precisely the bug this whole file guards.
|
|
66
99
|
*/
|
|
67
100
|
private fallBackToAutoMerge;
|
|
101
|
+
/**
|
|
102
|
+
* The one outcome that looks queued but is stranded. `queued` says whether auto-merge did get enabled,
|
|
103
|
+
* because "parked forever" and "not queued at all" need different sentences — but neither is done.
|
|
104
|
+
*
|
|
105
|
+
* The `result` now carries WHICH kind of behind, so the banner can stop describing a queue collision
|
|
106
|
+
* in the vocabulary of a merge conflict. The wording here is deliberately blame-free: nothing the
|
|
107
|
+
* author did caused this, and the PR itself is in perfectly good shape — pushed, bodied, gate-green.
|
|
108
|
+
*/
|
|
68
109
|
private behindOutcome;
|
|
69
110
|
private announceDirectFailure;
|
|
70
111
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GhResult = exports.PrMerger = exports.PrMergeState = exports.MergeOutcome = exports.MERGE_RESULT_FAILED = exports.MERGE_RESULT_BEHIND = exports.MERGE_RESULT_LEFT_TO_HUMAN = exports.MERGE_RESULT_AUTO_QUEUED = exports.MERGE_RESULT_MERGED = void 0;
|
|
3
|
+
exports.GhResult = exports.PrMerger = exports.PrMergeState = exports.MergeOutcome = exports.MERGE_RESULT_FAILED = exports.MERGE_RESULT_BEHIND_UNKNOWN = exports.MERGE_RESULT_BEHIND_CONFLICTING = exports.MERGE_RESULT_BEHIND_CLEAN = exports.MERGE_RESULT_BEHIND = exports.MERGE_RESULT_LEFT_TO_HUMAN = exports.MERGE_RESULT_AUTO_QUEUED = exports.MERGE_RESULT_MERGED = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
6
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
@@ -22,10 +22,33 @@ exports.MERGE_RESULT_LEFT_TO_HUMAN = 'LEFT_TO_HUMAN';
|
|
|
22
22
|
* green "✅ PR finished" and get abandoned.
|
|
23
23
|
*/
|
|
24
24
|
exports.MERGE_RESULT_BEHIND = 'BEHIND';
|
|
25
|
+
/**
|
|
26
|
+
* BEHIND *and* GitHub says the tree merges cleanly (`mergeable: MERGEABLE`). Nobody touched the same
|
|
27
|
+
* lines — somebody simply landed on main first. One clean re-run of ①②③ converges. Split out from the
|
|
28
|
+
* conflicting case because the two need completely different sentences: this one is not the author's
|
|
29
|
+
* problem to solve, it is a queue collision, and telling them "resolve the conflict" is a lie.
|
|
30
|
+
*/
|
|
31
|
+
exports.MERGE_RESULT_BEHIND_CLEAN = 'BEHIND_CLEAN';
|
|
32
|
+
/**
|
|
33
|
+
* BEHIND *and* `mergeable: CONFLICTING` — the landed work and this branch touch the same lines. Real
|
|
34
|
+
* human/AI judgement is owed, and if others keep landing first it genuinely repeats. That is inherent to
|
|
35
|
+
* concurrent editing, not a defect, and the banner says so rather than pretending a re-run is free.
|
|
36
|
+
*/
|
|
37
|
+
exports.MERGE_RESULT_BEHIND_CONFLICTING = 'BEHIND_CONFLICTING';
|
|
38
|
+
/**
|
|
39
|
+
* BEHIND but `mergeable: UNKNOWN` (or unreadable). GitHub computes mergeability ASYNCHRONOUSLY and we ask
|
|
40
|
+
* moments after a force-push, so UNKNOWN is the EXPECTED answer, not an error. Diagnosing from it would be
|
|
41
|
+
* guessing, so this outcome asks for a re-check instead of prescribing a remedy.
|
|
42
|
+
*/
|
|
43
|
+
exports.MERGE_RESULT_BEHIND_UNKNOWN = 'BEHIND_UNKNOWN';
|
|
25
44
|
/** Not merged for some other reason (config mismatch, gh error, no PR at all). Read `message`. */
|
|
26
45
|
exports.MERGE_RESULT_FAILED = 'FAILED';
|
|
27
46
|
// The `mergeStateStatus` value GitHub reports for "head branch is not up to date with the base branch".
|
|
28
47
|
const GH_STATE_BEHIND = 'BEHIND';
|
|
48
|
+
// `mergeable` values. GitHub returns UNKNOWN while it is still computing the merge — which is most of the
|
|
49
|
+
// time in the seconds after a push — so UNKNOWN must never be read as "no conflicts".
|
|
50
|
+
const GH_MERGEABLE_CLEAN = 'MERGEABLE';
|
|
51
|
+
const GH_MERGEABLE_CONFLICTING = 'CONFLICTING';
|
|
29
52
|
// What actually happened when we tried to land the squash merge. `message` is printed VERBATIM in the
|
|
30
53
|
// final wp-finish-upsert-pr summary, so a merge that did not happen can never be reported as done —
|
|
31
54
|
// the old code ignored `spawnSync().status` entirely and printed "✅ PR finished" even when `gh pr
|
|
@@ -44,6 +67,16 @@ class MergeOutcome {
|
|
|
44
67
|
this.message = message;
|
|
45
68
|
this.result = result;
|
|
46
69
|
}
|
|
70
|
+
// TRUE for every flavour of "the branch is out of date with main", so the banner can ask that one
|
|
71
|
+
// question once instead of listing four constants at each branch point. Kept here rather than as a
|
|
72
|
+
// module function because `no-function-outside-class` forbids the latter — and PrMergeState below
|
|
73
|
+
// already sets the precedent that a verdict class answers questions about itself.
|
|
74
|
+
isBehind() {
|
|
75
|
+
return this.result === exports.MERGE_RESULT_BEHIND
|
|
76
|
+
|| this.result === exports.MERGE_RESULT_BEHIND_CLEAN
|
|
77
|
+
|| this.result === exports.MERGE_RESULT_BEHIND_CONFLICTING
|
|
78
|
+
|| this.result === exports.MERGE_RESULT_BEHIND_UNKNOWN;
|
|
79
|
+
}
|
|
47
80
|
}
|
|
48
81
|
exports.MergeOutcome = MergeOutcome;
|
|
49
82
|
// GitHub's own verdict on the PR, straight from `gh pr view --json mergeable,mergeStateStatus,state`.
|
|
@@ -60,6 +93,25 @@ class PrMergeState {
|
|
|
60
93
|
isBehind() {
|
|
61
94
|
return this.mergeStateStatus === GH_STATE_BEHIND;
|
|
62
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* WHICH kind of BEHIND this is, from the `mergeable` field we have always fetched and never read.
|
|
98
|
+
*
|
|
99
|
+
* This is the whole point of the split. "Out of date" and "conflicting" are different situations with
|
|
100
|
+
* different costs, and collapsing them told every author the expensive story: a clean queue collision
|
|
101
|
+
* — nobody touched your lines, somebody just landed first — got reported in the same alarming words as
|
|
102
|
+
* a genuine textual conflict.
|
|
103
|
+
*
|
|
104
|
+
* UNKNOWN is its own answer, never folded into CLEAN. GitHub computes mergeability asynchronously and
|
|
105
|
+
* we ask seconds after a force-push, so UNKNOWN is the ordinary reply in exactly our situation.
|
|
106
|
+
* Treating it as "no conflicts" would promise a clean re-run we cannot see.
|
|
107
|
+
*/
|
|
108
|
+
behindKind() {
|
|
109
|
+
if (this.mergeable === GH_MERGEABLE_CLEAN)
|
|
110
|
+
return exports.MERGE_RESULT_BEHIND_CLEAN;
|
|
111
|
+
if (this.mergeable === GH_MERGEABLE_CONFLICTING)
|
|
112
|
+
return exports.MERGE_RESULT_BEHIND_CONFLICTING;
|
|
113
|
+
return exports.MERGE_RESULT_BEHIND_UNKNOWN;
|
|
114
|
+
}
|
|
63
115
|
// One-line rendering for the failure message, or '' when GitHub could not be asked.
|
|
64
116
|
describe() {
|
|
65
117
|
if (this.mergeStateStatus === '')
|
|
@@ -155,14 +207,19 @@ let PrMerger = class PrMerger {
|
|
|
155
207
|
return this.behindOutcome(state, true);
|
|
156
208
|
return new MergeOutcome(false, true, `enabled auto-merge — it will squash-merge as "${subject}" when the checks pass`, exports.MERGE_RESULT_AUTO_QUEUED);
|
|
157
209
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
210
|
+
/**
|
|
211
|
+
* The one outcome that looks queued but is stranded. `queued` says whether auto-merge did get enabled,
|
|
212
|
+
* because "parked forever" and "not queued at all" need different sentences — but neither is done.
|
|
213
|
+
*
|
|
214
|
+
* The `result` now carries WHICH kind of behind, so the banner can stop describing a queue collision
|
|
215
|
+
* in the vocabulary of a merge conflict. The wording here is deliberately blame-free: nothing the
|
|
216
|
+
* author did caused this, and the PR itself is in perfectly good shape — pushed, bodied, gate-green.
|
|
217
|
+
*/
|
|
161
218
|
behindOutcome(state, queued) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
`
|
|
165
|
-
|
|
219
|
+
const parked = queued ? 'Auto-merge is enabled but parked.' : 'Nothing is queued.';
|
|
220
|
+
return new MergeOutcome(false, queued, `did NOT merge — someone else landed on main first, so GitHub wants this branch rebuilt on\n` +
|
|
221
|
+
` top of theirs before it will merge (${state.describe()}).\n` +
|
|
222
|
+
` This does NOT self-heal: auto-merge never updates your branch. ${parked}`, state.behindKind());
|
|
166
223
|
}
|
|
167
224
|
// Reprint the expected first-attempt failure as CONTEXT, not as a verdict. gh's own `X …` line is
|
|
168
225
|
// captured (never inherited) so this framing is the only thing on screen.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-merger.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/pr-merger.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { MERGE_MODE_AUTO, MERGE_MODE_NONE } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * The five terminal shapes a merge attempt can take. The CALLER branches on these — a `message` string\n * is for humans to read, never for code to pattern-match, and the banner that frames it must be able to\n * tell \"done\" from \"stranded\" without parsing prose.\n */\nexport const MERGE_RESULT_MERGED = 'MERGED';\n/** Not merged YET, but queued: auto-merge is enabled and lands it when the checks pass. Self-healing. */\nexport const MERGE_RESULT_AUTO_QUEUED = 'AUTO_QUEUED';\n/** Not merged BY DESIGN: pr-gate.mergeMode is not AUTO, so a person merges. Nothing is owed here. */\nexport const MERGE_RESULT_LEFT_TO_HUMAN = 'LEFT_TO_HUMAN';\n/**\n * Not merged and it NEVER will be without action: `gh pr view --json mergeStateStatus` says BEHIND, i.e.\n * the head branch is out of date with base. Unlike BLOCKED (waiting on checks — self-healing), BEHIND\n * cannot resolve itself; the branch must be re-synced from main. This is the case that used to print a\n * green \"✅ PR finished\" and get abandoned.\n */\nexport const MERGE_RESULT_BEHIND = 'BEHIND';\n/** Not merged for some other reason (config mismatch, gh error, no PR at all). Read `message`. */\nexport const MERGE_RESULT_FAILED = 'FAILED';\n\n// The `mergeStateStatus` value GitHub reports for \"head branch is not up to date with the base branch\".\nconst GH_STATE_BEHIND = 'BEHIND';\n\n// What actually happened when we tried to land the squash merge. `message` is printed VERBATIM in the\n// final wp-finish-upsert-pr summary, so a merge that did not happen can never be reported as done —\n// the old code ignored `spawnSync().status` entirely and printed \"✅ PR finished\" even when `gh pr\n// merge` had errored out, which is what hid the auto-merge-disabled failure for weeks. `result` is the\n// machine-readable half of the same honesty: the banner picks its HEADER from it, so the frame around\n// the message can no longer say \"finished\" while the message says \"did NOT merge\".\nexport class MergeOutcome {\n merged: boolean;\n autoMergeEnabled: boolean;\n message: string;\n result: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(merged: boolean, autoMergeEnabled: boolean, message: string, result: string) {\n this.merged = merged;\n this.autoMergeEnabled = autoMergeEnabled;\n this.message = message;\n this.result = result;\n }\n}\n\n// GitHub's own verdict on the PR, straight from `gh pr view --json mergeable,mergeStateStatus,state`.\n// All '' when gh could not be asked — an unreadable answer must never be treated as a diagnosis.\nexport class PrMergeState {\n mergeable: string;\n mergeStateStatus: string;\n state: string;\n\n constructor(mergeable: string, mergeStateStatus: string, state: string) {\n this.mergeable = mergeable;\n this.mergeStateStatus = mergeStateStatus;\n this.state = state;\n }\n\n isBehind(): boolean {\n return this.mergeStateStatus === GH_STATE_BEHIND;\n }\n\n // One-line rendering for the failure message, or '' when GitHub could not be asked.\n describe(): string {\n if (this.mergeStateStatus === '') return '';\n return `mergeable=${this.mergeable}, mergeStateStatus=${this.mergeStateStatus}, state=${this.state}`;\n }\n}\n\n/**\n * Lands — or queues — the squash merge with an EXPLICIT subject/body, on BOTH kinds of repo:\n *\n * - auto-merge ALLOWED (`allow_auto_merge: true`): a PR whose checks are still running falls back to\n * the auto-merge queue, carrying the same subject/body so it lands when the checks pass.\n * - auto-merge DISALLOWED (`allow_auto_merge: false`, a deliberate policy control in many orgs): the\n * direct merge still works the moment the PR is mergeable, because `gh pr merge --squash --subject\n * --body-file` does not depend on that setting at all. When the PR is NOT yet mergeable there is no\n * queue to fall back to, so we say so loudly instead of firing a `--auto` that can only fail.\n *\n * WHICH of those a repo gets is not guessed — `pr-gate.mergeMode` is REQUIRED config. AUTO means the\n * tooling lands PRs; NONE means it only posts them and a person merges. No mode can force a queue the\n * repo has turned off, so AUTO on a repo with allow_auto_merge=false is a CONFIG error, reported as\n * one rather than papered over.\n *\n * Every `gh` status is checked. Nothing here is allowed to fail silently.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PrMerger {\n /**\n * @param subject the squash-commit subject, normally `<PR title> (#N)`\n * @param mergeBodyFile file holding the squash-commit body (risk/flags/PR link)\n * @param mergeMode pr-gate.mergeMode from webpieces.config.json — AUTO or NONE. Anything else\n * (including a repo still on a published rules-config that predates the field)\n * is treated as NONE: when the policy is unreadable, do NOT touch main.\n */\n merge(baseBranch: string, subject: string, mergeBodyFile: string, mergeMode: string): MergeOutcome {\n // Anything that is not an explicit AUTO leaves the PR alone. The PR itself is already\n // posted/updated by this point, which is the whole job in this mode.\n if (mergeMode !== MERGE_MODE_AUTO) {\n return new MergeOutcome(false, false,\n `did NOT merge — pr-gate.mergeMode is ${mergeMode === MERGE_MODE_NONE ? 'NONE' : `\"${mergeMode}\"`}, so the PR is left for a human to merge.\\n` +\n ` Subject GitHub will use is its own (squash_merge_commit_title), NOT: \"${subject}\"`,\n MERGE_RESULT_LEFT_TO_HUMAN);\n }\n\n // A direct `gh pr merge --squash --subject --body-file` writes exactly this subject/body to\n // main's history regardless of the repo's squash_merge_commit_title/message defaults — and\n // regardless of allow_auto_merge. It is the ONLY path that guarantees the good commit message.\n //\n // Its output is CAPTURED, not inherited: this attempt fails BY DESIGN on any repo whose policy\n // forbids a direct merge, and gh's raw `X Pull request #N is not mergeable: …` on the terminal\n // read as a hard failure to every human and agent who saw it. We reprint the reason ourselves,\n // framed as the expected first step of a two-step dance (see announceDirectFailure).\n const direct = this.ghCapture(['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (direct.status === 0) {\n process.stdout.write(direct.output);\n return new MergeOutcome(true, false, `squash-merged the PR as: \"${subject}\"`, MERGE_RESULT_MERGED);\n }\n\n // Past here the PR is not mergeable yet. WHY matters enormously and gh's exit status does not\n // say: checks-still-running (BLOCKED) is self-healing once auto-merge is on, while out-of-date\n // (BEHIND) can NEVER land unattended. Ask GitHub itself — one cheap, failure-tolerant call, made\n // only on the failure path, so a run whose direct merge succeeded costs no extra API call.\n const state = this.prMergeState(baseBranch);\n this.announceDirectFailure(direct.output, state);\n return this.fallBackToAutoMerge(baseBranch, subject, mergeBodyFile, state);\n }\n\n /**\n * The auto-merge queue: the only way to still land a not-yet-mergeable PR unattended. Returns the\n * BEHIND outcome regardless of how queuing went whenever GitHub says the branch is out of date —\n * queuing a BEHIND PR is not an error (a repo that auto-updates branches will still land it), but it\n * is NOT success either, and reporting it as such is precisely the bug this whole file guards.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private fallBackToAutoMerge(baseBranch: string, subject: string, mergeBodyFile: string, state: PrMergeState): MergeOutcome {\n if (!this.autoMergeAllowed()) {\n if (state.isBehind()) return this.behindOutcome(state, false);\n return new MergeOutcome(false, false,\n '⚠️ did NOT merge, and queued NOTHING — CONFIG MISMATCH: pr-gate.mergeMode is AUTO, but\\n' +\n ' this repo has allow_auto_merge=false, so there is no queue to fall back to and the\\n' +\n ' PR is not mergeable yet. `gh pr merge --auto` cannot override a repo that says no.\\n' +\n ' Fix it at ONE of the two ends: turn on \"Allow auto-merge\" in the repo settings, or\\n' +\n ' set commands.pr-gate.mergeMode to \"NONE\" (and set that repo\\'s\\n' +\n ' squash_merge_commit_title to PR_TITLE so a human merge still gets a real subject).',\n MERGE_RESULT_FAILED);\n }\n\n process.stdout.write(' … retrying with --auto (the auto-merge queue)\\n');\n // gh records the merge subject/body only at the moment auto-merge is FIRST enabled; a second\n // `--auto` on an already-enabled PR silently keeps the OLD body. Disabling first re-stamps the\n // current subject/body on every re-run (a harmless no-op when auto-merge is not enabled).\n this.gh(['pr', 'merge', baseBranch, '--disable-auto'], true);\n const auto = this.gh(['pr', 'merge', baseBranch, '--auto', '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (auto !== 0) {\n if (state.isBehind()) return this.behindOutcome(state, false);\n return new MergeOutcome(false, false,\n '⚠️ did NOT merge and could NOT enable auto-merge either (see the gh error above) —\\n' +\n ' NOTHING is queued. Re-run once the PR is healthy.',\n MERGE_RESULT_FAILED);\n }\n if (state.isBehind()) return this.behindOutcome(state, true);\n return new MergeOutcome(false, true,\n `enabled auto-merge — it will squash-merge as \"${subject}\" when the checks pass`,\n MERGE_RESULT_AUTO_QUEUED);\n }\n\n // The one outcome that looks queued but is stranded. `queued` says whether auto-merge did get\n // enabled, because \"parked forever\" and \"not queued at all\" need different sentences — but neither\n // of them is done, so both carry MERGE_RESULT_BEHIND.\n private behindOutcome(state: PrMergeState, queued: boolean): MergeOutcome {\n return new MergeOutcome(false, queued,\n `⛔ did NOT merge — the head branch is BEHIND its base (${state.describe()}).\\n` +\n ` BEHIND does NOT self-heal: auto-merge never updates your branch, so nothing will land\\n` +\n ` this PR until it is re-synced from main. ` +\n (queued ? 'Auto-merge is enabled but parked.' : 'Nothing is queued.'),\n MERGE_RESULT_BEHIND);\n }\n\n // Reprint the expected first-attempt failure as CONTEXT, not as a verdict. gh's own `X …` line is\n // captured (never inherited) so this framing is the only thing on screen.\n private announceDirectFailure(ghOutput: string, state: PrMergeState): void {\n const reason = ghOutput.trim().split('\\n').filter((l: string): boolean => l.trim() !== '').pop() ?? '';\n process.stdout.write(\n 'ℹ️ the direct squash-merge did not go through (expected while checks run, or when repo\\n' +\n ' policy forbids direct merges):\\n' +\n (reason === '' ? '' : ` ${reason}\\n`) +\n (state.describe() === '' ? '' : ` GitHub says: ${state.describe()}\\n`),\n );\n }\n\n /**\n * GitHub's own verdict on the PR. AUTHORITATIVE for the BEHIND check — the alternative is\n * pattern-matching gh's English error prose, which changes between releases. Failure-tolerant on\n * purpose: a gh hiccup returns an all-'' state (never BEHIND) so a transient API blip can only cost\n * us the extra diagnosis, never turn a working run into a crash.\n */\n protected prMergeState(baseBranch: string): PrMergeState {\n const result = spawnSync(\n 'gh',\n ['pr', 'view', baseBranch, '--json', 'mergeable,mergeStateStatus,state',\n '--jq', '\"\\\\(.mergeable)\\\\t\\\\(.mergeStateStatus)\\\\t\\\\(.state)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return new PrMergeState('', '', '');\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrMergeState(parts[0] ?? '', parts[1] ?? '', parts[2] ?? '');\n }\n\n // Whether `gh pr merge --auto` is even possible on this repo. Many orgs set allow_auto_merge=false\n // as a policy control, where `--auto` can only ever fail with `GraphQL: Auto merge is not allowed\n // for this repository`. One API call beats discovering that from an error string. Anything other\n // than a clean `true` counts as NOT allowed: if the setting cannot be read we must not claim the\n // queue is available.\n protected autoMergeAllowed(): boolean {\n const result = spawnSync('gh', ['api', 'repos/{owner}/{repo}', '--jq', '.allow_auto_merge'], { encoding: 'utf8' });\n return result.status === 0 && (result.stdout ?? '').trim() === 'true';\n }\n\n // Runs `gh`, returning its exit status (-1 when gh could not be spawned at all, which must NOT be\n // mistaken for the 0 that means success).\n protected gh(args: string[], quiet: boolean = false): number {\n const result = spawnSync('gh', args, { stdio: quiet ? 'ignore' : 'inherit' });\n return result.status ?? -1;\n }\n\n // Runs `gh` CAPTURING both streams instead of inheriting them, so an expected-to-fail attempt can be\n // reported in our own words rather than dumping a raw `X …` line the reader reads as fatal.\n protected ghCapture(args: string[]): GhResult {\n const result = spawnSync('gh', args, { encoding: 'utf8' });\n return new GhResult(result.status ?? -1, (result.stdout ?? '') + (result.stderr ?? ''));\n }\n}\n\n// A captured `gh` invocation: its exit status (-1 when gh could not be spawned) and both streams\n// combined, in the order a terminal would have shown them.\nexport class GhResult {\n status: number;\n output: string;\n\n constructor(status: number, output: string) {\n this.status = status;\n this.output = output;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pr-merger.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/pr-merger.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { MERGE_MODE_AUTO, MERGE_MODE_NONE } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * The five terminal shapes a merge attempt can take. The CALLER branches on these — a `message` string\n * is for humans to read, never for code to pattern-match, and the banner that frames it must be able to\n * tell \"done\" from \"stranded\" without parsing prose.\n */\nexport const MERGE_RESULT_MERGED = 'MERGED';\n/** Not merged YET, but queued: auto-merge is enabled and lands it when the checks pass. Self-healing. */\nexport const MERGE_RESULT_AUTO_QUEUED = 'AUTO_QUEUED';\n/** Not merged BY DESIGN: pr-gate.mergeMode is not AUTO, so a person merges. Nothing is owed here. */\nexport const MERGE_RESULT_LEFT_TO_HUMAN = 'LEFT_TO_HUMAN';\n/**\n * Not merged and it NEVER will be without action: `gh pr view --json mergeStateStatus` says BEHIND, i.e.\n * the head branch is out of date with base. Unlike BLOCKED (waiting on checks — self-healing), BEHIND\n * cannot resolve itself; the branch must be re-synced from main. This is the case that used to print a\n * green \"✅ PR finished\" and get abandoned.\n */\nexport const MERGE_RESULT_BEHIND = 'BEHIND';\n/**\n * BEHIND *and* GitHub says the tree merges cleanly (`mergeable: MERGEABLE`). Nobody touched the same\n * lines — somebody simply landed on main first. One clean re-run of ①②③ converges. Split out from the\n * conflicting case because the two need completely different sentences: this one is not the author's\n * problem to solve, it is a queue collision, and telling them \"resolve the conflict\" is a lie.\n */\nexport const MERGE_RESULT_BEHIND_CLEAN = 'BEHIND_CLEAN';\n/**\n * BEHIND *and* `mergeable: CONFLICTING` — the landed work and this branch touch the same lines. Real\n * human/AI judgement is owed, and if others keep landing first it genuinely repeats. That is inherent to\n * concurrent editing, not a defect, and the banner says so rather than pretending a re-run is free.\n */\nexport const MERGE_RESULT_BEHIND_CONFLICTING = 'BEHIND_CONFLICTING';\n/**\n * BEHIND but `mergeable: UNKNOWN` (or unreadable). GitHub computes mergeability ASYNCHRONOUSLY and we ask\n * moments after a force-push, so UNKNOWN is the EXPECTED answer, not an error. Diagnosing from it would be\n * guessing, so this outcome asks for a re-check instead of prescribing a remedy.\n */\nexport const MERGE_RESULT_BEHIND_UNKNOWN = 'BEHIND_UNKNOWN';\n/** Not merged for some other reason (config mismatch, gh error, no PR at all). Read `message`. */\nexport const MERGE_RESULT_FAILED = 'FAILED';\n\n// The `mergeStateStatus` value GitHub reports for \"head branch is not up to date with the base branch\".\nconst GH_STATE_BEHIND = 'BEHIND';\n// `mergeable` values. GitHub returns UNKNOWN while it is still computing the merge — which is most of the\n// time in the seconds after a push — so UNKNOWN must never be read as \"no conflicts\".\nconst GH_MERGEABLE_CLEAN = 'MERGEABLE';\nconst GH_MERGEABLE_CONFLICTING = 'CONFLICTING';\n\n// What actually happened when we tried to land the squash merge. `message` is printed VERBATIM in the\n// final wp-finish-upsert-pr summary, so a merge that did not happen can never be reported as done —\n// the old code ignored `spawnSync().status` entirely and printed \"✅ PR finished\" even when `gh pr\n// merge` had errored out, which is what hid the auto-merge-disabled failure for weeks. `result` is the\n// machine-readable half of the same honesty: the banner picks its HEADER from it, so the frame around\n// the message can no longer say \"finished\" while the message says \"did NOT merge\".\nexport class MergeOutcome {\n merged: boolean;\n autoMergeEnabled: boolean;\n message: string;\n result: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(merged: boolean, autoMergeEnabled: boolean, message: string, result: string) {\n this.merged = merged;\n this.autoMergeEnabled = autoMergeEnabled;\n this.message = message;\n this.result = result;\n }\n\n // TRUE for every flavour of \"the branch is out of date with main\", so the banner can ask that one\n // question once instead of listing four constants at each branch point. Kept here rather than as a\n // module function because `no-function-outside-class` forbids the latter — and PrMergeState below\n // already sets the precedent that a verdict class answers questions about itself.\n isBehind(): boolean {\n return this.result === MERGE_RESULT_BEHIND\n || this.result === MERGE_RESULT_BEHIND_CLEAN\n || this.result === MERGE_RESULT_BEHIND_CONFLICTING\n || this.result === MERGE_RESULT_BEHIND_UNKNOWN;\n }\n}\n\n// GitHub's own verdict on the PR, straight from `gh pr view --json mergeable,mergeStateStatus,state`.\n// All '' when gh could not be asked — an unreadable answer must never be treated as a diagnosis.\nexport class PrMergeState {\n mergeable: string;\n mergeStateStatus: string;\n state: string;\n\n constructor(mergeable: string, mergeStateStatus: string, state: string) {\n this.mergeable = mergeable;\n this.mergeStateStatus = mergeStateStatus;\n this.state = state;\n }\n\n isBehind(): boolean {\n return this.mergeStateStatus === GH_STATE_BEHIND;\n }\n\n /**\n * WHICH kind of BEHIND this is, from the `mergeable` field we have always fetched and never read.\n *\n * This is the whole point of the split. \"Out of date\" and \"conflicting\" are different situations with\n * different costs, and collapsing them told every author the expensive story: a clean queue collision\n * — nobody touched your lines, somebody just landed first — got reported in the same alarming words as\n * a genuine textual conflict.\n *\n * UNKNOWN is its own answer, never folded into CLEAN. GitHub computes mergeability asynchronously and\n * we ask seconds after a force-push, so UNKNOWN is the ordinary reply in exactly our situation.\n * Treating it as \"no conflicts\" would promise a clean re-run we cannot see.\n */\n behindKind(): string {\n if (this.mergeable === GH_MERGEABLE_CLEAN) return MERGE_RESULT_BEHIND_CLEAN;\n if (this.mergeable === GH_MERGEABLE_CONFLICTING) return MERGE_RESULT_BEHIND_CONFLICTING;\n return MERGE_RESULT_BEHIND_UNKNOWN;\n }\n\n // One-line rendering for the failure message, or '' when GitHub could not be asked.\n describe(): string {\n if (this.mergeStateStatus === '') return '';\n return `mergeable=${this.mergeable}, mergeStateStatus=${this.mergeStateStatus}, state=${this.state}`;\n }\n}\n\n/**\n * Lands — or queues — the squash merge with an EXPLICIT subject/body, on BOTH kinds of repo:\n *\n * - auto-merge ALLOWED (`allow_auto_merge: true`): a PR whose checks are still running falls back to\n * the auto-merge queue, carrying the same subject/body so it lands when the checks pass.\n * - auto-merge DISALLOWED (`allow_auto_merge: false`, a deliberate policy control in many orgs): the\n * direct merge still works the moment the PR is mergeable, because `gh pr merge --squash --subject\n * --body-file` does not depend on that setting at all. When the PR is NOT yet mergeable there is no\n * queue to fall back to, so we say so loudly instead of firing a `--auto` that can only fail.\n *\n * WHICH of those a repo gets is not guessed — `pr-gate.mergeMode` is REQUIRED config. AUTO means the\n * tooling lands PRs; NONE means it only posts them and a person merges. No mode can force a queue the\n * repo has turned off, so AUTO on a repo with allow_auto_merge=false is a CONFIG error, reported as\n * one rather than papered over.\n *\n * Every `gh` status is checked. Nothing here is allowed to fail silently.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PrMerger {\n /**\n * @param subject the squash-commit subject, normally `<PR title> (#N)`\n * @param mergeBodyFile file holding the squash-commit body (risk/flags/PR link)\n * @param mergeMode pr-gate.mergeMode from webpieces.config.json — AUTO or NONE. Anything else\n * (including a repo still on a published rules-config that predates the field)\n * is treated as NONE: when the policy is unreadable, do NOT touch main.\n */\n merge(baseBranch: string, subject: string, mergeBodyFile: string, mergeMode: string): MergeOutcome {\n // Anything that is not an explicit AUTO leaves the PR alone. The PR itself is already\n // posted/updated by this point, which is the whole job in this mode.\n if (mergeMode !== MERGE_MODE_AUTO) {\n return new MergeOutcome(false, false,\n `did NOT merge — pr-gate.mergeMode is ${mergeMode === MERGE_MODE_NONE ? 'NONE' : `\"${mergeMode}\"`}, so the PR is left for a human to merge.\\n` +\n ` Subject GitHub will use is its own (squash_merge_commit_title), NOT: \"${subject}\"`,\n MERGE_RESULT_LEFT_TO_HUMAN);\n }\n\n // A direct `gh pr merge --squash --subject --body-file` writes exactly this subject/body to\n // main's history regardless of the repo's squash_merge_commit_title/message defaults — and\n // regardless of allow_auto_merge. It is the ONLY path that guarantees the good commit message.\n //\n // Its output is CAPTURED, not inherited: this attempt fails BY DESIGN on any repo whose policy\n // forbids a direct merge, and gh's raw `X Pull request #N is not mergeable: …` on the terminal\n // read as a hard failure to every human and agent who saw it. We reprint the reason ourselves,\n // framed as the expected first step of a two-step dance (see announceDirectFailure).\n const direct = this.ghCapture(['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (direct.status === 0) {\n process.stdout.write(direct.output);\n return new MergeOutcome(true, false, `squash-merged the PR as: \"${subject}\"`, MERGE_RESULT_MERGED);\n }\n\n // Past here the PR is not mergeable yet. WHY matters enormously and gh's exit status does not\n // say: checks-still-running (BLOCKED) is self-healing once auto-merge is on, while out-of-date\n // (BEHIND) can NEVER land unattended. Ask GitHub itself — one cheap, failure-tolerant call, made\n // only on the failure path, so a run whose direct merge succeeded costs no extra API call.\n const state = this.prMergeState(baseBranch);\n this.announceDirectFailure(direct.output, state);\n return this.fallBackToAutoMerge(baseBranch, subject, mergeBodyFile, state);\n }\n\n /**\n * The auto-merge queue: the only way to still land a not-yet-mergeable PR unattended. Returns the\n * BEHIND outcome regardless of how queuing went whenever GitHub says the branch is out of date —\n * queuing a BEHIND PR is not an error (a repo that auto-updates branches will still land it), but it\n * is NOT success either, and reporting it as such is precisely the bug this whole file guards.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private fallBackToAutoMerge(baseBranch: string, subject: string, mergeBodyFile: string, state: PrMergeState): MergeOutcome {\n if (!this.autoMergeAllowed()) {\n if (state.isBehind()) return this.behindOutcome(state, false);\n return new MergeOutcome(false, false,\n '⚠️ did NOT merge, and queued NOTHING — CONFIG MISMATCH: pr-gate.mergeMode is AUTO, but\\n' +\n ' this repo has allow_auto_merge=false, so there is no queue to fall back to and the\\n' +\n ' PR is not mergeable yet. `gh pr merge --auto` cannot override a repo that says no.\\n' +\n ' Fix it at ONE of the two ends: turn on \"Allow auto-merge\" in the repo settings, or\\n' +\n ' set commands.pr-gate.mergeMode to \"NONE\" (and set that repo\\'s\\n' +\n ' squash_merge_commit_title to PR_TITLE so a human merge still gets a real subject).',\n MERGE_RESULT_FAILED);\n }\n\n process.stdout.write(' … retrying with --auto (the auto-merge queue)\\n');\n // gh records the merge subject/body only at the moment auto-merge is FIRST enabled; a second\n // `--auto` on an already-enabled PR silently keeps the OLD body. Disabling first re-stamps the\n // current subject/body on every re-run (a harmless no-op when auto-merge is not enabled).\n this.gh(['pr', 'merge', baseBranch, '--disable-auto'], true);\n const auto = this.gh(['pr', 'merge', baseBranch, '--auto', '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (auto !== 0) {\n if (state.isBehind()) return this.behindOutcome(state, false);\n return new MergeOutcome(false, false,\n '⚠️ did NOT merge and could NOT enable auto-merge either (see the gh error above) —\\n' +\n ' NOTHING is queued. Re-run once the PR is healthy.',\n MERGE_RESULT_FAILED);\n }\n if (state.isBehind()) return this.behindOutcome(state, true);\n return new MergeOutcome(false, true,\n `enabled auto-merge — it will squash-merge as \"${subject}\" when the checks pass`,\n MERGE_RESULT_AUTO_QUEUED);\n }\n\n /**\n * The one outcome that looks queued but is stranded. `queued` says whether auto-merge did get enabled,\n * because \"parked forever\" and \"not queued at all\" need different sentences — but neither is done.\n *\n * The `result` now carries WHICH kind of behind, so the banner can stop describing a queue collision\n * in the vocabulary of a merge conflict. The wording here is deliberately blame-free: nothing the\n * author did caused this, and the PR itself is in perfectly good shape — pushed, bodied, gate-green.\n */\n private behindOutcome(state: PrMergeState, queued: boolean): MergeOutcome {\n const parked = queued ? 'Auto-merge is enabled but parked.' : 'Nothing is queued.';\n return new MergeOutcome(false, queued,\n `did NOT merge — someone else landed on main first, so GitHub wants this branch rebuilt on\\n` +\n ` top of theirs before it will merge (${state.describe()}).\\n` +\n ` This does NOT self-heal: auto-merge never updates your branch. ${parked}`,\n state.behindKind());\n }\n\n // Reprint the expected first-attempt failure as CONTEXT, not as a verdict. gh's own `X …` line is\n // captured (never inherited) so this framing is the only thing on screen.\n private announceDirectFailure(ghOutput: string, state: PrMergeState): void {\n const reason = ghOutput.trim().split('\\n').filter((l: string): boolean => l.trim() !== '').pop() ?? '';\n process.stdout.write(\n 'ℹ️ the direct squash-merge did not go through (expected while checks run, or when repo\\n' +\n ' policy forbids direct merges):\\n' +\n (reason === '' ? '' : ` ${reason}\\n`) +\n (state.describe() === '' ? '' : ` GitHub says: ${state.describe()}\\n`),\n );\n }\n\n /**\n * GitHub's own verdict on the PR. AUTHORITATIVE for the BEHIND check — the alternative is\n * pattern-matching gh's English error prose, which changes between releases. Failure-tolerant on\n * purpose: a gh hiccup returns an all-'' state (never BEHIND) so a transient API blip can only cost\n * us the extra diagnosis, never turn a working run into a crash.\n */\n protected prMergeState(baseBranch: string): PrMergeState {\n const result = spawnSync(\n 'gh',\n ['pr', 'view', baseBranch, '--json', 'mergeable,mergeStateStatus,state',\n '--jq', '\"\\\\(.mergeable)\\\\t\\\\(.mergeStateStatus)\\\\t\\\\(.state)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return new PrMergeState('', '', '');\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrMergeState(parts[0] ?? '', parts[1] ?? '', parts[2] ?? '');\n }\n\n // Whether `gh pr merge --auto` is even possible on this repo. Many orgs set allow_auto_merge=false\n // as a policy control, where `--auto` can only ever fail with `GraphQL: Auto merge is not allowed\n // for this repository`. One API call beats discovering that from an error string. Anything other\n // than a clean `true` counts as NOT allowed: if the setting cannot be read we must not claim the\n // queue is available.\n protected autoMergeAllowed(): boolean {\n const result = spawnSync('gh', ['api', 'repos/{owner}/{repo}', '--jq', '.allow_auto_merge'], { encoding: 'utf8' });\n return result.status === 0 && (result.stdout ?? '').trim() === 'true';\n }\n\n // Runs `gh`, returning its exit status (-1 when gh could not be spawned at all, which must NOT be\n // mistaken for the 0 that means success).\n protected gh(args: string[], quiet: boolean = false): number {\n const result = spawnSync('gh', args, { stdio: quiet ? 'ignore' : 'inherit' });\n return result.status ?? -1;\n }\n\n // Runs `gh` CAPTURING both streams instead of inheriting them, so an expected-to-fail attempt can be\n // reported in our own words rather than dumping a raw `X …` line the reader reads as fatal.\n protected ghCapture(args: string[]): GhResult {\n const result = spawnSync('gh', args, { encoding: 'utf8' });\n return new GhResult(result.status ?? -1, (result.stdout ?? '') + (result.stderr ?? ''));\n }\n}\n\n// A captured `gh` invocation: its exit status (-1 when gh could not be spawned) and both streams\n// combined, in the order a terminal would have shown them.\nexport class GhResult {\n status: number;\n output: string;\n\n constructor(status: number, output: string) {\n this.status = status;\n this.output = output;\n }\n}\n"]}
|