@webpieces/pr-gate 0.4.452 → 0.4.454
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/finish-upsert-pr-command.d.ts +3 -1
- package/src/scripts/commands/finish-upsert-pr-command.js +30 -23
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/pr-merger.d.ts +35 -0
- package/src/scripts/workflow/pr-merger.js +104 -0
- package/src/scripts/workflow/pr-merger.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.454",
|
|
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",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"directory": "packages/tooling/pr-gate"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/rules-config": "0.4.
|
|
25
|
+
"@webpieces/rules-config": "0.4.454",
|
|
26
26
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
27
27
|
"inversify": "7.10.4",
|
|
28
28
|
"reflect-metadata": "0.2.2"
|
|
@@ -5,6 +5,7 @@ import { GitExec } from '../workflow/git-exec';
|
|
|
5
5
|
import { BuildAffected } from '../workflow/build-affected';
|
|
6
6
|
import { MergeState } from '../workflow/merge-state';
|
|
7
7
|
import { MergeEnd } from '../workflow/merge-end';
|
|
8
|
+
import { PrMerger } from '../workflow/pr-merger';
|
|
8
9
|
import { Dashboard } from '../../dashboard/dashboard';
|
|
9
10
|
export declare class FinishUpsertPrCommand {
|
|
10
11
|
private readonly repoRootFinder;
|
|
@@ -14,8 +15,9 @@ export declare class FinishUpsertPrCommand {
|
|
|
14
15
|
private readonly buildAffected;
|
|
15
16
|
private readonly mergeState;
|
|
16
17
|
private readonly mergeEnd;
|
|
18
|
+
private readonly prMerger;
|
|
17
19
|
private readonly dashboard;
|
|
18
|
-
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, mergeEnd: MergeEnd, dashboard: Dashboard);
|
|
20
|
+
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, mergeEnd: MergeEnd, prMerger: PrMerger, dashboard: Dashboard);
|
|
19
21
|
run(): Promise<void>;
|
|
20
22
|
private gitOut;
|
|
21
23
|
private prTitleFrom;
|
|
@@ -14,6 +14,7 @@ const build_affected_1 = require("../workflow/build-affected");
|
|
|
14
14
|
const merge_state_1 = require("../workflow/merge-state");
|
|
15
15
|
const merge_end_1 = require("../workflow/merge-end");
|
|
16
16
|
const merge_start_1 = require("../workflow/merge-start");
|
|
17
|
+
const pr_merger_1 = require("../workflow/pr-merger");
|
|
17
18
|
const dashboard_1 = require("../../dashboard/dashboard");
|
|
18
19
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
19
20
|
// A resolved PR's number + web URL. Both '' when the PR can't be resolved (e.g. create failed).
|
|
@@ -25,6 +26,16 @@ class PrRef {
|
|
|
25
26
|
this.url = url;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
// The outcome of the whole upsert: the PR number ('' when it could not be resolved) plus what actually
|
|
30
|
+
// happened to the merge, so the final summary reports the REAL result rather than assuming success.
|
|
31
|
+
class UpsertResult {
|
|
32
|
+
prNumber;
|
|
33
|
+
merge;
|
|
34
|
+
constructor(prNumber, merge) {
|
|
35
|
+
this.prNumber = prNumber;
|
|
36
|
+
this.merge = merge;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
28
39
|
// FINISH of the AI-first PR flow. Runs after the AI wrote review.json. In order: (1) if a 3-point merge
|
|
29
40
|
// was in progress, validate + commit + FINALIZE via merge-END; (2) REQUIRE review.json; (3) run the
|
|
30
41
|
// authoritative build gate; (4) render the dashboard; (5) create/update the PR via `gh`. The ONLY
|
|
@@ -37,8 +48,9 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
37
48
|
buildAffected;
|
|
38
49
|
mergeState;
|
|
39
50
|
mergeEnd;
|
|
51
|
+
prMerger;
|
|
40
52
|
dashboard;
|
|
41
|
-
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, mergeEnd, dashboard) {
|
|
53
|
+
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, mergeEnd, prMerger, dashboard) {
|
|
42
54
|
this.repoRootFinder = repoRootFinder;
|
|
43
55
|
this.aiBranchName = aiBranchName;
|
|
44
56
|
this.branchNaming = branchNaming;
|
|
@@ -46,6 +58,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
46
58
|
this.buildAffected = buildAffected;
|
|
47
59
|
this.mergeState = mergeState;
|
|
48
60
|
this.mergeEnd = mergeEnd;
|
|
61
|
+
this.prMerger = prMerger;
|
|
49
62
|
this.dashboard = dashboard;
|
|
50
63
|
}
|
|
51
64
|
async run() {
|
|
@@ -71,11 +84,13 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
71
84
|
const title = this.prTitleFrom(review);
|
|
72
85
|
const input = this.computeDashboardInput(repoRoot, true, review, title);
|
|
73
86
|
const body = this.dashboard.renderDashboard(input);
|
|
74
|
-
const
|
|
87
|
+
const result = this.upsertPr(repoRoot, base, body, title, input);
|
|
88
|
+
const prNum = result.prNumber;
|
|
75
89
|
process.stdout.write('\n' + SEP + '✅ PR finished — here is exactly what I did\n' + SEP + '\n' +
|
|
76
90
|
` 1. validated the build gate (authoritative)\n` +
|
|
77
91
|
` 2. force-pushed your work to origin/${base}\n` +
|
|
78
92
|
` 3. ${prNum ? `updated/created PR #${prNum}` : 'created the PR'} titled: "${title}"\n` +
|
|
93
|
+
` 4. ${result.merge.message}\n` +
|
|
79
94
|
` You are on ${base} — same name as the remote branch and the PR head.\n\n`);
|
|
80
95
|
}
|
|
81
96
|
gitOut(args) {
|
|
@@ -115,12 +130,15 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
115
130
|
const create = (0, child_process_1.spawnSync)('gh', ['pr', 'create', '--head', baseBranch, '--base', 'main', '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });
|
|
116
131
|
if (create.status !== 0) {
|
|
117
132
|
process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\n ' + bodyFile + '\n');
|
|
118
|
-
return '';
|
|
133
|
+
return new UpsertResult('', new pr_merger_1.MergeOutcome(false, false, '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)'));
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
136
|
else {
|
|
122
137
|
process.stdout.write(`Updating PR #${num}...\n`);
|
|
123
|
-
(0, child_process_1.spawnSync)('gh', ['pr', 'edit', num, '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });
|
|
138
|
+
const edit = (0, child_process_1.spawnSync)('gh', ['pr', 'edit', num, '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });
|
|
139
|
+
if (edit.status !== 0) {
|
|
140
|
+
process.stderr.write(`⚠️ gh pr edit failed — PR #${num} still shows its OLD title/body. The new body is in:\n ` + bodyFile + '\n');
|
|
141
|
+
}
|
|
124
142
|
}
|
|
125
143
|
// Set the squash-merge SUBJECT to the PR title (+ the `(#N)` GitHub normally appends, which an
|
|
126
144
|
// explicit --subject would otherwise drop) and the BODY to the compact commit summary, so main's
|
|
@@ -130,25 +148,13 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
130
148
|
const subject = ref.number !== '' ? `${title} (#${ref.number})` : title;
|
|
131
149
|
const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');
|
|
132
150
|
fs.writeFileSync(mergeBodyFile, this.dashboard.renderCommitBody(input, ref.url) + '\n');
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const direct = (0, child_process_1.spawnSync)('gh', ['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile], { stdio: 'inherit' });
|
|
141
|
-
if (direct.status !== 0) {
|
|
142
|
-
// Not mergeable yet (required checks still running, or the merge is otherwise blocked). Fall
|
|
143
|
-
// back to auto-merge carrying the SAME subject/body so it lands when checks pass. gh records
|
|
144
|
-
// the merge subject/body only at the moment auto-merge is FIRST enabled; a second `--auto` on
|
|
145
|
-
// an already-enabled PR silently keeps the OLD body, so disable-first re-stamps the current
|
|
146
|
-
// subject/body on every re-run (harmless no-op when auto-merge is not enabled).
|
|
147
|
-
process.stdout.write('Direct merge not possible yet — enabling auto-merge with the same subject/body...\n');
|
|
148
|
-
(0, child_process_1.spawnSync)('gh', ['pr', 'merge', baseBranch, '--disable-auto'], { stdio: 'ignore' });
|
|
149
|
-
(0, child_process_1.spawnSync)('gh', ['pr', 'merge', baseBranch, '--auto', '--squash', '--subject', subject, '--body-file', mergeBodyFile], { stdio: 'inherit' });
|
|
150
|
-
}
|
|
151
|
-
return ref.number !== '' ? ref.number : num;
|
|
151
|
+
// PrMerger owns the direct-merge / auto-merge-fallback decision AND checks every gh status, so a
|
|
152
|
+
// merge that did not happen is reported as such instead of being swallowed (see pr-merger.ts).
|
|
153
|
+
// REQUIRED config — no default here on purpose. A missing value (an older published
|
|
154
|
+
// rules-config that has no such field) reaches PrMerger as '' and is treated as "do not merge".
|
|
155
|
+
const mergeMode = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.mergeMode ?? '';
|
|
156
|
+
const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, mergeMode);
|
|
157
|
+
return new UpsertResult(ref.number !== '' ? ref.number : num, outcome);
|
|
152
158
|
}
|
|
153
159
|
// The PR's number + web URL (for the merge subject `(#N)` and the commit-body back-link). Both ''
|
|
154
160
|
// if it can't be resolved. Rendered via jq into one tab-separated line so no JSON parsing is needed.
|
|
@@ -171,6 +177,7 @@ exports.FinishUpsertPrCommand = FinishUpsertPrCommand = tslib_1.__decorate([
|
|
|
171
177
|
build_affected_1.BuildAffected,
|
|
172
178
|
merge_state_1.MergeState,
|
|
173
179
|
merge_end_1.MergeEnd,
|
|
180
|
+
pr_merger_1.PrMerger,
|
|
174
181
|
dashboard_1.Dashboard])
|
|
175
182
|
], FinishUpsertPrCommand);
|
|
176
183
|
//# sourceMappingURL=finish-upsert-pr-command.js.map
|
|
@@ -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, loadReviewJson, prDirFor, reviewJsonPath, ReviewJson, writeTemplate, RepoRootFinder,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\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 { Dashboard, DashboardInput } 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// 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 dashboard: Dashboard,\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 const review = loadReviewJson(reviewJsonPath(repoRoot, this.aiBranchName.getFeatureName()));\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);\n const body = this.dashboard.renderDashboard(input);\n const prNum = this.upsertPr(repoRoot, base, body, title, input);\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 ` 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 private computeDashboardInput(repoRoot: string, buildPassed: boolean, review: ReviewJson, title: string): 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 return new DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review);\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): string {\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 '';\n }\n } else {\n process.stdout.write(`Updating PR #${num}...\\n`);\n spawnSync('gh', ['pr', 'edit', num, '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });\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 // Merge DIRECTLY with the explicit subject/body. A direct `gh pr merge --squash --subject\n // --body-file` writes exactly this subject/body to main's history regardless of the repo's\n // squash_merge_commit_title/message defaults — so the good \"PR title (#N)\" subject + risk/flags\n // body lands even on client repos that have auto-merge DISABLED (allow_auto_merge=false), where\n // `--auto` errors out and a later manual UI merge inherits the ugly `Squash merge of <branch>`\n // subject from the branch's internal squash commit. This does NOT depend on any GitHub repo\n // setting or on the caller enabling auto-merge.\n const direct = spawnSync('gh', ['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile], { stdio: 'inherit' });\n if (direct.status !== 0) {\n // Not mergeable yet (required checks still running, or the merge is otherwise blocked). Fall\n // back to auto-merge carrying the SAME subject/body so it lands when checks pass. gh records\n // the merge subject/body only at the moment auto-merge is FIRST enabled; a second `--auto` on\n // an already-enabled PR silently keeps the OLD body, so disable-first re-stamps the current\n // subject/body on every re-run (harmless no-op when auto-merge is not enabled).\n process.stdout.write('Direct merge not possible yet — enabling auto-merge with the same subject/body...\\n');\n spawnSync('gh', ['pr', 'merge', baseBranch, '--disable-auto'], { stdio: 'ignore' });\n spawnSync('gh', ['pr', 'merge', baseBranch, '--auto', '--squash', '--subject', subject, '--body-file', mergeBodyFile], { stdio: 'inherit' });\n }\n return ref.number !== '' ? ref.number : num;\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, loadReviewJson, prDirFor, reviewJsonPath, ReviewJson, writeTemplate, RepoRootFinder,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\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 } 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 ) {}\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 const review = loadReviewJson(reviewJsonPath(repoRoot, this.aiBranchName.getFeatureName()));\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);\n const body = this.dashboard.renderDashboard(input);\n const result = this.upsertPr(repoRoot, base, body, title, input);\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 private computeDashboardInput(repoRoot: string, buildPassed: boolean, review: ReviewJson, title: string): 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 return new DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review);\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"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare class MergeOutcome {
|
|
2
|
+
merged: boolean;
|
|
3
|
+
autoMergeEnabled: boolean;
|
|
4
|
+
message: string;
|
|
5
|
+
constructor(merged: boolean, autoMergeEnabled: boolean, message: string);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Lands — or queues — the squash merge with an EXPLICIT subject/body, on BOTH kinds of repo:
|
|
9
|
+
*
|
|
10
|
+
* - auto-merge ALLOWED (`allow_auto_merge: true`): a PR whose checks are still running falls back to
|
|
11
|
+
* the auto-merge queue, carrying the same subject/body so it lands when the checks pass.
|
|
12
|
+
* - auto-merge DISALLOWED (`allow_auto_merge: false`, a deliberate policy control in many orgs): the
|
|
13
|
+
* direct merge still works the moment the PR is mergeable, because `gh pr merge --squash --subject
|
|
14
|
+
* --body-file` does not depend on that setting at all. When the PR is NOT yet mergeable there is no
|
|
15
|
+
* queue to fall back to, so we say so loudly instead of firing a `--auto` that can only fail.
|
|
16
|
+
*
|
|
17
|
+
* WHICH of those a repo gets is not guessed — `pr-gate.mergeMode` is REQUIRED config. AUTO means the
|
|
18
|
+
* tooling lands PRs; NONE means it only posts them and a person merges. No mode can force a queue the
|
|
19
|
+
* repo has turned off, so AUTO on a repo with allow_auto_merge=false is a CONFIG error, reported as
|
|
20
|
+
* one rather than papered over.
|
|
21
|
+
*
|
|
22
|
+
* Every `gh` status is checked. Nothing here is allowed to fail silently.
|
|
23
|
+
*/
|
|
24
|
+
export declare class PrMerger {
|
|
25
|
+
/**
|
|
26
|
+
* @param subject the squash-commit subject, normally `<PR title> (#N)`
|
|
27
|
+
* @param mergeBodyFile file holding the squash-commit body (risk/flags/PR link)
|
|
28
|
+
* @param mergeMode pr-gate.mergeMode from webpieces.config.json — AUTO or NONE. Anything else
|
|
29
|
+
* (including a repo still on a published rules-config that predates the field)
|
|
30
|
+
* is treated as NONE: when the policy is unreadable, do NOT touch main.
|
|
31
|
+
*/
|
|
32
|
+
merge(baseBranch: string, subject: string, mergeBodyFile: string, mergeMode: string): MergeOutcome;
|
|
33
|
+
protected autoMergeAllowed(): boolean;
|
|
34
|
+
protected gh(args: string[], quiet?: boolean): number;
|
|
35
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PrMerger = exports.MergeOutcome = 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
|
+
// What actually happened when we tried to land the squash merge. `message` is printed VERBATIM in the
|
|
9
|
+
// final wp-finish-upsert-pr summary, so a merge that did not happen can never be reported as done —
|
|
10
|
+
// the old code ignored `spawnSync().status` entirely and printed "✅ PR finished" even when `gh pr
|
|
11
|
+
// merge` had errored out, which is what hid the auto-merge-disabled failure for weeks.
|
|
12
|
+
class MergeOutcome {
|
|
13
|
+
merged;
|
|
14
|
+
autoMergeEnabled;
|
|
15
|
+
message;
|
|
16
|
+
constructor(merged, autoMergeEnabled, message) {
|
|
17
|
+
this.merged = merged;
|
|
18
|
+
this.autoMergeEnabled = autoMergeEnabled;
|
|
19
|
+
this.message = message;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.MergeOutcome = MergeOutcome;
|
|
23
|
+
/**
|
|
24
|
+
* Lands — or queues — the squash merge with an EXPLICIT subject/body, on BOTH kinds of repo:
|
|
25
|
+
*
|
|
26
|
+
* - auto-merge ALLOWED (`allow_auto_merge: true`): a PR whose checks are still running falls back to
|
|
27
|
+
* the auto-merge queue, carrying the same subject/body so it lands when the checks pass.
|
|
28
|
+
* - auto-merge DISALLOWED (`allow_auto_merge: false`, a deliberate policy control in many orgs): the
|
|
29
|
+
* direct merge still works the moment the PR is mergeable, because `gh pr merge --squash --subject
|
|
30
|
+
* --body-file` does not depend on that setting at all. When the PR is NOT yet mergeable there is no
|
|
31
|
+
* queue to fall back to, so we say so loudly instead of firing a `--auto` that can only fail.
|
|
32
|
+
*
|
|
33
|
+
* WHICH of those a repo gets is not guessed — `pr-gate.mergeMode` is REQUIRED config. AUTO means the
|
|
34
|
+
* tooling lands PRs; NONE means it only posts them and a person merges. No mode can force a queue the
|
|
35
|
+
* repo has turned off, so AUTO on a repo with allow_auto_merge=false is a CONFIG error, reported as
|
|
36
|
+
* one rather than papered over.
|
|
37
|
+
*
|
|
38
|
+
* Every `gh` status is checked. Nothing here is allowed to fail silently.
|
|
39
|
+
*/
|
|
40
|
+
let PrMerger = class PrMerger {
|
|
41
|
+
/**
|
|
42
|
+
* @param subject the squash-commit subject, normally `<PR title> (#N)`
|
|
43
|
+
* @param mergeBodyFile file holding the squash-commit body (risk/flags/PR link)
|
|
44
|
+
* @param mergeMode pr-gate.mergeMode from webpieces.config.json — AUTO or NONE. Anything else
|
|
45
|
+
* (including a repo still on a published rules-config that predates the field)
|
|
46
|
+
* is treated as NONE: when the policy is unreadable, do NOT touch main.
|
|
47
|
+
*/
|
|
48
|
+
merge(baseBranch, subject, mergeBodyFile, mergeMode) {
|
|
49
|
+
// Anything that is not an explicit AUTO leaves the PR alone. The PR itself is already
|
|
50
|
+
// posted/updated by this point, which is the whole job in this mode.
|
|
51
|
+
if (mergeMode !== rules_config_1.MERGE_MODE_AUTO) {
|
|
52
|
+
return new MergeOutcome(false, false, `did NOT merge — pr-gate.mergeMode is ${mergeMode === rules_config_1.MERGE_MODE_NONE ? 'NONE' : `"${mergeMode}"`}, so the PR is left for a human to merge.\n` +
|
|
53
|
+
` Subject GitHub will use is its own (squash_merge_commit_title), NOT: "${subject}"`);
|
|
54
|
+
}
|
|
55
|
+
// A direct `gh pr merge --squash --subject --body-file` writes exactly this subject/body to
|
|
56
|
+
// main's history regardless of the repo's squash_merge_commit_title/message defaults — and
|
|
57
|
+
// regardless of allow_auto_merge. It is the ONLY path that guarantees the good commit message.
|
|
58
|
+
const direct = this.gh(['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile]);
|
|
59
|
+
if (direct === 0) {
|
|
60
|
+
return new MergeOutcome(true, false, `squash-merged the PR as: "${subject}"`);
|
|
61
|
+
}
|
|
62
|
+
// Past here the PR is not mergeable yet — checks still running, or a review / branch protection
|
|
63
|
+
// is blocking it. The auto-merge queue is the only way to still land it unattended, so only ask
|
|
64
|
+
// GitHub about the queue HERE: a run whose direct merge succeeded costs no extra API call.
|
|
65
|
+
if (!this.autoMergeAllowed()) {
|
|
66
|
+
return new MergeOutcome(false, false, '⚠️ did NOT merge, and queued NOTHING — CONFIG MISMATCH: pr-gate.mergeMode is AUTO, but\n' +
|
|
67
|
+
' this repo has allow_auto_merge=false, so there is no queue to fall back to and the\n' +
|
|
68
|
+
' PR is not mergeable yet. `gh pr merge --auto` cannot override a repo that says no.\n' +
|
|
69
|
+
' Fix it at ONE of the two ends: turn on "Allow auto-merge" in the repo settings, or\n' +
|
|
70
|
+
' set commands.pr-gate.mergeMode to "NONE" (and set that repo\'s\n' +
|
|
71
|
+
' squash_merge_commit_title to PR_TITLE so a human merge still gets a real subject).');
|
|
72
|
+
}
|
|
73
|
+
// gh records the merge subject/body only at the moment auto-merge is FIRST enabled; a second
|
|
74
|
+
// `--auto` on an already-enabled PR silently keeps the OLD body. Disabling first re-stamps the
|
|
75
|
+
// current subject/body on every re-run (a harmless no-op when auto-merge is not enabled).
|
|
76
|
+
this.gh(['pr', 'merge', baseBranch, '--disable-auto'], true);
|
|
77
|
+
const auto = this.gh(['pr', 'merge', baseBranch, '--auto', '--squash', '--subject', subject, '--body-file', mergeBodyFile]);
|
|
78
|
+
if (auto !== 0) {
|
|
79
|
+
return new MergeOutcome(false, false, '⚠️ did NOT merge and could NOT enable auto-merge either (see the gh error above) —\n' +
|
|
80
|
+
' NOTHING is queued. Re-run once the PR is healthy.');
|
|
81
|
+
}
|
|
82
|
+
return new MergeOutcome(false, true, `enabled auto-merge — it will squash-merge as "${subject}" when the checks pass`);
|
|
83
|
+
}
|
|
84
|
+
// Whether `gh pr merge --auto` is even possible on this repo. Many orgs set allow_auto_merge=false
|
|
85
|
+
// as a policy control, where `--auto` can only ever fail with `GraphQL: Auto merge is not allowed
|
|
86
|
+
// for this repository`. One API call beats discovering that from an error string. Anything other
|
|
87
|
+
// than a clean `true` counts as NOT allowed: if the setting cannot be read we must not claim the
|
|
88
|
+
// queue is available.
|
|
89
|
+
autoMergeAllowed() {
|
|
90
|
+
const result = (0, child_process_1.spawnSync)('gh', ['api', 'repos/{owner}/{repo}', '--jq', '.allow_auto_merge'], { encoding: 'utf8' });
|
|
91
|
+
return result.status === 0 && (result.stdout ?? '').trim() === 'true';
|
|
92
|
+
}
|
|
93
|
+
// Runs `gh`, returning its exit status (-1 when gh could not be spawned at all, which must NOT be
|
|
94
|
+
// mistaken for the 0 that means success).
|
|
95
|
+
gh(args, quiet = false) {
|
|
96
|
+
const result = (0, child_process_1.spawnSync)('gh', args, { stdio: quiet ? 'ignore' : 'inherit' });
|
|
97
|
+
return result.status ?? -1;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
exports.PrMerger = PrMerger;
|
|
101
|
+
exports.PrMerger = PrMerger = tslib_1.__decorate([
|
|
102
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
|
|
103
|
+
], PrMerger);
|
|
104
|
+
//# sourceMappingURL=pr-merger.js.map
|
|
@@ -0,0 +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// 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.\nexport class MergeOutcome {\n merged: boolean;\n autoMergeEnabled: boolean;\n message: string;\n\n constructor(merged: boolean, autoMergeEnabled: boolean, message: string) {\n this.merged = merged;\n this.autoMergeEnabled = autoMergeEnabled;\n this.message = message;\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 }\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 const direct = this.gh(['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (direct === 0) {\n return new MergeOutcome(true, false, `squash-merged the PR as: \"${subject}\"`);\n }\n\n // Past here the PR is not mergeable yet — checks still running, or a review / branch protection\n // is blocking it. The auto-merge queue is the only way to still land it unattended, so only ask\n // GitHub about the queue HERE: a run whose direct merge succeeded costs no extra API call.\n if (!this.autoMergeAllowed()) {\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 }\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 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 }\n return new MergeOutcome(false, true, `enabled auto-merge — it will squash-merge as \"${subject}\" when the checks pass`);\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"]}
|