pr-shepherd 0.54.1 → 0.55.0
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 +3 -3
- package/bin/checks/unreported-required.d.mts +29 -0
- package/bin/checks/unreported-required.mjs +52 -0
- package/bin/classify/apply.d.mts +2 -0
- package/bin/classify/apply.mjs +31 -31
- package/bin/classify/rule-action.d.mts +8 -0
- package/bin/classify/rule-action.mjs +29 -0
- package/bin/classify/types.d.mts +1 -1
- package/bin/cli/help-command-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +6 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +3 -3
- package/bin/cli/iterate-formatter.mjs +29 -9
- package/bin/cli/iterate-instructions.mjs +6 -0
- package/bin/cli/iterate-lean.mjs +15 -0
- package/bin/cli/poll-summary-emitter.mjs +4 -1
- package/bin/cli/poll-summary-formatter.mjs +10 -3
- package/bin/cli/stack-overview.d.mts +48 -0
- package/bin/cli/stack-overview.mjs +138 -0
- package/bin/commands/check-fingerprint.mjs +2 -1
- package/bin/commands/check-status.d.mts +1 -1
- package/bin/commands/check-status.mjs +8 -3
- package/bin/commands/check-unreported.d.mts +26 -0
- package/bin/commands/check-unreported.mjs +73 -0
- package/bin/commands/check.mjs +44 -38
- package/bin/commands/iterate/base.mjs +9 -0
- package/bin/commands/iterate/escalate.mjs +4 -0
- package/bin/commands/iterate/index.mjs +69 -3
- package/bin/commands/iterate/merge-state.mjs +12 -12
- package/bin/commands/iterate/merge.d.mts +12 -2
- package/bin/commands/iterate/merge.mjs +32 -2
- package/bin/commands/iterate/unreported-required.d.mts +18 -0
- package/bin/commands/iterate/unreported-required.mjs +122 -0
- package/bin/commands/poll-summary.mjs +1 -0
- package/bin/commands/poll.mjs +9 -2
- package/bin/commands/ready-mergeability.d.mts +1 -1
- package/bin/commands/ready-mergeability.mjs +2 -2
- package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
- package/bin/commands/rule-auto-resolve-format.mjs +151 -0
- package/bin/commands/rule-auto-resolve.d.mts +21 -0
- package/bin/commands/rule-auto-resolve.mjs +113 -0
- package/bin/commands/stack-drain.d.mts +0 -5
- package/bin/commands/stack-drain.mjs +26 -16
- package/bin/commands/stack-merge-flag.d.mts +7 -0
- package/bin/commands/stack-merge-flag.mjs +6 -0
- package/bin/config/load.d.mts +3 -0
- package/bin/config/load.mjs +9 -2
- package/bin/config/merge-method.d.mts +27 -0
- package/bin/config/merge-method.mjs +56 -0
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +2 -1
- package/bin/github/batch-parse-suites.d.mts +3 -0
- package/bin/github/batch-parse-suites.mjs +15 -0
- package/bin/github/batch-parsers.d.mts +1 -1
- package/bin/github/batch-parsers.mjs +7 -0
- package/bin/github/batch-raw-types.d.mts +5 -0
- package/bin/github/batch.d.mts +3 -0
- package/bin/github/batch.mjs +9 -1
- package/bin/github/errors.d.mts +6 -0
- package/bin/github/errors.mjs +23 -5
- package/bin/github/fingerprint-fields.d.mts +1 -0
- package/bin/github/fingerprint-fields.mjs +1 -1
- package/bin/github/gql/batch-pr-page.gql +2 -0
- package/bin/github/gql/batch-pr.gql +5 -0
- package/bin/github/gql/commit-check-suites.gql +1 -0
- package/bin/github/gql/poll-stack-summary.gql +6 -0
- package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
- package/bin/github/gql/poll-summary-fragment.gql +16 -0
- package/bin/github/gql/pr-merge-policy.gql +1 -44
- package/bin/github/gql/ref-rules-query.gql +18 -0
- package/bin/github/gql/ref-rules.gql +46 -0
- package/bin/github/graphql-internal-retry.d.mts +1 -1
- package/bin/github/graphql-internal-retry.mjs +4 -3
- package/bin/github/merge-queue-checks.mjs +7 -3
- package/bin/github/merge-target-rules.d.mts +20 -0
- package/bin/github/merge-target-rules.mjs +54 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
- package/bin/github/poll-summary-annotation-probe.mjs +88 -0
- package/bin/github/poll-summary-fingerprint.mjs +1 -1
- package/bin/github/poll-summary-projector.d.mts +1 -1
- package/bin/github/poll-summary-projector.mjs +19 -12
- package/bin/github/poll-summary-queue-removal.mjs +7 -3
- package/bin/github/poll-summary-raw.d.mts +16 -0
- package/bin/github/poll-summary-readiness.mjs +1 -0
- package/bin/github/poll-summary-route.mjs +5 -0
- package/bin/github/poll-summary-unreported.d.mts +6 -0
- package/bin/github/poll-summary-unreported.mjs +51 -0
- package/bin/github/poll-summary.d.mts +1 -0
- package/bin/github/poll-summary.mjs +28 -3
- package/bin/github/queries.d.mts +7 -0
- package/bin/github/queries.mjs +9 -2
- package/bin/github/queue-removal-freshness.d.mts +20 -5
- package/bin/github/queue-removal-freshness.mjs +37 -10
- package/bin/github/stack-read.d.mts +4 -0
- package/bin/github/stack-read.mjs +13 -1
- package/bin/mcp/server.mjs +6 -1
- package/bin/state/ci-retrigger.d.mts +25 -0
- package/bin/state/ci-retrigger.mjs +48 -0
- package/bin/types/escalate.d.mts +1 -1
- package/bin/types/github.d.mts +11 -0
- package/bin/types/iterate.d.mts +14 -3
- package/bin/types/poll-summary.d.mts +13 -0
- package/bin/types/report.d.mts +32 -1
- 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 +2 -2
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
- package/src/classify/types.mts +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BatchPartition } from "../classify/apply.mts";
|
|
2
|
+
import type { AutoMinimizedItem, AutoResolvedThread, BatchPrData } from "../types.mts";
|
|
3
|
+
export interface RuleAutoResolveApplied {
|
|
4
|
+
threadIds: string[];
|
|
5
|
+
commentIds: string[];
|
|
6
|
+
reviewSummaryIds: string[];
|
|
7
|
+
autoResolved: AutoResolvedThread[];
|
|
8
|
+
autoMinimized: AutoMinimizedItem[];
|
|
9
|
+
autoResolveErrors: string[];
|
|
10
|
+
errorReasons: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function applySuppressedRuleAutoResolve(input: {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
partition: BatchPartition;
|
|
15
|
+
batch: BatchPrData;
|
|
16
|
+
prNumber: number;
|
|
17
|
+
repo: {
|
|
18
|
+
owner: string;
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
21
|
+
}): Promise<RuleAutoResolveApplied>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { autoMinimizeComments, autoResolveThreads } from "../comments/resolve.mjs";
|
|
2
|
+
import { runJournal } from "./journal/index.mjs";
|
|
3
|
+
import { decorateAutoResolveError, formatRuleAutoResolveJournalItem, minimizedItem, reasonClause, reasonsForError, resolvedThread, ruleAutoResolveBody, uniqueReasons, } from "./rule-auto-resolve-format.mjs";
|
|
4
|
+
const EMPTY = {
|
|
5
|
+
threadIds: [],
|
|
6
|
+
commentIds: [],
|
|
7
|
+
reviewSummaryIds: [],
|
|
8
|
+
autoResolved: [],
|
|
9
|
+
autoMinimized: [],
|
|
10
|
+
autoResolveErrors: [],
|
|
11
|
+
errorReasons: [],
|
|
12
|
+
};
|
|
13
|
+
function idsIn(ids, confirmed) {
|
|
14
|
+
return ids.filter((id) => confirmed.has(id));
|
|
15
|
+
}
|
|
16
|
+
function decorateErrors(errors, ids, ruleReasons) {
|
|
17
|
+
return errors.map((error) => decorateAutoResolveError(error, reasonsForError(error, ids, ruleReasons)));
|
|
18
|
+
}
|
|
19
|
+
function successReasons(ids, ruleReasons) {
|
|
20
|
+
return uniqueReasons(ids.flatMap((id) => [...(ruleReasons.get(id) ?? [])]));
|
|
21
|
+
}
|
|
22
|
+
export async function applySuppressedRuleAutoResolve(input) {
|
|
23
|
+
if (!input.enabled) {
|
|
24
|
+
return {
|
|
25
|
+
...EMPTY,
|
|
26
|
+
threadIds: input.partition.ruleAutoResolveThreadIds,
|
|
27
|
+
commentIds: input.partition.ruleAutoResolveCommentIds,
|
|
28
|
+
reviewSummaryIds: input.partition.ruleAutoResolveReviewSummaryIds,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const commentIds = input.partition.ruleAutoResolveCommentIds.filter((id) => input.partition.suppressedCommentIds.has(id));
|
|
32
|
+
const reviewSummaryIds = input.partition.ruleAutoResolveReviewSummaryIds.filter((id) => input.partition.suppressedReviewSummaryIds.has(id));
|
|
33
|
+
const threadIds = input.partition.ruleAutoResolveThreadIds.filter((id) => input.partition.suppressedThreadIds.has(id));
|
|
34
|
+
const minimizeIds = [...commentIds, ...reviewSummaryIds];
|
|
35
|
+
const [minimized, resolved] = await Promise.all([
|
|
36
|
+
minimizeIds.length > 0
|
|
37
|
+
? autoMinimizeComments(minimizeIds)
|
|
38
|
+
: Promise.resolve({ minimized: [], errors: [] }),
|
|
39
|
+
threadIds.length > 0
|
|
40
|
+
? autoResolveThreads(threadIds)
|
|
41
|
+
: Promise.resolve({ resolved: [], errors: [] }),
|
|
42
|
+
]);
|
|
43
|
+
const minimizedSet = new Set(minimized.minimized);
|
|
44
|
+
const resolvedSet = new Set(resolved.resolved);
|
|
45
|
+
const resolvedIds = idsIn(threadIds, resolvedSet);
|
|
46
|
+
const minimizedCommentIds = idsIn(commentIds, minimizedSet);
|
|
47
|
+
const minimizedSummaryIds = idsIn(reviewSummaryIds, minimizedSet);
|
|
48
|
+
const threadsById = new Map(input.batch.reviewThreads.map((thread) => [thread.id, thread]));
|
|
49
|
+
const commentsById = new Map(input.batch.comments.map((comment) => [comment.id, comment]));
|
|
50
|
+
const summariesById = new Map(input.batch.reviewSummaries.map((review) => [review.id, review]));
|
|
51
|
+
const autoResolved = resolvedIds.flatMap((id) => {
|
|
52
|
+
const thread = threadsById.get(id);
|
|
53
|
+
return thread ? [resolvedThread(thread, input.partition.ruleReasons.get(id) ?? [])] : [];
|
|
54
|
+
});
|
|
55
|
+
const autoMinimized = [
|
|
56
|
+
...minimizedCommentIds.map((id) => minimizedItem("pr-comment", id, commentsById.get(id)?.url, input.partition.ruleReasons.get(id) ?? [])),
|
|
57
|
+
...minimizedSummaryIds.map((id) => minimizedItem("review-summary", id, summariesById.get(id)?.url, input.partition.ruleReasons.get(id) ?? [])),
|
|
58
|
+
];
|
|
59
|
+
const autoResolveErrors = [
|
|
60
|
+
...decorateErrors(minimized.errors, minimizeIds, input.partition.ruleReasons),
|
|
61
|
+
...decorateErrors(resolved.errors, threadIds, input.partition.ruleReasons),
|
|
62
|
+
];
|
|
63
|
+
const errorReasons = uniqueReasons([
|
|
64
|
+
...minimized.errors.flatMap((error) => reasonsForError(error, minimizeIds, input.partition.ruleReasons)),
|
|
65
|
+
...resolved.errors.flatMap((error) => reasonsForError(error, threadIds, input.partition.ruleReasons)),
|
|
66
|
+
]);
|
|
67
|
+
const journalError = await journalSuccesses(input, autoResolved, autoMinimized);
|
|
68
|
+
if (journalError)
|
|
69
|
+
autoResolveErrors.push(journalError);
|
|
70
|
+
return {
|
|
71
|
+
threadIds: input.partition.ruleAutoResolveThreadIds.filter((id) => !resolvedSet.has(id)),
|
|
72
|
+
commentIds: input.partition.ruleAutoResolveCommentIds.filter((id) => !minimizedSet.has(id)),
|
|
73
|
+
reviewSummaryIds: input.partition.ruleAutoResolveReviewSummaryIds.filter((id) => !minimizedSet.has(id)),
|
|
74
|
+
autoResolved,
|
|
75
|
+
autoMinimized,
|
|
76
|
+
autoResolveErrors,
|
|
77
|
+
errorReasons,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function journalSuccesses(input, autoResolved, autoMinimized) {
|
|
81
|
+
const body = ruleAutoResolveBody({
|
|
82
|
+
threads: autoResolved.length,
|
|
83
|
+
comments: autoMinimized.filter((item) => item.kind === "pr-comment").length,
|
|
84
|
+
reviewSummaries: autoMinimized.filter((item) => item.kind === "review-summary").length,
|
|
85
|
+
});
|
|
86
|
+
if (!body)
|
|
87
|
+
return undefined;
|
|
88
|
+
const ids = [...autoResolved.map((thread) => thread.id), ...autoMinimized.map((item) => item.id)];
|
|
89
|
+
const reasons = successReasons(ids, input.partition.ruleReasons);
|
|
90
|
+
const item = formatRuleAutoResolveJournalItem({
|
|
91
|
+
body,
|
|
92
|
+
reasons,
|
|
93
|
+
...(input.batch.viewerLogin ? { viewerLogin: input.batch.viewerLogin } : {}),
|
|
94
|
+
urls: [
|
|
95
|
+
...autoResolved.map((thread) => thread.url),
|
|
96
|
+
...autoMinimized.map((item) => item.url ?? ""),
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
await runJournal({
|
|
101
|
+
prNumber: input.prNumber,
|
|
102
|
+
targetRepository: { owner: input.repo.owner, name: input.repo.name },
|
|
103
|
+
rawItem: item,
|
|
104
|
+
dryRun: false,
|
|
105
|
+
});
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
110
|
+
const clause = reasonClause(reasons);
|
|
111
|
+
return clause ? `journal: ${message} ${clause}` : `journal: ${message}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -7,11 +7,6 @@ export interface StackPlan {
|
|
|
7
7
|
idle?: PollSummaryItem[];
|
|
8
8
|
instructions: string[];
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Merge the ready prefix by its highest PR number. `gh stack merge <n>` tries a
|
|
12
|
-
* stack number first, but stack numbers come from the repository's issue and
|
|
13
|
-
* pull request sequence, so a PR number never names a stack.
|
|
14
|
-
*/
|
|
15
10
|
export declare function planPrefixDrain(result: PollSummaryResult, mergeRequested: boolean): StackPlan | undefined;
|
|
16
11
|
/**
|
|
17
12
|
* A merge-requested, fully ready stack whose bottom open layer still targets
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
+
import { stackMergeFlag } from "./stack-merge-flag.mjs";
|
|
1
2
|
import { stackLayerBlockReason } from "./stack-layer-readiness.mjs";
|
|
2
3
|
import { appendMarkReadyInstructions, splitStackWork } from "./stack-work.mjs";
|
|
3
|
-
/**
|
|
4
|
-
* Highest open layer such that it and every open layer below it are ready to
|
|
5
|
-
* merge together. `gh stack merge <PR>` lands that PR and every unmerged layer
|
|
6
|
-
* below it. A merge queue accepts the same prefix and evaluates each layer
|
|
7
|
-
* from the bottom; a failure ejects that layer and those above it.
|
|
8
|
-
*/
|
|
9
4
|
function readyPrefixTop(result) {
|
|
10
5
|
const layers = [...result.prs].sort((left, right) => stackPosition(left) - stackPosition(right));
|
|
11
6
|
if (layers.some((item) => item.state === "OPEN" && item.isInMergeQueue))
|
|
@@ -29,11 +24,6 @@ function readyPrefixTop(result) {
|
|
|
29
24
|
}
|
|
30
25
|
return top;
|
|
31
26
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Merge the ready prefix by its highest PR number. `gh stack merge <n>` tries a
|
|
34
|
-
* stack number first, but stack numbers come from the repository's issue and
|
|
35
|
-
* pull request sequence, so a PR number never names a stack.
|
|
36
|
-
*/
|
|
37
27
|
export function planPrefixDrain(result, mergeRequested) {
|
|
38
28
|
if (!mergeRequested)
|
|
39
29
|
return undefined;
|
|
@@ -49,8 +39,16 @@ export function planPrefixDrain(result, mergeRequested) {
|
|
|
49
39
|
item.action !== "escalate" &&
|
|
50
40
|
(!isStackLayerReady(item) || staleChildren.has(item.pr))), staleChildren);
|
|
51
41
|
const span = open[0]?.pr === top.pr ? "that layer alone" : `PR #${top.pr} and every unmerged layer below it`;
|
|
42
|
+
const method = stackMergeFlag(result.allowedMergeMethods);
|
|
43
|
+
if ("unavailable" in method) {
|
|
44
|
+
return {
|
|
45
|
+
action: "escalate",
|
|
46
|
+
stackMergeable: false,
|
|
47
|
+
instructions: [`1. ${method.unavailable}`],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
52
50
|
const instructions = [
|
|
53
|
-
`1. PR #${top.pr} is the highest open layer of stack #${top.stack.number} in \`${result.repo}\` whose open lower layers are all ready. Run \`GH_REPO=${result.repo} gh stack merge ${top.pr} --yes
|
|
51
|
+
`1. PR #${top.pr} is the highest open layer of stack #${top.stack.number} in \`${result.repo}\` whose open lower layers are all ready. Run \`GH_REPO=${result.repo} gh stack merge ${top.pr} --yes ${method.flag}\` to merge ${span}. When the base uses a merge queue, the same command queues that prefix together and GitHub evaluates each layer from the bottom; a failure ejects that layer and the layers above it. If \`gh stack\` is an unknown command, run \`gh extension install github/gh-stack\` first.`,
|
|
54
52
|
];
|
|
55
53
|
appendAutonomousInstructions(instructions, above.sessions);
|
|
56
54
|
appendMarkReadyInstructions(instructions, above.markReady);
|
|
@@ -90,17 +88,29 @@ export function idleWaitPlan(idle) {
|
|
|
90
88
|
],
|
|
91
89
|
};
|
|
92
90
|
}
|
|
91
|
+
function sessionInstruction(item) {
|
|
92
|
+
if (!item.owned)
|
|
93
|
+
return ` PR #${item.pr} is not owned. Do not run a session for it.`;
|
|
94
|
+
const removal = item.queueRemoval
|
|
95
|
+
? ` PR #${item.pr} was removed from the merge queue (\`${item.queueRemoval.reason ?? "unknown reason"}\`${item.queueRemoval.actor ? ` by @${item.queueRemoval.actor}` : ""}).`
|
|
96
|
+
: "";
|
|
97
|
+
if (!item.pollCommand) {
|
|
98
|
+
return ` PR #${item.pr} needs a one-PR Shepherd session, but no command was available.`;
|
|
99
|
+
}
|
|
100
|
+
const queueNote = item.queueRemoval
|
|
101
|
+
? " That session fixes failing queue CI, or escalates when the removal has no concrete fix."
|
|
102
|
+
: "";
|
|
103
|
+
return `${removal} Run \`${item.pollCommand}\` for PR #${item.pr}.${queueNote}`;
|
|
104
|
+
}
|
|
93
105
|
export function describeIdleLayers(idle) {
|
|
94
106
|
return idle.map((item) => `PR #${item.pr} (${item.reasons.join(", ")})`).join("; ");
|
|
95
107
|
}
|
|
96
108
|
export function appendAutonomousInstructions(instructions, candidates) {
|
|
97
109
|
if (candidates.length === 0)
|
|
98
110
|
return;
|
|
99
|
-
instructions.push(`${instructions.length + 1}. Start or delegate
|
|
111
|
+
instructions.push(`${instructions.length + 1}. Start or delegate one-PR sessions only for rows marked \`owned\`. Leave every other author's layer untouched. Owned layers can proceed concurrently.`);
|
|
100
112
|
for (const item of candidates) {
|
|
101
|
-
instructions.push(item
|
|
102
|
-
? `${instructions.length + 1}. Run \`${item.pollCommand}\` for PR #${item.pr}${item.queueRemoval ? `; GitHub removed it from the merge queue (${item.queueRemoval.reason ?? "unknown reason"})` : ""}.`
|
|
103
|
-
: `${instructions.length + 1}. PR #${item.pr} needs a one-PR Shepherd session, but no command was available.`);
|
|
113
|
+
instructions.push(`${instructions.length + 1}.${sessionInstruction(item)}`);
|
|
104
114
|
}
|
|
105
115
|
}
|
|
106
116
|
export function findHumanHandoffs(result) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type MergeMethod } from "../config/merge-method.mts";
|
|
2
|
+
/** Stack commands prefer squash unless config or the repository selects another method. */
|
|
3
|
+
export declare function stackMergeFlag(allowed: readonly MergeMethod[] | undefined): {
|
|
4
|
+
flag: string;
|
|
5
|
+
} | {
|
|
6
|
+
unavailable: string;
|
|
7
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { loadConfig } from "../config/load.mjs";
|
|
2
|
+
import { configuredMergeMethod, mergeMethodFlag, } from "../config/merge-method.mjs";
|
|
3
|
+
/** Stack commands prefer squash unless config or the repository selects another method. */
|
|
4
|
+
export function stackMergeFlag(allowed) {
|
|
5
|
+
return mergeMethodFlag(allowed, "squash", configuredMergeMethod(loadConfig().merge ?? {}));
|
|
6
|
+
}
|
package/bin/config/load.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MergeMethod } from "./merge-method.mts";
|
|
1
2
|
declare const MINIMIZE_COMMENTS_POLICIES: readonly ["all", "bots", "users", "none"];
|
|
2
3
|
export type MinimizeCommentsPolicy = (typeof MINIMIZE_COMMENTS_POLICIES)[number];
|
|
3
4
|
declare const RESOLVE_OTHER_HUMAN_THREADS: readonly ["none", "outdated", "always"];
|
|
@@ -70,6 +71,8 @@ export interface PrShepherdConfig {
|
|
|
70
71
|
blockingReviewerLogins: string[];
|
|
71
72
|
};
|
|
72
73
|
merge?: {
|
|
74
|
+
/** Preferred merge method. Used only when the repository allows it. */
|
|
75
|
+
method?: MergeMethod;
|
|
73
76
|
/** Options added to ordinary `gh pr merge` commands. Queue commands never use these. */
|
|
74
77
|
commandArgs: string[];
|
|
75
78
|
};
|
package/bin/config/load.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { parse } from "yaml";
|
|
|
6
6
|
import builtins from "../config.json" with { type: "json" };
|
|
7
7
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
8
8
|
import { findMergeStrategies } from "./merge-command-args.mjs";
|
|
9
|
+
import { parseMergeMethod } from "./merge-method.mjs";
|
|
9
10
|
const MINIMIZE_COMMENTS_POLICIES = ["all", "bots", "users", "none"];
|
|
10
11
|
const RESOLVE_OTHER_HUMAN_THREADS = ["none", "outdated", "always"];
|
|
11
12
|
const RC_FILENAME = ".pr-shepherdrc.yml";
|
|
@@ -143,7 +144,7 @@ function parseMergeCommandArgs(value) {
|
|
|
143
144
|
if (strategies.length > 1) {
|
|
144
145
|
throw new Error(`Invalid config: merge.commandArgs includes multiple merge strategies: ${strategies.join(", ")}`);
|
|
145
146
|
}
|
|
146
|
-
return
|
|
147
|
+
return [...value];
|
|
147
148
|
}
|
|
148
149
|
function parsePollConfig(value) {
|
|
149
150
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -279,7 +280,7 @@ const KNOWN_NESTED_KEYS = {
|
|
|
279
280
|
resolve: new Set(["shaPoll"]),
|
|
280
281
|
checks: new Set(["ciTriggerEvents", "ignoreLogLines"]),
|
|
281
282
|
mergeStatus: new Set(["blockingReviewerLogins"]),
|
|
282
|
-
merge: new Set(["commandArgs"]),
|
|
283
|
+
merge: new Set(["commandArgs", "method"]),
|
|
283
284
|
actions: new Set([
|
|
284
285
|
"autoMinimizeSuppressed",
|
|
285
286
|
"autoMarkReady",
|
|
@@ -321,6 +322,12 @@ function parseConfig(value, normalizeMergeArgs = true) {
|
|
|
321
322
|
config.actions.neverCancelRuns = parseNeverCancelRuns(config.actions.neverCancelRuns);
|
|
322
323
|
if (config.merge && normalizeMergeArgs) {
|
|
323
324
|
config.merge.commandArgs = parseMergeCommandArgs(config.merge.commandArgs);
|
|
325
|
+
if (config.merge.method !== undefined)
|
|
326
|
+
config.merge.method = parseMergeMethod(config.merge.method);
|
|
327
|
+
const fromArgs = findMergeStrategies(config.merge.commandArgs)[0];
|
|
328
|
+
if (config.merge.method && fromArgs && fromArgs !== config.merge.method) {
|
|
329
|
+
throw new Error(`Invalid config: merge.method ${config.merge.method} conflicts with merge.commandArgs strategy ${fromArgs}`);
|
|
330
|
+
}
|
|
324
331
|
}
|
|
325
332
|
config.poll = parsePollConfig(config.poll);
|
|
326
333
|
config.watch.graphqlQuotaWarnings = parseGraphqlQuotaWarnings(config.watch.graphqlQuotaWarnings, config.poll.intervalSeconds);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type MergeMethod = "merge" | "squash" | "rebase";
|
|
2
|
+
export interface RepositoryMergeSettings {
|
|
3
|
+
mergeCommitAllowed?: boolean;
|
|
4
|
+
squashMergeAllowed?: boolean;
|
|
5
|
+
rebaseMergeAllowed?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** `undefined` when the query did not select repository merge settings. */
|
|
8
|
+
export declare function readAllowedMergeMethods(repository: RepositoryMergeSettings | null | undefined): MergeMethod[] | undefined;
|
|
9
|
+
export declare function parseMergeMethod(value: unknown): MergeMethod;
|
|
10
|
+
export declare function configuredMergeMethod(merge: {
|
|
11
|
+
method?: MergeMethod;
|
|
12
|
+
commandArgs?: string[];
|
|
13
|
+
}): MergeMethod | null;
|
|
14
|
+
export declare function chooseMergeMethod(input: {
|
|
15
|
+
allowed: readonly MergeMethod[] | undefined;
|
|
16
|
+
configured: MergeMethod | null;
|
|
17
|
+
fallback: MergeMethod;
|
|
18
|
+
}): {
|
|
19
|
+
method: MergeMethod;
|
|
20
|
+
} | {
|
|
21
|
+
unavailable: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function mergeMethodFlag(allowed: readonly MergeMethod[] | undefined, fallback: MergeMethod, configured: MergeMethod | null): {
|
|
24
|
+
flag: string;
|
|
25
|
+
} | {
|
|
26
|
+
unavailable: string;
|
|
27
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { findMergeStrategies } from "./merge-command-args.mjs";
|
|
2
|
+
const MERGE_METHODS = new Set(["merge", "squash", "rebase"]);
|
|
3
|
+
const METHOD_ORDER = ["merge", "squash", "rebase"];
|
|
4
|
+
/** `undefined` when the query did not select repository merge settings. */
|
|
5
|
+
export function readAllowedMergeMethods(repository) {
|
|
6
|
+
if (!repository ||
|
|
7
|
+
(repository.mergeCommitAllowed === undefined &&
|
|
8
|
+
repository.squashMergeAllowed === undefined &&
|
|
9
|
+
repository.rebaseMergeAllowed === undefined)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const allowed = [];
|
|
13
|
+
if (repository.mergeCommitAllowed)
|
|
14
|
+
allowed.push("merge");
|
|
15
|
+
if (repository.squashMergeAllowed)
|
|
16
|
+
allowed.push("squash");
|
|
17
|
+
if (repository.rebaseMergeAllowed)
|
|
18
|
+
allowed.push("rebase");
|
|
19
|
+
return allowed;
|
|
20
|
+
}
|
|
21
|
+
export function parseMergeMethod(value) {
|
|
22
|
+
if (typeof value === "string" && MERGE_METHODS.has(value)) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
throw new Error("Invalid config: merge.method must be merge, squash, or rebase");
|
|
26
|
+
}
|
|
27
|
+
export function configuredMergeMethod(merge) {
|
|
28
|
+
const fromArgs = findMergeStrategies(merge.commandArgs ?? [])[0];
|
|
29
|
+
return merge.method ?? fromArgs ?? null;
|
|
30
|
+
}
|
|
31
|
+
export function chooseMergeMethod(input) {
|
|
32
|
+
if (input.allowed === undefined)
|
|
33
|
+
return { method: input.configured ?? input.fallback };
|
|
34
|
+
if (input.configured) {
|
|
35
|
+
if (input.allowed.includes(input.configured))
|
|
36
|
+
return { method: input.configured };
|
|
37
|
+
const allowedText = input.allowed.length > 0 ? input.allowed.join(", ") : "none";
|
|
38
|
+
return {
|
|
39
|
+
unavailable: `Configured merge method \`${input.configured}\` is not allowed by this repository. Allowed methods: ${allowedText}.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (input.allowed.includes(input.fallback))
|
|
43
|
+
return { method: input.fallback };
|
|
44
|
+
const method = METHOD_ORDER.find((item) => input.allowed?.includes(item));
|
|
45
|
+
if (method)
|
|
46
|
+
return { method };
|
|
47
|
+
return {
|
|
48
|
+
unavailable: "This repository allows no merge method (merge commits, squash, and rebase are all disabled).",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function mergeMethodFlag(allowed, fallback, configured) {
|
|
52
|
+
const decision = chooseMergeMethod({ allowed, configured, fallback });
|
|
53
|
+
if ("unavailable" in decision)
|
|
54
|
+
return decision;
|
|
55
|
+
return { flag: `--${decision.method}` };
|
|
56
|
+
}
|
package/bin/exit-codes.d.mts
CHANGED
|
@@ -13,7 +13,7 @@ import type { IterateResult } from "./types.mts";
|
|
|
13
13
|
export declare const EXIT: Readonly<{
|
|
14
14
|
/** `cancel` + `merged` or `ready-delay-elapsed` — shepherd finished cleanly. */
|
|
15
15
|
readonly OK: 0;
|
|
16
|
-
/** Nothing to do yet
|
|
16
|
+
/** Nothing to do yet, or a clean PR whose ready-delay is still counting. */
|
|
17
17
|
readonly WAIT: 10;
|
|
18
18
|
/** Draft PR converted to ready for review. */
|
|
19
19
|
readonly MARK_READY: 11;
|
package/bin/exit-codes.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
export const EXIT = Object.freeze({
|
|
13
13
|
/** `cancel` + `merged` or `ready-delay-elapsed` — shepherd finished cleanly. */
|
|
14
14
|
OK: 0,
|
|
15
|
-
/** Nothing to do yet
|
|
15
|
+
/** Nothing to do yet, or a clean PR whose ready-delay is still counting. */
|
|
16
16
|
WAIT: 10,
|
|
17
17
|
/** Draft PR converted to ready for review. */
|
|
18
18
|
MARK_READY: 11,
|
|
@@ -62,6 +62,7 @@ export function iterateResultToExitCode(result) {
|
|
|
62
62
|
case "cancel":
|
|
63
63
|
return CANCEL_REASON_EXIT_CODE[result.reason];
|
|
64
64
|
case "wait":
|
|
65
|
+
case "ready":
|
|
65
66
|
return EXIT.WAIT;
|
|
66
67
|
case "mark_ready":
|
|
67
68
|
return EXIT.MARK_READY;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { WorkflowSuiteSnapshot } from "../checks/unreported-required.mts";
|
|
1
2
|
import type { CheckRun } from "../types.mts";
|
|
2
3
|
import type { RawPr } from "./batch-raw-types.mts";
|
|
3
4
|
export declare function parseCheckSuitesComplete(raw: RawPr): boolean;
|
|
4
5
|
/** True only when the head commit's suite page finished and listed nothing. */
|
|
5
6
|
export declare function parseHeadCheckSuitesEmpty(raw: RawPr): boolean;
|
|
7
|
+
/** Actions suites on the head. Third-party suites with no workflow run are omitted. */
|
|
8
|
+
export declare function parseHeadWorkflowSuites(raw: RawPr): WorkflowSuiteSnapshot[];
|
|
6
9
|
export declare function parseSuiteStartupFailures(raw: RawPr): CheckRun[];
|
|
@@ -6,6 +6,21 @@ export function parseHeadCheckSuitesEmpty(raw) {
|
|
|
6
6
|
const suites = raw.commits.nodes[0]?.commit.checkSuites;
|
|
7
7
|
return suites?.pageInfo.hasNextPage === false && suites.nodes.length === 0;
|
|
8
8
|
}
|
|
9
|
+
/** Actions suites on the head. Third-party suites with no workflow run are omitted. */
|
|
10
|
+
export function parseHeadWorkflowSuites(raw) {
|
|
11
|
+
const nodes = raw.commits.nodes[0]?.commit.checkSuites?.nodes ?? [];
|
|
12
|
+
return nodes.flatMap((node) => {
|
|
13
|
+
if (!node.workflowRun)
|
|
14
|
+
return [];
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
status: node.status ?? null,
|
|
18
|
+
conclusion: node.conclusion,
|
|
19
|
+
workflowRun: { event: node.workflowRun.event },
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
9
24
|
export function parseSuiteStartupFailures(raw) {
|
|
10
25
|
const suites = raw.commits.nodes[0]?.commit.checkSuites;
|
|
11
26
|
if (!suites)
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { BatchPrData } from "../types.mts";
|
|
2
2
|
import type { RawBatchResponse, RawPr, RawThread, RawComment, RawReview, RawReviewSummary, RawContextNode } from "./batch-raw-types.mts";
|
|
3
|
-
export declare function parseRawPr(raw: RawPr, rawThreadPages: RawThread[], rawCommentNodes: RawComment[], rawReviewNodes: RawReview[], rawReviewSummaryNodes: RawReviewSummary[], rawApprovedReviewNodes: RawReviewSummary[], rawCheckNodes: RawContextNode[], repository?: Pick<NonNullable<RawBatchResponse["repository"]>, "viewerPermission" | "viewerCanAdminister">): BatchPrData;
|
|
3
|
+
export declare function parseRawPr(raw: RawPr, rawThreadPages: RawThread[], rawCommentNodes: RawComment[], rawReviewNodes: RawReview[], rawReviewSummaryNodes: RawReviewSummary[], rawApprovedReviewNodes: RawReviewSummary[], rawCheckNodes: RawContextNode[], repository?: Pick<NonNullable<RawBatchResponse["repository"]>, "viewerPermission" | "viewerCanAdminister" | "mergeCommitAllowed" | "squashMergeAllowed" | "rebaseMergeAllowed">): BatchPrData;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { mapAuthorType, parseCreatedAt, latestApprovedLogins, isReviewStale, } from "./batch-parser-helpers.mjs";
|
|
2
2
|
import { parseCheckNodes } from "./batch-parse-checks.mjs";
|
|
3
|
+
import { headPushUnixFromCheckNodes } from "./queue-removal-freshness.mjs";
|
|
3
4
|
import { requireContextNodes } from "./batch-response.mjs";
|
|
4
5
|
import { buildPrActivitySummary } from "./activity.mjs";
|
|
5
6
|
import { parseBranchProtection } from "./branch-protection.mjs";
|
|
7
|
+
import { readAllowedMergeMethods } from "../config/merge-method.mjs";
|
|
6
8
|
import { parseAutoMergeRequest, parseBranchRules, parseLatestMergeQueueRemoval, parseMergeQueueEntry, parseStack, } from "./batch-parsers-rules.mjs";
|
|
7
9
|
function parseReviewNode(r) {
|
|
8
10
|
const base = {
|
|
@@ -13,6 +15,7 @@ function parseReviewNode(r) {
|
|
|
13
15
|
viewerCanMinimize: "viewerCanMinimize" in r && r.viewerCanMinimize === true,
|
|
14
16
|
body: r.body,
|
|
15
17
|
createdAtUnix: r.createdAt ? parseCreatedAt(r.createdAt) : 0,
|
|
18
|
+
...("url" in r && r.url ? { url: r.url } : {}),
|
|
16
19
|
};
|
|
17
20
|
if ("commit" in r && r.commit?.oid) {
|
|
18
21
|
base.commitOid = r.commit.oid;
|
|
@@ -100,9 +103,11 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
100
103
|
const mergeQueueChecks = parseCheckNodes(queueHead?.statusCheckRollup
|
|
101
104
|
? requireContextNodes(queueHead.statusCheckRollup.contexts.nodes)
|
|
102
105
|
: undefined, queueHead?.oid);
|
|
106
|
+
const headPushedAtUnix = headPushUnixFromCheckNodes(rawCheckNodes);
|
|
103
107
|
const removedMergeQueueChecks = parseCheckNodes(removedQueueHead?.statusCheckRollup
|
|
104
108
|
? requireContextNodes(removedQueueHead.statusCheckRollup.contexts.nodes)
|
|
105
109
|
: undefined, removedQueueHead?.oid);
|
|
110
|
+
const allowedMergeMethods = readAllowedMergeMethods(repository);
|
|
106
111
|
return {
|
|
107
112
|
nodeId: raw.id,
|
|
108
113
|
number: raw.number,
|
|
@@ -112,6 +117,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
112
117
|
mergeStateStatus: raw.mergeStateStatus,
|
|
113
118
|
reviewDecision: (raw.reviewDecision ?? null),
|
|
114
119
|
headRefOid: raw.headRefOid,
|
|
120
|
+
...(headPushedAtUnix !== undefined && { headPushedAtUnix }),
|
|
115
121
|
headRefName: raw.headRefName,
|
|
116
122
|
headRepoWithOwner: raw.headRepository?.nameWithOwner ?? null,
|
|
117
123
|
viewerAuthorization: {
|
|
@@ -135,6 +141,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
135
141
|
approvedReviews,
|
|
136
142
|
checks,
|
|
137
143
|
branchProtection: parseBranchProtection(raw),
|
|
144
|
+
...(allowedMergeMethods && { allowedMergeMethods }),
|
|
138
145
|
branchRules: parseBranchRules(raw.baseRef),
|
|
139
146
|
isInMergeQueue: raw.isInMergeQueue ?? false,
|
|
140
147
|
isMergeQueueEnabled: raw.isMergeQueueEnabled ?? false,
|
|
@@ -7,6 +7,9 @@ export interface RawBatchResponse {
|
|
|
7
7
|
repository: {
|
|
8
8
|
viewerPermission: string | null;
|
|
9
9
|
viewerCanAdminister: boolean;
|
|
10
|
+
mergeCommitAllowed?: boolean;
|
|
11
|
+
squashMergeAllowed?: boolean;
|
|
12
|
+
rebaseMergeAllowed?: boolean;
|
|
10
13
|
pullRequest: RawPr | null;
|
|
11
14
|
} | null;
|
|
12
15
|
}
|
|
@@ -118,6 +121,7 @@ interface RawCheckSuites {
|
|
|
118
121
|
};
|
|
119
122
|
nodes: Array<{
|
|
120
123
|
id?: string;
|
|
124
|
+
status?: string | null;
|
|
121
125
|
conclusion: string | null;
|
|
122
126
|
workflowRun: {
|
|
123
127
|
databaseId: number | null;
|
|
@@ -208,6 +212,7 @@ export interface RawReviewSummary {
|
|
|
208
212
|
author: RawAuthor | null;
|
|
209
213
|
body: string;
|
|
210
214
|
createdAt?: string;
|
|
215
|
+
url?: string;
|
|
211
216
|
}
|
|
212
217
|
export type RawContextNode = {
|
|
213
218
|
__typename: "CheckRun";
|
package/bin/github/batch.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RateLimitInfo, type RepoInfo } from "./client.mts";
|
|
2
|
+
import type { WorkflowSuiteSnapshot } from "../checks/unreported-required.mts";
|
|
2
3
|
import type { BatchPrData } from "../types.mts";
|
|
3
4
|
import { type PrFingerprint } from "./fingerprint.mts";
|
|
4
5
|
interface BatchResult {
|
|
@@ -9,6 +10,8 @@ interface BatchResult {
|
|
|
9
10
|
checkSuitesComplete?: boolean;
|
|
10
11
|
/** True when that complete page listed no check suites on the head commit. */
|
|
11
12
|
headCheckSuitesEmpty?: true;
|
|
13
|
+
/** Actions workflow suites on the head, excluding apps that have no workflow run. */
|
|
14
|
+
headWorkflowSuites?: WorkflowSuiteSnapshot[];
|
|
12
15
|
}
|
|
13
16
|
interface FetchPrBatchOptions {
|
|
14
17
|
/**
|
package/bin/github/batch.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { graphqlWithRateLimit } from "./client.mjs";
|
|
|
2
2
|
import { hydrateThreadCommentPages } from "./thread-comments.mjs";
|
|
3
3
|
import { BATCH_PR_QUERY } from "./queries.mjs";
|
|
4
4
|
import { parseRawPr } from "./batch-parsers.mjs";
|
|
5
|
-
import { parseCheckSuitesComplete, parseHeadCheckSuitesEmpty, parseSuiteStartupFailures, } from "./batch-parse-suites.mjs";
|
|
5
|
+
import { parseCheckSuitesComplete, parseHeadCheckSuitesEmpty, parseHeadWorkflowSuites, parseSuiteStartupFailures, } from "./batch-parse-suites.mjs";
|
|
6
6
|
import { mergeStartupFailureChecks } from "../checks/startup-failures.mjs";
|
|
7
7
|
import { paginateBatchConnections } from "./batch-page.mjs";
|
|
8
8
|
import { requireRawPr } from "./batch-response.mjs";
|
|
@@ -22,6 +22,9 @@ export async function fetchPrBatch(pr, repo, opts = {}) {
|
|
|
22
22
|
const paged = await paginateBatchConnections(pr, repo, raw, opts, result.rateLimit);
|
|
23
23
|
const rawThreadPages = await hydrateThreadCommentPages(paged.threads);
|
|
24
24
|
const data = parseRawPr(raw, rawThreadPages, paged.comments, paged.changesRequested, paged.reviewSummaries, paged.approvedReviews, paged.checks, result.data.repository);
|
|
25
|
+
const viewerLogin = result.data.viewer?.login;
|
|
26
|
+
if (viewerLogin)
|
|
27
|
+
data.viewerLogin = viewerLogin;
|
|
25
28
|
data.checks = mergeStartupFailureChecks(data.checks, parseSuiteStartupFailures(raw));
|
|
26
29
|
return {
|
|
27
30
|
data,
|
|
@@ -29,5 +32,10 @@ export async function fetchPrBatch(pr, repo, opts = {}) {
|
|
|
29
32
|
rateLimit: paged.rateLimit ?? result.rateLimit,
|
|
30
33
|
...(parseCheckSuitesComplete(raw) && { checkSuitesComplete: true }),
|
|
31
34
|
...(parseHeadCheckSuitesEmpty(raw) && { headCheckSuitesEmpty: true }),
|
|
35
|
+
...workflowSuites(raw),
|
|
32
36
|
};
|
|
33
37
|
}
|
|
38
|
+
function workflowSuites(raw) {
|
|
39
|
+
const suites = parseHeadWorkflowSuites(raw);
|
|
40
|
+
return suites.length > 0 ? { headWorkflowSuites: suites } : {};
|
|
41
|
+
}
|
package/bin/github/errors.d.mts
CHANGED
|
@@ -10,6 +10,12 @@ export interface GitHubGraphQlError {
|
|
|
10
10
|
}
|
|
11
11
|
/** GitHub GraphQL engine crash: HTTP 200, `data: null`, INTERNAL type/code or message. */
|
|
12
12
|
export declare function isRetryableGraphQlInternal(graphqlErrors?: GitHubGraphQlError[]): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* GitHub refused the query for size or load (`Resource limits for this query
|
|
15
|
+
* exceeded`). The same document can succeed on a later attempt, and a smaller
|
|
16
|
+
* page can succeed when the full one cannot.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isRetryableGraphQlResourceLimit(graphqlErrors?: GitHubGraphQlError[]): boolean;
|
|
13
19
|
/** HTTP 200 GraphQL response whose `repository` field resolved to null. */
|
|
14
20
|
export declare function missingRepositoryError(repo: {
|
|
15
21
|
owner: string;
|
package/bin/github/errors.mjs
CHANGED
|
@@ -6,8 +6,9 @@ import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
|
6
6
|
// GraphQL error messages themselves.
|
|
7
7
|
const GRAPHQL_PERMISSION_ERROR = /resource not accessible/i;
|
|
8
8
|
const GRAPHQL_INTERNAL_MESSAGE = /something went wrong while executing your query/i;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const GRAPHQL_RESOURCE_LIMIT_MESSAGE = /resource limits for this query exceeded/i;
|
|
10
|
+
function isToken(value, token) {
|
|
11
|
+
return typeof value === "string" && value.toUpperCase() === token;
|
|
11
12
|
}
|
|
12
13
|
function extensionsCode(extensions) {
|
|
13
14
|
if (typeof extensions !== "object" || extensions === null || Array.isArray(extensions)) {
|
|
@@ -23,13 +24,29 @@ export function isRetryableGraphQlInternal(graphqlErrors) {
|
|
|
23
24
|
if (!graphqlErrors?.length)
|
|
24
25
|
return false;
|
|
25
26
|
return graphqlErrors.some((error) => {
|
|
26
|
-
if (
|
|
27
|
+
if (isToken(error.type, "INTERNAL"))
|
|
27
28
|
return true;
|
|
28
|
-
if (
|
|
29
|
+
if (isToken(extensionsCode(error.extensions), "INTERNAL"))
|
|
29
30
|
return true;
|
|
30
31
|
return GRAPHQL_INTERNAL_MESSAGE.test(error.message);
|
|
31
32
|
});
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* GitHub refused the query for size or load (`Resource limits for this query
|
|
36
|
+
* exceeded`). The same document can succeed on a later attempt, and a smaller
|
|
37
|
+
* page can succeed when the full one cannot.
|
|
38
|
+
*/
|
|
39
|
+
export function isRetryableGraphQlResourceLimit(graphqlErrors) {
|
|
40
|
+
if (!graphqlErrors?.length)
|
|
41
|
+
return false;
|
|
42
|
+
return graphqlErrors.some((error) => {
|
|
43
|
+
if (isToken(error.type, "RESOURCE_LIMITS_EXCEEDED"))
|
|
44
|
+
return true;
|
|
45
|
+
if (isToken(extensionsCode(error.extensions), "RESOURCE_LIMITS_EXCEEDED"))
|
|
46
|
+
return true;
|
|
47
|
+
return GRAPHQL_RESOURCE_LIMIT_MESSAGE.test(error.message);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
33
50
|
function classifyStatus(status, rateLimit, retryAfterSeconds, graphqlErrors) {
|
|
34
51
|
// Retry signals take priority over everything else: GitHub's secondary rate limit
|
|
35
52
|
// returns 403 with a Retry-After header, which is a transient throttle — not the
|
|
@@ -40,7 +57,8 @@ function classifyStatus(status, rateLimit, retryAfterSeconds, graphqlErrors) {
|
|
|
40
57
|
status >= 500 ||
|
|
41
58
|
retryAfterSeconds !== undefined ||
|
|
42
59
|
rateLimitExhausted ||
|
|
43
|
-
isRetryableGraphQlInternal(graphqlErrors)
|
|
60
|
+
isRetryableGraphQlInternal(graphqlErrors) ||
|
|
61
|
+
isRetryableGraphQlResourceLimit(graphqlErrors)) {
|
|
44
62
|
return EXIT.TEMPFAIL;
|
|
45
63
|
}
|
|
46
64
|
if (status === 401 || status === 403 || hasPermissionError(graphqlErrors))
|