@henryqw/pi-pr 6.2.14 → 9.0.2
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/README.md +42 -13
- package/docs/pr-routing.svg +137 -88
- package/extensions/pr-ci.ts +49 -14
- package/extensions/pr-command.ts +76 -24
- package/extensions/pr-comment-sweep.ts +172 -34
- package/extensions/pr-create.ts +6 -0
- package/extensions/pr-github.ts +10 -3
- package/extensions/pr-merge.ts +4 -1
- package/extensions/pr-publish-work.ts +37 -18
- package/extensions/pr-routing.ts +20 -22
- package/extensions/pr-run.ts +107 -0
- package/extensions/pr-sync.ts +92 -0
- package/extensions/pr-ui.ts +9 -1
- package/extensions/pr-update-branch.ts +84 -23
- package/extensions/pr.ts +132 -56
- package/package.json +3 -3
- package/skills/pi-pr-comment-sweep/SKILL.md +6 -4
- package/skills/pi-pr-comment-sweep/references/recovery.md +7 -1
- package/skills/pi-pr-fix-ci/SKILL.md +13 -2
- package/skills/pi-pr-publish-work/SKILL.md +4 -2
- package/skills/pi-pr-update-branch/SKILL.md +4 -4
package/extensions/pr-ci.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { PrRun } from "./pr-run.ts";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
4
|
import { spawnBounded, type Exec, type ExecOptions } from "@henryqw/pi-process";
|
|
3
5
|
import {
|
|
4
6
|
cloneCurrentPullRequest,
|
|
@@ -43,6 +45,13 @@ const STATUSES = new Set(["completed", "in_progress", "pending", "queued", "requ
|
|
|
43
45
|
|
|
44
46
|
type Load = typeof loadCurrentPullRequest;
|
|
45
47
|
|
|
48
|
+
// Only initial preflight, before any CI evidence read, can authorize rediscovery.
|
|
49
|
+
export class StaleCiCollect extends Error {}
|
|
50
|
+
|
|
51
|
+
function sanitizeLog(text: string): string {
|
|
52
|
+
return stripVTControlCharacters(text).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "");
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
type StepIdentity = {
|
|
47
56
|
number: number;
|
|
48
57
|
name: string;
|
|
@@ -113,6 +122,7 @@ type CiFixPhase = "ready" | "collecting" | "collected" | "published" | "blocked"
|
|
|
113
122
|
type CiFixState = { phase: CiFixPhase };
|
|
114
123
|
|
|
115
124
|
export type PullRequestCiFixOptions = {
|
|
125
|
+
run?: PrRun;
|
|
116
126
|
cwd: string;
|
|
117
127
|
authority: CurrentPullRequest;
|
|
118
128
|
signal?: AbortSignal;
|
|
@@ -291,6 +301,7 @@ function errorMessage(error: unknown): string {
|
|
|
291
301
|
export class PullRequestCiFixer {
|
|
292
302
|
readonly state: CiFixState = { phase: "ready" };
|
|
293
303
|
|
|
304
|
+
private readonly run?: PrRun;
|
|
294
305
|
private readonly cwd: string;
|
|
295
306
|
private readonly authority: CurrentPullRequest;
|
|
296
307
|
private readonly signal?: AbortSignal;
|
|
@@ -307,6 +318,7 @@ export class PullRequestCiFixer {
|
|
|
307
318
|
throw new TypeError("CI repair requires the pull request head to match the configured remote OID");
|
|
308
319
|
}
|
|
309
320
|
this.cwd = options.cwd;
|
|
321
|
+
this.run = options.run;
|
|
310
322
|
this.authority = cloneCurrentPullRequest(options.authority);
|
|
311
323
|
this.signal = options.signal;
|
|
312
324
|
this.agentDir = options.agentDir;
|
|
@@ -326,21 +338,22 @@ export class PullRequestCiFixer {
|
|
|
326
338
|
return { cwd: this.cwd, signal: this.signal };
|
|
327
339
|
}
|
|
328
340
|
|
|
329
|
-
private async freshAuthority(requireOriginalLocal: boolean): Promise<CurrentPullRequest> {
|
|
341
|
+
private async freshAuthority(requireOriginalLocal: boolean, initial = false): Promise<CurrentPullRequest> {
|
|
330
342
|
const discovery = await this.load(this.pi(), this.context());
|
|
331
343
|
if (discovery.kind !== "current" || !samePullRequestSnapshot(this.authority, discovery.pullRequest) ||
|
|
332
|
-
discovery.pullRequest.base.oid !== this.authority.base.oid ||
|
|
344
|
+
(!initial && discovery.pullRequest.base.oid !== this.authority.base.oid) ||
|
|
333
345
|
discovery.pullRequest.conditions.ci !== "failure" ||
|
|
334
346
|
discovery.pullRequest.target.remoteOid !== this.authority.head.oid) {
|
|
335
347
|
throw new Error("CI repair cancelled: frozen pull request, base, target, head, or failed-CI authority changed");
|
|
336
348
|
}
|
|
337
|
-
if (requireOriginalLocal && (discovery.pullRequest.local.worktree !== "clean" ||
|
|
349
|
+
if (requireOriginalLocal && (discovery.pullRequest.local.worktree !== "clean" ||
|
|
350
|
+
(discovery.pullRequest.local.head !== "equal" && !(initial && discovery.pullRequest.local.head === "ahead")))) {
|
|
338
351
|
throw new Error("CI repair evidence requires the original clean equal local HEAD");
|
|
339
352
|
}
|
|
340
353
|
return discovery.pullRequest;
|
|
341
354
|
}
|
|
342
355
|
|
|
343
|
-
private async requireOriginalLocal(): Promise<
|
|
356
|
+
private async requireOriginalLocal(initial = false): Promise<string> {
|
|
344
357
|
const branch = requiredText(parseSingleOutputLine(
|
|
345
358
|
(await runChecked(this.exec, "git", ["branch", "--show-current"], this.options())).stdout,
|
|
346
359
|
"current branch",
|
|
@@ -349,9 +362,12 @@ export class PullRequestCiFixer {
|
|
|
349
362
|
if (await inspectWorktree(this.exec, this.options()) !== "clean") {
|
|
350
363
|
throw new Error("CI repair requires a clean worktree with no Git operation in progress");
|
|
351
364
|
}
|
|
352
|
-
|
|
365
|
+
const head = await readHead(this.exec, this.options());
|
|
366
|
+
if (head !== this.authority.head.oid &&
|
|
367
|
+
!(initial && await isAncestor(this.exec, this.options(), this.authority.head.oid, head))) {
|
|
353
368
|
throw new Error("CI repair evidence requires local HEAD to equal the frozen pull request head");
|
|
354
369
|
}
|
|
370
|
+
return head;
|
|
355
371
|
}
|
|
356
372
|
|
|
357
373
|
private async api(endpoint: string, label: string, options: Partial<ExecOptions> = {}): Promise<string> {
|
|
@@ -545,9 +561,17 @@ export class PullRequestCiFixer {
|
|
|
545
561
|
return run;
|
|
546
562
|
}
|
|
547
563
|
|
|
548
|
-
private async readSnapshot(requireOriginalLocal: boolean): Promise<CiSnapshot> {
|
|
549
|
-
const fresh = await this.freshAuthority(requireOriginalLocal);
|
|
550
|
-
|
|
564
|
+
private async readSnapshot(requireOriginalLocal: boolean, initial = false): Promise<CiSnapshot> {
|
|
565
|
+
const fresh = await this.freshAuthority(requireOriginalLocal, initial);
|
|
566
|
+
const head = requireOriginalLocal ? await this.requireOriginalLocal(initial) : undefined;
|
|
567
|
+
if (initial && (head !== this.authority.head.oid || fresh.base.oid !== this.authority.base.oid)) {
|
|
568
|
+
await this.requireSavedDestination(this.authority.head.oid);
|
|
569
|
+
if (await this.requireOriginalLocal(true) !== head) throw new Error("CI repair HEAD changed during initial preflight");
|
|
570
|
+
this.signal?.throwIfAborted();
|
|
571
|
+
throw new StaleCiCollect(head !== this.authority.head.oid
|
|
572
|
+
? "Local HEAD is ahead of the frozen PR head; cancelled before CI evidence or publication"
|
|
573
|
+
: "Base tip changed on the same ref; cancelled before CI evidence or publication");
|
|
574
|
+
}
|
|
551
575
|
const pageBudget = { pages: 0, records: 0 };
|
|
552
576
|
const checks = await this.readCheckRuns(pageBudget);
|
|
553
577
|
const failed = checks.filter(({ conclusion: value }) => value !== null && FAILED_CONCLUSIONS.has(value));
|
|
@@ -600,13 +624,22 @@ export class PullRequestCiFixer {
|
|
|
600
624
|
}
|
|
601
625
|
|
|
602
626
|
private async readJobLog(jobId: number): Promise<{ text: string; truncated: boolean }> {
|
|
603
|
-
|
|
604
|
-
|
|
627
|
+
// Sanitize before runChecked can include output in a failure diagnostic.
|
|
628
|
+
const exec: Exec = async (command, args, options) => {
|
|
629
|
+
const result = await this.exec(command, args, options);
|
|
630
|
+
if (Buffer.byteLength(result.stdout, "utf8") > LOG_TAIL_BYTES) {
|
|
631
|
+
throw new Error(`Read failed job ${jobId} log executor exceeded its retained tail limit`);
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
...result,
|
|
635
|
+
stdout: sanitizeLog(result.stdout),
|
|
636
|
+
stderr: sanitizeLog(result.stderr),
|
|
637
|
+
};
|
|
638
|
+
};
|
|
639
|
+
const result = await runChecked(exec, "gh", [
|
|
640
|
+
"api", "--allow-escape-sequences", "--hostname", this.authority.host, ...API_HEADERS,
|
|
605
641
|
`repos/${this.authority.base.repository}/actions/jobs/${jobId}/logs`,
|
|
606
642
|
], this.options({ stdoutTailBytes: LOG_TAIL_BYTES, timeoutMs: LOG_TIMEOUT_MS }));
|
|
607
|
-
if (Buffer.byteLength(result.stdout, "utf8") > LOG_TAIL_BYTES) {
|
|
608
|
-
throw new Error(`Read failed job ${jobId} log executor exceeded its retained tail limit`);
|
|
609
|
-
}
|
|
610
643
|
return { text: result.stdout, truncated: result.stdoutTruncated === true };
|
|
611
644
|
}
|
|
612
645
|
|
|
@@ -614,7 +647,7 @@ export class PullRequestCiFixer {
|
|
|
614
647
|
if (this.state.phase !== "ready") throw new Error("CI evidence collect action was already consumed");
|
|
615
648
|
this.state.phase = "collecting";
|
|
616
649
|
try {
|
|
617
|
-
const before = await this.readSnapshot(true);
|
|
650
|
+
const before = await this.readSnapshot(true, true);
|
|
618
651
|
const metadata = before.failures.map((failure) => failureEvidence(this.authority, failure, { text: "", truncated: false }));
|
|
619
652
|
const metadataBytes = metadata.map(jsonBytes);
|
|
620
653
|
const metadataTotal = jsonBytes(metadata);
|
|
@@ -710,6 +743,7 @@ export class PullRequestCiFixer {
|
|
|
710
743
|
const repairHead = await this.validatePublishAuthority();
|
|
711
744
|
const original = this.authority.head.oid;
|
|
712
745
|
await this.finalPublishRevalidation(original, repairHead);
|
|
746
|
+
this.run?.beforePush(original, repairHead);
|
|
713
747
|
let pushError: unknown;
|
|
714
748
|
try {
|
|
715
749
|
await runChecked(this.exec, "git", [
|
|
@@ -733,6 +767,7 @@ export class PullRequestCiFixer {
|
|
|
733
767
|
throw new Error(`CI repair push outcome is unknown: ${errorMessage(error)}`);
|
|
734
768
|
}
|
|
735
769
|
if (postcondition === repairHead) {
|
|
770
|
+
this.run?.observeRemote(repairHead);
|
|
736
771
|
this.state.phase = "published";
|
|
737
772
|
return { kind: "published", head: repairHead, attempt: "applied" };
|
|
738
773
|
}
|
package/extensions/pr-command.ts
CHANGED
|
@@ -2,7 +2,10 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
6
|
import { executeGitHubMerge } from "./pr-merge.ts";
|
|
7
|
+
import type { MergeMethod, PrRun } from "./pr-run.ts";
|
|
8
|
+
import { syncLocalHead } from "./pr-sync.ts";
|
|
6
9
|
import { needsFeedbackAttention } from "./pr-feedback-attention.ts";
|
|
7
10
|
import {
|
|
8
11
|
linkInferredPullRequest,
|
|
@@ -26,6 +29,9 @@ const WORKFLOWS: Record<WorkflowNextStep, string> = {
|
|
|
26
29
|
"fix-ci": "skill:pi-pr-fix-ci",
|
|
27
30
|
};
|
|
28
31
|
export const WORKFLOW_ROUTES: ReadonlySet<string> = new Set(Object.keys(WORKFLOWS));
|
|
32
|
+
/** GitHub recomputes mergeability shortly after a push; re-read a few times before giving up. */
|
|
33
|
+
const REFRESH_ATTEMPTS = 3;
|
|
34
|
+
const REFRESH_DELAY_MS = 5_000;
|
|
29
35
|
type WorkflowReservation =
|
|
30
36
|
| { route: "create"; target: PullRequestTarget }
|
|
31
37
|
| { route: Exclude<WorkflowNextStep, "create">; pullRequest: CurrentPullRequest };
|
|
@@ -36,7 +42,9 @@ type PrCommandPi = Pick<ExtensionAPI, "exec" | "getCommands" | "sendUserMessage"
|
|
|
36
42
|
export type PrCommandInvocation = ((nextStep: NextStep) => void) & {
|
|
37
43
|
sessionGeneration: number;
|
|
38
44
|
assertCurrent(): void;
|
|
39
|
-
|
|
45
|
+
run?: PrRun;
|
|
46
|
+
staleRediscoveries?: number;
|
|
47
|
+
replanAuthority?: CurrentPullRequest;
|
|
40
48
|
};
|
|
41
49
|
export type PrCommandHandler = (
|
|
42
50
|
args: string,
|
|
@@ -47,6 +55,7 @@ export type PrCommandHandler = (
|
|
|
47
55
|
export type WorkflowPromptIdentity = Readonly<{
|
|
48
56
|
route: WorkflowNextStep;
|
|
49
57
|
skill: string;
|
|
58
|
+
path: string;
|
|
50
59
|
runId: string;
|
|
51
60
|
action: WorkflowLaunchAction;
|
|
52
61
|
}>;
|
|
@@ -57,6 +66,11 @@ export type PrCommandDependencies = {
|
|
|
57
66
|
linkInferredPullRequest?: typeof linkInferredPullRequest;
|
|
58
67
|
inspectSweepRecovery?: (pullRequest: CurrentPullRequest, ctx: ExtensionContext) => Promise<boolean>;
|
|
59
68
|
inspectBranchRecovery?: (pullRequest: CurrentPullRequest, ctx: ExtensionContext) => Promise<boolean>;
|
|
69
|
+
syncLocalHead?: typeof syncLocalHead;
|
|
70
|
+
refreshAttempts?: number;
|
|
71
|
+
refreshDelayMs?: number;
|
|
72
|
+
/** Test override for the CI poll sleep; the budget still follows the run policy. */
|
|
73
|
+
ciPollMs?: number;
|
|
60
74
|
reserveWorkflow?: (
|
|
61
75
|
reservation: WorkflowReservation,
|
|
62
76
|
ctx: ExtensionContext,
|
|
@@ -106,13 +120,11 @@ async function dispatchWorkflow(
|
|
|
106
120
|
runId = reserved.runId;
|
|
107
121
|
invocation?.assertCurrent();
|
|
108
122
|
const queued = !ctx.isIdle();
|
|
109
|
-
const identity = { route, skill: workflow.name, runId, action: reserved.action };
|
|
123
|
+
const identity = { route, skill: workflow.name, path: workflow.sourceInfo.path, runId, action: reserved.action };
|
|
110
124
|
markPromptQueued(identity, queued);
|
|
111
|
-
const options = queued
|
|
112
|
-
? { deliverAs: "followUp" as const, expandPromptTemplates: true }
|
|
113
|
-
: { expandPromptTemplates: true };
|
|
114
125
|
invocation?.assertCurrent();
|
|
115
|
-
|
|
126
|
+
// Busy-session launches are consumed by agent_before_settle, not user input queues.
|
|
127
|
+
if (!queued) pi.sendUserMessage(`/${identity.skill} runId=${identity.runId} action=${identity.action}`, { expandPromptTemplates: true });
|
|
116
128
|
} catch (error) {
|
|
117
129
|
if (runId !== undefined) release(runId, invocation);
|
|
118
130
|
throw error;
|
|
@@ -129,15 +141,6 @@ function noActionNotification(pullRequest: CurrentPullRequest): { message: strin
|
|
|
129
141
|
if (pullRequest.conditions.ci === "failure-blocked") {
|
|
130
142
|
return { message: `PR #${pullRequest.number} has a failed CI check that cannot run the CI fix workflow`, type: "warning" };
|
|
131
143
|
}
|
|
132
|
-
const mutatingWorkflowSelected = pullRequest.conditions.conflict ||
|
|
133
|
-
pullRequest.conditions.changesRequested || pullRequest.conditions.unresolvedThreads > 0 ||
|
|
134
|
-
pullRequest.conditions.ci === "failure";
|
|
135
|
-
if (mutatingWorkflowSelected && pullRequest.local.worktree === "dirty") {
|
|
136
|
-
return { message: `PR #${pullRequest.number} is blocked by a dirty worktree`, type: "warning" };
|
|
137
|
-
}
|
|
138
|
-
if (mutatingWorkflowSelected && pullRequest.local.head !== "equal") {
|
|
139
|
-
return { message: `PR #${pullRequest.number} is blocked by local HEAD ${pullRequest.local.head}`, type: "warning" };
|
|
140
|
-
}
|
|
141
144
|
if (pullRequest.conditions.ci === "running") {
|
|
142
145
|
return { message: `PR #${pullRequest.number} is waiting for CI`, type: "warning" };
|
|
143
146
|
}
|
|
@@ -147,12 +150,6 @@ function noActionNotification(pullRequest: CurrentPullRequest): { message: strin
|
|
|
147
150
|
if (pullRequest.conditions.policy === "pending") {
|
|
148
151
|
return { message: `PR #${pullRequest.number} is blocked by merge policy`, type: "warning" };
|
|
149
152
|
}
|
|
150
|
-
if (pullRequest.local.worktree === "dirty") {
|
|
151
|
-
return { message: `PR #${pullRequest.number} is blocked by a dirty worktree`, type: "warning" };
|
|
152
|
-
}
|
|
153
|
-
if (pullRequest.local.head === "ahead" || pullRequest.local.head === "diverged") {
|
|
154
|
-
return { message: `PR #${pullRequest.number} is blocked by local HEAD ${pullRequest.local.head}`, type: "warning" };
|
|
155
|
-
}
|
|
156
153
|
return { message: `PR #${pullRequest.number} has no available action`, type: "warning" };
|
|
157
154
|
}
|
|
158
155
|
|
|
@@ -171,8 +168,10 @@ async function mergePullRequest(
|
|
|
171
168
|
current: CurrentPullRequest,
|
|
172
169
|
load: typeof loadCurrentPullRequest,
|
|
173
170
|
needsFeedback: typeof needsFeedbackAttention,
|
|
171
|
+
mergeMethod: MergeMethod | undefined,
|
|
174
172
|
): Promise<void> {
|
|
175
173
|
await executeGitHubMerge({
|
|
174
|
+
mergeMethod,
|
|
176
175
|
exec: (command, args, options) => pi.exec(command, args, {
|
|
177
176
|
...options,
|
|
178
177
|
signal: ctx.signal,
|
|
@@ -228,6 +227,9 @@ export function createPrCommandHandler(
|
|
|
228
227
|
const link = dependencies.linkInferredPullRequest ?? linkInferredPullRequest;
|
|
229
228
|
const inspectSweepRecovery = dependencies.inspectSweepRecovery ?? (async () => false);
|
|
230
229
|
const inspectBranchRecovery = dependencies.inspectBranchRecovery ?? (async () => false);
|
|
230
|
+
const sync = dependencies.syncLocalHead ?? syncLocalHead;
|
|
231
|
+
const refreshAttempts = dependencies.refreshAttempts ?? REFRESH_ATTEMPTS;
|
|
232
|
+
const refreshDelayMs = dependencies.refreshDelayMs ?? REFRESH_DELAY_MS;
|
|
231
233
|
const reserve = dependencies.reserveWorkflow ?? (async () => {
|
|
232
234
|
throw new Error("/pr workflow tools are unavailable");
|
|
233
235
|
});
|
|
@@ -237,10 +239,17 @@ export function createPrCommandHandler(
|
|
|
237
239
|
args: string, ctx: ExtensionContext,
|
|
238
240
|
onRouteResolved?: PrCommandInvocation,
|
|
239
241
|
linkedAuthority?: CurrentPullRequest,
|
|
242
|
+
refreshes = 0,
|
|
243
|
+
ciWaits = 0,
|
|
240
244
|
): Promise<NextStep> => {
|
|
241
245
|
if (args.trim()) throw new Error("/pr does not accept arguments");
|
|
242
246
|
const discovery = await load(pi, ctx);
|
|
243
247
|
onRouteResolved?.assertCurrent();
|
|
248
|
+
if (discovery.kind === "current") onRouteResolved?.run?.observeRemote(discovery.pullRequest.head.oid);
|
|
249
|
+
if (onRouteResolved?.replanAuthority && (discovery.kind !== "current" ||
|
|
250
|
+
!samePullRequestSnapshot(onRouteResolved.replanAuthority, discovery.pullRequest))) {
|
|
251
|
+
throw new Error("PR stale-route rediscovery cancelled: frozen PR identity, destination, or remote head changed");
|
|
252
|
+
}
|
|
244
253
|
if (linkedAuthority && (discovery.kind !== "current" || discovery.pullRequest.target.provenance !== "configured" ||
|
|
245
254
|
!isSameConfirmedMerge(linkedAuthority, discovery.pullRequest))) {
|
|
246
255
|
throw new Error("Link branch continuation cancelled: configured pull request context changed");
|
|
@@ -258,7 +267,7 @@ export function createPrCommandHandler(
|
|
|
258
267
|
nextStep = "sweep";
|
|
259
268
|
onRouteResolved?.assertCurrent();
|
|
260
269
|
}
|
|
261
|
-
if (discovery.kind === "current" && (nextStep === "merge" || nextStep === "none") &&
|
|
270
|
+
if (discovery.kind === "current" && (nextStep === "merge" || nextStep === "none" || nextStep === "wait-ci") &&
|
|
262
271
|
discovery.pullRequest.lifecycle === "open" && !discovery.pullRequest.conditions.draft &&
|
|
263
272
|
discovery.pullRequest.target.provenance === "configured" &&
|
|
264
273
|
discovery.pullRequest.local.worktree === "clean" && discovery.pullRequest.local.head === "equal") {
|
|
@@ -283,15 +292,58 @@ export function createPrCommandHandler(
|
|
|
283
292
|
if (linked.target.provenance !== "configured") throw new Error("Link branch failed: target was not configured");
|
|
284
293
|
return await handle("", ctx, onRouteResolved, linked);
|
|
285
294
|
}
|
|
295
|
+
if (nextStep === "sync-local") {
|
|
296
|
+
if (discovery.kind !== "current") throw new Error("/pr sync failed: pull request is unavailable");
|
|
297
|
+
const current = discovery.pullRequest;
|
|
298
|
+
if (onRouteResolved?.run?.hasCompleted("sync-local", current.head.oid)) {
|
|
299
|
+
ctx.ui.notify("PR sync-local already ran; inspect fresh state before retrying", "warning");
|
|
300
|
+
return "none";
|
|
301
|
+
}
|
|
302
|
+
const synced = await sync({ cwd: ctx.cwd, authority: current, signal: ctx.signal, loadCurrentPullRequest: load });
|
|
303
|
+
onRouteResolved?.assertCurrent();
|
|
304
|
+
onRouteResolved?.run?.complete("sync-local", current.head.oid);
|
|
305
|
+
const verb = synced.kind === "fast-forwarded" ? "fast-forwarded to" : synced.kind === "rebased" ? "rebased onto" : "already at";
|
|
306
|
+
ctx.ui.notify(`PR #${current.number}: local branch ${verb} the PR head`, "info");
|
|
307
|
+
return await handle("", ctx, onRouteResolved);
|
|
308
|
+
}
|
|
309
|
+
if (nextStep === "refresh") {
|
|
310
|
+
if (discovery.kind !== "current") throw new Error("/pr refresh failed: pull request is unavailable");
|
|
311
|
+
if (refreshes >= refreshAttempts) {
|
|
312
|
+
ctx.ui.notify(`PR #${discovery.pullRequest.number} mergeability is still being computed by GitHub; run /pr again`, "warning");
|
|
313
|
+
return "none";
|
|
314
|
+
}
|
|
315
|
+
await delay(refreshDelayMs, undefined, { signal: ctx.signal });
|
|
316
|
+
onRouteResolved?.assertCurrent();
|
|
317
|
+
return await handle("", ctx, onRouteResolved, undefined, refreshes + 1);
|
|
318
|
+
}
|
|
319
|
+
if (nextStep === "wait-ci") {
|
|
320
|
+
if (discovery.kind !== "current") throw new Error("/pr wait failed: pull request is unavailable");
|
|
321
|
+
const current = discovery.pullRequest;
|
|
322
|
+
const run = onRouteResolved?.run;
|
|
323
|
+
const interval = run?.consumeCiWait() ?? null;
|
|
324
|
+
if (interval === null) {
|
|
325
|
+
const budget = run?.policy.ciWaitMinutes ?? 0;
|
|
326
|
+
ctx.ui.notify(budget > 0
|
|
327
|
+
? `PR #${current.number} is still waiting for CI after ${budget} min; run /pr again later`
|
|
328
|
+
: `PR #${current.number} is waiting for CI`, "warning");
|
|
329
|
+
return "none";
|
|
330
|
+
}
|
|
331
|
+
if (ciWaits === 0) {
|
|
332
|
+
ctx.ui.notify(`PR #${current.number}: CI is running; checking every ${run!.policy.ciPollSeconds} s for up to ${run!.policy.ciWaitMinutes} min`, "info");
|
|
333
|
+
}
|
|
334
|
+
await delay(dependencies.ciPollMs ?? interval, undefined, { signal: ctx.signal });
|
|
335
|
+
onRouteResolved?.assertCurrent();
|
|
336
|
+
return await handle("", ctx, onRouteResolved, undefined, 0, ciWaits + 1);
|
|
337
|
+
}
|
|
286
338
|
if (nextStep === "merge") {
|
|
287
339
|
if (discovery.kind !== "current") throw new Error("/pr merge failed: pull request is unavailable");
|
|
288
|
-
await mergePullRequest(pi, ctx, discovery.pullRequest, load, needsFeedback);
|
|
340
|
+
await mergePullRequest(pi, ctx, discovery.pullRequest, load, needsFeedback, onRouteResolved?.run?.policy.mergeMethod);
|
|
289
341
|
return "merge";
|
|
290
342
|
}
|
|
291
343
|
|
|
292
344
|
if (!(nextStep in WORKFLOWS)) throw new Error(`/pr cannot dispatch route ${nextStep}`);
|
|
293
345
|
const route = nextStep as WorkflowNextStep;
|
|
294
|
-
if (onRouteResolved?.
|
|
346
|
+
if (onRouteResolved?.run?.hasCompleted(route, discovery.kind === "current" ? discovery.pullRequest.head.oid : null)) {
|
|
295
347
|
ctx.ui.notify(`PR ${route} already ran; inspect fresh state before retrying`, "warning");
|
|
296
348
|
return "none";
|
|
297
349
|
}
|