pr-shepherd 0.53.1 → 0.54.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 +1 -1
- package/bin/cli/args.mjs +3 -0
- package/bin/cli/check-blocker-handler.d.mts +2 -0
- package/bin/cli/check-blocker-handler.mjs +82 -0
- package/bin/cli/help-command-pages.d.mts +22 -3
- package/bin/cli/help-command-pages.mjs +21 -2
- 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-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +3 -1
- package/bin/cli/help.d.mts +23 -4
- package/bin/cli/help.mjs +2 -0
- package/bin/cli/poll-handler.mjs +5 -1
- package/bin/cli-parser.mjs +4 -0
- package/bin/commands/apply-check-blocker.d.mts +22 -0
- package/bin/commands/apply-check-blocker.mjs +36 -0
- package/bin/commands/check-annotations.d.mts +1 -1
- package/bin/commands/check-annotations.mjs +27 -17
- package/bin/commands/check-blocker-ref.d.mts +3 -0
- package/bin/commands/check-blocker-ref.mjs +50 -0
- package/bin/commands/iterate/check-blocker-gate.d.mts +18 -0
- package/bin/commands/iterate/check-blocker-gate.mjs +115 -0
- package/bin/commands/iterate/check-instructions.d.mts +2 -0
- package/bin/commands/iterate/check-instructions.mjs +4 -0
- package/bin/commands/iterate/fix-code.d.mts +2 -0
- package/bin/commands/iterate/fix-code.mjs +31 -15
- package/bin/commands/iterate/index.mjs +22 -8
- package/bin/commands/poll-quota.d.mts +2 -0
- package/bin/commands/poll-quota.mjs +25 -12
- package/bin/commands/poll-rate-limit-cancel.d.mts +35 -0
- package/bin/commands/poll-rate-limit-cancel.mjs +101 -0
- package/bin/commands/poll-rate-limit-delay.d.mts +1 -0
- package/bin/commands/poll-rate-limit-delay.mjs +7 -0
- package/bin/commands/poll-rate-limit-wait.d.mts +14 -0
- package/bin/commands/poll-rate-limit-wait.mjs +125 -0
- package/bin/commands/poll-summary.mjs +21 -9
- package/bin/commands/poll.mjs +22 -17
- package/bin/config/load.d.mts +2 -0
- package/bin/config/load.mjs +24 -2
- package/bin/config.json +1 -0
- package/bin/github/check-annotation-cache.d.mts +8 -0
- package/bin/github/check-annotation-cache.mjs +23 -0
- package/bin/github/check-annotation-pages.d.mts +11 -0
- package/bin/github/check-annotation-pages.mjs +36 -0
- package/bin/github/check-annotation-shape.d.mts +21 -0
- package/bin/github/check-annotation-shape.mjs +50 -0
- package/bin/github/check-annotations-batch.d.mts +17 -0
- package/bin/github/check-annotations-batch.mjs +92 -0
- package/bin/github/check-annotations.d.mts +8 -11
- package/bin/github/check-annotations.mjs +14 -103
- package/bin/github/gql/batch-pr.gql +1 -1
- package/bin/github/gql/check-run-annotations-batch.gql +41 -0
- package/bin/github/pagination.d.mts +1 -1
- package/bin/github/pagination.mjs +1 -1
- package/bin/github/poll-summary-check-blockers.d.mts +16 -0
- package/bin/github/poll-summary-check-blockers.mjs +25 -0
- package/bin/github/poll-summary-checks.d.mts +2 -0
- package/bin/github/poll-summary-checks.mjs +35 -20
- package/bin/github/poll-summary-projector.mjs +5 -0
- package/bin/github/queries.d.mts +6 -0
- package/bin/github/queries.mjs +6 -0
- package/bin/state/check-blockers.d.mts +30 -0
- package/bin/state/check-blockers.mjs +88 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
|
@@ -1,109 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { CHECK_RUN_ANNOTATIONS_QUERY } from "./queries.mjs";
|
|
4
|
-
import { loadDerived, storeDerived } from "../state/rest-cache.mjs";
|
|
5
|
-
const ANNOTATIONS_PER_PAGE = 100;
|
|
6
|
-
const MAX_ANNOTATION_PAGES = 10;
|
|
7
|
-
const ANNOTATION_TEXT_MAX_CHARS = 4_000;
|
|
8
|
-
const TRUNCATED_SUFFIX = "\n[truncated]";
|
|
1
|
+
import { readFreshCheckAnnotations, storeCheckAnnotations, } from "./check-annotation-cache.mjs";
|
|
2
|
+
import { collectAnnotations } from "./check-annotation-pages.mjs";
|
|
9
3
|
/**
|
|
10
|
-
*
|
|
11
|
-
* onto an already-COMPLETED check run without minting a new check-run node
|
|
12
|
-
* ID, so "COMPLETED" is not a reliable immutability signal on its own. Bound
|
|
13
|
-
* the cache instead of trusting it forever, so a long-running poll session
|
|
14
|
-
* eventually revalidates.
|
|
15
|
-
*/
|
|
16
|
-
const ANNOTATION_CACHE_MAX_AGE_MS = 60 * 60 * 1000;
|
|
17
|
-
/**
|
|
18
|
-
* Fetches all inline annotations for a check run.
|
|
4
|
+
* Fetches all inline annotations for one check run.
|
|
19
5
|
*
|
|
20
|
-
* When `cacheOpts` is provided, the result is cached by `checkRunId
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
6
|
+
* When `cacheOpts` is provided, the result is cached by `checkRunId`. Callers
|
|
7
|
+
* only pass `cacheOpts` for COMPLETED runs (a re-run mints a new node id).
|
|
8
|
+
* Prefer `fetchCheckRunAnnotationsBatch` when attaching many checks at once;
|
|
9
|
+
* this single-node query remains for direct callers and follow-up pages.
|
|
24
10
|
*/
|
|
25
|
-
export async function fetchCheckRunAnnotations(checkRunId, cacheOpts) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return cached.value;
|
|
31
|
-
}
|
|
32
|
-
let cursor = null;
|
|
33
|
-
const nodes = [];
|
|
34
|
-
for (let page = 1; page <= MAX_ANNOTATION_PAGES; page++) {
|
|
35
|
-
// eslint-disable-next-line no-await-in-loop
|
|
36
|
-
const result = await fetchAnnotationPage(checkRunId, cursor);
|
|
37
|
-
nodes.push(...result.nodes);
|
|
38
|
-
if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor)
|
|
39
|
-
break;
|
|
40
|
-
if (page === MAX_ANNOTATION_PAGES) {
|
|
41
|
-
process.stderr.write(`pr-shepherd: annotation pagination cap (${MAX_ANNOTATION_PAGES * ANNOTATIONS_PER_PAGE} annotations) reached for check run ${checkRunId} — annotation output may be incomplete\n`);
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
cursor = result.pageInfo.endCursor;
|
|
45
|
-
}
|
|
46
|
-
const annotations = nodes.map((node) => toCheckAnnotation(checkRunId, node));
|
|
47
|
-
if (cacheOpts) {
|
|
48
|
-
await storeDerived(cacheOpts.stateKey, cacheName, annotations, cacheOpts.headSha);
|
|
11
|
+
export async function fetchCheckRunAnnotations(checkRunId, cacheOpts, initialPage) {
|
|
12
|
+
if (initialPage === undefined) {
|
|
13
|
+
const cached = await readFreshCheckAnnotations(checkRunId, cacheOpts);
|
|
14
|
+
if (cached)
|
|
15
|
+
return cached;
|
|
49
16
|
}
|
|
17
|
+
const annotations = await collectAnnotations(checkRunId, initialPage);
|
|
18
|
+
await storeCheckAnnotations(checkRunId, annotations, cacheOpts);
|
|
50
19
|
return annotations;
|
|
51
20
|
}
|
|
52
|
-
async function fetchAnnotationPage(checkRunId, cursor) {
|
|
53
|
-
const res = await graphql(CHECK_RUN_ANNOTATIONS_QUERY, {
|
|
54
|
-
id: checkRunId,
|
|
55
|
-
...(cursor ? { cursor } : {}),
|
|
56
|
-
});
|
|
57
|
-
const node = res.data.node;
|
|
58
|
-
if (node?.__typename !== "CheckRun" || node.annotations === undefined) {
|
|
59
|
-
return { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [] };
|
|
60
|
-
}
|
|
61
|
-
return node.annotations;
|
|
62
|
-
}
|
|
63
|
-
function toCheckAnnotation(checkRunId, raw) {
|
|
64
|
-
const id = `check_annotation_${raw.fullDatabaseId ?? fallbackId(checkRunId, raw)}`;
|
|
65
|
-
const title = raw.title?.trim() || undefined;
|
|
66
|
-
const rawDetails = raw.rawDetails?.trim() || undefined;
|
|
67
|
-
const blobUrl = raw.blobUrl?.trim() || undefined;
|
|
68
|
-
return {
|
|
69
|
-
id,
|
|
70
|
-
path: raw.path,
|
|
71
|
-
startLine: raw.location?.start.line ?? null,
|
|
72
|
-
endLine: raw.location?.end.line ?? raw.location?.start.line ?? null,
|
|
73
|
-
...(raw.location?.start.column !== undefined && {
|
|
74
|
-
startColumn: raw.location.start.column,
|
|
75
|
-
}),
|
|
76
|
-
...(raw.location?.end.column !== undefined && {
|
|
77
|
-
endColumn: raw.location.end.column,
|
|
78
|
-
}),
|
|
79
|
-
level: raw.annotationLevel,
|
|
80
|
-
...(title !== undefined && { title }),
|
|
81
|
-
message: truncateAnnotationText(raw.message),
|
|
82
|
-
...(rawDetails !== undefined && { rawDetails: truncateAnnotationText(rawDetails) }),
|
|
83
|
-
...(blobUrl !== undefined && { blobUrl }),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function truncateAnnotationText(text) {
|
|
87
|
-
if (text.length <= ANNOTATION_TEXT_MAX_CHARS)
|
|
88
|
-
return text;
|
|
89
|
-
return `${text.slice(0, ANNOTATION_TEXT_MAX_CHARS - TRUNCATED_SUFFIX.length).trimEnd()}${TRUNCATED_SUFFIX}`;
|
|
90
|
-
}
|
|
91
|
-
function fallbackId(checkRunId, raw) {
|
|
92
|
-
const start = raw.location?.start;
|
|
93
|
-
const end = raw.location?.end;
|
|
94
|
-
const parts = [
|
|
95
|
-
checkRunId,
|
|
96
|
-
raw.path,
|
|
97
|
-
raw.annotationLevel,
|
|
98
|
-
raw.title ?? "",
|
|
99
|
-
raw.message,
|
|
100
|
-
raw.rawDetails ?? "",
|
|
101
|
-
raw.blobUrl ?? "",
|
|
102
|
-
String(start?.line ?? ""),
|
|
103
|
-
String(start?.column ?? ""),
|
|
104
|
-
String(end?.line ?? ""),
|
|
105
|
-
String(end?.column ?? ""),
|
|
106
|
-
];
|
|
107
|
-
const input = parts.map((part) => `${part.length}:${part}`).join("|");
|
|
108
|
-
return createHash("sha256").update(input).digest("hex").slice(0, 24);
|
|
109
|
-
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
query CheckRunAnnotationsBatch($ids: [ID!]!) {
|
|
2
|
+
_shepherdRateLimit: rateLimit {
|
|
3
|
+
cost
|
|
4
|
+
limit
|
|
5
|
+
nodeCount
|
|
6
|
+
remaining
|
|
7
|
+
resetAt
|
|
8
|
+
used
|
|
9
|
+
}
|
|
10
|
+
nodes(ids: $ids) {
|
|
11
|
+
id
|
|
12
|
+
__typename
|
|
13
|
+
... on CheckRun {
|
|
14
|
+
annotations(first: 100) {
|
|
15
|
+
pageInfo {
|
|
16
|
+
hasNextPage
|
|
17
|
+
endCursor
|
|
18
|
+
}
|
|
19
|
+
nodes {
|
|
20
|
+
fullDatabaseId
|
|
21
|
+
path
|
|
22
|
+
annotationLevel
|
|
23
|
+
title
|
|
24
|
+
message
|
|
25
|
+
rawDetails
|
|
26
|
+
blobUrl
|
|
27
|
+
location {
|
|
28
|
+
start {
|
|
29
|
+
line
|
|
30
|
+
column
|
|
31
|
+
}
|
|
32
|
+
end {
|
|
33
|
+
line
|
|
34
|
+
column
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -33,7 +33,7 @@ export declare function paginateForward<T>(fetchFn: (cursor: string | null) => P
|
|
|
33
33
|
/**
|
|
34
34
|
* Paginate backward through a GraphQL connection (`last` / `before` cursors).
|
|
35
35
|
*
|
|
36
|
-
* Used for `reviewThreads(last: 100, before: $before)
|
|
36
|
+
* Used for `reviewThreads(last: 100, before: $before)` follow-up pages.
|
|
37
37
|
*
|
|
38
38
|
* @param fetchFn Called once per page. Receives the cursor (or null for
|
|
39
39
|
* the very first page). Returns a Connection<T> with
|
|
@@ -35,7 +35,7 @@ export async function paginateForward(fetchFn, initialCursor) {
|
|
|
35
35
|
/**
|
|
36
36
|
* Paginate backward through a GraphQL connection (`last` / `before` cursors).
|
|
37
37
|
*
|
|
38
|
-
* Used for `reviewThreads(last: 100, before: $before)
|
|
38
|
+
* Used for `reviewThreads(last: 100, before: $before)` follow-up pages.
|
|
39
39
|
*
|
|
40
40
|
* @param fetchFn Called once per page. Receives the cursor (or null for
|
|
41
41
|
* the very first page). Returns a Connection<T> with
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PollSummaryChecks, PollSummaryItem, PollSummaryReview } from "../types.mts";
|
|
2
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
3
|
+
type Routed = Pick<PollSummaryItem, "action" | "reasons">;
|
|
4
|
+
/**
|
|
5
|
+
* A layer whose only failing checks are deferred on open blockers waits.
|
|
6
|
+
* Review work, conflicts, an incomplete rollup, or any check that is not
|
|
7
|
+
* deferred stay on the routed action. Stack rows are probed so the planner
|
|
8
|
+
* does not start a one-PR session for a wait it would otherwise shepherd.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyOpenCheckBlockers(raw: RawSummaryPr, repo: {
|
|
11
|
+
owner: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}, routed: Routed, checks: PollSummaryChecks, review: PollSummaryReview, stack: boolean): Promise<Routed & {
|
|
14
|
+
pollProbe?: true;
|
|
15
|
+
}>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { resolveCheckBlockerGate } from "../commands/iterate/check-blocker-gate.mjs";
|
|
2
|
+
import { failingSummaryCheckNames } from "./poll-summary-checks.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* A layer whose only failing checks are deferred on open blockers waits.
|
|
5
|
+
* Review work, conflicts, an incomplete rollup, or any check that is not
|
|
6
|
+
* deferred stay on the routed action. Stack rows are probed so the planner
|
|
7
|
+
* does not start a one-PR session for a wait it would otherwise shepherd.
|
|
8
|
+
*/
|
|
9
|
+
export async function applyOpenCheckBlockers(raw, repo, routed, checks, review, stack) {
|
|
10
|
+
if (routed.action !== "fix_code" || !routed.reasons.includes("failing-checks"))
|
|
11
|
+
return routed;
|
|
12
|
+
if ((review.actionable ?? 0) > 0 || checks.incomplete)
|
|
13
|
+
return routed;
|
|
14
|
+
const names = failingSummaryCheckNames(raw);
|
|
15
|
+
if (names.length === 0)
|
|
16
|
+
return routed;
|
|
17
|
+
const gate = await resolveCheckBlockerGate({ owner: repo.owner, repo: repo.name, pr: raw.number }, names.map((name) => ({ name })));
|
|
18
|
+
if (!gate || !names.every((name) => gate.deferredNames.has(name)))
|
|
19
|
+
return routed;
|
|
20
|
+
return {
|
|
21
|
+
action: "wait",
|
|
22
|
+
reasons: gate.openBlockers.map((ref) => `blocked-by:${ref}`),
|
|
23
|
+
...(stack ? { pollProbe: true } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { PollSummaryChecks } from "../types.mts";
|
|
2
2
|
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
3
3
|
export declare function summarizePollSummaryChecks(raw: RawSummaryPr): PollSummaryChecks;
|
|
4
|
+
/** Names of failing checks in the loaded summary rollup. */
|
|
5
|
+
export declare function failingSummaryCheckNames(raw: RawSummaryPr): string[];
|
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import { classifyChecks } from "../checks/classify.mjs";
|
|
2
2
|
export function summarizePollSummaryChecks(raw) {
|
|
3
|
-
const rollups =
|
|
3
|
+
const rollups = summaryRollups(raw);
|
|
4
|
+
const counts = {};
|
|
5
|
+
for (const check of classifyChecks(checkRuns(rollups), {
|
|
6
|
+
additionalRelevantEvents: ["merge_group"],
|
|
7
|
+
})) {
|
|
8
|
+
const key = {
|
|
9
|
+
passed: "passing",
|
|
10
|
+
failing: "failing",
|
|
11
|
+
in_progress: "inProgress",
|
|
12
|
+
skipped: "skipped",
|
|
13
|
+
filtered: "filtered",
|
|
14
|
+
ignored: "ignored",
|
|
15
|
+
superseded: "superseded",
|
|
16
|
+
}[check.category];
|
|
17
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
18
|
+
}
|
|
19
|
+
const summary = counts;
|
|
20
|
+
if (rollups.some((rollup) => rollup.contexts.pageInfo.hasPreviousPage)) {
|
|
21
|
+
summary.incomplete = true;
|
|
22
|
+
}
|
|
23
|
+
return summary;
|
|
24
|
+
}
|
|
25
|
+
/** Names of failing checks in the loaded summary rollup. */
|
|
26
|
+
export function failingSummaryCheckNames(raw) {
|
|
27
|
+
return classifyChecks(checkRuns(summaryRollups(raw)), {
|
|
28
|
+
additionalRelevantEvents: ["merge_group"],
|
|
29
|
+
})
|
|
30
|
+
.filter((check) => check.category === "failing")
|
|
31
|
+
.map((check) => check.name);
|
|
32
|
+
}
|
|
33
|
+
function summaryRollups(raw) {
|
|
34
|
+
return [
|
|
4
35
|
raw.commits.nodes[0]?.commit.statusCheckRollup,
|
|
5
36
|
raw.mergeQueueEntry?.headCommit?.statusCheckRollup,
|
|
6
37
|
].filter((rollup) => rollup !== null && rollup !== undefined);
|
|
7
|
-
|
|
38
|
+
}
|
|
39
|
+
function checkRuns(rollups) {
|
|
40
|
+
return rollups.flatMap((rollup, rollupIndex) => rollup.contexts.nodes.map((context) => {
|
|
8
41
|
if (context.__typename === "StatusContext") {
|
|
9
42
|
return {
|
|
10
43
|
name: context.context,
|
|
@@ -40,22 +73,4 @@ export function summarizePollSummaryChecks(raw) {
|
|
|
40
73
|
...(rollupIndex === 1 && { scope: "merge_group" }),
|
|
41
74
|
};
|
|
42
75
|
}));
|
|
43
|
-
const counts = {};
|
|
44
|
-
for (const check of classifyChecks(checks, { additionalRelevantEvents: ["merge_group"] })) {
|
|
45
|
-
const key = {
|
|
46
|
-
passed: "passing",
|
|
47
|
-
failing: "failing",
|
|
48
|
-
in_progress: "inProgress",
|
|
49
|
-
skipped: "skipped",
|
|
50
|
-
filtered: "filtered",
|
|
51
|
-
ignored: "ignored",
|
|
52
|
-
superseded: "superseded",
|
|
53
|
-
}[check.category];
|
|
54
|
-
counts[key] = (counts[key] ?? 0) + 1;
|
|
55
|
-
}
|
|
56
|
-
const summary = counts;
|
|
57
|
-
if (rollups.some((rollup) => rollup.contexts.pageInfo.hasPreviousPage)) {
|
|
58
|
-
summary.incomplete = true;
|
|
59
|
-
}
|
|
60
|
-
return summary;
|
|
61
76
|
}
|
|
@@ -9,6 +9,7 @@ import { summarizePollSummaryReview } from "./poll-summary-review.mjs";
|
|
|
9
9
|
import { fingerprintRawSummaryPr } from "./poll-summary-fingerprint.mjs";
|
|
10
10
|
import { currentQueueRemovalEvent } from "./poll-summary-queue-removal.mjs";
|
|
11
11
|
import { isCurrentSummaryReady } from "./poll-summary-readiness.mjs";
|
|
12
|
+
import { applyOpenCheckBlockers } from "./poll-summary-check-blockers.mjs";
|
|
12
13
|
import { normalizePollSummaryState, routePollSummary } from "./poll-summary-route.mjs";
|
|
13
14
|
export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdminister = false) {
|
|
14
15
|
const repoName = `${repo.owner}/${repo.name}`;
|
|
@@ -69,6 +70,9 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
69
70
|
: removalEvent
|
|
70
71
|
? projectQueueRemoval(removalEvent)
|
|
71
72
|
: undefined;
|
|
73
|
+
const blocked = await applyOpenCheckBlockers(raw, repo, { action, reasons }, checks, review, opts.stackPrNumber !== undefined);
|
|
74
|
+
action = blocked.action;
|
|
75
|
+
reasons = blocked.reasons;
|
|
72
76
|
return {
|
|
73
77
|
pr: raw.number,
|
|
74
78
|
repo: repoName,
|
|
@@ -97,6 +101,7 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
97
101
|
!(opts.stackPrNumber !== undefined && raw.stack && action === "merge"))
|
|
98
102
|
? pollCommandFields(repoName, raw.number, raw.isDraft, opts)
|
|
99
103
|
: {}),
|
|
104
|
+
...(blocked.pollProbe ? { pollProbe: true } : {}),
|
|
100
105
|
};
|
|
101
106
|
}
|
|
102
107
|
function projectQueueRemoval(removal) {
|
package/bin/github/queries.d.mts
CHANGED
|
@@ -29,6 +29,12 @@ export declare const SUGGESTION_THREADS_QUERY: string;
|
|
|
29
29
|
export declare const REVIEW_THREAD_COMMENTS_QUERY: string;
|
|
30
30
|
/** Fetch inline annotations for a single CheckRun by node ID. */
|
|
31
31
|
export declare const CHECK_RUN_ANNOTATIONS_QUERY: string;
|
|
32
|
+
/**
|
|
33
|
+
* First annotation page for many CheckRuns. Chunked at 20 ids so the nested
|
|
34
|
+
* `annotations` connections stay a 1-point query. Further pages use
|
|
35
|
+
* `CHECK_RUN_ANNOTATIONS_QUERY`.
|
|
36
|
+
*/
|
|
37
|
+
export declare const CHECK_RUN_ANNOTATIONS_BATCH_QUERY: string;
|
|
32
38
|
/** Fetches additional status contexts for a merge queue synthetic commit. */
|
|
33
39
|
export declare const COMMIT_CHECK_CONTEXTS_QUERY: string;
|
|
34
40
|
/** Returns the current head commit SHA for a PR. Used by waitForSha polling. */
|
package/bin/github/queries.mjs
CHANGED
|
@@ -34,6 +34,12 @@ export const SUGGESTION_THREADS_QUERY = gql("suggestion-threads.gql");
|
|
|
34
34
|
export const REVIEW_THREAD_COMMENTS_QUERY = gql("review-thread-comments.gql");
|
|
35
35
|
/** Fetch inline annotations for a single CheckRun by node ID. */
|
|
36
36
|
export const CHECK_RUN_ANNOTATIONS_QUERY = gql("check-run-annotations.gql");
|
|
37
|
+
/**
|
|
38
|
+
* First annotation page for many CheckRuns. Chunked at 20 ids so the nested
|
|
39
|
+
* `annotations` connections stay a 1-point query. Further pages use
|
|
40
|
+
* `CHECK_RUN_ANNOTATIONS_QUERY`.
|
|
41
|
+
*/
|
|
42
|
+
export const CHECK_RUN_ANNOTATIONS_BATCH_QUERY = gql("check-run-annotations-batch.gql");
|
|
37
43
|
/** Fetches additional status contexts for a merge queue synthetic commit. */
|
|
38
44
|
export const COMMIT_CHECK_CONTEXTS_QUERY = gql("commit-check-contexts.gql");
|
|
39
45
|
/** Returns the current head commit SHA for a PR. Used by waitForSha polling. */
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-PR records that a failing check is blocked on an external PR or issue.
|
|
3
|
+
*
|
|
4
|
+
* `$PR_SHEPHERD_STATE_DIR/<owner>/<repo>/<pr>/check-blockers.json`
|
|
5
|
+
*/
|
|
6
|
+
export interface CheckBlockerRef {
|
|
7
|
+
owner: string;
|
|
8
|
+
name: string;
|
|
9
|
+
number: number;
|
|
10
|
+
kind: "pull" | "issue";
|
|
11
|
+
}
|
|
12
|
+
export interface CheckBlockerRecord {
|
|
13
|
+
checkName: string;
|
|
14
|
+
blocker: CheckBlockerRef;
|
|
15
|
+
/** Unix seconds. */
|
|
16
|
+
recordedAt: number;
|
|
17
|
+
}
|
|
18
|
+
interface StateKey {
|
|
19
|
+
owner: string;
|
|
20
|
+
repo: string;
|
|
21
|
+
pr: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function formatCheckBlockerRef(blocker: CheckBlockerRef): string;
|
|
24
|
+
/** Missing, unreadable, or malformed state is an empty list. Does not throw. */
|
|
25
|
+
export declare function readCheckBlockers(key: StateKey): Promise<CheckBlockerRecord[]>;
|
|
26
|
+
/** Insert or replace the record for `checkName`. Returns false when the file cannot be written. */
|
|
27
|
+
export declare function writeCheckBlocker(key: StateKey, record: CheckBlockerRecord): Promise<boolean>;
|
|
28
|
+
/** Drop one check's record. A missing record is success and does not create a file. */
|
|
29
|
+
export declare function clearCheckBlocker(key: StateKey, checkName: string): Promise<boolean>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-PR records that a failing check is blocked on an external PR or issue.
|
|
3
|
+
*
|
|
4
|
+
* `$PR_SHEPHERD_STATE_DIR/<owner>/<repo>/<pr>/check-blockers.json`
|
|
5
|
+
*/
|
|
6
|
+
import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
import { resolvePrStatePath } from "./base.mjs";
|
|
10
|
+
const FILE = "check-blockers.json";
|
|
11
|
+
export function formatCheckBlockerRef(blocker) {
|
|
12
|
+
return `${blocker.owner}/${blocker.name}#${blocker.number}`;
|
|
13
|
+
}
|
|
14
|
+
/** Missing, unreadable, or malformed state is an empty list. Does not throw. */
|
|
15
|
+
export async function readCheckBlockers(key) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(await readFile(resolvePrStatePath(key, FILE), "utf8"));
|
|
18
|
+
if (parsed === null || typeof parsed !== "object")
|
|
19
|
+
return [];
|
|
20
|
+
const blockers = parsed.blockers;
|
|
21
|
+
if (!Array.isArray(blockers))
|
|
22
|
+
return [];
|
|
23
|
+
return blockers.filter(isRecord);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Insert or replace the record for `checkName`. Returns false when the file cannot be written. */
|
|
30
|
+
export async function writeCheckBlocker(key, record) {
|
|
31
|
+
const blockers = (await readCheckBlockers(key)).filter((item) => item.checkName !== record.checkName);
|
|
32
|
+
blockers.push(record);
|
|
33
|
+
return writeAll(key, blockers);
|
|
34
|
+
}
|
|
35
|
+
/** Drop one check's record. A missing record is success and does not create a file. */
|
|
36
|
+
export async function clearCheckBlocker(key, checkName) {
|
|
37
|
+
const current = await readCheckBlockers(key);
|
|
38
|
+
const blockers = current.filter((item) => item.checkName !== checkName);
|
|
39
|
+
if (blockers.length === current.length)
|
|
40
|
+
return true;
|
|
41
|
+
return writeAll(key, blockers);
|
|
42
|
+
}
|
|
43
|
+
async function writeAll(key, blockers) {
|
|
44
|
+
let tmp;
|
|
45
|
+
try {
|
|
46
|
+
const path = resolvePrStatePath(key, FILE);
|
|
47
|
+
tmp = `${path}.${randomUUID()}.tmp`;
|
|
48
|
+
await mkdir(dirname(path), { recursive: true });
|
|
49
|
+
await writeFile(tmp, `${JSON.stringify({ blockers })}\n`, "utf8");
|
|
50
|
+
await rename(tmp, path);
|
|
51
|
+
tmp = undefined;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (tmp !== undefined) {
|
|
59
|
+
try {
|
|
60
|
+
await unlink(tmp);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// The failed write may not have created the temp file.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function isRecord(value) {
|
|
69
|
+
if (value === null || typeof value !== "object")
|
|
70
|
+
return false;
|
|
71
|
+
const record = value;
|
|
72
|
+
return (typeof record.checkName === "string" &&
|
|
73
|
+
record.checkName.length > 0 &&
|
|
74
|
+
typeof record.recordedAt === "number" &&
|
|
75
|
+
Number.isFinite(record.recordedAt) &&
|
|
76
|
+
isBlocker(record.blocker));
|
|
77
|
+
}
|
|
78
|
+
function isBlocker(value) {
|
|
79
|
+
if (value === null || typeof value !== "object")
|
|
80
|
+
return false;
|
|
81
|
+
const blocker = value;
|
|
82
|
+
return ((blocker.kind === "pull" || blocker.kind === "issue") &&
|
|
83
|
+
typeof blocker.owner === "string" &&
|
|
84
|
+
typeof blocker.name === "string" &&
|
|
85
|
+
typeof blocker.number === "number" &&
|
|
86
|
+
Number.isInteger(blocker.number) &&
|
|
87
|
+
blocker.number > 0);
|
|
88
|
+
}
|
package/package.json
CHANGED