@h-rig/bundle-default-lifecycle 0.0.6-alpha.155 → 0.0.6-alpha.157
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/dist/src/branch-naming.d.ts +15 -0
- package/dist/src/branch-naming.js +33 -0
- package/dist/src/cli.js +4 -4
- package/dist/src/closeoutEquivalence.d.ts +1 -1
- package/dist/src/control-plane/completion-verification.d.ts +19 -0
- package/dist/src/control-plane/completion-verification.js +1917 -0
- package/dist/src/control-plane/pr-automation.d.ts +87 -0
- package/dist/src/control-plane/pr-automation.js +638 -0
- package/dist/src/control-plane/task-verify.d.ts +1 -0
- package/dist/src/control-plane/task-verify.js +1484 -0
- package/dist/src/control-plane/verifier.d.ts +138 -0
- package/dist/src/control-plane/verifier.js +1478 -0
- package/dist/src/defaultPipeline.js +4 -4
- package/dist/src/index.js +2716 -54
- package/dist/src/native/closeout-runners.d.ts +5 -0
- package/dist/src/native/closeout-runners.js +41 -0
- package/dist/src/native/in-process-closeout.d.ts +40 -0
- package/dist/src/native/in-process-closeout.js +802 -0
- package/dist/src/pipelineCloseout.d.ts +1 -1
- package/dist/src/pipelineCloseout.js +2712 -52
- package/dist/src/plugin.d.ts +4 -17
- package/dist/src/plugin.js +2029 -25
- package/dist/src/stages/auto-merge.d.ts +1 -2
- package/dist/src/stages/auto-merge.js +657 -3
- package/dist/src/stages/commit.d.ts +1 -1
- package/dist/src/stages/commit.js +657 -3
- package/dist/src/stages/isolation.js +3 -2
- package/dist/src/stages/merge-gate.d.ts +1 -2
- package/dist/src/stages/merge-gate.js +1 -1
- package/dist/src/stages/open-pr.d.ts +2 -2
- package/dist/src/stages/open-pr.js +657 -3
- package/dist/src/stages/push.d.ts +1 -1
- package/dist/src/stages/push.js +657 -3
- package/dist/src/stages/source-closeout.d.ts +1 -1
- package/dist/src/stages/source-closeout.js +657 -3
- package/dist/src/stages/validate.d.ts +1 -1
- package/package.json +32 -5
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/bundle-default-lifecycle/src/control-plane/pr-automation.ts
|
|
3
|
+
import { assertSafeGitBranchName } from "@rig/shared/safe-identifiers";
|
|
4
|
+
import { runStrictPrMergeGate } from "@rig/pr-review-plugin";
|
|
5
|
+
import {
|
|
6
|
+
strictMergeHeadShaFromGate
|
|
7
|
+
} from "@rig/contracts";
|
|
8
|
+
var UPLOADED_SNAPSHOT_PR_MARKER = "<!-- rig:uploaded-snapshot -->";
|
|
9
|
+
function positiveInt(value, fallback) {
|
|
10
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
11
|
+
}
|
|
12
|
+
function resolvePrAutomationLimits(config) {
|
|
13
|
+
return {
|
|
14
|
+
maxPrFixIterations: positiveInt(config?.automation?.maxPrFixIterations, 100500)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function buildPrAutomationBody(input) {
|
|
18
|
+
const lines = [
|
|
19
|
+
input.summary?.trim() || "Rig completed this task autonomously.",
|
|
20
|
+
"",
|
|
21
|
+
`Run: ${input.runId}`
|
|
22
|
+
];
|
|
23
|
+
if (/^\d+$/.test(input.taskId)) {
|
|
24
|
+
lines.push("", `Closes #${input.taskId}`);
|
|
25
|
+
}
|
|
26
|
+
if (input.uploadedSnapshot) {
|
|
27
|
+
lines.push("", UPLOADED_SNAPSHOT_PR_MARKER, "This PR was seeded from an uploaded local working-tree snapshot. Review local snapshot provenance before merging if repository policy requires it.");
|
|
28
|
+
}
|
|
29
|
+
return lines.join(`
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
function wildcardToRegExp(pattern) {
|
|
33
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
34
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
35
|
+
}
|
|
36
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
37
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
38
|
+
}
|
|
39
|
+
function isPendingCheck(check) {
|
|
40
|
+
const conclusion = String(check.conclusion ?? "").toLowerCase();
|
|
41
|
+
const state = String(check.state ?? check.status ?? "").toLowerCase();
|
|
42
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(conclusion) || ["pending", "queued", "in_progress", "waiting", "requested", "expected"].includes(state);
|
|
43
|
+
}
|
|
44
|
+
function isPassingCheck(check) {
|
|
45
|
+
const conclusion = String(check.conclusion ?? "").toLowerCase();
|
|
46
|
+
const state = String(check.state ?? check.status ?? "").toLowerCase();
|
|
47
|
+
return ["success", "successful", "passed", "neutral", "skipped"].includes(conclusion) || ["success", "successful", "passed", "completed"].includes(state);
|
|
48
|
+
}
|
|
49
|
+
function isFailingCheck(check) {
|
|
50
|
+
const conclusion = String(check.conclusion ?? "").toLowerCase();
|
|
51
|
+
const state = String(check.state ?? check.status ?? "").toLowerCase();
|
|
52
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "error"].includes(conclusion) || ["failure", "failed", "timed_out", "action_required", "cancelled", "error"].includes(state);
|
|
53
|
+
}
|
|
54
|
+
function collectPendingPrChecks(input) {
|
|
55
|
+
const allowedFailures = input.allowedFailures ?? [];
|
|
56
|
+
const pending = [];
|
|
57
|
+
for (const check of input.checks ?? []) {
|
|
58
|
+
const name = check.name.trim();
|
|
59
|
+
if (!name || isAllowedFailure(name, allowedFailures))
|
|
60
|
+
continue;
|
|
61
|
+
if (isPendingCheck(check) && !isPassingCheck(check))
|
|
62
|
+
pending.push(name);
|
|
63
|
+
}
|
|
64
|
+
return pending;
|
|
65
|
+
}
|
|
66
|
+
function collectActionablePrFeedback(input) {
|
|
67
|
+
const allowedFailures = input.allowedFailures ?? [];
|
|
68
|
+
const feedback = [];
|
|
69
|
+
for (const check of input.checks ?? []) {
|
|
70
|
+
const name = check.name.trim();
|
|
71
|
+
if (!name || !isFailingCheck(check) || isAllowedFailure(name, allowedFailures))
|
|
72
|
+
continue;
|
|
73
|
+
feedback.push(`Check failed: ${name}${check.detailsUrl ? ` (${check.detailsUrl})` : ""}`);
|
|
74
|
+
}
|
|
75
|
+
for (const thread of input.reviewThreads ?? []) {
|
|
76
|
+
if (thread.resolved === true)
|
|
77
|
+
continue;
|
|
78
|
+
const body = thread.body.trim();
|
|
79
|
+
if (!body)
|
|
80
|
+
continue;
|
|
81
|
+
feedback.push(`Review feedback from ${thread.author?.trim() || "reviewer"}: ${body}`);
|
|
82
|
+
}
|
|
83
|
+
return feedback;
|
|
84
|
+
}
|
|
85
|
+
function parseJsonArray(value) {
|
|
86
|
+
if (!value?.trim())
|
|
87
|
+
return [];
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(value);
|
|
90
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function parsePrChecks(value) {
|
|
96
|
+
return parseJsonArray(value).flatMap((entry) => {
|
|
97
|
+
if (!entry || typeof entry !== "object")
|
|
98
|
+
return [];
|
|
99
|
+
const record = entry;
|
|
100
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
101
|
+
if (!name.trim())
|
|
102
|
+
return [];
|
|
103
|
+
return [{
|
|
104
|
+
name,
|
|
105
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
106
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
107
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
108
|
+
detailsUrl: typeof record.detailsUrl === "string" ? record.detailsUrl : typeof record.link === "string" ? record.link : null
|
|
109
|
+
}];
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function parsePrViewStatusCheckRollup(value) {
|
|
113
|
+
if (!value?.trim())
|
|
114
|
+
return [];
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(value);
|
|
117
|
+
const rollup = Array.isArray(parsed.statusCheckRollup) ? parsed.statusCheckRollup : [];
|
|
118
|
+
return rollup.flatMap((entry) => {
|
|
119
|
+
if (!entry || typeof entry !== "object")
|
|
120
|
+
return [];
|
|
121
|
+
const record = entry;
|
|
122
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
123
|
+
if (!name.trim())
|
|
124
|
+
return [];
|
|
125
|
+
return [{
|
|
126
|
+
name,
|
|
127
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
128
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
129
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
130
|
+
detailsUrl: typeof record.detailsUrl === "string" ? record.detailsUrl : typeof record.link === "string" ? record.link : null
|
|
131
|
+
}];
|
|
132
|
+
});
|
|
133
|
+
} catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function readPrChecks(input) {
|
|
138
|
+
const checks = await input.command(["pr", "checks", input.prUrl, "--json", "name,state,link"], input.cwd ? { cwd: input.cwd } : undefined);
|
|
139
|
+
if (checks.exitCode === 0) {
|
|
140
|
+
return parsePrChecks(checks.stdout);
|
|
141
|
+
}
|
|
142
|
+
const combined = `${checks.stderr ?? ""}
|
|
143
|
+
${checks.stdout ?? ""}`;
|
|
144
|
+
if (!/unknown flag.*--json|unknown flag: --json|unknown shorthand flag/i.test(combined)) {
|
|
145
|
+
throw new Error(`gh pr checks ${input.prUrl} --json name,state,link failed (${checks.exitCode}): ${checks.stderr ?? checks.stdout ?? ""}`.trim());
|
|
146
|
+
}
|
|
147
|
+
const view = await runChecked(input.command, ["pr", "view", input.prUrl, "--json", "statusCheckRollup"], input.cwd, "gh");
|
|
148
|
+
return parsePrViewStatusCheckRollup(view.stdout);
|
|
149
|
+
}
|
|
150
|
+
function parsePrViewReviewThreads(value) {
|
|
151
|
+
if (!value?.trim())
|
|
152
|
+
return [];
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(value);
|
|
155
|
+
const feedback = [];
|
|
156
|
+
const reviewThreads = parsed.reviewThreads;
|
|
157
|
+
if (Array.isArray(reviewThreads)) {
|
|
158
|
+
feedback.push(...reviewThreads.flatMap((entry) => {
|
|
159
|
+
if (!entry || typeof entry !== "object")
|
|
160
|
+
return [];
|
|
161
|
+
const record = entry;
|
|
162
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
163
|
+
if (!body)
|
|
164
|
+
return [];
|
|
165
|
+
return [{
|
|
166
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
167
|
+
body,
|
|
168
|
+
resolved: typeof record.resolved === "boolean" ? record.resolved : false,
|
|
169
|
+
author: typeof record.author === "string" ? record.author : null
|
|
170
|
+
}];
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
const reviews = parsed.reviews;
|
|
174
|
+
if (Array.isArray(reviews)) {
|
|
175
|
+
feedback.push(...reviews.flatMap((entry) => {
|
|
176
|
+
if (!entry || typeof entry !== "object")
|
|
177
|
+
return [];
|
|
178
|
+
const record = entry;
|
|
179
|
+
const state = typeof record.state === "string" ? record.state.toUpperCase() : "";
|
|
180
|
+
if (state !== "CHANGES_REQUESTED")
|
|
181
|
+
return [];
|
|
182
|
+
const body = typeof record.body === "string" && record.body.trim() ? record.body : "Changes requested by reviewer.";
|
|
183
|
+
const author = record.author && typeof record.author === "object" ? record.author.login : null;
|
|
184
|
+
return [{
|
|
185
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
186
|
+
body,
|
|
187
|
+
resolved: false,
|
|
188
|
+
author: typeof author === "string" ? author : null
|
|
189
|
+
}];
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
const reviewDecision = typeof parsed.reviewDecision === "string" ? parsed.reviewDecision.toUpperCase() : "";
|
|
193
|
+
if (reviewDecision === "CHANGES_REQUESTED" && feedback.length === 0) {
|
|
194
|
+
feedback.push({ body: "Changes requested by reviewer.", resolved: false });
|
|
195
|
+
}
|
|
196
|
+
return feedback;
|
|
197
|
+
} catch {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function findPrUrl(output) {
|
|
202
|
+
return output?.split(/\s+/).map((part) => part.trim()).find((part) => /^https?:\/\/\S+\/pull\/\d+$/i.test(part)) ?? null;
|
|
203
|
+
}
|
|
204
|
+
function normalizePrUrl(stdout) {
|
|
205
|
+
const url = findPrUrl(stdout);
|
|
206
|
+
if (!url)
|
|
207
|
+
throw new Error("gh pr create did not return a PR URL");
|
|
208
|
+
return url;
|
|
209
|
+
}
|
|
210
|
+
function parseGitHubPullRequestUrl(prUrl) {
|
|
211
|
+
try {
|
|
212
|
+
const parsed = new URL(prUrl);
|
|
213
|
+
const [owner, repo, kind, number] = parsed.pathname.split("/").filter(Boolean);
|
|
214
|
+
if (!owner || !repo || kind !== "pull" || !number || !/^\d+$/.test(number))
|
|
215
|
+
return null;
|
|
216
|
+
return { owner, repo, number };
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function ensureExistingPrBodyHasRigMarkers(input) {
|
|
222
|
+
const view = await input.command(["pr", "view", input.prUrl, "--json", "body"], input.cwd ? { cwd: input.cwd } : undefined);
|
|
223
|
+
if (view.exitCode !== 0) {
|
|
224
|
+
throw new Error(`gh pr view ${input.prUrl} --json body failed (${view.exitCode}): ${view.stderr ?? view.stdout ?? ""}`.trim());
|
|
225
|
+
}
|
|
226
|
+
let currentBody = "";
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(view.stdout ?? "{}");
|
|
229
|
+
currentBody = typeof parsed.body === "string" ? parsed.body : "";
|
|
230
|
+
} catch {
|
|
231
|
+
currentBody = "";
|
|
232
|
+
}
|
|
233
|
+
const requiredBlocks = input.body.split(/\n{2,}/).map((block) => block.trim()).filter((block) => /^Run: /i.test(block) || /^Closes #\d+/i.test(block));
|
|
234
|
+
const missing = requiredBlocks.filter((block) => {
|
|
235
|
+
if (/^Run: /i.test(block))
|
|
236
|
+
return !/^Run: \S+/im.test(currentBody);
|
|
237
|
+
return !currentBody.includes(block);
|
|
238
|
+
});
|
|
239
|
+
if (missing.length === 0)
|
|
240
|
+
return;
|
|
241
|
+
const nextBody = [currentBody.trim(), ...missing].filter(Boolean).join(`
|
|
242
|
+
|
|
243
|
+
`);
|
|
244
|
+
const pr = parseGitHubPullRequestUrl(input.prUrl);
|
|
245
|
+
if (!pr)
|
|
246
|
+
throw new Error(`Cannot update existing PR body for unrecognized PR URL: ${input.prUrl}`);
|
|
247
|
+
const edit = await input.command(["api", `repos/${pr.owner}/${pr.repo}/issues/${pr.number}`, "-X", "PATCH", "-f", `body=${nextBody}`], input.cwd ? { cwd: input.cwd } : undefined);
|
|
248
|
+
if (edit.exitCode !== 0) {
|
|
249
|
+
throw new Error(`gh api repos/${pr.owner}/${pr.repo}/issues/${pr.number} -X PATCH -f body=<redacted> failed (${edit.exitCode}): ${edit.stderr ?? edit.stdout ?? ""}`.trim());
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function runChecked(command, args, cwd, label = "gh") {
|
|
253
|
+
const result = await command(args, cwd ? { cwd } : undefined);
|
|
254
|
+
if (result.exitCode !== 0) {
|
|
255
|
+
throw new Error(`${label} ${args.join(" ")} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim());
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
var RIG_RUNTIME_COMMIT_EXCLUDES = [
|
|
260
|
+
".rig",
|
|
261
|
+
"artifacts",
|
|
262
|
+
"node_modules"
|
|
263
|
+
];
|
|
264
|
+
function statusPathFromShortLine(line) {
|
|
265
|
+
const rawPath = line.length > 3 ? line.slice(3).trim() : "";
|
|
266
|
+
const renamedPath = rawPath.includes(" -> ") ? rawPath.split(" -> ").at(-1).trim() : rawPath;
|
|
267
|
+
if (renamedPath.startsWith('"') && renamedPath.endsWith('"')) {
|
|
268
|
+
try {
|
|
269
|
+
return JSON.parse(renamedPath);
|
|
270
|
+
} catch {
|
|
271
|
+
return renamedPath.slice(1, -1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return renamedPath;
|
|
275
|
+
}
|
|
276
|
+
function isRuntimeCommitExcludedPath(path) {
|
|
277
|
+
const normalized = path.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
278
|
+
return RIG_RUNTIME_COMMIT_EXCLUDES.some((excluded) => normalized === excluded || normalized.startsWith(`${excluded}/`));
|
|
279
|
+
}
|
|
280
|
+
function committableRunChangePaths(statusText) {
|
|
281
|
+
const seen = new Set;
|
|
282
|
+
const paths = [];
|
|
283
|
+
for (const line of statusText.split(/\r?\n/)) {
|
|
284
|
+
const path = statusPathFromShortLine(line);
|
|
285
|
+
if (!path || isRuntimeCommitExcludedPath(path) || seen.has(path))
|
|
286
|
+
continue;
|
|
287
|
+
seen.add(path);
|
|
288
|
+
paths.push(path);
|
|
289
|
+
}
|
|
290
|
+
return paths;
|
|
291
|
+
}
|
|
292
|
+
function hasCommittableRunChanges(statusText) {
|
|
293
|
+
return committableRunChangePaths(statusText).length > 0;
|
|
294
|
+
}
|
|
295
|
+
async function commitRunChanges(input) {
|
|
296
|
+
const status = await runChecked(input.command, ["status", "--short", "--untracked-files=all"], input.cwd, "git");
|
|
297
|
+
const statusText = status.stdout ?? "";
|
|
298
|
+
const committablePaths = committableRunChangePaths(statusText);
|
|
299
|
+
if (!statusText.trim() || committablePaths.length === 0) {
|
|
300
|
+
return { committed: false, status: statusText };
|
|
301
|
+
}
|
|
302
|
+
await runChecked(input.command, ["add", "-A", "--", ...committablePaths], input.cwd, "git");
|
|
303
|
+
const staged = await input.command(["diff", "--cached", "--quiet"], { cwd: input.cwd });
|
|
304
|
+
if (staged.exitCode === 0) {
|
|
305
|
+
return { committed: false, status: statusText };
|
|
306
|
+
}
|
|
307
|
+
if (staged.exitCode !== 1) {
|
|
308
|
+
throw new Error(`git diff --cached --quiet failed (${staged.exitCode}): ${staged.stderr ?? staged.stdout ?? ""}`.trim());
|
|
309
|
+
}
|
|
310
|
+
await runChecked(input.command, ["commit", "-m", input.message], input.cwd, "git");
|
|
311
|
+
return { committed: true, status: statusText };
|
|
312
|
+
}
|
|
313
|
+
async function closeIssueAfterMergedPr(input) {
|
|
314
|
+
await input.updateTaskSource(input.projectRoot, {
|
|
315
|
+
taskId: input.taskId,
|
|
316
|
+
sourceTask: input.sourceTask,
|
|
317
|
+
update: {
|
|
318
|
+
status: "closed",
|
|
319
|
+
comment: [
|
|
320
|
+
"<!-- rig:status-comment -->",
|
|
321
|
+
"### Rig status: closed",
|
|
322
|
+
"",
|
|
323
|
+
"Rig PR merged and closed this task.",
|
|
324
|
+
"",
|
|
325
|
+
`- Run: ${input.runId}`,
|
|
326
|
+
`- PR merged: ${input.prUrl}`
|
|
327
|
+
].join(`
|
|
328
|
+
`)
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function parseGitHubRepoFromPrUrl(prUrl) {
|
|
333
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/\d+\/?$/i.exec(prUrl.trim());
|
|
334
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
335
|
+
}
|
|
336
|
+
async function resolveRepoDefaultMergeFlag(input) {
|
|
337
|
+
const repo = parseGitHubRepoFromPrUrl(input.prUrl);
|
|
338
|
+
if (!repo)
|
|
339
|
+
throw new Error(`Cannot resolve GitHub repository from PR URL: ${input.prUrl}`);
|
|
340
|
+
const result = await input.command(["api", `repos/${repo.owner}/${repo.repo}`], input.cwd ? { cwd: input.cwd } : undefined);
|
|
341
|
+
if (result.exitCode !== 0) {
|
|
342
|
+
throw new Error(`Could not read repository merge policy for ${repo.owner}/${repo.repo}: ${result.stderr ?? result.stdout ?? "gh api failed"}`.trim());
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const parsed = JSON.parse(result.stdout ?? "{}");
|
|
346
|
+
if (parsed.allow_merge_commit !== false)
|
|
347
|
+
return "--merge";
|
|
348
|
+
if (parsed.allow_squash_merge !== false)
|
|
349
|
+
return "--squash";
|
|
350
|
+
if (parsed.allow_rebase_merge !== false)
|
|
351
|
+
return "--rebase";
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw new Error(`Could not parse repository merge policy for ${repo.owner}/${repo.repo}: ${error instanceof Error ? error.message : String(error)}`);
|
|
354
|
+
}
|
|
355
|
+
throw new Error(`Repository ${repo.owner}/${repo.repo} has no enabled merge method for repo-default merge.`);
|
|
356
|
+
}
|
|
357
|
+
async function runRepoDefaultMerge(input) {
|
|
358
|
+
const merge = input.config?.merge ?? {};
|
|
359
|
+
if (merge.mode === "off")
|
|
360
|
+
return;
|
|
361
|
+
const requireGreptile = (input.config?.review?.provider ?? "greptile") === "greptile";
|
|
362
|
+
const matchHeadSha = strictMergeHeadShaFromGate(input.strictGate, input.prUrl, requireGreptile);
|
|
363
|
+
const method = merge.method ?? "repo-default";
|
|
364
|
+
const args = ["pr", "merge", input.prUrl];
|
|
365
|
+
if (method === "repo-default") {
|
|
366
|
+
args.push(await resolveRepoDefaultMergeFlag({ prUrl: input.prUrl, command: input.command, cwd: input.cwd }));
|
|
367
|
+
} else {
|
|
368
|
+
args.push(`--${method}`);
|
|
369
|
+
}
|
|
370
|
+
args.push("--match-head-commit", matchHeadSha);
|
|
371
|
+
if (merge.deleteBranch === true) {
|
|
372
|
+
args.push("--delete-branch");
|
|
373
|
+
}
|
|
374
|
+
if (merge.bypass === true) {
|
|
375
|
+
args.push("--admin");
|
|
376
|
+
}
|
|
377
|
+
await runChecked(input.command, args, input.cwd);
|
|
378
|
+
}
|
|
379
|
+
function shouldAttemptRigMerge(config) {
|
|
380
|
+
const mode = config?.merge?.mode;
|
|
381
|
+
return mode !== "off" && mode !== "pr-ready";
|
|
382
|
+
}
|
|
383
|
+
function isPendingOnlyGate(result) {
|
|
384
|
+
return result.pending && result.reasonDetails.length > 0 && result.reasonDetails.every((reason) => reason.reasonClass === "pending" && reason.suggestedAction === "wait");
|
|
385
|
+
}
|
|
386
|
+
function gateNeedsGreptileRereview(result) {
|
|
387
|
+
if (result.approved)
|
|
388
|
+
return false;
|
|
389
|
+
const staleShaEvidence = result.reasonDetails.some((reason) => reason.surface === "greptile" && (reason.code === "greptile_stale" || reason.code === "greptile_not_current_head" && !!reason.reviewedSha && reason.reviewedSha !== reason.headSha));
|
|
390
|
+
const hasParseableGreptileScore = !!result.evidence.greptile.score;
|
|
391
|
+
const greptileNeedsPrompt = !hasParseableGreptileScore && result.reasonDetails.some((reason) => reason.surface === "greptile" && reason.suggestedAction === "ask_greptile" && (reason.code === "greptile_not_current_head" || reason.code === "greptile_score_missing" || reason.code === "greptile_mapping_unproven"));
|
|
392
|
+
const greptileStillRunning = result.reasonDetails.some((reason) => reason.code === "greptile_pending" || reason.code === "greptile_missing");
|
|
393
|
+
return (staleShaEvidence || greptileNeedsPrompt) && !greptileStillRunning;
|
|
394
|
+
}
|
|
395
|
+
var GREPTILE_REREVIEW_MARKER_PREFIX = "rig:greptile-rereview";
|
|
396
|
+
async function requestGreptileRereview(input) {
|
|
397
|
+
const sha = input.headSha?.trim() || "unknown";
|
|
398
|
+
const marker = `<!-- ${GREPTILE_REREVIEW_MARKER_PREFIX}:${sha} -->`;
|
|
399
|
+
const existing = await input.command(["pr", "view", input.prUrl, "--json", "comments"], input.cwd ? { cwd: input.cwd } : undefined);
|
|
400
|
+
if (existing.exitCode === 0 && (existing.stdout ?? "").includes(marker)) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
await runChecked(input.command, [
|
|
404
|
+
"pr",
|
|
405
|
+
"comment",
|
|
406
|
+
input.prUrl,
|
|
407
|
+
"--body",
|
|
408
|
+
`${marker}
|
|
409
|
+
@greptileai review
|
|
410
|
+
|
|
411
|
+
Rig strict merge gate: Greptile evidence is bound to an older commit. Requesting a fresh review of the current head (${sha}). Merge waits until Greptile re-reviews this exact commit.`
|
|
412
|
+
], input.cwd);
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
async function pushBranchSyncedWithOrigin(input) {
|
|
416
|
+
const branch = assertSafeGitBranchName(input.branch, "PR branch");
|
|
417
|
+
const fetched = await input.gitCommand(["fetch", "origin", branch], { cwd: input.projectRoot });
|
|
418
|
+
if (fetched.exitCode === 0) {
|
|
419
|
+
const originRef = `origin/${branch}`;
|
|
420
|
+
const behind = await input.gitCommand(["rev-list", "--count", `HEAD..${originRef}`], { cwd: input.projectRoot });
|
|
421
|
+
const behindCount = Number.parseInt((behind.stdout ?? "").trim(), 10);
|
|
422
|
+
if (behind.exitCode === 0 && Number.isFinite(behindCount) && behindCount > 0) {
|
|
423
|
+
await runChecked(input.gitCommand, ["rebase", "--autostash", originRef], input.projectRoot, "git");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const pushed = await input.gitCommand(["push", "--set-upstream", "origin", branch], { cwd: input.projectRoot });
|
|
427
|
+
if (pushed.exitCode !== 0) {
|
|
428
|
+
await runChecked(input.gitCommand, ["push", "--set-upstream", "--force-with-lease", "origin", branch], input.projectRoot, "git");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function syncBranchAfterPrFeedback(input) {
|
|
432
|
+
if (!input.gitCommand)
|
|
433
|
+
return;
|
|
434
|
+
const branch = assertSafeGitBranchName(input.branch, "PR branch");
|
|
435
|
+
await commitRunChanges({
|
|
436
|
+
cwd: input.projectRoot,
|
|
437
|
+
message: `rig: address PR feedback for task ${input.taskId}`,
|
|
438
|
+
command: input.gitCommand
|
|
439
|
+
});
|
|
440
|
+
await pushBranchSyncedWithOrigin({ projectRoot: input.projectRoot, branch, gitCommand: input.gitCommand });
|
|
441
|
+
}
|
|
442
|
+
async function runPrAutomation(input) {
|
|
443
|
+
const branch = assertSafeGitBranchName(input.branch, "PR branch");
|
|
444
|
+
const prConfig = input.config?.pr ?? {};
|
|
445
|
+
const requireGreptile = (input.config?.review?.provider ?? "greptile") === "greptile";
|
|
446
|
+
if (prConfig.mode === "off" || prConfig.mode === "ask") {
|
|
447
|
+
return { status: "skipped", iterations: 0, actionableFeedback: [] };
|
|
448
|
+
}
|
|
449
|
+
const body = buildPrAutomationBody({
|
|
450
|
+
taskId: input.taskId,
|
|
451
|
+
runId: input.runId,
|
|
452
|
+
summary: input.sourceTask?.title ? `Rig completed: ${input.sourceTask.title}` : null,
|
|
453
|
+
uploadedSnapshot: input.uploadedSnapshot
|
|
454
|
+
});
|
|
455
|
+
if (input.gitCommand) {
|
|
456
|
+
await pushBranchSyncedWithOrigin({ projectRoot: input.projectRoot, branch, gitCommand: input.gitCommand });
|
|
457
|
+
}
|
|
458
|
+
const createArgs = [
|
|
459
|
+
"pr",
|
|
460
|
+
"create",
|
|
461
|
+
"--head",
|
|
462
|
+
branch,
|
|
463
|
+
"--title",
|
|
464
|
+
input.sourceTask?.title?.trim() || `Rig task ${input.taskId}`,
|
|
465
|
+
"--body",
|
|
466
|
+
body
|
|
467
|
+
];
|
|
468
|
+
const createResult = await input.command(createArgs, { cwd: input.projectRoot });
|
|
469
|
+
const existingPrUrl = createResult.exitCode === 0 ? null : /pull request .*already exists/i.test(`${createResult.stderr ?? ""}
|
|
470
|
+
${createResult.stdout ?? ""}`) ? findPrUrl(`${createResult.stderr ?? ""}
|
|
471
|
+
${createResult.stdout ?? ""}`) : null;
|
|
472
|
+
if (createResult.exitCode !== 0 && !existingPrUrl) {
|
|
473
|
+
throw new Error(`gh ${createArgs.join(" ")} failed (${createResult.exitCode}): ${createResult.stderr ?? createResult.stdout ?? ""}`.trim());
|
|
474
|
+
}
|
|
475
|
+
const prUrl = existingPrUrl ?? normalizePrUrl(createResult.stdout);
|
|
476
|
+
if (existingPrUrl) {
|
|
477
|
+
await ensureExistingPrBodyHasRigMarkers({ prUrl, body, command: input.command, cwd: input.projectRoot });
|
|
478
|
+
}
|
|
479
|
+
await input.lifecycle?.onPrOpened?.({ prUrl });
|
|
480
|
+
const { maxPrFixIterations } = resolvePrAutomationLimits(input.config);
|
|
481
|
+
let latestFeedback = [];
|
|
482
|
+
let pendingElapsedMs = 0;
|
|
483
|
+
const shouldMerge = shouldAttemptRigMerge(input.config);
|
|
484
|
+
for (let iteration = 1;iteration <= maxPrFixIterations; iteration += 1) {
|
|
485
|
+
await input.lifecycle?.onReviewCiStarted?.({ prUrl, iteration });
|
|
486
|
+
if (!shouldMerge) {
|
|
487
|
+
const checks = prConfig.watchChecks === false ? [] : await readPrChecks({ prUrl, command: input.command, cwd: input.projectRoot });
|
|
488
|
+
const reviewThreads = prConfig.autoFixReview === false ? [] : parsePrViewReviewThreads((await runChecked(input.command, ["pr", "view", prUrl, "--json", "reviewDecision,reviews"], input.projectRoot)).stdout);
|
|
489
|
+
latestFeedback = collectActionablePrFeedback({
|
|
490
|
+
checks,
|
|
491
|
+
reviewThreads,
|
|
492
|
+
allowedFailures: input.config?.merge?.allowedFailures ?? []
|
|
493
|
+
});
|
|
494
|
+
const pendingChecks = collectPendingPrChecks({ checks, allowedFailures: input.config?.merge?.allowedFailures ?? [] });
|
|
495
|
+
if (latestFeedback.length === 0 && pendingChecks.length > 0) {
|
|
496
|
+
const timeoutMs = positiveInt(prConfig.pendingTimeoutMs, 600000);
|
|
497
|
+
const pollMs = positiveInt(prConfig.pendingPollMs, 15000);
|
|
498
|
+
if (iteration >= maxPrFixIterations || timeoutMs <= 0 || pendingElapsedMs >= timeoutMs) {
|
|
499
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: pendingChecks.map((name) => `Check still pending: ${name}`), merged: false };
|
|
500
|
+
}
|
|
501
|
+
const sleepMs = Math.min(pollMs, timeoutMs - pendingElapsedMs);
|
|
502
|
+
await (input.sleep ?? Bun.sleep)(sleepMs);
|
|
503
|
+
pendingElapsedMs += sleepMs;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (latestFeedback.length === 0) {
|
|
507
|
+
pendingElapsedMs = 0;
|
|
508
|
+
return { status: "opened", prUrl, iterations: iteration, actionableFeedback: [], merged: false };
|
|
509
|
+
}
|
|
510
|
+
pendingElapsedMs = 0;
|
|
511
|
+
if (iteration >= maxPrFixIterations || prConfig.autoFixChecks === false && prConfig.autoFixReview === false) {
|
|
512
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
513
|
+
}
|
|
514
|
+
await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
|
|
515
|
+
await input.steerPi([
|
|
516
|
+
`PR automation found actionable feedback on ${prUrl}.`,
|
|
517
|
+
`Fix iteration ${iteration + 1}/${maxPrFixIterations}.`,
|
|
518
|
+
"",
|
|
519
|
+
...latestFeedback.map((entry) => `- ${entry}`)
|
|
520
|
+
].join(`
|
|
521
|
+
`));
|
|
522
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch, gitCommand: input.gitCommand });
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const gate = await runStrictPrMergeGate({
|
|
526
|
+
projectRoot: input.projectRoot,
|
|
527
|
+
prUrl,
|
|
528
|
+
taskId: input.taskId,
|
|
529
|
+
runId: input.runId,
|
|
530
|
+
cycle: iteration,
|
|
531
|
+
command: input.command,
|
|
532
|
+
artifactRoot: input.artifactRoot,
|
|
533
|
+
allowedFailures: input.config?.merge?.allowedFailures ?? [],
|
|
534
|
+
greptileApi: requireGreptile ? input.greptileApi : undefined,
|
|
535
|
+
requireGreptile
|
|
536
|
+
});
|
|
537
|
+
latestFeedback = [...gate.actionableFeedback];
|
|
538
|
+
if (requireGreptile && gateNeedsGreptileRereview(gate)) {
|
|
539
|
+
const requested = await requestGreptileRereview({
|
|
540
|
+
prUrl,
|
|
541
|
+
headSha: gate.evidence.headSha ?? null,
|
|
542
|
+
command: input.command,
|
|
543
|
+
cwd: input.projectRoot
|
|
544
|
+
});
|
|
545
|
+
if (requested) {
|
|
546
|
+
await input.lifecycle?.onFeedback?.({
|
|
547
|
+
prUrl,
|
|
548
|
+
iteration,
|
|
549
|
+
feedback: [`Requested a fresh Greptile review for current head ${gate.evidence.headSha ?? "unknown"}; merge stays blocked until Greptile re-reviews it.`]
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const timeoutMs = positiveInt(prConfig.pendingTimeoutMs, 600000);
|
|
553
|
+
const pollMs = positiveInt(prConfig.pendingPollMs, 15000);
|
|
554
|
+
if (iteration >= maxPrFixIterations || timeoutMs <= 0 || pendingElapsedMs >= timeoutMs) {
|
|
555
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
556
|
+
}
|
|
557
|
+
const sleepMs = Math.min(pollMs, timeoutMs - pendingElapsedMs);
|
|
558
|
+
await (input.sleep ?? Bun.sleep)(sleepMs);
|
|
559
|
+
pendingElapsedMs += sleepMs;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (gate.approved) {
|
|
563
|
+
pendingElapsedMs = 0;
|
|
564
|
+
const finalGate = await runStrictPrMergeGate({
|
|
565
|
+
projectRoot: input.projectRoot,
|
|
566
|
+
prUrl,
|
|
567
|
+
taskId: input.taskId,
|
|
568
|
+
runId: input.runId,
|
|
569
|
+
cycle: iteration,
|
|
570
|
+
command: input.command,
|
|
571
|
+
artifactRoot: input.artifactRoot,
|
|
572
|
+
allowedFailures: input.config?.merge?.allowedFailures ?? [],
|
|
573
|
+
greptileApi: requireGreptile ? input.greptileApi : undefined,
|
|
574
|
+
requireGreptile,
|
|
575
|
+
final: true
|
|
576
|
+
});
|
|
577
|
+
if (finalGate.approved) {
|
|
578
|
+
await input.lifecycle?.onMergeStarted?.({ prUrl });
|
|
579
|
+
await runRepoDefaultMerge({ prUrl, config: input.config, command: input.command, cwd: input.projectRoot, strictGate: finalGate });
|
|
580
|
+
await input.lifecycle?.onMerged?.({ prUrl });
|
|
581
|
+
return { status: "merged", prUrl, iterations: iteration, actionableFeedback: [], merged: true };
|
|
582
|
+
}
|
|
583
|
+
latestFeedback = [...finalGate.actionableFeedback];
|
|
584
|
+
if (isPendingOnlyGate(finalGate)) {
|
|
585
|
+
const timeoutMs = positiveInt(prConfig.pendingTimeoutMs, 600000);
|
|
586
|
+
const pollMs = positiveInt(prConfig.pendingPollMs, 15000);
|
|
587
|
+
if (iteration >= maxPrFixIterations || timeoutMs <= 0 || pendingElapsedMs >= timeoutMs) {
|
|
588
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
589
|
+
}
|
|
590
|
+
const sleepMs = Math.min(pollMs, timeoutMs - pendingElapsedMs);
|
|
591
|
+
await (input.sleep ?? Bun.sleep)(sleepMs);
|
|
592
|
+
pendingElapsedMs += sleepMs;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (iteration >= maxPrFixIterations || prConfig.autoFixChecks === false && prConfig.autoFixReview === false) {
|
|
596
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
597
|
+
}
|
|
598
|
+
await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
|
|
599
|
+
await input.steerPi(finalGate.steeringPrompt);
|
|
600
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch, gitCommand: input.gitCommand });
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (isPendingOnlyGate(gate)) {
|
|
604
|
+
const timeoutMs = positiveInt(prConfig.pendingTimeoutMs, 600000);
|
|
605
|
+
const pollMs = positiveInt(prConfig.pendingPollMs, 15000);
|
|
606
|
+
if (iteration >= maxPrFixIterations || timeoutMs <= 0 || pendingElapsedMs >= timeoutMs) {
|
|
607
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
608
|
+
}
|
|
609
|
+
const sleepMs = Math.min(pollMs, timeoutMs - pendingElapsedMs);
|
|
610
|
+
await (input.sleep ?? Bun.sleep)(sleepMs);
|
|
611
|
+
pendingElapsedMs += sleepMs;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
pendingElapsedMs = 0;
|
|
615
|
+
if (iteration >= maxPrFixIterations || prConfig.autoFixChecks === false && prConfig.autoFixReview === false) {
|
|
616
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
617
|
+
}
|
|
618
|
+
await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
|
|
619
|
+
await input.steerPi(gate.steeringPrompt);
|
|
620
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch, gitCommand: input.gitCommand });
|
|
621
|
+
}
|
|
622
|
+
return { status: "needs_attention", prUrl, iterations: maxPrFixIterations, actionableFeedback: latestFeedback, merged: false };
|
|
623
|
+
}
|
|
624
|
+
export {
|
|
625
|
+
runRepoDefaultMerge,
|
|
626
|
+
runPrAutomation,
|
|
627
|
+
resolvePrAutomationLimits,
|
|
628
|
+
requestGreptileRereview,
|
|
629
|
+
pushBranchSyncedWithOrigin,
|
|
630
|
+
hasCommittableRunChanges,
|
|
631
|
+
gateNeedsGreptileRereview,
|
|
632
|
+
commitRunChanges,
|
|
633
|
+
collectPendingPrChecks,
|
|
634
|
+
collectActionablePrFeedback,
|
|
635
|
+
closeIssueAfterMergedPr,
|
|
636
|
+
buildPrAutomationBody,
|
|
637
|
+
UPLOADED_SNAPSHOT_PR_MARKER
|
|
638
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function taskVerify(projectRoot: string, taskId?: string): Promise<boolean>;
|