pr-shepherd 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/checks/classify.d.mts +3 -1
- package/bin/checks/classify.mjs +8 -1
- package/bin/cli/triage-formatter.mjs +5 -1
- package/bin/commands/check-annotations.d.mts +0 -3
- package/bin/commands/check-annotations.mjs +1 -10
- package/bin/commands/check-status.d.mts +6 -1
- package/bin/commands/check-status.mjs +9 -4
- package/bin/commands/check-unreported.mjs +1 -1
- package/bin/commands/check.mjs +6 -13
- package/bin/commands/iterate/conflicting-head-ci.d.mts +1 -1
- package/bin/commands/iterate/conflicting-head-ci.mjs +1 -1
- package/bin/commands/iterate/escalate.mjs +4 -2
- package/bin/commands/poll-summary-explicit-instructions.d.mts +1 -1
- package/bin/commands/poll-summary-explicit-instructions.mjs +9 -7
- package/bin/commands/poll-summary-instructions.d.mts +2 -2
- package/bin/commands/poll-summary-instructions.mjs +16 -15
- package/bin/commands/poll-summary.mjs +15 -14
- package/bin/commands/ready-mergeability.d.mts +1 -1
- package/bin/commands/ready-mergeability.mjs +2 -2
- package/bin/commands/stack-drain.d.mts +1 -1
- package/bin/commands/stack-drain.mjs +3 -3
- package/bin/commands/stack-work.d.mts +4 -1
- package/bin/commands/stack-work.mjs +10 -2
- package/bin/commands/tick.mjs +1 -0
- package/bin/github/batch-parser-helpers.mjs +2 -0
- package/bin/github/batch-raw-types.d.mts +1 -0
- package/bin/github/github-fetch.d.mts +6 -2
- package/bin/github/github-fetch.mjs +37 -2
- package/bin/github/github-timeouts.d.mts +8 -0
- package/bin/github/github-timeouts.mjs +8 -0
- package/bin/github/gql/batch-pr-page.gql +1 -0
- package/bin/github/gql/batch-pr.gql +1 -0
- package/bin/github/gql/commit-check-contexts.gql +1 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +1 -0
- package/bin/github/http-utils.mjs +3 -0
- package/bin/github/poll-summary-annotations.d.mts +1 -1
- package/bin/github/poll-summary-annotations.mjs +2 -2
- package/bin/github/poll-summary-checks.mjs +1 -0
- package/bin/github/poll-summary-command.d.mts +7 -1
- package/bin/github/poll-summary-command.mjs +29 -0
- package/bin/github/poll-summary-projector.mjs +2 -1
- package/bin/github/poll-summary-raw.d.mts +1 -0
- package/bin/github/poll-summary-readiness.mjs +2 -0
- package/bin/github/poll-summary-route.mjs +2 -0
- package/bin/github/poll-summary-unreported.d.mts +7 -0
- package/bin/github/poll-summary-unreported.mjs +16 -0
- package/bin/github/rest-check-read.mjs +3 -1
- package/bin/github/rest-text.mjs +3 -2
- package/bin/github/stack-handoff-page.mjs +1 -0
- package/bin/reporters/agent.mjs +2 -0
- package/bin/types/github.d.mts +6 -0
- package/bin/types/report.d.mts +5 -3
- package/package.json +1 -1
- package/plugins/pr-shepherd/.claude-plugin/plugin.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/references/ci-failure-triage.md +1 -0
|
@@ -17,8 +17,11 @@ export declare function onlyDraftReadinessVerification(sessions: PollSummaryItem
|
|
|
17
17
|
* A stale-ancestry child keeps its session, which returns the ancestry repair.
|
|
18
18
|
*/
|
|
19
19
|
export declare function splitStackWork(candidates: PollSummaryItem[], staleChildren: ReadonlySet<number>): StackWork;
|
|
20
|
+
/** The literal stack command to rerun, or the generic name when the caller's command is unknown. */
|
|
21
|
+
export declare function stackRerunTarget(command?: string): string;
|
|
20
22
|
/**
|
|
21
23
|
* The closing step after routed sessions: rerun the stack command, or stop when nothing is owned.
|
|
22
24
|
* Returns the plan action: a stop is a handoff, so no rerun, quota cadence, or wake-up follows it.
|
|
25
|
+
* `keepWatching` is false when a human handoff may end the run before every layer is terminal.
|
|
23
26
|
*/
|
|
24
|
-
export declare function appendStackRerunInstruction(instructions: string[], layers: PollSummaryItem[], rerun: string): "shepherd" | "escalate";
|
|
27
|
+
export declare function appendStackRerunInstruction(instructions: string[], layers: PollSummaryItem[], rerun: string, keepWatching?: boolean): "shepherd" | "escalate";
|
|
@@ -35,12 +35,20 @@ function isProbed(item) {
|
|
|
35
35
|
function onlyUnownedWork(layers) {
|
|
36
36
|
return layers.length > 0 && layers.every((item) => !item.owned);
|
|
37
37
|
}
|
|
38
|
+
/** The turn must outlive every non-terminal layer; a one-PR session ending does not end the stack. */
|
|
39
|
+
const STACK_KEEP_WATCHING = "Keep watching until every layer is terminal.";
|
|
40
|
+
/** The literal stack command to rerun, or the generic name when the caller's command is unknown. */
|
|
41
|
+
export function stackRerunTarget(command) {
|
|
42
|
+
return command ? `\`${command}\`` : "this same `stack` command";
|
|
43
|
+
}
|
|
38
44
|
/**
|
|
39
45
|
* The closing step after routed sessions: rerun the stack command, or stop when nothing is owned.
|
|
40
46
|
* Returns the plan action: a stop is a handoff, so no rerun, quota cadence, or wake-up follows it.
|
|
47
|
+
* `keepWatching` is false when a human handoff may end the run before every layer is terminal.
|
|
41
48
|
*/
|
|
42
|
-
export function appendStackRerunInstruction(instructions, layers, rerun) {
|
|
49
|
+
export function appendStackRerunInstruction(instructions, layers, rerun, keepWatching = true) {
|
|
43
50
|
const stop = onlyUnownedWork(layers);
|
|
44
|
-
|
|
51
|
+
const step = keepWatching ? `${rerun} ${STACK_KEEP_WATCHING}` : rerun;
|
|
52
|
+
instructions.push(`${instructions.length + 1}. ${stop ? "No owned layer needs a session. Report this overview and stop." : step}`);
|
|
45
53
|
return stop ? "escalate" : "shepherd";
|
|
46
54
|
}
|
package/bin/commands/tick.mjs
CHANGED
|
@@ -86,6 +86,7 @@ async function withAggregateNextCheck(result, opts) {
|
|
|
86
86
|
function withoutPollingCadence(step) {
|
|
87
87
|
return step
|
|
88
88
|
.replace(/ Recheck(?: them)? at the configured polling cadence\./g, "")
|
|
89
|
+
.replace(/Do not end the turn while any selected row is non-terminal: /g, "Keep watching every non-terminal selected row through the wake-up below: ")
|
|
89
90
|
.replace("rerun that command at the configured cadence to resume its UUID", "rerun that command after the wake-up below to resume its UUID");
|
|
90
91
|
}
|
|
91
92
|
function aggregateNextCheck(result, stallDeadlineSeconds) {
|
|
@@ -28,6 +28,7 @@ export function mapCheckRunNode(node) {
|
|
|
28
28
|
const summary = extractCheckRunSummary(node.title, node.summary);
|
|
29
29
|
const rawCreatedAt = node.checkSuite?.workflowRun?.createdAt ?? node.checkSuite?.createdAt;
|
|
30
30
|
const rawUpdatedAt = node.checkSuite?.workflowRun?.updatedAt ?? node.checkSuite?.updatedAt;
|
|
31
|
+
const suiteStatus = node.checkSuite?.status || undefined;
|
|
31
32
|
const createdAtUnix = rawCreatedAt ? parseCreatedAt(rawCreatedAt) : undefined;
|
|
32
33
|
const startedAtUnix = node.startedAt ? parseCreatedAt(node.startedAt) : undefined;
|
|
33
34
|
const completedAtUnix = node.completedAt ? parseCreatedAt(node.completedAt) : undefined;
|
|
@@ -43,6 +44,7 @@ export function mapCheckRunNode(node) {
|
|
|
43
44
|
runId,
|
|
44
45
|
...(workflowName !== undefined && { workflowName }),
|
|
45
46
|
...(workflowId !== undefined && { workflowId }),
|
|
47
|
+
...(suiteStatus !== undefined && { suiteStatus }),
|
|
46
48
|
...(createdAtUnix !== undefined && { createdAtUnix }),
|
|
47
49
|
...(startedAtUnix !== undefined && { startedAtUnix }),
|
|
48
50
|
...(completedAtUnix !== undefined && { completedAtUnix }),
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Attach proxy routing to GitHub requests without changing the host's dispatcher.
|
|
3
|
+
* The request, including reading its body, is bounded by `timeoutMs`; a stall
|
|
4
|
+
* rejects with an ETIMEDOUT error that the transport retry treats as transient.
|
|
5
|
+
*/
|
|
6
|
+
export declare function githubFetch(input: string | URL, init?: RequestInit, timeoutMs?: number): Promise<Response>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { createCapturedProxyAgent } from "./captured-proxy-agent.mjs";
|
|
3
|
+
import { GITHUB_API_TIMEOUT_MS } from "./github-timeouts.mjs";
|
|
4
|
+
import { redactUrl } from "./http-utils.mjs";
|
|
3
5
|
import { loadUndiciKeepingLegacyDispatcher } from "./legacy-dispatcher.mjs";
|
|
4
6
|
const require = createRequire(import.meta.url);
|
|
5
7
|
let proxyAgent;
|
|
@@ -9,8 +11,41 @@ function loadUndici() {
|
|
|
9
11
|
undiciModule ??= loadUndiciKeepingLegacyDispatcher(() => require("undici"));
|
|
10
12
|
return undiciModule;
|
|
11
13
|
}
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Attach proxy routing to GitHub requests without changing the host's dispatcher.
|
|
16
|
+
* The request, including reading its body, is bounded by `timeoutMs`; a stall
|
|
17
|
+
* rejects with an ETIMEDOUT error that the transport retry treats as transient.
|
|
18
|
+
*/
|
|
19
|
+
export async function githubFetch(input, init, timeoutMs = GITHUB_API_TIMEOUT_MS) {
|
|
20
|
+
const deadline = AbortSignal.timeout(timeoutMs);
|
|
21
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, deadline]) : deadline;
|
|
22
|
+
try {
|
|
23
|
+
return await bufferBody(await routedFetch(input, { ...init, signal }));
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (deadline.aborted && !init?.signal?.aborted) {
|
|
27
|
+
const method = init?.method ?? "GET";
|
|
28
|
+
throw Object.assign(new Error(`GitHub request timed out after ${timeoutMs / 1000}s: ${method} ${redactUrl(String(input))}`, { cause: err }), { code: "ETIMEDOUT" });
|
|
29
|
+
}
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read a streamed body inside the deadline and the callers' transport retry, so a
|
|
35
|
+
* body that stalls after the headers is retried like a stalled request. Callers
|
|
36
|
+
* read every body in full anyway.
|
|
37
|
+
*/
|
|
38
|
+
async function bufferBody(response) {
|
|
39
|
+
if (typeof response.body?.getReader !== "function")
|
|
40
|
+
return response;
|
|
41
|
+
const bytes = await response.arrayBuffer();
|
|
42
|
+
return new Response(bytes.byteLength > 0 ? bytes : null, {
|
|
43
|
+
status: response.status,
|
|
44
|
+
statusText: response.statusText,
|
|
45
|
+
headers: response.headers,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function routedFetch(input, init) {
|
|
14
49
|
const httpProxy = process.env["http_proxy"] ?? process.env["HTTP_PROXY"] ?? "";
|
|
15
50
|
const httpsProxy = process.env["https_proxy"] ?? process.env["HTTPS_PROXY"] ?? "";
|
|
16
51
|
if (!httpProxy && !httpsProxy)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deadline for one GitHub REST or GraphQL request, headers and body included.
|
|
3
|
+
* Without it a half-open connection waits for undici's 300s headers/body
|
|
4
|
+
* defaults (10s for connect) and the tick prints nothing meanwhile.
|
|
5
|
+
*/
|
|
6
|
+
export declare const GITHUB_API_TIMEOUT_MS = 60000;
|
|
7
|
+
/** Deadline for Actions log downloads, which can be several megabytes. */
|
|
8
|
+
export declare const GITHUB_LOG_TIMEOUT_MS = 180000;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deadline for one GitHub REST or GraphQL request, headers and body included.
|
|
3
|
+
* Without it a half-open connection waits for undici's 300s headers/body
|
|
4
|
+
* defaults (10s for connect) and the tick prints nothing meanwhile.
|
|
5
|
+
*/
|
|
6
|
+
export const GITHUB_API_TIMEOUT_MS = 60_000;
|
|
7
|
+
/** Deadline for Actions log downloads, which can be several megabytes. */
|
|
8
|
+
export const GITHUB_LOG_TIMEOUT_MS = 180_000;
|
|
@@ -15,6 +15,9 @@ const TRANSPORT_ERROR_CODES = new Set([
|
|
|
15
15
|
export function isTransportError(err) {
|
|
16
16
|
if (!(err instanceof Error))
|
|
17
17
|
return false;
|
|
18
|
+
// The request deadline aborts a stalled body read with a TimeoutError.
|
|
19
|
+
if (err.name === "TimeoutError")
|
|
20
|
+
return true;
|
|
18
21
|
const code = err.code;
|
|
19
22
|
if (code && TRANSPORT_ERROR_CODES.has(code))
|
|
20
23
|
return true;
|
|
@@ -4,7 +4,7 @@ import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
|
4
4
|
type Routed = Pick<PollSummaryItem, "action" | "reasons">;
|
|
5
5
|
/**
|
|
6
6
|
* The one-PR check treats unseen annotations on any non-SUCCESS check (failing, skipped,
|
|
7
|
-
* filtered
|
|
7
|
+
* filtered or neutral) as actionable work; `checks.ignore` checks never gate, annotations included. The compact summary only has annotation
|
|
8
8
|
* totals, and only for a row that otherwise claims readiness, so it loads them then and reads
|
|
9
9
|
* the annotations of just the non-SUCCESS checks that have any. A row with unseen annotations,
|
|
10
10
|
* or whose annotations cannot be read, goes to its one-PR `pollCommand` instead of
|
|
@@ -11,7 +11,7 @@ const READY_CLAIMS = new Set([
|
|
|
11
11
|
]);
|
|
12
12
|
/**
|
|
13
13
|
* The one-PR check treats unseen annotations on any non-SUCCESS check (failing, skipped,
|
|
14
|
-
* filtered
|
|
14
|
+
* filtered or neutral) as actionable work; `checks.ignore` checks never gate, annotations included. The compact summary only has annotation
|
|
15
15
|
* totals, and only for a row that otherwise claims readiness, so it loads them then and reads
|
|
16
16
|
* the annotations of just the non-SUCCESS checks that have any. A row with unseen annotations,
|
|
17
17
|
* or whose annotations cannot be read, goes to its one-PR `pollCommand` instead of
|
|
@@ -44,7 +44,7 @@ function nonSuccessCheckTotals(raw, required, totalsKnown) {
|
|
|
44
44
|
for (const check of classifiedSummaryChecks(raw, required)) {
|
|
45
45
|
if (check.scope || check.id == null || !head.has(check.id))
|
|
46
46
|
continue;
|
|
47
|
-
if (
|
|
47
|
+
if (["in_progress", "superseded", "ignored"].includes(check.category))
|
|
48
48
|
continue;
|
|
49
49
|
if (check.status !== "COMPLETED" || check.conclusion === "SUCCESS")
|
|
50
50
|
continue;
|
|
@@ -93,6 +93,7 @@ function checkRuns(rollup, scope) {
|
|
|
93
93
|
...(run?.workflow?.databaseId != null && {
|
|
94
94
|
workflowId: String(run.workflow.databaseId),
|
|
95
95
|
}),
|
|
96
|
+
...(context.checkSuite?.status && { suiteStatus: context.checkSuite.status }),
|
|
96
97
|
...(context.startedAt && { startedAtUnix: parseCreatedAt(context.startedAt) }),
|
|
97
98
|
...(context.completedAt && { completedAtUnix: parseCreatedAt(context.completedAt) }),
|
|
98
99
|
...(scope && { scope }),
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
import type { PollSummaryCommandOptions, PollSummaryItem } from "../types.mts";
|
|
1
|
+
import type { PollSummaryCommandOptions, PollSummaryItem, PollSummarySelection } from "../types.mts";
|
|
2
2
|
export declare function pollCommandFields(repo: string, pr: number, isDraft: boolean, opts: PollSummaryCommandOptions): Pick<PollSummaryItem, "pollCommand" | "pollProbe">;
|
|
3
|
+
/**
|
|
4
|
+
* The literal command that re-runs this aggregate selection with the options the caller used.
|
|
5
|
+
* Intervals and timeouts are omitted: they are loop cadence, not selection. Options equal to the
|
|
6
|
+
* configured defaults are omitted too, since the parser fills them from config.
|
|
7
|
+
*/
|
|
8
|
+
export declare function aggregateRerunCommand(repo: string, selection: PollSummarySelection, opts: PollSummaryCommandOptions): string;
|
|
@@ -24,3 +24,32 @@ export function pollCommandFields(repo, pr, isDraft, opts) {
|
|
|
24
24
|
const pollCommand = buildPrShepherdCommand(args).text;
|
|
25
25
|
return boundedDraft ? { pollCommand, pollProbe: true } : { pollCommand };
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The literal command that re-runs this aggregate selection with the options the caller used.
|
|
29
|
+
* Intervals and timeouts are omitted: they are loop cadence, not selection. Options equal to the
|
|
30
|
+
* configured defaults are omitted too, since the parser fills them from config.
|
|
31
|
+
*/
|
|
32
|
+
export function aggregateRerunCommand(repo, selection, opts) {
|
|
33
|
+
const args = selection.kind === "stack"
|
|
34
|
+
? ["stack", formatPrUrl(repo, selection.anchor), opts.tick ? "--once" : "--until-terminal"]
|
|
35
|
+
: [
|
|
36
|
+
...(opts.tick ? ["iterate"] : []),
|
|
37
|
+
...selection.requested.map((pr) => formatPrUrl(repo, pr)),
|
|
38
|
+
...(opts.tick ? [] : ["--until-terminal"]),
|
|
39
|
+
];
|
|
40
|
+
if (opts.merge)
|
|
41
|
+
args.push("--merge");
|
|
42
|
+
// The parser always fills these from config, so a default value adds nothing to the command.
|
|
43
|
+
const iterate = loadConfig().iterate;
|
|
44
|
+
if (opts.readyDelaySeconds !== undefined &&
|
|
45
|
+
opts.readyDelaySeconds !== iterate["ready-delay-minutes"] * 60)
|
|
46
|
+
args.push("--ready-delay", `${opts.readyDelaySeconds}s`);
|
|
47
|
+
if (opts.stallTimeoutSeconds !== undefined &&
|
|
48
|
+
opts.stallTimeoutSeconds !== iterate["stall-timeout-minutes"] * 60)
|
|
49
|
+
args.push("--stall-timeout", `${opts.stallTimeoutSeconds}s`);
|
|
50
|
+
if (opts.instructions)
|
|
51
|
+
args.push("--instructions", opts.instructions);
|
|
52
|
+
if (opts.noAutoMarkReady)
|
|
53
|
+
args.push("--no-auto-mark-ready");
|
|
54
|
+
return buildPrShepherdCommand(args).text;
|
|
55
|
+
}
|
|
@@ -12,7 +12,7 @@ import { isCurrentSummaryReady } from "./poll-summary-readiness.mjs";
|
|
|
12
12
|
import { applyOpenCheckBlockers } from "./poll-summary-check-blockers.mjs";
|
|
13
13
|
import { normalizePollSummaryState, routePollSummary } from "./poll-summary-route.mjs";
|
|
14
14
|
import { hydrateReadyAnnotationProbe, readyFingerprint } from "./poll-summary-annotation-probe.mjs";
|
|
15
|
-
import { applyUnreportedRequiredChecks, summaryRequiredContexts, } from "./poll-summary-unreported.mjs";
|
|
15
|
+
import { applyPendingWorkflowSuites, applyUnreportedRequiredChecks, summaryRequiredContexts, } from "./poll-summary-unreported.mjs";
|
|
16
16
|
import { aliasThreadSeenMarkers } from "./rest-identities.mjs";
|
|
17
17
|
import { restQueueRequirement } from "./poll-summary-rest-queue.mjs";
|
|
18
18
|
import { detectBlockingReviewer } from "./poll-summary-blocking-reviewer.mjs";
|
|
@@ -25,6 +25,7 @@ applyRuleAutoResolve = false) {
|
|
|
25
25
|
const required = summaryRequiredContexts(raw, mergeTargetContexts);
|
|
26
26
|
const checks = summarizePollSummaryChecks(raw, required);
|
|
27
27
|
applyUnreportedRequiredChecks(raw, checks, required);
|
|
28
|
+
applyPendingWorkflowSuites(raw, checks);
|
|
28
29
|
const { review, ruleAutoResolve } = await evaluatePollSummaryReview(raw, seen, viewerCanAdminister, applyRuleAutoResolve && normalizePollSummaryState(raw.state) === "OPEN"
|
|
29
30
|
? { owner: repo.owner, repo: repo.name, pr: raw.number }
|
|
30
31
|
: undefined);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { summarizePollSummaryChecks } from "./poll-summary-checks.mjs";
|
|
2
2
|
import { parseBranchRules } from "./batch-parsers-rules.mjs";
|
|
3
3
|
import { rulesComplete } from "./fingerprint-fields.mjs";
|
|
4
|
+
import { hasNoRelevantChecks } from "./poll-summary-unreported.mjs";
|
|
4
5
|
import { isKnownMergeStateStatus, transportReadinessGaps } from "./transport-evidence.mjs";
|
|
5
6
|
/**
|
|
6
7
|
* Fresh compact evidence required before a READY receipt can be used.
|
|
@@ -42,5 +43,6 @@ export function isCurrentSummaryReady(raw, checks, review, options = {}) {
|
|
|
42
43
|
(sourceChecks.failing ?? 0) === 0 &&
|
|
43
44
|
(sourceChecks.inProgress ?? 0) === 0 &&
|
|
44
45
|
(checks.unreportedRequired?.length ?? 0) === 0 &&
|
|
46
|
+
!(checks.actionsWorkflowInProgress === true && hasNoRelevantChecks(checks)) &&
|
|
45
47
|
(review.actionable ?? 0) === 0);
|
|
46
48
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from "../config/load.mjs";
|
|
2
|
+
import { hasNoRelevantChecks } from "./poll-summary-unreported.mjs";
|
|
2
3
|
import { isKnownMergeStateStatus, transportReadinessGaps } from "./transport-evidence.mjs";
|
|
3
4
|
import { canGenerateGithubMutation } from "./mutation-policy.mjs";
|
|
4
5
|
import { getGithubTransport, isCcrTransport } from "./transport.mjs";
|
|
@@ -25,6 +26,7 @@ export function routePollSummary(raw, checks, review, opts) {
|
|
|
25
26
|
return { action: "triage", reasons: ["unreported-required-checks"] };
|
|
26
27
|
}
|
|
27
28
|
if ((checks.inProgress ?? 0) > 0 ||
|
|
29
|
+
(checks.actionsWorkflowInProgress === true && hasNoRelevantChecks(checks)) ||
|
|
28
30
|
!isKnownMergeStateStatus(raw.mergeStateStatus) ||
|
|
29
31
|
raw.mergeable === "UNKNOWN" ||
|
|
30
32
|
raw.mergeStateStatus === "UNKNOWN")
|
|
@@ -7,3 +7,10 @@ export declare function trunkRequiredContexts(ordered: readonly RawSummaryPr[],
|
|
|
7
7
|
export declare function summaryRequiredContexts(raw: RawSummaryPr, mergeTargetContexts: readonly string[] | undefined): string[];
|
|
8
8
|
/** Record missing required contexts. A running Actions workflow does not clear the names. */
|
|
9
9
|
export declare function applyUnreportedRequiredChecks(raw: RawSummaryPr, checks: PollSummaryChecks, required: readonly string[]): void;
|
|
10
|
+
/** No relevant check yet. A skipped, filtered, ignored or superseded check is not relevant. */
|
|
11
|
+
export declare function hasNoRelevantChecks(checks: PollSummaryChecks): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Flag a running Actions workflow suite when the head has no relevant check yet. Suites exist
|
|
14
|
+
* before their check runs, so CLEAN with no checks is not proof that CI passed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function applyPendingWorkflowSuites(raw: RawSummaryPr, checks: PollSummaryChecks): void;
|
|
@@ -37,6 +37,22 @@ export function applyUnreportedRequiredChecks(raw, checks, required) {
|
|
|
37
37
|
checks.actionsWorkflowInProgress = true;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
/** No relevant check yet. A skipped, filtered, ignored or superseded check is not relevant. */
|
|
41
|
+
export function hasNoRelevantChecks(checks) {
|
|
42
|
+
return (checks.passing ?? 0) + (checks.failing ?? 0) + (checks.inProgress ?? 0) === 0;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Flag a running Actions workflow suite when the head has no relevant check yet. Suites exist
|
|
46
|
+
* before their check runs, so CLEAN with no checks is not proof that CI passed.
|
|
47
|
+
*/
|
|
48
|
+
export function applyPendingWorkflowSuites(raw, checks) {
|
|
49
|
+
if (checks.actionsWorkflowInProgress === true || !hasNoRelevantChecks(checks))
|
|
50
|
+
return;
|
|
51
|
+
const suites = raw.commits.nodes[0]?.commit.checkSuites?.nodes ?? [];
|
|
52
|
+
if (actionsWorkflowInProgress(suites, new Set(loadConfig().checks["ci-trigger-events"]))) {
|
|
53
|
+
checks.actionsWorkflowInProgress = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
40
56
|
function reportedNames(raw) {
|
|
41
57
|
const names = new Set();
|
|
42
58
|
const rollups = [
|
|
@@ -3,8 +3,9 @@ import { recordRestIdentity } from "./rest-identities.mjs";
|
|
|
3
3
|
import { readRest as rest } from "./rest-reader-core.mjs";
|
|
4
4
|
export async function readRestCommitChecks(oid, repo, pr = 0) {
|
|
5
5
|
const prefix = restRepoPath(repo);
|
|
6
|
-
|
|
6
|
+
// Suites first: a COMPLETED suite read before the runs means a run still not COMPLETED is stuck.
|
|
7
7
|
const suites = await readRestPages(`${prefix}/commits/${encodeURIComponent(oid)}/check-suites`, (body) => restCollection(body, "check_suites"));
|
|
8
|
+
const checks = await readRestPages(`${prefix}/commits/${encodeURIComponent(oid)}/check-runs?filter=latest`, (body) => restCollection(body, "check_runs"));
|
|
8
9
|
const statuses = await readRestPages(`${prefix}/commits/${encodeURIComponent(oid)}/statuses`);
|
|
9
10
|
const workflowRuns = await readRestPages(`${prefix}/actions/runs?head_sha=${encodeURIComponent(oid)}`, (body) => restCollection(body, "workflow_runs"));
|
|
10
11
|
for (const suite of suites.nodes) {
|
|
@@ -64,6 +65,7 @@ export async function readRestCommitChecks(oid, repo, pr = 0) {
|
|
|
64
65
|
annotations: { totalCount: check.output.annotations_count, nodes: [] },
|
|
65
66
|
checkSuite: suite
|
|
66
67
|
? {
|
|
68
|
+
status: suite.status.toUpperCase(),
|
|
67
69
|
createdAt: suite.created_at,
|
|
68
70
|
updatedAt: suite.updated_at,
|
|
69
71
|
workflowRun: run
|
package/bin/github/rest-text.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { appendEntry, nextEntry } from "../log/log-file.mjs";
|
|
|
2
2
|
import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
|
|
3
3
|
import { GitHubRequestError } from "./errors.mjs";
|
|
4
4
|
import { githubFetch } from "./github-fetch.mjs";
|
|
5
|
+
import { GITHUB_LOG_TIMEOUT_MS } from "./github-timeouts.mjs";
|
|
5
6
|
import { makeAuthHeaders } from "./http-auth.mjs";
|
|
6
7
|
import { requestWithTokenRetry } from "./http-request.mjs";
|
|
7
8
|
import { parseRateLimit, parseRetryAfter, redactUrl, sanitizeBody, } from "./http-utils.mjs";
|
|
@@ -19,7 +20,7 @@ export async function restText(path, onRateLimit) {
|
|
|
19
20
|
const auth = await makeAuthHeaders();
|
|
20
21
|
authSource = auth.source;
|
|
21
22
|
credentialFingerprint = auth.fingerprint;
|
|
22
|
-
return githubFetch(url, { method: "GET", headers: auth.headers, redirect: "manual" });
|
|
23
|
+
return githubFetch(url, { method: "GET", headers: auth.headers, redirect: "manual" }, GITHUB_LOG_TIMEOUT_MS);
|
|
23
24
|
}, t0, (response, durationMs) => recordIntermediateResponse({
|
|
24
25
|
n,
|
|
25
26
|
kind: "restText",
|
|
@@ -105,7 +106,7 @@ async function followRestTextRedirect(res, entry) {
|
|
|
105
106
|
const logUrl = redactUrl(location);
|
|
106
107
|
appendEntry(formatRequestEntry({ n: n2, kind: "restText", method: "GET", url: logUrl }));
|
|
107
108
|
const t1 = performance.now();
|
|
108
|
-
const redirectRes = await githubFetch(location);
|
|
109
|
+
const redirectRes = await githubFetch(location, undefined, GITHUB_LOG_TIMEOUT_MS);
|
|
109
110
|
appendEntry(formatResponseEntry({
|
|
110
111
|
n: n2,
|
|
111
112
|
kind: "restText",
|
|
@@ -90,6 +90,7 @@ export function batchResponseFromSummary(layer, source, runs) {
|
|
|
90
90
|
...(run.annotations && { annotations: run.annotations }),
|
|
91
91
|
checkSuite: suite
|
|
92
92
|
? {
|
|
93
|
+
...(suite.status && { status: suite.status }),
|
|
93
94
|
...(suite.createdAt && { createdAt: suite.createdAt }),
|
|
94
95
|
...(suite.updatedAt && { updatedAt: suite.updatedAt }),
|
|
95
96
|
workflowRun: workflowRun
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -59,6 +59,8 @@ export function toAgentCheck(c) {
|
|
|
59
59
|
runId: c.runId,
|
|
60
60
|
detailsUrl: c.detailsUrl,
|
|
61
61
|
conclusion: c.conclusion,
|
|
62
|
+
...(c.status !== "COMPLETED" &&
|
|
63
|
+
c.suiteStatus !== undefined && { status: c.status, suiteStatus: c.suiteStatus }),
|
|
62
64
|
...(c.runAttempt !== undefined && c.runAttempt > 1 && { runAttempt: c.runAttempt }),
|
|
63
65
|
...(c.workflowName !== undefined && { workflowName: c.workflowName }),
|
|
64
66
|
...(c.jobName !== undefined && { jobName: c.jobName }),
|
package/bin/types/github.d.mts
CHANGED
|
@@ -57,6 +57,12 @@ export interface CheckRun {
|
|
|
57
57
|
summary?: string;
|
|
58
58
|
/** Workflow display name for GitHub Actions check runs, when GraphQL exposes it. */
|
|
59
59
|
workflowName?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Raw status of the check suite this run belongs to. Absent when GitHub did not report one
|
|
62
|
+
* (status contexts, synthetic checks, or a transport that cannot read it). A `COMPLETED`
|
|
63
|
+
* suite with a run that is not `COMPLETED` is a stuck check.
|
|
64
|
+
*/
|
|
65
|
+
suiteStatus?: string;
|
|
60
66
|
workflowId?: string;
|
|
61
67
|
/** True when GraphQL reported at least one CheckRun annotation. Omitted when false. */
|
|
62
68
|
hasAnnotations?: boolean;
|
package/bin/types/report.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthorType, ReviewThread, PrComment, Review, MergeStatusResult, ViewerAuthorization, CheckConclusion, SuggestionBlock } from "./github.mts";
|
|
1
|
+
import type { AuthorType, ReviewThread, PrComment, Review, MergeStatusResult, ViewerAuthorization, CheckConclusion, CheckStatus, SuggestionBlock } from "./github.mts";
|
|
2
2
|
import type { ClassifiedCheck, RelatedFailedJob, TriagedCheck } from "./check-classification.mts";
|
|
3
3
|
import type { AgentThreadComment } from "./agent-thread.mts";
|
|
4
4
|
import type { CheckAnnotation } from "./check-annotations.mts";
|
|
@@ -69,8 +69,6 @@ export interface ShepherdReport {
|
|
|
69
69
|
skipped: ClassifiedCheck[];
|
|
70
70
|
/** Checks filtered out because they were triggered by a non-PR event (push, schedule, etc.). */
|
|
71
71
|
filtered: ClassifiedCheck[];
|
|
72
|
-
/** Ignored checks with unseen annotations; omitted when empty. */
|
|
73
|
-
ignored?: ClassifiedCheck[];
|
|
74
72
|
filteredNames: string[];
|
|
75
73
|
blockedByFilteredCheck: boolean;
|
|
76
74
|
ignoredNames?: string[];
|
|
@@ -196,6 +194,10 @@ export interface AgentCheck {
|
|
|
196
194
|
detailsUrl: string | null;
|
|
197
195
|
/** Raw GitHub check conclusion; may be null for some completed checks from upstream data. */
|
|
198
196
|
conclusion: CheckConclusion;
|
|
197
|
+
/** Raw check status, present only for a stuck check: not `COMPLETED` while its suite is. */
|
|
198
|
+
status?: CheckStatus;
|
|
199
|
+
/** Raw check-suite status (`COMPLETED`), present only with `status` for a stuck check. */
|
|
200
|
+
suiteStatus?: string;
|
|
199
201
|
/** Workflow display name (e.g. `"CI"`). Populated on a best-effort basis when available from the jobs API. */
|
|
200
202
|
workflowName?: string;
|
|
201
203
|
/** Name of the matched job (e.g. `"tests (ubuntu)"`). Distinct from check name for matrix builds. */
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ Apply when a step says `Playbook: "CI failure triage"`. Use the excerpts and tag
|
|
|
13
13
|
- Actions failure with a run id: read the excerpt. Fix a real test or build failure in code and never rerun it, even with a printed `rerun:`. Run a printed `rerun:` only for a transient cause; without one, finish the other work. Then iterate without waiting.
|
|
14
14
|
- An empty excerpt is not evidence of a transient failure. A gate-job excerpt naming failed jobs (for example `test-playwright: failure`) is test-failure evidence even without a stack trace: read the `Other failed jobs in this run` tails first, and only when a named job's tail is absent or truncated run `gh run view <runId> --log-failed -R <owner/repo>`. Rerun only when those logs show a transient cause.
|
|
15
15
|
- `CANCELLED` or `STARTUP_FAILURE` (no excerpt): run a printed `rerun:`, else finish the other work; iterate without waiting.
|
|
16
|
+
- `[status: …, suite: COMPLETED]` (stuck run): the workflow finished but this check never completed, so there is nothing to fix in code. Run a printed `rerun:`, else finish the other work; iterate without waiting.
|
|
16
17
|
- `ACTION_REQUIRED`: finish the other work and iterate. Shepherd escalates if manual approval is still required.
|
|
17
18
|
- `external` with a URL: inspect the provider or reproduce locally, fix, and iterate. The URL alone is not `[ESCALATE]`.
|
|
18
19
|
- `(no runId)` without a URL: keep the metadata. Shepherd escalates when nothing else remains.
|