pr-shepherd 0.55.1 → 0.56.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 +12 -9
- package/bin/api.d.mts +5 -0
- package/bin/api.mjs +12 -1
- package/bin/checks/job-log.d.mts +9 -0
- package/bin/checks/job-log.mjs +30 -0
- package/bin/checks/jobs-types.d.mts +14 -0
- package/bin/checks/jobs-types.mjs +7 -0
- package/bin/checks/related-jobs.d.mts +11 -0
- package/bin/checks/related-jobs.mjs +32 -0
- package/bin/checks/triage-budget.d.mts +17 -0
- package/bin/checks/triage-budget.mjs +49 -0
- package/bin/checks/triage.d.mts +6 -3
- package/bin/checks/triage.mjs +89 -61
- package/bin/cli/api-usage-formatter.mjs +2 -2
- package/bin/cli/fix-formatter.mjs +2 -0
- package/bin/cli/help-command-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.mjs +1 -1
- package/bin/cli/help.d.mts +1 -1
- package/bin/cli/iterate-checks-formatter.mjs +2 -0
- package/bin/cli/iterate-instructions.mjs +1 -1
- package/bin/cli/related-jobs-format.d.mts +3 -0
- package/bin/cli/related-jobs-format.mjs +16 -0
- package/bin/commands/check-execution-context.d.mts +12 -0
- package/bin/commands/check-execution-context.mjs +38 -0
- package/bin/commands/check-fingerprint.mjs +14 -8
- package/bin/commands/check-unreported.d.mts +3 -2
- package/bin/commands/check-unreported.mjs +4 -4
- package/bin/commands/check.d.mts +2 -1
- package/bin/commands/check.mjs +21 -6
- package/bin/commands/commit-suggestion-instruction.mjs +2 -1
- package/bin/commands/iterate/check-instructions.d.mts +9 -5
- package/bin/commands/iterate/check-instructions.mjs +15 -35
- package/bin/commands/iterate/escalate.mjs +3 -0
- package/bin/commands/iterate/fix-code.mjs +6 -2
- package/bin/commands/iterate/helpers.mjs +1 -0
- package/bin/commands/iterate/index.mjs +20 -8
- package/bin/commands/iterate/native-stack-rebase.mjs +3 -2
- package/bin/commands/iterate/render.mjs +13 -5
- package/bin/commands/iterate/stale-ancestry.d.mts +2 -1
- package/bin/commands/iterate/stale-ancestry.mjs +3 -2
- package/bin/commands/iterate/unreported-required.mjs +1 -1
- package/bin/commands/playbook-pointer.d.mts +2 -0
- package/bin/commands/playbook-pointer.mjs +4 -0
- package/bin/commands/poll-quota.d.mts +2 -0
- package/bin/commands/poll-quota.mjs +18 -25
- package/bin/commands/poll-rate-limit-wait.mjs +13 -8
- package/bin/commands/poll-summary-instructions.mjs +1 -1
- package/bin/commands/ready-delay.d.mts +2 -0
- package/bin/commands/ready-delay.mjs +18 -0
- package/bin/commands/resolve-mutate.mjs +8 -9
- package/bin/commands/shepherd-journal.d.mts +1 -1
- package/bin/commands/shepherd-journal.mjs +3 -2
- package/bin/commands/stack-drain.mjs +2 -1
- package/bin/github/batch-raw-types.d.mts +2 -0
- package/bin/github/batch-receipt-evidence.d.mts +5 -0
- package/bin/github/batch-receipt-evidence.mjs +61 -0
- package/bin/github/batch.d.mts +4 -0
- package/bin/github/batch.mjs +42 -8
- package/bin/github/errors.d.mts +3 -0
- package/bin/github/errors.mjs +15 -6
- package/bin/github/gql/batch-pr-page.gql +1 -0
- package/bin/github/gql/batch-pr.gql +1 -0
- package/bin/github/gql/poll-summary-annotation-probe.gql +8 -0
- package/bin/github/gql/reply-thread-comments.gql +25 -0
- package/bin/github/gql/reply-thread-transcripts.gql +31 -0
- package/bin/github/merge-queue-checks.d.mts +2 -1
- package/bin/github/merge-queue-checks.mjs +18 -8
- package/bin/github/merge-target-rules.d.mts +2 -1
- package/bin/github/merge-target-rules.mjs +4 -4
- package/bin/github/poll-summary-annotation-probe.d.mts +2 -0
- package/bin/github/poll-summary-annotation-probe.mjs +7 -0
- package/bin/github/queries.d.mts +5 -0
- package/bin/github/queries.mjs +5 -0
- package/bin/github/rate-limit-kind.d.mts +13 -0
- package/bin/github/rate-limit-kind.mjs +25 -0
- package/bin/github/reply-thread-transcripts.d.mts +3 -0
- package/bin/github/reply-thread-transcripts.mjs +89 -0
- package/bin/github/rest-http.mjs +1 -0
- package/bin/github/rest-text.d.mts +2 -1
- package/bin/github/rest-text.mjs +5 -2
- package/bin/github/thread-comments.d.mts +5 -1
- package/bin/github/thread-comments.mjs +30 -6
- package/bin/mcp/server.mjs +32 -5
- package/bin/quota-warning.mjs +2 -2
- package/bin/reporters/agent.mjs +1 -0
- package/bin/threads/transcript.d.mts +1 -0
- package/bin/threads/transcript.mjs +4 -1
- package/bin/types/check-classification.d.mts +10 -0
- package/bin/types/report.d.mts +4 -1
- package/package.json +2 -2
- 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 +57 -70
- package/plugins/pr-shepherd/skills/pr-shepherd/references/branch-update.md +9 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +21 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/journal.md +7 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/review-mutations.md +9 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/stack-merge.md +7 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/suggestion-patches.md +10 -0
|
@@ -38,8 +38,15 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, st
|
|
|
38
38
|
const sectionRef = actionableSections.length > 0 ? `under ${actionableSections.join(", ")}` : "above";
|
|
39
39
|
instructions.push(`Review each item ${sectionRef} and decide whether it needs a code change.`);
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// A conflicting native stack layer follows the printed gh-stack route, so it omits the hint.
|
|
42
|
+
const branchUpdateHint = buildBehindBaseHintInstruction(baseBranch, behindBaseHint, {
|
|
43
|
+
isBehind,
|
|
44
|
+
hasConflicts: hasConflicts && !stackRebase,
|
|
45
|
+
});
|
|
46
|
+
// The conflict hint belongs with the conflict step; otherwise it precedes the push step.
|
|
47
|
+
const hintWithConflictStep = hasConflicts && !hasRepeatedWorkflowBranchRecovery;
|
|
48
|
+
if (hintWithConflictStep) {
|
|
49
|
+
instructions.push(buildConflictInstruction(stackRebase), ...branchUpdateHint);
|
|
43
50
|
}
|
|
44
51
|
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
45
52
|
if (firstLookTotal > 0) {
|
|
@@ -77,14 +84,15 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, st
|
|
|
77
84
|
const staleClause = buildCrStaleClause(changesRequestedReviews);
|
|
78
85
|
instructions.push(`Read every body under \`## Changes-requested reviews\` and apply any warranted change.${staleClause}`);
|
|
79
86
|
}
|
|
80
|
-
|
|
87
|
+
if (!hintWithConflictStep)
|
|
88
|
+
instructions.push(...branchUpdateHint);
|
|
81
89
|
const hasReviewMutations = resolveCommand.hasMutations || resolveOnlyCommand?.hasMutations === true;
|
|
82
90
|
const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
|
|
83
91
|
if (hasConflicts || hasRepeatedWorkflowBranchRecovery) {
|
|
84
92
|
instructions.push(buildBranchPushInstruction(stackRebase, hasConflicts, mutationSuffix));
|
|
85
93
|
}
|
|
86
94
|
else if (hasNonConflictHints) {
|
|
87
|
-
instructions.push("If you changed code, commit any remaining changes and push to the PR head branch
|
|
95
|
+
instructions.push("If you changed code, commit any remaining changes and push to the PR head branch. If you did not, do not commit.");
|
|
88
96
|
}
|
|
89
97
|
if (viewerCanUpdate &&
|
|
90
98
|
(hasReviewMutations ||
|
|
@@ -96,6 +104,6 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, st
|
|
|
96
104
|
}
|
|
97
105
|
if (resolveOnlyCommand?.hasMutations)
|
|
98
106
|
instructions.push("Run the `resolve-only:` command shown above.");
|
|
99
|
-
instructions.push(...buildResolveCommandInstruction(resolveCommand), buildFixCompletionInstruction(
|
|
107
|
+
instructions.push(...buildResolveCommandInstruction(resolveCommand), buildFixCompletionInstruction());
|
|
100
108
|
return instructions;
|
|
101
109
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RepoInfo } from "../../github/client.mts";
|
|
2
2
|
import type { PollSummaryStackAncestry } from "../../types.mts";
|
|
3
3
|
import type { ShepherdReport } from "../../types/report.mts";
|
|
4
|
+
import type { CheckExecutionContext } from "../check-execution-context.mts";
|
|
4
5
|
/**
|
|
5
6
|
* A verified stale boundary for the PR being shepherded.
|
|
6
7
|
*
|
|
@@ -19,4 +20,4 @@ export interface StaleNativeStackAncestry extends PollSummaryStackAncestry {
|
|
|
19
20
|
* could not be verified. Callers must not emit a repair command from an
|
|
20
21
|
* unverified snapshot.
|
|
21
22
|
*/
|
|
22
|
-
export declare function findStaleNativeStackAncestry(report: Pick<ShepherdReport, "pr" | "mergeStatus">, repo: RepoInfo): Promise<StaleNativeStackAncestry | null>;
|
|
23
|
+
export declare function findStaleNativeStackAncestry(report: Pick<ShepherdReport, "pr" | "mergeStatus">, repo: RepoInfo, context?: CheckExecutionContext): Promise<StaleNativeStackAncestry | null>;
|
|
@@ -8,12 +8,13 @@ import { buildNativeStackRebaseInstruction } from "./native-stack-rebase.mjs";
|
|
|
8
8
|
* could not be verified. Callers must not emit a repair command from an
|
|
9
9
|
* unverified snapshot.
|
|
10
10
|
*/
|
|
11
|
-
export async function findStaleNativeStackAncestry(report, repo) {
|
|
11
|
+
export async function findStaleNativeStackAncestry(report, repo, context) {
|
|
12
12
|
const stack = report.mergeStatus.mergeRequirements?.stack;
|
|
13
13
|
if (!stack || stack.position <= 1)
|
|
14
14
|
return null;
|
|
15
15
|
try {
|
|
16
|
-
const topology = await readStackTopology(report.pr, repo)
|
|
16
|
+
const topology = await (context?.readStackTopology(report.pr, repo) ??
|
|
17
|
+
readStackTopology(report.pr, repo));
|
|
17
18
|
const ancestry = stackAncestryGaps(topology.ordered).find((gap) => gap.childPr === report.pr);
|
|
18
19
|
if (!ancestry)
|
|
19
20
|
return null;
|
|
@@ -98,7 +98,7 @@ export function buildUnreportedFixResult(base, report, instructions) {
|
|
|
98
98
|
requiresDismissMessage: false,
|
|
99
99
|
hasMutations: false,
|
|
100
100
|
},
|
|
101
|
-
instructions: [...instructions, buildFixCompletionInstruction(
|
|
101
|
+
instructions: [...instructions, buildFixCompletionInstruction()],
|
|
102
102
|
inProgressRunIds: [],
|
|
103
103
|
protectedRuns: [],
|
|
104
104
|
firstLookThreads: [],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GraphqlQuotaWarningBand } from "../config/load.mts";
|
|
2
|
+
import { type RateLimitKind } from "../github/rate-limit-kind.mts";
|
|
2
3
|
import type { ApiResourceUsage, GraphqlApiUsage, PollSummaryResult } from "../types.mts";
|
|
3
4
|
/** Slow the poll for whichever of GraphQL or REST core is in a tighter band. */
|
|
4
5
|
export declare function quotaPollIntervalMs(bands: GraphqlQuotaWarningBand[], usage: {
|
|
@@ -8,6 +9,7 @@ export declare function quotaPollIntervalMs(bands: GraphqlQuotaWarningBand[], us
|
|
|
8
9
|
export interface RateLimitRetry {
|
|
9
10
|
ms: number;
|
|
10
11
|
resource: string;
|
|
12
|
+
kind: RateLimitKind;
|
|
11
13
|
remaining?: number;
|
|
12
14
|
limit?: number;
|
|
13
15
|
resetAt?: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { summarizeApiTelemetry } from "../github/api-telemetry.mjs";
|
|
2
2
|
import { GitHubRequestError } from "../github/errors.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { rateLimitKind } from "../github/rate-limit-kind.mjs";
|
|
4
4
|
import { exhaustedPrimaryLimitDelayMs } from "./poll-rate-limit-delay.mjs";
|
|
5
5
|
import { selectQuotaWarning } from "./quota-selection.mjs";
|
|
6
6
|
const GRAPHQL_RETRY_AFTER_DEFAULT_MS = 60_000;
|
|
@@ -28,36 +28,29 @@ export function quotaPollIntervalMs(bands, usage, fallbackMs, maxMs) {
|
|
|
28
28
|
export function pollRateLimitRetryAfterMs(err) {
|
|
29
29
|
if (!(err instanceof GitHubRequestError))
|
|
30
30
|
return null;
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const exhausted = err.rateLimit !== undefined && err.rateLimit.remaining <= 0;
|
|
34
|
-
const retryable = err.status === 429 || err.retryAfterSeconds !== undefined || rateLimitMessage || exhausted;
|
|
35
|
-
if (!retryable)
|
|
31
|
+
const kind = rateLimitKind(err);
|
|
32
|
+
if (kind === null)
|
|
36
33
|
return null;
|
|
37
|
-
const resource =
|
|
34
|
+
const resource = kind === "secondary" ? "secondary" : (err.rateLimit?.resource ?? "graphql");
|
|
38
35
|
const rateLimit = err.rateLimit;
|
|
39
36
|
const details = {
|
|
37
|
+
kind,
|
|
40
38
|
resource,
|
|
41
|
-
...(
|
|
42
|
-
|
|
43
|
-
...(rateLimit?.
|
|
39
|
+
...(kind === "primary" &&
|
|
40
|
+
rateLimit?.remaining !== undefined && { remaining: rateLimit.remaining }),
|
|
41
|
+
...(kind === "primary" && rateLimit?.limit !== undefined && { limit: rateLimit.limit }),
|
|
42
|
+
...(kind === "primary" && rateLimit?.resetAt !== undefined && { resetAt: rateLimit.resetAt }),
|
|
44
43
|
};
|
|
45
|
-
if (
|
|
46
|
-
return {
|
|
44
|
+
if (kind === "primary" && rateLimit !== undefined) {
|
|
45
|
+
return {
|
|
46
|
+
...details,
|
|
47
|
+
ms: Math.max(exhaustedPrimaryLimitDelayMs(rateLimit.resetAt, Date.now()), Math.max(err.retryAfterSeconds ?? 0, 0) * 1000),
|
|
48
|
+
};
|
|
47
49
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
function retryResource(err) {
|
|
54
|
-
if (err.rateLimit?.resource)
|
|
55
|
-
return err.rateLimit.resource;
|
|
56
|
-
const secondary = err.status === 429 ||
|
|
57
|
-
err.retryAfterSeconds !== undefined ||
|
|
58
|
-
/secondary/i.test(err.message) ||
|
|
59
|
-
(err.graphqlErrors?.some((error) => /secondary/i.test(error.message)) ?? false);
|
|
60
|
-
return secondary ? "secondary" : "graphql";
|
|
50
|
+
return {
|
|
51
|
+
...details,
|
|
52
|
+
ms: Math.max(GRAPHQL_RETRY_AFTER_DEFAULT_MS, Math.max(err.retryAfterSeconds ?? 0, 0) * 1000),
|
|
53
|
+
};
|
|
61
54
|
}
|
|
62
55
|
function rateLimitResourceLabel(resource) {
|
|
63
56
|
if (resource === "graphql")
|
|
@@ -2,7 +2,8 @@ import { rest } from "../github/rest-http.mjs";
|
|
|
2
2
|
import { sleep } from "../util/sleep.mjs";
|
|
3
3
|
import { formatRateLimitGiveUpLine, formatRateLimitRetryLine, pollRateLimitRetryAfterMs, } from "./poll-quota.mjs";
|
|
4
4
|
const MAX_NO_PROGRESS_ATTEMPTS = 5;
|
|
5
|
-
const
|
|
5
|
+
const PRIMARY_NO_PROGRESS_BACKOFF_MS = [15_000, 30_000, 60_000];
|
|
6
|
+
const SECONDARY_NO_PROGRESS_BACKOFF_MS = [60_000, 120_000, 240_000, 480_000, 960_000];
|
|
6
7
|
const MERGE_STATES = new Set([
|
|
7
8
|
"BEHIND",
|
|
8
9
|
"BLOCKED",
|
|
@@ -26,8 +27,10 @@ export function createUntilTerminalRateLimitRetry() {
|
|
|
26
27
|
},
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
|
-
function backoffMs(attempt) {
|
|
30
|
-
|
|
30
|
+
function backoffMs(attempt, kind) {
|
|
31
|
+
const backoff = kind === "secondary" ? SECONDARY_NO_PROGRESS_BACKOFF_MS : PRIMARY_NO_PROGRESS_BACKOFF_MS;
|
|
32
|
+
const index = kind === "secondary" ? attempt : attempt - 1;
|
|
33
|
+
return backoff[Math.min(index, backoff.length - 1)] ?? backoff.at(-1);
|
|
31
34
|
}
|
|
32
35
|
async function waitForRateLimit(state, err, opts) {
|
|
33
36
|
const retry = opts.untilTerminal ? pollRateLimitRetryAfterMs(err) : null;
|
|
@@ -35,14 +38,16 @@ async function waitForRateLimit(state, err, opts) {
|
|
|
35
38
|
throw err;
|
|
36
39
|
const elapsed = Math.round((Date.now() - opts.startedAt) / 1000);
|
|
37
40
|
const progressed = state.armed &&
|
|
41
|
+
retry.kind === "primary" &&
|
|
38
42
|
retry.resetAt !== undefined &&
|
|
39
43
|
(state.lastResetAt === undefined || retry.resetAt > state.lastResetAt);
|
|
40
44
|
let sleepMs = retry.ms;
|
|
41
45
|
if (!state.armed || progressed) {
|
|
42
46
|
state.armed = true;
|
|
43
47
|
state.noProgress = 0;
|
|
44
|
-
if (retry.resetAt !== undefined)
|
|
48
|
+
if (retry.kind === "primary" && retry.resetAt !== undefined) {
|
|
45
49
|
state.lastResetAt = retry.resetAt;
|
|
50
|
+
}
|
|
46
51
|
}
|
|
47
52
|
else {
|
|
48
53
|
state.noProgress += 1;
|
|
@@ -52,13 +57,13 @@ async function waitForRateLimit(state, err, opts) {
|
|
|
52
57
|
}
|
|
53
58
|
// Never sleep less than GitHub asked for. A short no-progress backoff must
|
|
54
59
|
// not undercut an explicit Retry-After or the secondary-limit default.
|
|
55
|
-
sleepMs = Math.max(retry.ms, backoffMs(state.noProgress));
|
|
60
|
+
sleepMs = Math.max(retry.ms, backoffMs(state.noProgress, retry.kind));
|
|
56
61
|
}
|
|
57
62
|
process.stderr.write(formatRateLimitRetryLine(opts.tickLabel, elapsed, { ...retry, ms: sleepMs }));
|
|
58
|
-
return sleepRateLimit(sleepMs, opts, retry.resource);
|
|
63
|
+
return sleepRateLimit(sleepMs, opts, retry.kind, retry.resource);
|
|
59
64
|
}
|
|
60
|
-
async function sleepRateLimit(ms, opts, resource) {
|
|
61
|
-
const canProbe = resource
|
|
65
|
+
async function sleepRateLimit(ms, opts, kind, resource) {
|
|
66
|
+
const canProbe = kind === "primary" && resource === "graphql" && opts.targets.length > 0;
|
|
62
67
|
if (ms <= opts.intervalMs || opts.intervalMs <= 0) {
|
|
63
68
|
await sleep(ms);
|
|
64
69
|
return undefined;
|
|
@@ -137,7 +137,7 @@ function planStack(result, mergeRequested) {
|
|
|
137
137
|
stackMergeable: true,
|
|
138
138
|
waiting: true,
|
|
139
139
|
instructions: [
|
|
140
|
-
"1. The
|
|
140
|
+
"1. The queued layers are waiting on the merge queue. Recheck them at the configured polling cadence. Do not rewrite a queued layer. This wait does not block work on a layer that is not in the queue. Route any ejected layer to its one-PR session.",
|
|
141
141
|
],
|
|
142
142
|
};
|
|
143
143
|
}
|
|
@@ -33,4 +33,6 @@ export declare function updateReadyDelay(prNumber: number, isReady: boolean, rea
|
|
|
33
33
|
}): Promise<ReadyDelayState>;
|
|
34
34
|
/** Delete the ready-delay marker once an elapsed delay has been consumed. */
|
|
35
35
|
export declare function clearReadyDelay(prNumber: number, owner: string, repo: string): Promise<void>;
|
|
36
|
+
/** Read-only hint for including compact receipt evidence in the next full PR snapshot. */
|
|
37
|
+
export declare function readyDelayElapsed(prNumber: number, owner: string, repo: string, readyDelaySeconds: number): Promise<boolean>;
|
|
36
38
|
export {};
|
|
@@ -54,6 +54,24 @@ export async function updateReadyDelay(prNumber, isReady, readyDelaySeconds, own
|
|
|
54
54
|
export async function clearReadyDelay(prNumber, owner, repo) {
|
|
55
55
|
await safeUnlink(readySincePath(prNumber, owner, repo));
|
|
56
56
|
}
|
|
57
|
+
/** Read-only hint for including compact receipt evidence in the next full PR snapshot. */
|
|
58
|
+
export async function readyDelayElapsed(prNumber, owner, repo, readyDelaySeconds) {
|
|
59
|
+
const markerPath = readySincePath(prNumber, owner, repo);
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = await readFile(markerPath, "utf8");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const [since, head] = raw.trim().split(" ");
|
|
68
|
+
const readySince = Number(since);
|
|
69
|
+
const now = Math.floor(Date.now() / 1000);
|
|
70
|
+
return Boolean(head &&
|
|
71
|
+
Number.isSafeInteger(readySince) &&
|
|
72
|
+
readySince <= now &&
|
|
73
|
+
now - readySince >= readyDelaySeconds);
|
|
74
|
+
}
|
|
57
75
|
// ---------------------------------------------------------------------------
|
|
58
76
|
// Helpers
|
|
59
77
|
// ---------------------------------------------------------------------------
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getRepoInfo, getCurrentPrNumber } from "../github/client.mjs";
|
|
2
2
|
import { applyResolveOptions } from "../comments/resolve.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { fetchReplyThreadTranscripts } from "../github/reply-thread-transcripts.mjs";
|
|
4
4
|
import { markReplySeen } from "../state/seen-comments.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { threadTranscriptBodies } from "../threads/transcript.mjs";
|
|
6
6
|
import { addPrShepherdMarker } from "../comments/marker.mjs";
|
|
7
7
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
8
8
|
/** @deprecated Hidden implementation for `resolve`; use `apply review`. */
|
|
@@ -14,10 +14,10 @@ export async function runResolveMutate(opts) {
|
|
|
14
14
|
}
|
|
15
15
|
// Fetch only to retain the pre-reply transcript for successful-reply seen
|
|
16
16
|
// markers. It never determines which user-supplied IDs are sent to GitHub.
|
|
17
|
-
let
|
|
17
|
+
let transcriptById;
|
|
18
18
|
if (opts.replyThreadIds?.length) {
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
transcriptById = await fetchReplyThreadTranscripts(prNumber, repo, opts.replyThreadIds);
|
|
21
21
|
}
|
|
22
22
|
catch {
|
|
23
23
|
// Seen-marker bookkeeping is best-effort. A failed read must not block
|
|
@@ -35,14 +35,13 @@ export async function runResolveMutate(opts) {
|
|
|
35
35
|
dismissMessage: opts.dismissMessage,
|
|
36
36
|
requireSha: opts.requireSha,
|
|
37
37
|
});
|
|
38
|
-
if (opts.dismissMessage &&
|
|
38
|
+
if (opts.dismissMessage && transcriptById) {
|
|
39
39
|
const markedMessage = addPrShepherdMarker(opts.dismissMessage);
|
|
40
40
|
await Promise.all(result.repliedThreads.map((id) => {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
41
|
+
const previousBody = transcriptById.get(id);
|
|
42
|
+
if (previousBody === undefined)
|
|
43
43
|
return Promise.resolve();
|
|
44
|
-
|
|
45
|
-
return markReplySeen({ owner: repo.owner, repo: repo.name, pr: prNumber }, id, previousBody, threadTranscriptBody(thread, [markedMessage]), markedMessage);
|
|
44
|
+
return markReplySeen({ owner: repo.owner, repo: repo.name, pr: prNumber }, id, previousBody, threadTranscriptBodies([previousBody, markedMessage]), markedMessage);
|
|
46
45
|
}));
|
|
47
46
|
}
|
|
48
47
|
return result;
|
|
@@ -4,7 +4,7 @@ export declare const SHEPHERD_JOURNAL_DETAILS_OPEN = "<details>";
|
|
|
4
4
|
export declare const SHEPHERD_JOURNAL_DETAILS_SUMMARY = "<summary>Shepherd Journal</summary>";
|
|
5
5
|
export declare const SHEPHERD_JOURNAL_DETAILS_CLOSE = "</details>";
|
|
6
6
|
export declare const SHEPHERD_JOURNAL_APPEND_HINT = "If Shepherd Journal details already exist, append entries inside them instead of creating another container.";
|
|
7
|
-
export declare const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE
|
|
7
|
+
export declare const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE: string;
|
|
8
8
|
/**
|
|
9
9
|
* Build the Shepherd Journal instruction step. The reference-citation convention (link
|
|
10
10
|
* threads/comments from their headings, cite reviews by ID) is invariant across every
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
2
|
+
import { playbookPointer } from "./playbook-pointer.mjs";
|
|
2
3
|
export const SHEPHERD_JOURNAL_SECTION = "Shepherd Journal";
|
|
3
4
|
export const SHEPHERD_JOURNAL_SECTION_PATTERN = /^##\s+Shepherd\s+Journal$/;
|
|
4
5
|
export const SHEPHERD_JOURNAL_DETAILS_OPEN = "<details>";
|
|
5
6
|
export const SHEPHERD_JOURNAL_DETAILS_SUMMARY = "<summary>Shepherd Journal</summary>";
|
|
6
7
|
export const SHEPHERD_JOURNAL_DETAILS_CLOSE = "</details>";
|
|
7
8
|
export const SHEPHERD_JOURNAL_APPEND_HINT = "If Shepherd Journal details already exist, append entries inside them instead of creating another container.";
|
|
8
|
-
export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE =
|
|
9
|
+
export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = `Read each body under \`## Review summaries (first look)\` and journal any warranted note before review mutations. ${playbookPointer("Shepherd Journal")}`;
|
|
9
10
|
/**
|
|
10
11
|
* Build the Shepherd Journal instruction step. The reference-citation convention (link
|
|
11
12
|
* threads/comments from their headings, cite reviews by ID) is invariant across every
|
|
@@ -15,5 +16,5 @@ export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review each body under `##
|
|
|
15
16
|
export function buildShepherdJournalInstruction(prReference) {
|
|
16
17
|
// Single-quote the placeholder so a substituted decision stays literal in the shell.
|
|
17
18
|
const command = `${buildPrShepherdCommand(["apply", "journal", String(prReference)]).text} '- <decision>'`;
|
|
18
|
-
return `For any substantial decision or rejection, append \`- <decision>\` to Shepherd Journal with \`${command}\`.
|
|
19
|
+
return `For any substantial decision or rejection, append \`- <decision>\` to Shepherd Journal with \`${command}\`. ${playbookPointer("Shepherd Journal")}`;
|
|
19
20
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { playbookPointer } from "./playbook-pointer.mjs";
|
|
1
2
|
import { stackMergeFlag } from "./stack-merge-flag.mjs";
|
|
2
3
|
import { stackLayerBlockReason } from "./stack-layer-readiness.mjs";
|
|
3
4
|
import { appendMarkReadyInstructions, splitStackWork } from "./stack-work.mjs";
|
|
@@ -48,7 +49,7 @@ export function planPrefixDrain(result, mergeRequested) {
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
const instructions = [
|
|
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}.
|
|
52
|
+
`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}. ${playbookPointer("Stack merge")}`,
|
|
52
53
|
];
|
|
53
54
|
appendAutonomousInstructions(instructions, above.sessions);
|
|
54
55
|
appendMarkReadyInstructions(instructions, above.markReady);
|
|
@@ -11,6 +11,7 @@ export interface RawBatchResponse {
|
|
|
11
11
|
squashMergeAllowed?: boolean;
|
|
12
12
|
rebaseMergeAllowed?: boolean;
|
|
13
13
|
pullRequest: RawPr | null;
|
|
14
|
+
receiptSummary?: import("./poll-summary-raw.mts").RawSummaryPr | null;
|
|
14
15
|
} | null;
|
|
15
16
|
}
|
|
16
17
|
export interface RawPr extends RawPrMergeFields {
|
|
@@ -226,6 +227,7 @@ export type RawContextNode = {
|
|
|
226
227
|
title: string | null;
|
|
227
228
|
summary: string | null;
|
|
228
229
|
annotations?: {
|
|
230
|
+
totalCount?: number;
|
|
229
231
|
nodes: Array<{
|
|
230
232
|
message: string;
|
|
231
233
|
}>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RepoInfo } from "./client.mts";
|
|
2
|
+
import type { RawContextNode, RawPr } from "./batch-raw-types.mts";
|
|
3
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
4
|
+
/** Keep the exact PollSummaryPr shape used by v1 receipts; add only complete annotation totals. */
|
|
5
|
+
export declare function prepareBatchReceiptEvidence(summary: RawSummaryPr | null | undefined, batch: RawPr, checks: RawContextNode[], repo: RepoInfo): Promise<RawSummaryPr | null>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { hydratePollSummaryChecks } from "./poll-summary-check-hydration.mjs";
|
|
2
|
+
import { markReadyAnnotationProbeComplete } from "./poll-summary-annotation-probe.mjs";
|
|
3
|
+
/** Keep the exact PollSummaryPr shape used by v1 receipts; add only complete annotation totals. */
|
|
4
|
+
export async function prepareBatchReceiptEvidence(summary, batch, checks, repo) {
|
|
5
|
+
if (!summary ||
|
|
6
|
+
summary.number !== batch.number ||
|
|
7
|
+
summary.headRefOid !== batch.headRefOid ||
|
|
8
|
+
summary.baseRefOid !== batch.baseRefOid)
|
|
9
|
+
return null;
|
|
10
|
+
// Candidate evidence is best-effort. Wider windows need their own summary
|
|
11
|
+
// pagination, so use the existing standalone receipt path instead of issuing
|
|
12
|
+
// follow-ups from inside a full BatchPr read (which may have exhausted quota).
|
|
13
|
+
const headContexts = summary.commits.nodes[0]?.commit.statusCheckRollup?.contexts;
|
|
14
|
+
const queueContexts = summary.mergeQueueEntry?.headCommit?.statusCheckRollup?.contexts;
|
|
15
|
+
if (headContexts?.pageInfo.hasPreviousPage || queueContexts?.pageInfo.hasPreviousPage)
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
await hydratePollSummaryChecks(summary, repo);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (summary.mergeQueueEntry?.headCommit?.statusCheckRollup?.contexts.pageInfo.hasPreviousPage)
|
|
24
|
+
return null;
|
|
25
|
+
const summaryCommit = summary.commits.nodes[0]?.commit;
|
|
26
|
+
const batchCommit = batch.commits.nodes[0]?.commit;
|
|
27
|
+
if (!summaryCommit || summaryCommit.oid !== batchCommit?.oid)
|
|
28
|
+
return null;
|
|
29
|
+
const contexts = summaryCommit.statusCheckRollup?.contexts;
|
|
30
|
+
if (!contexts) {
|
|
31
|
+
if (batchCommit.statusCheckRollup !== null || checks.length > 0)
|
|
32
|
+
return null;
|
|
33
|
+
markReadyAnnotationProbeComplete(summary);
|
|
34
|
+
return summary;
|
|
35
|
+
}
|
|
36
|
+
if (contexts.pageInfo.hasPreviousPage || contexts.nodes.length !== contexts.totalCount)
|
|
37
|
+
return null;
|
|
38
|
+
if (checks.length !== contexts.totalCount)
|
|
39
|
+
return null;
|
|
40
|
+
const batchRuns = new Map();
|
|
41
|
+
for (const check of checks) {
|
|
42
|
+
if (check.__typename !== "CheckRun")
|
|
43
|
+
continue;
|
|
44
|
+
const count = check.annotations?.totalCount;
|
|
45
|
+
if (!Number.isSafeInteger(count) || count < 0 || batchRuns.has(check.id))
|
|
46
|
+
return null;
|
|
47
|
+
batchRuns.set(check.id, count);
|
|
48
|
+
}
|
|
49
|
+
const summaryRuns = contexts.nodes.filter((node) => node.__typename === "CheckRun");
|
|
50
|
+
if (summaryRuns.length !== batchRuns.size)
|
|
51
|
+
return null;
|
|
52
|
+
const seenSummaryIds = new Set();
|
|
53
|
+
for (const node of summaryRuns) {
|
|
54
|
+
if (!node.id || seenSummaryIds.has(node.id) || !batchRuns.has(node.id))
|
|
55
|
+
return null;
|
|
56
|
+
seenSummaryIds.add(node.id);
|
|
57
|
+
node.annotations = { totalCount: batchRuns.get(node.id) };
|
|
58
|
+
}
|
|
59
|
+
markReadyAnnotationProbeComplete(summary);
|
|
60
|
+
return summary;
|
|
61
|
+
}
|
package/bin/github/batch.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type RateLimitInfo, type RepoInfo } from "./client.mts";
|
|
2
2
|
import type { WorkflowSuiteSnapshot } from "../checks/unreported-required.mts";
|
|
3
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
3
4
|
import type { BatchPrData } from "../types.mts";
|
|
4
5
|
import { type PrFingerprint } from "./fingerprint.mts";
|
|
5
6
|
interface BatchResult {
|
|
@@ -12,6 +13,8 @@ interface BatchResult {
|
|
|
12
13
|
headCheckSuitesEmpty?: true;
|
|
13
14
|
/** Actions workflow suites on the head, excluding apps that have no workflow run. */
|
|
14
15
|
headWorkflowSuites?: WorkflowSuiteSnapshot[];
|
|
16
|
+
/** Internal READY-receipt evidence from the same request, if complete. */
|
|
17
|
+
receiptSummary?: RawSummaryPr;
|
|
15
18
|
}
|
|
16
19
|
interface FetchPrBatchOptions {
|
|
17
20
|
/**
|
|
@@ -23,6 +26,7 @@ interface FetchPrBatchOptions {
|
|
|
23
26
|
* request — so there's no need to conditionally omit the field itself.
|
|
24
27
|
*/
|
|
25
28
|
paginateApprovedReviews?: boolean;
|
|
29
|
+
includeReceiptSummary?: boolean;
|
|
26
30
|
}
|
|
27
31
|
/**
|
|
28
32
|
* Fetch all PR data needed for a `shepherd check` in one (or a few, if paginating) GraphQL requests.
|
package/bin/github/batch.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { graphqlWithRateLimit } from "./client.mjs";
|
|
2
2
|
import { hydrateThreadCommentPages } from "./thread-comments.mjs";
|
|
3
|
-
import { BATCH_PR_QUERY } from "./queries.mjs";
|
|
3
|
+
import { BATCH_PR_QUERY, BATCH_PR_RECEIPT_QUERY } from "./queries.mjs";
|
|
4
|
+
import { prepareBatchReceiptEvidence } from "./batch-receipt-evidence.mjs";
|
|
5
|
+
import { GitHubRequestError, isRetryableGraphQlResourceLimit, } from "./errors.mjs";
|
|
6
|
+
import { rateLimitKind } from "./rate-limit-kind.mjs";
|
|
4
7
|
import { parseRawPr } from "./batch-parsers.mjs";
|
|
5
8
|
import { parseCheckSuitesComplete, parseHeadCheckSuitesEmpty, parseHeadWorkflowSuites, parseSuiteStartupFailures, } from "./batch-parse-suites.mjs";
|
|
6
9
|
import { mergeStartupFailureChecks } from "../checks/startup-failures.mjs";
|
|
@@ -8,20 +11,50 @@ import { paginateBatchConnections } from "./batch-page.mjs";
|
|
|
8
11
|
import { requireRawPr } from "./batch-response.mjs";
|
|
9
12
|
import { hydrateMergeQueueChecks } from "./merge-queue-checks.mjs";
|
|
10
13
|
import { fingerprintFromRaw } from "./fingerprint.mjs";
|
|
14
|
+
function onlyReceiptSummaryErrors(errors) {
|
|
15
|
+
return (!!errors?.length &&
|
|
16
|
+
errors.every((error) => Array.isArray(error.path) &&
|
|
17
|
+
error.path[0] === "repository" &&
|
|
18
|
+
error.path[1] === "receiptSummary"));
|
|
19
|
+
}
|
|
11
20
|
/**
|
|
12
21
|
* Fetch all PR data needed for a `shepherd check` in one (or a few, if paginating) GraphQL requests.
|
|
13
22
|
*/
|
|
14
23
|
export async function fetchPrBatch(pr, repo, opts = {}) {
|
|
15
|
-
const
|
|
24
|
+
const variables = {
|
|
16
25
|
owner: repo.owner,
|
|
17
26
|
repo: repo.name,
|
|
18
27
|
pr,
|
|
19
|
-
}
|
|
28
|
+
};
|
|
29
|
+
let result;
|
|
30
|
+
try {
|
|
31
|
+
result = await graphqlWithRateLimit(opts.includeReceiptSummary ? BATCH_PR_RECEIPT_QUERY : BATCH_PR_QUERY, variables);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (!opts.includeReceiptSummary ||
|
|
35
|
+
!(error instanceof GitHubRequestError) ||
|
|
36
|
+
rateLimitKind(error) !== null ||
|
|
37
|
+
!(isRetryableGraphQlResourceLimit(error.graphqlErrors) ||
|
|
38
|
+
(error.status === 200 && onlyReceiptSummaryErrors(error.graphqlErrors))))
|
|
39
|
+
throw error;
|
|
40
|
+
// Receipt evidence is optional; a resource-limited combined query or an
|
|
41
|
+
// error confined to its sibling must not prevent the ordinary snapshot.
|
|
42
|
+
result = await graphqlWithRateLimit(BATCH_PR_QUERY, variables);
|
|
43
|
+
}
|
|
20
44
|
const raw = requireRawPr(result.data, pr, repo);
|
|
21
|
-
await hydrateMergeQueueChecks(raw, repo);
|
|
22
|
-
const paged = await paginateBatchConnections(pr, repo, raw, opts,
|
|
23
|
-
const
|
|
24
|
-
|
|
45
|
+
const queueRateLimit = await hydrateMergeQueueChecks(raw, repo, result.rateLimit);
|
|
46
|
+
const paged = await paginateBatchConnections(pr, repo, raw, opts, queueRateLimit);
|
|
47
|
+
const threadPages = await hydrateThreadCommentPages(paged.threads, paged.rateLimit);
|
|
48
|
+
let receiptSummary = null;
|
|
49
|
+
if (opts.includeReceiptSummary) {
|
|
50
|
+
try {
|
|
51
|
+
receiptSummary = await prepareBatchReceiptEvidence(result.data.repository?.receiptSummary, raw, paged.checks, repo);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Malformed supplemental evidence must not discard a complete BatchPr.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const data = parseRawPr(raw, threadPages.threads, paged.comments, paged.changesRequested, paged.reviewSummaries, paged.approvedReviews, paged.checks, result.data.repository);
|
|
25
58
|
const viewerLogin = result.data.viewer?.login;
|
|
26
59
|
if (viewerLogin)
|
|
27
60
|
data.viewerLogin = viewerLogin;
|
|
@@ -29,10 +62,11 @@ export async function fetchPrBatch(pr, repo, opts = {}) {
|
|
|
29
62
|
return {
|
|
30
63
|
data,
|
|
31
64
|
fingerprint: fingerprintFromRaw(raw, result.data.repository?.viewerPermission ?? null, result.data.viewer?.login ?? null),
|
|
32
|
-
rateLimit: paged.rateLimit ?? result.rateLimit,
|
|
65
|
+
rateLimit: threadPages.rateLimit ?? paged.rateLimit ?? result.rateLimit,
|
|
33
66
|
...(parseCheckSuitesComplete(raw) && { checkSuitesComplete: true }),
|
|
34
67
|
...(parseHeadCheckSuitesEmpty(raw) && { headCheckSuitesEmpty: true }),
|
|
35
68
|
...workflowSuites(raw),
|
|
69
|
+
...(receiptSummary && { receiptSummary }),
|
|
36
70
|
};
|
|
37
71
|
}
|
|
38
72
|
function workflowSuites(raw) {
|
package/bin/github/errors.d.mts
CHANGED
|
@@ -27,12 +27,15 @@ export declare class GitHubRequestError extends ShepherdError {
|
|
|
27
27
|
readonly retryAfterSeconds?: number;
|
|
28
28
|
readonly graphqlErrors?: GitHubGraphQlError[];
|
|
29
29
|
readonly authSource?: string;
|
|
30
|
+
/** Response text without a request path; safe input for throttle classification. */
|
|
31
|
+
readonly responseMessage?: string;
|
|
30
32
|
constructor(message: string, opts: {
|
|
31
33
|
status: number;
|
|
32
34
|
rateLimit?: RateLimitInfo;
|
|
33
35
|
retryAfterSeconds?: number;
|
|
34
36
|
graphqlErrors?: GitHubGraphQlError[];
|
|
35
37
|
authSource?: string;
|
|
38
|
+
responseMessage?: string;
|
|
36
39
|
/**
|
|
37
40
|
* Bypasses status-based classification entirely — for callers that already
|
|
38
41
|
* know the failure kind better than the HTTP status can express (e.g. a
|
package/bin/github/errors.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
2
|
+
import { rateLimitKind } from "./rate-limit-kind.mjs";
|
|
2
3
|
// GitHub's GraphQL API reports field-level permission failures (e.g. a fine-grained
|
|
3
4
|
// PAT missing a scope) as an `errors[].message` entry at HTTP 200, not as an HTTP
|
|
4
5
|
// 401/403 — the transport-level request succeeded even though one field could not
|
|
@@ -47,16 +48,21 @@ export function isRetryableGraphQlResourceLimit(graphqlErrors) {
|
|
|
47
48
|
return GRAPHQL_RESOURCE_LIMIT_MESSAGE.test(error.message);
|
|
48
49
|
});
|
|
49
50
|
}
|
|
50
|
-
function classifyStatus(status, rateLimit, retryAfterSeconds, graphqlErrors) {
|
|
51
|
+
function classifyStatus(status, message, responseMessage, rateLimit, retryAfterSeconds, graphqlErrors) {
|
|
51
52
|
// Retry signals take priority over everything else: GitHub's secondary rate limit
|
|
52
53
|
// returns 403 with a Retry-After header, which is a transient throttle — not the
|
|
53
54
|
// permission-denied 403 a bad/missing token produces. Treat any retry signal as
|
|
54
55
|
// TEMPFAIL first so it isn't shadowed by the checks below.
|
|
55
|
-
const
|
|
56
|
-
|
|
56
|
+
const throttle = rateLimitKind({
|
|
57
|
+
status,
|
|
58
|
+
message,
|
|
59
|
+
responseMessage,
|
|
60
|
+
rateLimit,
|
|
61
|
+
retryAfterSeconds,
|
|
62
|
+
graphqlErrors,
|
|
63
|
+
});
|
|
64
|
+
if (throttle !== null ||
|
|
57
65
|
status >= 500 ||
|
|
58
|
-
retryAfterSeconds !== undefined ||
|
|
59
|
-
rateLimitExhausted ||
|
|
60
66
|
isRetryableGraphQlInternal(graphqlErrors) ||
|
|
61
67
|
isRetryableGraphQlResourceLimit(graphqlErrors)) {
|
|
62
68
|
return EXIT.TEMPFAIL;
|
|
@@ -75,14 +81,17 @@ export class GitHubRequestError extends ShepherdError {
|
|
|
75
81
|
retryAfterSeconds;
|
|
76
82
|
graphqlErrors;
|
|
77
83
|
authSource;
|
|
84
|
+
/** Response text without a request path; safe input for throttle classification. */
|
|
85
|
+
responseMessage;
|
|
78
86
|
constructor(message, opts) {
|
|
79
87
|
super(message, opts.exitCodeOverride ??
|
|
80
|
-
classifyStatus(opts.status, opts.rateLimit, opts.retryAfterSeconds, opts.graphqlErrors));
|
|
88
|
+
classifyStatus(opts.status, message, opts.responseMessage, opts.rateLimit, opts.retryAfterSeconds, opts.graphqlErrors));
|
|
81
89
|
this.name = "GitHubRequestError";
|
|
82
90
|
this.status = opts.status;
|
|
83
91
|
this.rateLimit = opts.rateLimit;
|
|
84
92
|
this.retryAfterSeconds = opts.retryAfterSeconds;
|
|
85
93
|
this.graphqlErrors = opts.graphqlErrors;
|
|
86
94
|
this.authSource = opts.authSource;
|
|
95
|
+
this.responseMessage = opts.responseMessage;
|
|
87
96
|
}
|
|
88
97
|
}
|