pr-shepherd 0.56.3 → 0.56.4
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/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/bin/cli/iterate-merge-formatter.mjs +2 -0
- package/bin/commands/apply-queue-removal.mjs +3 -4
- package/bin/commands/check.mjs +15 -11
- package/bin/commands/iterate/fix-code.mjs +14 -17
- package/bin/commands/iterate/merge.mjs +3 -0
- package/bin/commands/iterate/native-stack-rebase.d.mts +5 -2
- package/bin/commands/iterate/native-stack-rebase.mjs +11 -2
- package/bin/commands/iterate/queue-recovery-instructions.d.mts +8 -0
- package/bin/commands/iterate/queue-recovery-instructions.mjs +38 -0
- package/bin/commands/iterate/render.d.mts +3 -1
- package/bin/commands/iterate/render.mjs +13 -16
- package/bin/commands/iterate/stall.mjs +2 -0
- package/bin/github/batch-parsers.mjs +6 -0
- package/bin/github/batch-raw-rules.d.mts +12 -0
- package/bin/github/gql/batch-pr.gql +14 -0
- package/bin/github/gql/poll-summary-fragment.gql +7 -0
- package/bin/github/merge-queue-checks.mjs +2 -1
- package/bin/github/poll-summary-queue-removal.mjs +2 -1
- package/bin/github/poll-summary-raw.d.mts +6 -0
- package/bin/github/queue-removal-freshness.d.mts +24 -6
- package/bin/github/queue-removal-freshness.mjs +34 -12
- package/bin/state/queue-removal-ack.d.mts +5 -1
- package/bin/state/queue-removal-ack.mjs +6 -2
- package/bin/types/github.d.mts +4 -0
- package/bin/types/merge-queue.d.mts +2 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +1 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/merge-queue-ejection.md +17 -0
package/README.md
CHANGED
|
@@ -164,7 +164,7 @@ receipts, and whose bottom open layer GitHub has retargeted onto the stack base,
|
|
|
164
164
|
with `gh stack merge <that PR number> --yes` and the allowed method flag (`--squash` unless config or the repository selects another). That lands the named layer and every
|
|
165
165
|
unmerged layer below it. When the base uses a merge queue, the same command queues the prefix
|
|
166
166
|
together and GitHub evaluates each layer from the bottom; a failure ejects that layer and those
|
|
167
|
-
above it.
|
|
167
|
+
above it. For a `failed_checks` removal on the layer's first ejection, the one-PR session emits a head-, queue-commit-, and timestamp-pinned local acknowledgment command; run it only when the failure does not reproduce after updating from the latest base, the head did not change, and no source changes or other blockers remain. Fresh source checks and a new READY receipt then let the aggregate selector recover the eligible prefix. Manual or stale removals cannot use this path. Layers above the prefix keep their one-PR sessions. After the merge, GitHub retargets
|
|
168
168
|
the next layer, so the rerun continues until the stack returns `CANCEL`. API and MCP aggregate
|
|
169
169
|
calls perform one summary tick and leave recurrence to the caller.
|
|
170
170
|
|
|
@@ -40,6 +40,8 @@ export function appendMergeQueueHeader(lines, result) {
|
|
|
40
40
|
parts.push("checks incomplete (first 100 shown)");
|
|
41
41
|
if (queue.headUpdatedAfterRemoval)
|
|
42
42
|
parts.push("head updated after removal");
|
|
43
|
+
if (queue.removalsOnHead)
|
|
44
|
+
parts.push(`removals on this head \`${queue.removalsOnHead}\``);
|
|
43
45
|
if (queue.removalAcknowledged)
|
|
44
46
|
parts.push("removal acknowledged");
|
|
45
47
|
lines.push(`**merge queue** ${parts.join(" · ")}`);
|
|
@@ -24,10 +24,9 @@ export async function applyQueueRemovalAck(input) {
|
|
|
24
24
|
!queueRemovalAppliesToHead({
|
|
25
25
|
parentOids: removal.beforeCommitParentOids,
|
|
26
26
|
headOid: data.headRefOid,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
...(data.headPushedAtUnix !== undefined && { headPushedAtUnix: data.headPushedAtUnix }),
|
|
27
|
+
headCommittedAtUnix: data.activity?.latestCommitCommittedAtUnix,
|
|
28
|
+
headPushedAtUnix: data.headPushedAtUnix,
|
|
29
|
+
headForcePushedAtUnix: data.headForcePushedAtUnix,
|
|
31
30
|
removedAtUnix: removal.createdAtUnix,
|
|
32
31
|
})) {
|
|
33
32
|
throw new ShepherdError("The supplied queue-removal evidence is stale or is not a current CI-driven removal for this native-stack PR.", EXIT.UNAVAILABLE);
|
package/bin/commands/check.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
2
|
-
import { queueRemovalAppliesToHead } from "../github/queue-removal-freshness.mjs";
|
|
2
|
+
import { headArrivalUnix, queueRemovalAppliesToHead } from "../github/queue-removal-freshness.mjs";
|
|
3
3
|
import { readQueueRemovalAcknowledgment, matchesQueueRemovalAcknowledgment, isCiQueueRemovalReason, } from "../state/queue-removal-ack.mjs";
|
|
4
4
|
import { storePrFingerprint } from "../state/pr-fingerprint.mjs";
|
|
5
5
|
import { tryReuseFingerprintReport } from "./check-fingerprint.mjs";
|
|
@@ -76,22 +76,25 @@ export async function runCheck(opts, context) {
|
|
|
76
76
|
// it as stale/updated rather than as still current, so Shepherd doesn't escalate
|
|
77
77
|
// `merge-queue-removed` permanently on data it can no longer check. A squash or rebase
|
|
78
78
|
// queue commit has one parent and does not list the PR head; that removal stays current
|
|
79
|
-
// until this head reached the PR after it. The
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
// until this head reached the PR after it (`headArrivalUnix`). The raw removal fields
|
|
80
|
+
// still render in the merge-queue header regardless of this flag.
|
|
81
|
+
const headTimes = {
|
|
82
|
+
headCommittedAtUnix: batchData.activity?.latestCommitCommittedAtUnix,
|
|
83
|
+
headPushedAtUnix: batchData.headPushedAtUnix,
|
|
84
|
+
headForcePushedAtUnix: batchData.headForcePushedAtUnix,
|
|
85
|
+
};
|
|
84
86
|
const headUpdatedAfterRemoval = Boolean(latestRemoval &&
|
|
85
87
|
!queueRemovalAppliesToHead({
|
|
88
|
+
...headTimes,
|
|
86
89
|
parentOids: latestRemoval.beforeCommitParentOids,
|
|
87
90
|
headOid: batchData.headRefOid,
|
|
88
|
-
...(headCommittedAtUnix !== undefined &&
|
|
89
|
-
headCommittedAtUnix !== null && { headCommittedAtUnix }),
|
|
90
|
-
...(batchData.headPushedAtUnix !== undefined && {
|
|
91
|
-
headPushedAtUnix: batchData.headPushedAtUnix,
|
|
92
|
-
}),
|
|
93
91
|
removedAtUnix: latestRemoval.createdAtUnix,
|
|
94
92
|
}));
|
|
93
|
+
// Repeat ejections of this head: removals after it reached the PR.
|
|
94
|
+
const headSinceUnix = headArrivalUnix(headTimes);
|
|
95
|
+
const removalsOnHead = latestRemoval && !headUpdatedAfterRemoval && headSinceUnix !== undefined
|
|
96
|
+
? (batchData.mergeQueueRemovalTimesUnix ?? []).filter((t) => t >= headSinceUnix).length
|
|
97
|
+
: 0;
|
|
95
98
|
const removalAcknowledged = Boolean(batchData.stack &&
|
|
96
99
|
latestRemoval?.beforeCommitOid &&
|
|
97
100
|
isCiQueueRemovalReason(latestRemoval.reason) &&
|
|
@@ -367,6 +370,7 @@ export async function runCheck(opts, context) {
|
|
|
367
370
|
checksIncomplete: true,
|
|
368
371
|
}),
|
|
369
372
|
...(headUpdatedAfterRemoval && { headUpdatedAfterRemoval: true }),
|
|
373
|
+
...(removalsOnHead > 1 && { removalsOnHead }),
|
|
370
374
|
...(removalAcknowledged && { removalAcknowledged: true }),
|
|
371
375
|
},
|
|
372
376
|
}),
|
|
@@ -8,6 +8,7 @@ import { buildThreadMutationRouting, threadHasAuthorizedMutation, } from "./thre
|
|
|
8
8
|
import { buildFixInstructions } from "./render.mjs";
|
|
9
9
|
import { buildRemovedQueueRecovery, buildStackQueueRemovalAcknowledgment } from "./merge.mjs";
|
|
10
10
|
import { hasLogEvidence } from "./check-evidence.mjs";
|
|
11
|
+
import { currentEjectionCommit, } from "./queue-recovery-instructions.mjs";
|
|
11
12
|
import { buildReleasedBlockerInstruction } from "./check-instructions.mjs";
|
|
12
13
|
import { buildNativeStackLayerRebase } from "./native-stack-rebase.mjs";
|
|
13
14
|
import { lookupUpperLayerTrunkConflict } from "./stack-trunk-conflict.mjs";
|
|
@@ -301,25 +302,21 @@ export async function handleFixCode(ctx) {
|
|
|
301
302
|
trunk: stack.baseRefName,
|
|
302
303
|
})
|
|
303
304
|
: undefined;
|
|
304
|
-
// Conflicts, and a behind branch whose rerun already failed, both ask for a branch update.
|
|
305
|
-
const stackRebase = hasConflicts || (isBehind && exhaustedAttempts.length > 0)
|
|
306
|
-
? buildNativeStackLayerRebase(report.repo, { number: prNumber, baseBranch: baseLookup.branch }, stack, trunkConflict)
|
|
307
|
-
: undefined;
|
|
308
|
-
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, report.viewerAuthorization?.viewerCanUpdate === true, exhaustedAttempts.length > 0, stackRebase);
|
|
309
305
|
const requeue = buildRemovedQueueRecovery(report, failingAgentChecks, opts.merge);
|
|
310
306
|
const queueRemovalAcknowledgment = buildStackQueueRemovalAcknowledgment(report, failingAgentChecks);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
307
|
+
const queueEjection = !currentEjectionCommit(report, failingAgentChecks)
|
|
308
|
+
? undefined
|
|
309
|
+
: requeue
|
|
310
|
+
? "requeue"
|
|
311
|
+
: queueRemovalAcknowledgment
|
|
312
|
+
? "acknowledge"
|
|
313
|
+
: "none";
|
|
314
|
+
// Conflicts, a behind branch whose rerun already failed, and a queue ejection that may be
|
|
315
|
+
// reproduced on the latest base all print the branch update route.
|
|
316
|
+
const stackRebase = hasConflicts || (isBehind && exhaustedAttempts.length > 0) || queueEjection
|
|
317
|
+
? buildNativeStackLayerRebase(report.repo, { number: prNumber, baseBranch: baseLookup.branch }, stack, trunkConflict)
|
|
318
|
+
: undefined;
|
|
319
|
+
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, report.viewerAuthorization?.viewerCanUpdate === true, exhaustedAttempts.length > 0, stackRebase, queueEjection);
|
|
323
320
|
if (failingAgentChecks.some((check) => releasedCheckNames.has(check.name))) {
|
|
324
321
|
const completion = instructions.pop();
|
|
325
322
|
instructions.push(buildReleasedBlockerInstruction(prNumber));
|
|
@@ -99,6 +99,9 @@ function removedQueueRecoveryAvailable(report, checks, merge) {
|
|
|
99
99
|
!queue?.enabled ||
|
|
100
100
|
queue.inQueue ||
|
|
101
101
|
queue.headUpdatedAfterRemoval ||
|
|
102
|
+
// A repeat ejection of the same head is not retried blindly; a conflict forces a new head.
|
|
103
|
+
(queue.removalsOnHead ?? 1) > 1 ||
|
|
104
|
+
report.mergeStatus.status === "CONFLICTS" ||
|
|
102
105
|
// GitHub exposes a raw string, not a capability to reverse a human's queue removal.
|
|
103
106
|
// Only known CI-driven reasons authorize offering automated recovery.
|
|
104
107
|
!isCiQueueRemovalReason(queue.latestRemoval?.reason) ||
|
|
@@ -36,5 +36,8 @@ export declare function buildNativeStackLayerRebase(repo: string, pr: {
|
|
|
36
36
|
}): string | undefined;
|
|
37
37
|
/** Point at the conflicts, and at the stack rebase when the PR is a native stack layer. */
|
|
38
38
|
export declare function buildConflictInstruction(stackRebase: string | undefined): string;
|
|
39
|
-
/**
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Push a conflict resolution or branch refresh: one branch, or the whole rewritten native stack.
|
|
41
|
+
* After a merge-queue ejection update, push only when the update or a fix changed the head.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildBranchPushInstruction(stackRebase: string | undefined, hasConflicts: boolean, mutationSuffix: string, ejectionUpdate?: boolean): string;
|
|
@@ -44,8 +44,17 @@ export function buildConflictInstruction(stackRebase) {
|
|
|
44
44
|
const pointer = "The branch has merge conflicts (see `**branch**` above).";
|
|
45
45
|
return stackRebase ? `${pointer} ${stackRebase}` : `${pointer} Resolve them before committing.`;
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Push a conflict resolution or branch refresh: one branch, or the whole rewritten native stack.
|
|
49
|
+
* After a merge-queue ejection update, push only when the update or a fix changed the head.
|
|
50
|
+
*/
|
|
51
|
+
export function buildBranchPushInstruction(stackRebase, hasConflicts, mutationSuffix, ejectionUpdate = false) {
|
|
52
|
+
if (ejectionUpdate) {
|
|
53
|
+
const push = stackRebase
|
|
54
|
+
? "commit any remaining changes on the PR head branch and push the rewritten stack with `gh stack push`"
|
|
55
|
+
: "commit any remaining changes and push to the PR head branch";
|
|
56
|
+
return `If the base update or a fix changed the head, ${push}${mutationSuffix}. If neither did, do not push.`;
|
|
57
|
+
}
|
|
49
58
|
if (stackRebase)
|
|
50
59
|
return `Commit any remaining changes on the PR head branch and push the rewritten stack with \`gh stack push\`${mutationSuffix}.`;
|
|
51
60
|
return hasConflicts
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Instruction text for a PR that GitHub removed from the merge queue after failed checks. */
|
|
2
|
+
import type { AgentCheck, ShepherdReport } from "../../types.mts";
|
|
3
|
+
/** Which recovery command, if any, Shepherd printed for a failure that does not reproduce. */
|
|
4
|
+
export type QueueEjectionRecovery = "requeue" | "acknowledge" | "none";
|
|
5
|
+
/** The removed queue commit whose failed checks this tick surfaces, if the removal is current. */
|
|
6
|
+
export declare function currentEjectionCommit(report: ShepherdReport, checks: AgentCheck[]): string | undefined;
|
|
7
|
+
/** The ejection step after failing-check triage; points at a stack route already printed. */
|
|
8
|
+
export declare function queueEjectionSteps(recovery: QueueEjectionRecovery | undefined, stackRebase: string | undefined, routePrinted: boolean): string[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Instruction text for a PR that GitHub removed from the merge queue after failed checks. */
|
|
2
|
+
import { playbookPointer } from "../playbook-pointer.mjs";
|
|
3
|
+
/** The removed queue commit whose failed checks this tick surfaces, if the removal is current. */
|
|
4
|
+
export function currentEjectionCommit(report, checks) {
|
|
5
|
+
const queue = report.mergeQueue;
|
|
6
|
+
const commit = queue?.latestRemoval?.beforeCommitOid;
|
|
7
|
+
if (!commit || queue.inQueue || queue.headUpdatedAfterRemoval)
|
|
8
|
+
return undefined;
|
|
9
|
+
return checks.some((check) => check.scope === "merge_group" && check.commitOid === commit)
|
|
10
|
+
? commit
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
/** The ejection step after failing-check triage; points at a stack route already printed. */
|
|
14
|
+
export function queueEjectionSteps(recovery, stackRebase, routePrinted) {
|
|
15
|
+
if (!recovery)
|
|
16
|
+
return [];
|
|
17
|
+
const route = stackRebase && routePrinted ? "use the stack route printed above." : stackRebase;
|
|
18
|
+
return [buildQueueEjectionInstruction(recovery, route)];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The trigger, update route, and command guard; the fixed procedure lives in the playbook.
|
|
22
|
+
* A printed command implies a `failed_checks` removal. Without one, the agent reads the raw
|
|
23
|
+
* removal reason, since a person's dequeue must not be undone by a branch update.
|
|
24
|
+
*/
|
|
25
|
+
function buildQueueEjectionInstruction(recovery, stackRoute) {
|
|
26
|
+
const target = stackRoute ? "the stack" : "the PR head";
|
|
27
|
+
const route = stackRoute ? `: ${stackRoute}` : ".";
|
|
28
|
+
const update = recovery === "none"
|
|
29
|
+
? `If the \`**queue removal**\` reason shows GitHub removed the entry itself, update ${target} from the latest base first${route} If a person may have dequeued the PR, skip that update unless a conflict step above requires it.`
|
|
30
|
+
: `Update ${target} from the latest base first${route}`;
|
|
31
|
+
const reproduce = "only if the failure does not reproduce on the updated head, neither the update nor a code change altered the head, and no other blocker remains.";
|
|
32
|
+
const guard = recovery === "requeue"
|
|
33
|
+
? `Run \`requeue:\` ${reproduce} If gh reports auto-merge is disabled, run \`requeue API fallback:\` instead.`
|
|
34
|
+
: recovery === "acknowledge"
|
|
35
|
+
? `Run \`acknowledge queue removal:\` ${reproduce}`
|
|
36
|
+
: "Shepherd printed no queue command for this session, so do not enqueue the PR.";
|
|
37
|
+
return `Triage the merge-queue ejection before any requeue. ${update} ${guard} ${playbookPointer("Merge queue ejection")}`;
|
|
38
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AgentThread, AgentComment, AgentCheck, Review, ResolveCommand, FirstLookThread, FirstLookComment, ReviewThread } from "../../types.mts";
|
|
2
|
+
import { type QueueEjectionRecovery } from "./queue-recovery-instructions.mts";
|
|
2
3
|
/** Render a resolve command as a shell snippet. Appends `--require-sha "$HEAD_SHA"` when set. */
|
|
3
4
|
export declare function renderResolveCommand(rc: ResolveCommand): string;
|
|
4
5
|
export declare function buildFixInstructions(threads: AgentThread[], actionableComments: AgentComment[], checks: AgentCheck[], changesRequestedReviews: Review[], baseBranch: string, resolveCommand: ResolveCommand, hasConflicts: boolean, prReference: string | number, _cancelledCount: number, firstLookThreads?: FirstLookThread[], firstLookComments?: FirstLookComment[], firstLookSummaries?: Review[], editedSummaries?: Review[], _inProgressRunIds?: string[], resolutionOnlyThreads?: ReviewThread[], resolveOnlyCommand?: ResolveCommand, behindBaseHint?: string, // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
5
|
-
isBehind?: boolean, viewerCanUpdate?: boolean, hasExhaustedWorkflowRerun?: boolean, stackRebase?: string
|
|
6
|
+
isBehind?: boolean, viewerCanUpdate?: boolean, hasExhaustedWorkflowRerun?: boolean, stackRebase?: string, // native stack layers rebase with gh-stack, not branch by branch
|
|
7
|
+
queueEjection?: QueueEjectionRecovery): string[];
|
|
@@ -5,6 +5,7 @@ import { isFailingAgentCheck } from "../../checks/conclusions.mjs";
|
|
|
5
5
|
import { buildCommitSuggestionInstruction } from "../commit-suggestion-instruction.mjs";
|
|
6
6
|
import { partitionFixThreads, reviewSectionRefs } from "./fix-instruction-threads.mjs";
|
|
7
7
|
import { buildBranchPushInstruction, buildConflictInstruction } from "./native-stack-rebase.mjs";
|
|
8
|
+
import { queueEjectionSteps } from "./queue-recovery-instructions.mjs";
|
|
8
9
|
/** Render a resolve command as a shell snippet. Appends `--require-sha "$HEAD_SHA"` when set. */
|
|
9
10
|
export function renderResolveCommand(rc) {
|
|
10
11
|
const parts = [...rc.argv];
|
|
@@ -13,7 +14,8 @@ export function renderResolveCommand(rc) {
|
|
|
13
14
|
return renderShellCommand(parts);
|
|
14
15
|
}
|
|
15
16
|
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prReference, _cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], _inProgressRunIds = [], resolutionOnlyThreads = [], resolveOnlyCommand, behindBaseHint = "", // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
16
|
-
isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, stackRebase
|
|
17
|
+
isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, stackRebase, // native stack layers rebase with gh-stack, not branch by branch
|
|
18
|
+
queueEjection) {
|
|
17
19
|
const instructions = [];
|
|
18
20
|
const { locatedThreads, unlocatedMutatedThreads, unlocatedThreads } = partitionFixThreads(threads, resolveCommand, resolveOnlyCommand);
|
|
19
21
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
@@ -45,25 +47,22 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, st
|
|
|
45
47
|
});
|
|
46
48
|
// The conflict hint belongs with the conflict step; otherwise it precedes the push step.
|
|
47
49
|
const hintWithConflictStep = hasConflicts && !hasRepeatedWorkflowBranchRecovery;
|
|
48
|
-
|
|
50
|
+
const branchRecovery = hasConflicts || hasRepeatedWorkflowBranchRecovery;
|
|
51
|
+
if (hintWithConflictStep)
|
|
49
52
|
instructions.push(buildConflictInstruction(stackRebase), ...branchUpdateHint);
|
|
50
|
-
}
|
|
51
53
|
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
52
|
-
if (firstLookTotal > 0)
|
|
54
|
+
if (firstLookTotal > 0)
|
|
53
55
|
instructions.push("Review every item under `## First-look items` before acting.");
|
|
54
|
-
}
|
|
55
56
|
if (firstLookSummaries.length > 0 && viewerCanUpdate)
|
|
56
57
|
instructions.push(SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE);
|
|
57
58
|
const editedTotal = editedSummaries.length +
|
|
58
59
|
actionableComments.filter((c) => c.edited).length +
|
|
59
60
|
firstLookThreads.filter((t) => t.edited).length +
|
|
60
61
|
firstLookComments.filter((c) => c.edited).length;
|
|
61
|
-
if (editedTotal > 0)
|
|
62
|
+
if (editedTotal > 0)
|
|
62
63
|
instructions.push("Read every item marked `[edited since first look]`, including edited summaries and edited first-look bullets, before deciding whether to resolve a matching thread.");
|
|
63
|
-
|
|
64
|
-
if (unlocatedThreads.length > 0) {
|
|
64
|
+
if (unlocatedThreads.length > 0)
|
|
65
65
|
instructions.push("Acknowledge each item under `## Unlocated review threads (logged once — no mutation)`. Shepherd cannot route a code fix or review mutation without a path and line; the unchanged item will be skipped on later ticks.");
|
|
66
|
-
}
|
|
67
66
|
const hasSuggestions = locatedThreads.some((t) => t.suggestion);
|
|
68
67
|
if (hasSuggestions)
|
|
69
68
|
instructions.push(buildCommitSuggestionInstruction(prReference, "## Review threads"));
|
|
@@ -73,13 +72,11 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, st
|
|
|
73
72
|
const filesRef = locatedThreads.length > 0 ? "each file referenced above" : "the relevant files";
|
|
74
73
|
instructions.push(`Apply every warranted review fix in ${filesRef}.`);
|
|
75
74
|
}
|
|
76
|
-
if (resolutionOnlyThreads.length > 0)
|
|
75
|
+
if (resolutionOnlyThreads.length > 0)
|
|
77
76
|
instructions.push("Review the threads under `## Review threads to resolve` before running the generated mutations.");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (hasAnnotations) {
|
|
77
|
+
instructions.push(...buildFailingCheckInstructions(failingChecks), ...repeatedWorkflowBranchRecoveryInstructions, ...queueEjectionSteps(queueEjection, stackRebase, branchRecovery));
|
|
78
|
+
if (hasAnnotations)
|
|
81
79
|
instructions.push("Inspect every referenced range under `## Check annotations` and apply any warranted change.");
|
|
82
|
-
}
|
|
83
80
|
if (changesRequestedReviews.length > 0) {
|
|
84
81
|
const staleClause = buildCrStaleClause(changesRequestedReviews);
|
|
85
82
|
instructions.push(`Read every body under \`## Changes-requested reviews\` and apply any warranted change.${staleClause}`);
|
|
@@ -88,8 +85,8 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, st
|
|
|
88
85
|
instructions.push(...branchUpdateHint);
|
|
89
86
|
const hasReviewMutations = resolveCommand.hasMutations || resolveOnlyCommand?.hasMutations === true;
|
|
90
87
|
const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
|
|
91
|
-
if (
|
|
92
|
-
instructions.push(buildBranchPushInstruction(stackRebase, hasConflicts, mutationSuffix));
|
|
88
|
+
if (branchRecovery || queueEjection) {
|
|
89
|
+
instructions.push(buildBranchPushInstruction(stackRebase, hasConflicts, mutationSuffix, !branchRecovery));
|
|
93
90
|
}
|
|
94
91
|
else if (hasNonConflictHints) {
|
|
95
92
|
instructions.push("If you changed code, commit any remaining changes and push to the PR head branch. If you did not, do not commit.");
|
|
@@ -50,6 +50,8 @@ function computeStallFingerprint(action, headSha, base, report, reviewSummaryIds
|
|
|
50
50
|
state: base.state,
|
|
51
51
|
isDraft: base.isDraft,
|
|
52
52
|
checks,
|
|
53
|
+
// A requeued head ejected again gets its own triage window.
|
|
54
|
+
queueRemovalCommit: report.mergeQueue?.latestRemoval?.beforeCommitOid,
|
|
53
55
|
threads,
|
|
54
56
|
resolutionOnlyThreads,
|
|
55
57
|
ruleAutoResolveThreads,
|
|
@@ -118,6 +118,12 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
118
118
|
reviewDecision: (raw.reviewDecision ?? null),
|
|
119
119
|
headRefOid: raw.headRefOid,
|
|
120
120
|
...(headPushedAtUnix !== undefined && { headPushedAtUnix }),
|
|
121
|
+
...(raw.headRefForcePushes?.nodes[0] && {
|
|
122
|
+
headForcePushedAtUnix: Math.floor(Date.parse(raw.headRefForcePushes.nodes[0].createdAt) / 1000),
|
|
123
|
+
}),
|
|
124
|
+
...(raw.mergeQueueRemovalTimes && {
|
|
125
|
+
mergeQueueRemovalTimesUnix: raw.mergeQueueRemovalTimes.nodes.map((node) => Math.floor(Date.parse(node.createdAt) / 1000)),
|
|
126
|
+
}),
|
|
121
127
|
headRefName: raw.headRefName,
|
|
122
128
|
headRepoWithOwner: raw.headRepository?.nameWithOwner ?? null,
|
|
123
129
|
viewerAuthorization: {
|
|
@@ -101,6 +101,18 @@ export interface RawPrMergeFields {
|
|
|
101
101
|
createdAt: string;
|
|
102
102
|
}>;
|
|
103
103
|
} | null;
|
|
104
|
+
/** Timestamps of the last 10 queue removals; counts repeat ejections of one head. */
|
|
105
|
+
mergeQueueRemovalTimes?: {
|
|
106
|
+
nodes: Array<{
|
|
107
|
+
createdAt: string;
|
|
108
|
+
}>;
|
|
109
|
+
} | null;
|
|
110
|
+
/** The latest force-push, which dates a head whose commit time predates its push. */
|
|
111
|
+
headRefForcePushes?: {
|
|
112
|
+
nodes: Array<{
|
|
113
|
+
createdAt: string;
|
|
114
|
+
}>;
|
|
115
|
+
} | null;
|
|
104
116
|
stack?: RawStack | null;
|
|
105
117
|
stackEntry?: {
|
|
106
118
|
position: number;
|
|
@@ -88,6 +88,20 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
mergeQueueRemovalTimes: timelineItems(last: 10, itemTypes: [REMOVED_FROM_MERGE_QUEUE_EVENT]) {
|
|
92
|
+
nodes {
|
|
93
|
+
... on RemovedFromMergeQueueEvent {
|
|
94
|
+
createdAt
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
headRefForcePushes: timelineItems(last: 1, itemTypes: [HEAD_REF_FORCE_PUSHED_EVENT]) {
|
|
99
|
+
nodes {
|
|
100
|
+
... on HeadRefForcePushedEvent {
|
|
101
|
+
createdAt
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
91
105
|
stack {
|
|
92
106
|
number
|
|
93
107
|
size
|
|
@@ -93,6 +93,13 @@ fragment PollSummaryPr on PullRequest {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
headRefForcePushes: timelineItems(last: 1, itemTypes: [HEAD_REF_FORCE_PUSHED_EVENT]) {
|
|
97
|
+
nodes {
|
|
98
|
+
... on HeadRefForcePushedEvent {
|
|
99
|
+
createdAt
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
96
103
|
mergeQueueEntry {
|
|
97
104
|
headCommit {
|
|
98
105
|
oid
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseCreatedAt } from "./batch-parser-helpers.mjs";
|
|
2
2
|
import { graphqlWithRateLimit } from "./client.mjs";
|
|
3
3
|
import { GitHubRequestError } from "./errors.mjs";
|
|
4
|
-
import { headPushUnixFromCheckNodes, queueRemovalAppliesToHead, } from "./queue-removal-freshness.mjs";
|
|
4
|
+
import { forcePushUnix, headPushUnixFromCheckNodes, queueRemovalAppliesToHead, } from "./queue-removal-freshness.mjs";
|
|
5
5
|
import { requireContextNodes } from "./batch-response.mjs";
|
|
6
6
|
import { COMMIT_CHECK_CONTEXTS_QUERY } from "./queries.mjs";
|
|
7
7
|
function omittedCursorError(oid) {
|
|
@@ -79,6 +79,7 @@ function currentRemovalCommit(raw) {
|
|
|
79
79
|
headCommittedAtUnix: parseCreatedAt(headCommit.committedDate),
|
|
80
80
|
}),
|
|
81
81
|
...(headPushedAtUnix !== undefined && { headPushedAtUnix }),
|
|
82
|
+
headForcePushedAtUnix: forcePushUnix(raw.headRefForcePushes),
|
|
82
83
|
removedAtUnix: Math.floor(Date.parse(removal.createdAt) / 1000),
|
|
83
84
|
})) {
|
|
84
85
|
return undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { headPushUnixFromCheckNodes, queueRemovalAppliesToHead, } from "./queue-removal-freshness.mjs";
|
|
1
|
+
import { forcePushUnix, headPushUnixFromCheckNodes, queueRemovalAppliesToHead, } from "./queue-removal-freshness.mjs";
|
|
2
2
|
import { parseCreatedAt } from "./batch-parser-helpers.mjs";
|
|
3
3
|
/** Latest queue removal that still applies to this PR head, not an older attempt. */
|
|
4
4
|
export function currentQueueRemovalEvent(raw) {
|
|
@@ -21,6 +21,7 @@ export function currentQueueRemovalEvent(raw) {
|
|
|
21
21
|
headCommittedAtUnix: parseCreatedAt(headCommit.committedDate),
|
|
22
22
|
}),
|
|
23
23
|
...(headPushedAtUnix !== undefined && { headPushedAtUnix }),
|
|
24
|
+
headForcePushedAtUnix: forcePushUnix(raw.headRefForcePushes),
|
|
24
25
|
removedAtUnix: Math.floor(removalTime / 1000),
|
|
25
26
|
})) {
|
|
26
27
|
return null;
|
|
@@ -138,6 +138,12 @@ export interface RawSummaryPr {
|
|
|
138
138
|
mergeQueueEntry: {
|
|
139
139
|
headCommit: RawSummaryCommit | null;
|
|
140
140
|
} | null;
|
|
141
|
+
/** The latest force-push, which dates a head whose commit and check times predate its push. */
|
|
142
|
+
headRefForcePushes?: {
|
|
143
|
+
nodes: Array<{
|
|
144
|
+
createdAt: string;
|
|
145
|
+
}>;
|
|
146
|
+
} | null;
|
|
141
147
|
stack: {
|
|
142
148
|
number: number;
|
|
143
149
|
size: number;
|
|
@@ -15,17 +15,35 @@ type PushCheckNode = {
|
|
|
15
15
|
* build a one-parent commit on the base, so parent identity cannot show a
|
|
16
16
|
* later push. Missing parents are unverifiable: GitHub keeps returning the
|
|
17
17
|
* latest removal after the synthetic commit is gone. A one-parent removal is
|
|
18
|
-
* stale once this head reached the PR after the removal
|
|
19
|
-
*
|
|
20
|
-
* the
|
|
18
|
+
* stale once this head reached the PR after the removal (see `headArrivalUnix`).
|
|
19
|
+
* A force-push after the removal makes any removal stale, even one whose
|
|
20
|
+
* parents list the head: that SHA was pushed back for a new attempt.
|
|
21
21
|
*/
|
|
22
|
-
export declare function queueRemovalAppliesToHead(input: {
|
|
22
|
+
export declare function queueRemovalAppliesToHead(input: HeadTimes & {
|
|
23
23
|
parentOids: readonly string[] | null | undefined;
|
|
24
24
|
headOid: string;
|
|
25
|
-
headCommittedAtUnix?: number;
|
|
26
|
-
headPushedAtUnix?: number;
|
|
27
25
|
removedAtUnix?: number;
|
|
28
26
|
}): boolean;
|
|
27
|
+
type HeadTimes = {
|
|
28
|
+
headCommittedAtUnix?: number | null;
|
|
29
|
+
headPushedAtUnix?: number;
|
|
30
|
+
headForcePushedAtUnix?: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* When the current head reached the PR. The earliest pull_request check on the
|
|
34
|
+
* head is its push time, but it can come from an earlier tenure of a commit
|
|
35
|
+
* that was later force-pushed back, so the latest force-push wins when it is
|
|
36
|
+
* later. Without a check time, the committer time is the fallback, again
|
|
37
|
+
* superseded by a later force-push. A committer clock that runs ahead only
|
|
38
|
+
* matters in that fallback.
|
|
39
|
+
*/
|
|
40
|
+
export declare function headArrivalUnix(input: HeadTimes): number | undefined;
|
|
41
|
+
/** Latest head-ref force-push time from a `headRefForcePushes` timeline field. */
|
|
42
|
+
export declare function forcePushUnix(field: {
|
|
43
|
+
nodes: Array<{
|
|
44
|
+
createdAt: string;
|
|
45
|
+
}>;
|
|
46
|
+
} | null | undefined): number | undefined;
|
|
29
47
|
/** Earliest pull_request check-suite time on the head commit, when one exists. */
|
|
30
48
|
export declare function headPushUnixFromCheckNodes(nodes: readonly (PushCheckNode | null)[] | null | undefined): number | undefined;
|
|
31
49
|
export {};
|
|
@@ -7,28 +7,50 @@ const PULL_REQUEST_EVENTS = new Set(["pull_request", "pull_request_target"]);
|
|
|
7
7
|
* build a one-parent commit on the base, so parent identity cannot show a
|
|
8
8
|
* later push. Missing parents are unverifiable: GitHub keeps returning the
|
|
9
9
|
* latest removal after the synthetic commit is gone. A one-parent removal is
|
|
10
|
-
* stale once this head reached the PR after the removal
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
10
|
+
* stale once this head reached the PR after the removal (see `headArrivalUnix`).
|
|
11
|
+
* A force-push after the removal makes any removal stale, even one whose
|
|
12
|
+
* parents list the head: that SHA was pushed back for a new attempt.
|
|
13
13
|
*/
|
|
14
14
|
export function queueRemovalAppliesToHead(input) {
|
|
15
15
|
const parents = input.parentOids ?? [];
|
|
16
16
|
if (parents.length === 0)
|
|
17
17
|
return false;
|
|
18
|
+
const removedAt = positive(input.removedAtUnix);
|
|
19
|
+
const forcePushedAt = positive(input.headForcePushedAtUnix);
|
|
20
|
+
// The same SHA force-pushed back after the removal starts a new tenure.
|
|
21
|
+
if (removedAt !== undefined && forcePushedAt !== undefined && forcePushedAt > removedAt)
|
|
22
|
+
return false;
|
|
18
23
|
if (parents.includes(input.headOid))
|
|
19
24
|
return true;
|
|
20
25
|
if (parents.length > 1)
|
|
21
26
|
return false;
|
|
22
|
-
|
|
23
|
-
if (removedAt === undefined || removedAt <= 0)
|
|
27
|
+
if (removedAt === undefined)
|
|
24
28
|
return true;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const arrivedAt = headArrivalUnix(input);
|
|
30
|
+
return arrivedAt === undefined || arrivedAt <= removedAt;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* When the current head reached the PR. The earliest pull_request check on the
|
|
34
|
+
* head is its push time, but it can come from an earlier tenure of a commit
|
|
35
|
+
* that was later force-pushed back, so the latest force-push wins when it is
|
|
36
|
+
* later. Without a check time, the committer time is the fallback, again
|
|
37
|
+
* superseded by a later force-push. A committer clock that runs ahead only
|
|
38
|
+
* matters in that fallback.
|
|
39
|
+
*/
|
|
40
|
+
export function headArrivalUnix(input) {
|
|
41
|
+
const pushedAt = positive(input.headPushedAtUnix) ?? positive(input.headCommittedAtUnix);
|
|
42
|
+
const forcePushedAt = positive(input.headForcePushedAtUnix);
|
|
43
|
+
if (pushedAt === undefined)
|
|
44
|
+
return forcePushedAt;
|
|
45
|
+
return forcePushedAt === undefined ? pushedAt : Math.max(pushedAt, forcePushedAt);
|
|
46
|
+
}
|
|
47
|
+
function positive(value) {
|
|
48
|
+
return value !== undefined && value !== null && value > 0 ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Latest head-ref force-push time from a `headRefForcePushes` timeline field. */
|
|
51
|
+
export function forcePushUnix(field) {
|
|
52
|
+
const raw = field?.nodes[0]?.createdAt;
|
|
53
|
+
return raw ? positive(parseCreatedAt(raw)) : undefined;
|
|
32
54
|
}
|
|
33
55
|
/** Earliest pull_request check-suite time on the head commit, when one exists. */
|
|
34
56
|
export function headPushUnixFromCheckNodes(nodes) {
|
|
@@ -9,7 +9,11 @@ type StateKey = {
|
|
|
9
9
|
repo: string;
|
|
10
10
|
pr: number;
|
|
11
11
|
};
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The only removal reason that indicates a CI-driven queue ejection. GitHub sends lowercase raw
|
|
14
|
+
* strings; observed values also include `merged`, `merge_conflict`, `invalid_merge_commit`, and
|
|
15
|
+
* `stack_invalidated`, none of which a requeue can recover.
|
|
16
|
+
*/
|
|
13
17
|
export declare function isCiQueueRemovalReason(reason: string | null | undefined): boolean;
|
|
14
18
|
/** Missing, unreadable, or malformed acknowledgment state is treated as absent. */
|
|
15
19
|
export declare function readQueueRemovalAcknowledgment(key: StateKey): Promise<QueueRemovalAcknowledgment | null>;
|
|
@@ -4,9 +4,13 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { resolvePrStatePath } from "./base.mjs";
|
|
6
6
|
const FILE = "queue-removal-ack.json";
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* The only removal reason that indicates a CI-driven queue ejection. GitHub sends lowercase raw
|
|
9
|
+
* strings; observed values also include `merged`, `merge_conflict`, `invalid_merge_commit`, and
|
|
10
|
+
* `stack_invalidated`, none of which a requeue can recover.
|
|
11
|
+
*/
|
|
8
12
|
export function isCiQueueRemovalReason(reason) {
|
|
9
|
-
return reason === "
|
|
13
|
+
return reason === "failed_checks";
|
|
10
14
|
}
|
|
11
15
|
/** Missing, unreadable, or malformed acknowledgment state is treated as absent. */
|
|
12
16
|
export async function readQueueRemovalAcknowledgment(key) {
|
package/bin/types/github.d.mts
CHANGED
|
@@ -146,6 +146,10 @@ export interface BatchPrData extends BatchPrMergeFields {
|
|
|
146
146
|
* that signal is unavailable; queue-removal freshness then uses committer time.
|
|
147
147
|
*/
|
|
148
148
|
headPushedAtUnix?: number;
|
|
149
|
+
/** Unix times of the last 10 merge-queue removals, oldest first. */
|
|
150
|
+
mergeQueueRemovalTimesUnix?: number[];
|
|
151
|
+
/** Unix time of the latest head-ref force-push, when GitHub reports one. */
|
|
152
|
+
headForcePushedAtUnix?: number;
|
|
149
153
|
headRefName: string;
|
|
150
154
|
/** `"owner/name"` of the head repository; null when the fork has been deleted. */
|
|
151
155
|
headRepoWithOwner: string | null;
|
|
@@ -10,6 +10,8 @@ export interface MergeQueueReport {
|
|
|
10
10
|
checksIncomplete?: true;
|
|
11
11
|
/** The current PR head is not a parent of the removed synthetic queue commit. */
|
|
12
12
|
headUpdatedAfterRemoval?: true;
|
|
13
|
+
/** Queue removals since the current head reached the PR; emitted only when above 1. */
|
|
14
|
+
removalsOnHead?: number;
|
|
13
15
|
/** The caller acknowledged this exact native-stack removal on the current head. */
|
|
14
16
|
removalAcknowledged?: true;
|
|
15
17
|
}
|
package/package.json
CHANGED
|
@@ -73,3 +73,4 @@ When a step says `Playbook: "<name>"`, read that file once and apply it before t
|
|
|
73
73
|
- [Shepherd Journal](references/journal.md)
|
|
74
74
|
- [Branch update](references/branch-update.md)
|
|
75
75
|
- [Stack merge](references/stack-merge.md)
|
|
76
|
+
- [Merge queue ejection](references/merge-queue-ejection.md)
|
|
@@ -6,7 +6,7 @@ Apply when a step says `Playbook: "CI failure triage"`. For a GitHub Actions row
|
|
|
6
6
|
- `[rerun authorized]` plus a `rerun:` command means the viewer can rerun Actions (WRITE+) and this is the original attempt. Shepherd checked `repositoryPermission` and `run_attempt`.
|
|
7
7
|
- Run that printed command at most once. An `[attempt: N]` check never gets another rerun. A log excerpt on a later attempt is still investigation work. A later attempt with no usable evidence escalates when nothing else remains.
|
|
8
8
|
- A run in progress, `[conclusion: ACTION_REQUIRED]`, a check whose run id is not a GitHub Actions workflow, or a run with no attempt metadata never gets `[rerun authorized]`.
|
|
9
|
-
- A check with `scope: merge_group` never gets a rerun command. Rerunning cannot restore a removed queue entry and overwrites the failure evidence. If the failure belongs to this PR, fix the PR head. If it does not
|
|
9
|
+
- A check with `scope: merge_group` never gets a rerun command. Rerunning cannot restore a removed queue entry and overwrites the failure evidence. If the failure belongs to this PR, fix the PR head. If it does not and an ejection step is printed, apply the Merge queue ejection playbook it names. Without an ejection step the entry is still queued: make no queue mutation and iterate until GitHub reports the removal.
|
|
10
10
|
- Do not invent a handoff from `[FIX_CODE]`. Shepherd returns `[ESCALATE]` when no autonomous follow-up remains.
|
|
11
11
|
- Several bullets can share one run id (matrix jobs). The `rerun:` command is printed once, on the first bullet. Run it once.
|
|
12
12
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Merge queue ejection
|
|
2
|
+
|
|
3
|
+
Apply when a step says `Playbook: "Merge queue ejection"`. The `**queue removal**` header names the queue commit. That commit combined the PR head with the base and any entries queued ahead of it, so a failure there may come from this PR, the base, another entry, or a flake. Do not requeue without triage. After triage, continue with the remaining numbered steps; iterate only at the final step.
|
|
4
|
+
|
|
5
|
+
1. Update the PR head from the latest base following the repository's branch-update convention. On a native stack, use the printed stack update route instead. Reproduce the failing step on the updated head.
|
|
6
|
+
2. If the failure belongs to this PR, fix it. The printed push step commits and pushes the fix.
|
|
7
|
+
3. If the failure does not reproduce and the update changed the head, the printed push step pushes it (`gh stack push` on a native stack). Never run a printed `requeue:` or `acknowledge queue removal:` command after a push. Shepherd prints a fresh queue command once the new head is READY.
|
|
8
|
+
4. If the failure comes from the base itself, do not requeue or acknowledge it. If a Shepherd Journal step is printed, record the finding with it. Make no change. The unchanged failure escalates through the stall timeout.
|
|
9
|
+
5. If the removal reason suggests a person dequeued the PR, skip the step 1 update unless a conflict step requires it. Fix a failure that belongs to this PR; otherwise make no change. Never enqueue.
|
|
10
|
+
6. If the step says no queue command was printed, never enqueue. A failure that does not reproduce follows step 4. Shepherd prints no queue command once the same head was already removed before (`removals on this head` in the header), so a flaky failure is not retried indefinitely.
|
|
11
|
+
7. Otherwise, run the printed `requeue:` or `acknowledge queue removal:` command exactly as printed, and only if all of these hold:
|
|
12
|
+
- the head did not change;
|
|
13
|
+
- no code changed;
|
|
14
|
+
- no other blocker remains;
|
|
15
|
+
- the logs or the check's details page show a transient failure, or a failure caused by another entry in the same queue group.
|
|
16
|
+
8. If gh reports auto-merge is disabled instead of adding the PR to the queue, run the `requeue API fallback:` command. Both requeue commands require the observed PR head SHA. If the head changed, iterate for a fresh command.
|
|
17
|
+
9. After `acknowledge queue removal:`, finish this one-PR session so it validates current source CI and records its READY receipt. Then return to the aggregate `--stack` selector with its original options. In merge mode it verifies lower-layer readiness before merging. Never enqueue or merge a stack layer directly.
|