pr-shepherd 0.35.0 → 0.37.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.
Files changed (91) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +32 -12
  3. package/bin/checks/conclusions.d.mts +6 -0
  4. package/bin/checks/conclusions.mjs +9 -0
  5. package/bin/checks/triage.mjs +11 -3
  6. package/bin/cli/args.mjs +1 -0
  7. package/bin/cli/default-poll.mjs +1 -0
  8. package/bin/cli/fix-formatter.mjs +4 -2
  9. package/bin/cli/help-command-pages.d.mts +1 -1
  10. package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
  11. package/bin/cli/help-iterate-poll-pages.mjs +10 -5
  12. package/bin/cli/help-top-page.d.mts +1 -1
  13. package/bin/cli/help-top-page.mjs +2 -1
  14. package/bin/cli/help.d.mts +2 -2
  15. package/bin/cli/iterate-formatter.mjs +11 -2
  16. package/bin/cli/iterate-lean.mjs +1 -0
  17. package/bin/cli/poll-handler.mjs +7 -0
  18. package/bin/commands/check-annotations.d.mts +16 -3
  19. package/bin/commands/check-annotations.mjs +37 -1
  20. package/bin/commands/check.d.mts +1 -0
  21. package/bin/commands/check.mjs +35 -28
  22. package/bin/commands/commit-suggestion.mjs +11 -5
  23. package/bin/commands/iterate/fix-code.mjs +13 -6
  24. package/bin/commands/iterate/helpers.d.mts +1 -0
  25. package/bin/commands/iterate/helpers.mjs +15 -6
  26. package/bin/commands/iterate/index.mjs +5 -10
  27. package/bin/commands/iterate/render.mjs +9 -5
  28. package/bin/commands/iterate/stall.mjs +5 -0
  29. package/bin/commands/poll.d.mts +2 -0
  30. package/bin/commands/poll.mjs +51 -17
  31. package/bin/commands/ready-delay.mjs +3 -13
  32. package/bin/config/load.mjs +67 -25
  33. package/bin/github/batch-page-helpers.d.mts +45 -0
  34. package/bin/github/batch-page-helpers.mjs +63 -0
  35. package/bin/github/batch-page.d.mts +14 -0
  36. package/bin/github/batch-page.mjs +62 -0
  37. package/bin/github/batch-parse-suites.d.mts +4 -0
  38. package/bin/github/batch-parse-suites.mjs +25 -0
  39. package/bin/github/batch-parser-helpers.mjs +1 -0
  40. package/bin/github/batch-parsers-rules.d.mts +6 -0
  41. package/bin/github/batch-parsers-rules.mjs +122 -0
  42. package/bin/github/batch-parsers.mjs +6 -0
  43. package/bin/github/batch-raw-rules.d.mts +59 -0
  44. package/bin/github/batch-raw-rules.mjs +1 -0
  45. package/bin/github/batch-raw-types.d.mts +25 -11
  46. package/bin/github/batch.d.mts +2 -0
  47. package/bin/github/batch.mjs +14 -120
  48. package/bin/github/gql/batch-pr-page.gql +194 -0
  49. package/bin/github/gql/batch-pr.gql +79 -21
  50. package/bin/github/gql/commit-suggestion-thread.gql +40 -0
  51. package/bin/github/http.d.mts +2 -1
  52. package/bin/github/http.mjs +2 -1
  53. package/bin/github/pagination.d.mts +3 -2
  54. package/bin/github/pagination.mjs +3 -2
  55. package/bin/github/queries.d.mts +4 -0
  56. package/bin/github/queries.mjs +4 -0
  57. package/bin/github/rest-http.d.mts +6 -1
  58. package/bin/github/rest-http.mjs +25 -86
  59. package/bin/github/rest-text.d.mts +1 -0
  60. package/bin/github/rest-text.mjs +88 -0
  61. package/bin/github/suggestion-thread.d.mts +9 -0
  62. package/bin/github/suggestion-thread.mjs +45 -0
  63. package/bin/github/thread-comments.mjs +12 -8
  64. package/bin/merge-status/derive.mjs +2 -0
  65. package/bin/merge-status/requirements-format.d.mts +3 -0
  66. package/bin/merge-status/requirements-format.mjs +88 -0
  67. package/bin/merge-status/requirements.d.mts +2 -0
  68. package/bin/merge-status/requirements.mjs +51 -0
  69. package/bin/reporters/agent.mjs +0 -3
  70. package/bin/state/base.d.mts +9 -0
  71. package/bin/state/base.mjs +23 -0
  72. package/bin/state/bot-cr-seen.mjs +3 -13
  73. package/bin/state/fix-attempts.mjs +3 -13
  74. package/bin/state/iterate-stall.mjs +3 -13
  75. package/bin/state/seen-comments.mjs +6 -13
  76. package/bin/types/check-classification.d.mts +2 -2
  77. package/bin/types/github.d.mts +6 -7
  78. package/bin/types/iterate.d.mts +6 -7
  79. package/bin/types/merge-requirements.d.mts +82 -0
  80. package/bin/types/merge-requirements.mjs +2 -0
  81. package/bin/types/report.d.mts +5 -5
  82. package/bin/types.d.mts +1 -0
  83. package/bin/types.mjs +1 -0
  84. package/bin/util/path-segment.d.mts +2 -0
  85. package/bin/util/path-segment.mjs +2 -0
  86. package/bin/util/pool.d.mts +2 -0
  87. package/bin/util/pool.mjs +18 -0
  88. package/package.json +1 -1
  89. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  90. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  91. package/plugins/pr-shepherd/.mcp.json +1 -1
@@ -1,24 +1,40 @@
1
1
  /* eslint-disable max-lines */
2
2
  import { readFileSync, statSync } from "node:fs";
3
- import { join, dirname } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { parse } from "yaml";
6
6
  import builtins from "../config.json" with { type: "json" };
7
7
  import { getEffectiveCwd } from "../execution-context.mjs";
8
8
  const MINIMIZE_COMMENTS_POLICIES = ["all", "bots", "users", "none"];
9
9
  const RC_FILENAME = ".pr-shepherdrc.yml";
10
- function findRcFile(startDir) {
11
- const home = homedir();
12
- let current = startDir;
13
- while (true) {
14
- const candidate = join(current, RC_FILENAME);
10
+ /**
11
+ * Collect `.pr-shepherdrc.yml` files from `startDir` toward `$HOME` (closest first).
12
+ * `$HOME/.pr-shepherdrc.yml` is always included when it exists, even if cwd is
13
+ * outside the home directory. The walk never includes the filesystem root, so a
14
+ * `/.pr-shepherdrc.yml` cannot override the user-level file.
15
+ */
16
+ function collectRcFiles(startDir) {
17
+ const home = resolve(homedir());
18
+ const seen = new Set();
19
+ const files = [];
20
+ const add = (dir) => {
21
+ const candidate = resolve(join(dir, RC_FILENAME));
22
+ if (seen.has(candidate))
23
+ return;
15
24
  if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) {
16
- return candidate;
25
+ seen.add(candidate);
26
+ files.push(candidate);
17
27
  }
18
- if (current === home || current === dirname(current))
19
- return null;
28
+ };
29
+ let current = resolve(startDir);
30
+ while (current !== dirname(current)) {
31
+ add(current);
32
+ if (current === home)
33
+ break;
20
34
  current = dirname(current);
21
35
  }
36
+ add(home);
37
+ return files;
22
38
  }
23
39
  function deepMerge(base, override) {
24
40
  const result = { ...base };
@@ -125,30 +141,56 @@ function warnUnknownConfigKeys(config) {
125
141
  }
126
142
  const defaults = builtins;
127
143
  const configCache = new Map();
144
+ function stripDeprecatedActionKeys(parsed) {
145
+ const rawActions = parsed.actions;
146
+ if (rawActions === null || typeof rawActions !== "object" || Array.isArray(rawActions))
147
+ return;
148
+ const actions = rawActions;
149
+ for (const key of ["autoResolveOutdated", "commitSuggestions"]) {
150
+ if (key in actions) {
151
+ process.stderr.write(`pr-shepherd: config: actions.${key} is deprecated and ignored.\n`);
152
+ delete actions[key];
153
+ }
154
+ }
155
+ }
156
+ function readRcFile(rcPath) {
157
+ try {
158
+ const raw = readFileSync(rcPath, "utf8");
159
+ const parsed = (parse(raw) ?? {});
160
+ stripDeprecatedActionKeys(parsed);
161
+ warnUnknownConfigKeys(parsed);
162
+ return parsed;
163
+ }
164
+ catch (err) {
165
+ process.stderr.write(`pr-shepherd: failed to parse ${rcPath}: ${err instanceof Error ? err.message : String(err)}\n`);
166
+ return null;
167
+ }
168
+ }
128
169
  export function loadConfig() {
129
170
  const cwd = getEffectiveCwd();
130
171
  if (configCache.has(cwd))
131
172
  return configCache.get(cwd);
132
- const rcPath = findRcFile(cwd);
133
- if (!rcPath) {
173
+ const rcPaths = collectRcFiles(cwd);
174
+ if (rcPaths.length === 0) {
134
175
  configCache.set(cwd, defaults);
135
176
  return defaults;
136
177
  }
137
178
  try {
138
- const raw = readFileSync(rcPath, "utf8");
139
- const parsed = (parse(raw) ?? {});
140
- const rawActions = parsed.actions;
141
- if (rawActions !== null && typeof rawActions === "object" && !Array.isArray(rawActions)) {
142
- const actions = rawActions;
143
- for (const key of ["autoResolveOutdated", "commitSuggestions"]) {
144
- if (key in actions) {
145
- process.stderr.write(`pr-shepherd: config: actions.${key} is deprecated and ignored.\n`);
146
- delete actions[key];
147
- }
148
- }
179
+ // Farthest file first so closer directories override, ESLint-style.
180
+ let overlay = {};
181
+ let loaded = false;
182
+ for (const rcPath of [...rcPaths].reverse()) {
183
+ const parsed = readRcFile(rcPath);
184
+ if (parsed === null)
185
+ continue;
186
+ overlay = deepMerge(overlay, parsed);
187
+ loaded = true;
149
188
  }
150
- warnUnknownConfigKeys(parsed);
151
- const config = deepMerge(defaults, parsed);
189
+ if (!loaded) {
190
+ configCache.set(cwd, defaults);
191
+ return defaults;
192
+ }
193
+ const config = deepMerge(structuredClone(defaults), overlay);
152
194
  config.botUsernames = parseBotUsernames(config.botUsernames);
153
195
  config.ignoreChecks = parseIgnoreChecks(config.ignoreChecks);
154
196
  config.actions.neverCancelRuns = parseNeverCancelRuns(config.actions.neverCancelRuns);
@@ -157,7 +199,7 @@ export function loadConfig() {
157
199
  return config;
158
200
  }
159
201
  catch (err) {
160
- process.stderr.write(`pr-shepherd: failed to parse ${rcPath}: ${err instanceof Error ? err.message : String(err)}\n`);
202
+ process.stderr.write(`pr-shepherd: failed to parse ${rcPaths[0]}: ${err instanceof Error ? err.message : String(err)}\n`);
161
203
  const fallback = { ...defaults };
162
204
  configCache.set(cwd, fallback);
163
205
  return fallback;
@@ -0,0 +1,45 @@
1
+ import type { RawContextNode, RawComment, RawPr, RawReview, RawReviewSummary, RawThread } from "./batch-raw-types.mts";
2
+ export interface RawPageResponse {
3
+ repository: {
4
+ pullRequest: {
5
+ reviewThreads?: RawPr["reviewThreads"];
6
+ comments?: RawPr["comments"];
7
+ changesRequestedReviews?: RawPr["changesRequestedReviews"];
8
+ reviewSummaries?: RawPr["reviewSummaries"];
9
+ approvedReviews?: RawPr["approvedReviews"];
10
+ commits?: RawPr["commits"];
11
+ } | null;
12
+ };
13
+ }
14
+ export declare function backwardCursor(conn: {
15
+ pageInfo: {
16
+ hasPreviousPage: boolean;
17
+ startCursor: string | null;
18
+ };
19
+ } | undefined): string | undefined;
20
+ export declare function forwardCursor(conn: {
21
+ pageInfo: {
22
+ hasNextPage: boolean;
23
+ endCursor: string | null;
24
+ };
25
+ } | undefined): string | undefined;
26
+ export interface PageCursors {
27
+ threads?: string;
28
+ comments?: string;
29
+ changesRequested?: string;
30
+ reviewSummaries?: string;
31
+ approvedReviews?: string;
32
+ checks?: string;
33
+ }
34
+ export interface PageAccumulator {
35
+ threads: RawThread[];
36
+ comments: RawComment[];
37
+ changesRequested: RawReview[];
38
+ reviewSummaries: RawReviewSummary[];
39
+ approvedReviews: RawReviewSummary[];
40
+ checks: RawContextNode[];
41
+ }
42
+ export declare function applyIncludedPage(dest: PageAccumulator, pr2: NonNullable<RawPageResponse["repository"]["pullRequest"]>, cursors: PageCursors, firstOid: string | undefined, pageCount: number, pr: number): {
43
+ cursors: PageCursors;
44
+ pageCount: number;
45
+ };
@@ -0,0 +1,63 @@
1
+ import { requireContextNodes } from "./batch-response.mjs";
2
+ export function backwardCursor(conn) {
3
+ if (conn?.pageInfo.hasPreviousPage === true && conn.pageInfo.startCursor) {
4
+ return conn.pageInfo.startCursor;
5
+ }
6
+ return undefined;
7
+ }
8
+ export function forwardCursor(conn) {
9
+ if (conn?.pageInfo.hasNextPage === true && conn.pageInfo.endCursor) {
10
+ return conn.pageInfo.endCursor;
11
+ }
12
+ return undefined;
13
+ }
14
+ function prependConnection(dest, conn, pr) {
15
+ if (!conn)
16
+ throw new Error(`PR #${pr} not found`);
17
+ dest.unshift(...conn.nodes);
18
+ }
19
+ function takeCheckPage(pr2, firstOid, pageCount) {
20
+ if (!pr2.commits?.nodes[0]?.commit.statusCheckRollup) {
21
+ throw new Error(`Check-context pagination interrupted: statusCheckRollup disappeared on page ${pageCount + 1} (possible force-push race). Retry after the push stabilizes.`);
22
+ }
23
+ const currentOid = pr2.commits.nodes[0]?.commit.oid;
24
+ if (firstOid !== undefined && currentOid !== undefined && currentOid !== firstOid) {
25
+ throw new Error(`Check-context pagination interrupted: head commit changed from ${firstOid} to ${currentOid} between pages (force-push race). Retry.`);
26
+ }
27
+ const ctxs = pr2.commits.nodes[0]?.commit.statusCheckRollup?.contexts;
28
+ if (!ctxs) {
29
+ return { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [] };
30
+ }
31
+ return { ...ctxs, nodes: requireContextNodes(ctxs.nodes) };
32
+ }
33
+ export function applyIncludedPage(dest, pr2, cursors, firstOid, pageCount, pr) {
34
+ const next = {};
35
+ let nextPageCount = pageCount;
36
+ if (cursors.threads !== undefined) {
37
+ prependConnection(dest.threads, pr2.reviewThreads, pr);
38
+ next.threads = backwardCursor(pr2.reviewThreads);
39
+ }
40
+ if (cursors.comments !== undefined) {
41
+ prependConnection(dest.comments, pr2.comments, pr);
42
+ next.comments = backwardCursor(pr2.comments);
43
+ }
44
+ if (cursors.changesRequested !== undefined) {
45
+ prependConnection(dest.changesRequested, pr2.changesRequestedReviews, pr);
46
+ next.changesRequested = backwardCursor(pr2.changesRequestedReviews);
47
+ }
48
+ if (cursors.reviewSummaries !== undefined) {
49
+ prependConnection(dest.reviewSummaries, pr2.reviewSummaries, pr);
50
+ next.reviewSummaries = backwardCursor(pr2.reviewSummaries);
51
+ }
52
+ if (cursors.approvedReviews !== undefined) {
53
+ prependConnection(dest.approvedReviews, pr2.approvedReviews, pr);
54
+ next.approvedReviews = backwardCursor(pr2.approvedReviews);
55
+ }
56
+ if (cursors.checks !== undefined) {
57
+ nextPageCount++;
58
+ const extra = takeCheckPage(pr2, firstOid, nextPageCount);
59
+ dest.checks.push(...extra.nodes);
60
+ next.checks = forwardCursor(extra);
61
+ }
62
+ return { cursors: next, pageCount: nextPageCount };
63
+ }
@@ -0,0 +1,14 @@
1
+ import { type RateLimitInfo, type RepoInfo } from "./client.mts";
2
+ import { type PageAccumulator } from "./batch-page-helpers.mts";
3
+ import type { RawPr } from "./batch-raw-types.mts";
4
+ export interface PaginatedBatchNodes extends PageAccumulator {
5
+ rateLimit?: RateLimitInfo;
6
+ }
7
+ export interface PaginateBatchOptions {
8
+ paginateApprovedReviews?: boolean;
9
+ }
10
+ /**
11
+ * Fetch remaining connection pages with a slim `@include` query. Independent
12
+ * connections that still have cursors are combined into one request per round.
13
+ */
14
+ export declare function paginateBatchConnections(pr: number, repo: RepoInfo, raw: RawPr, opts: PaginateBatchOptions, initialRateLimit?: RateLimitInfo): Promise<PaginatedBatchNodes>;
@@ -0,0 +1,62 @@
1
+ import { graphqlWithRateLimit } from "./client.mjs";
2
+ import { GitHubRequestError } from "./errors.mjs";
3
+ import { BATCH_PR_PAGE_QUERY } from "./queries.mjs";
4
+ import { requireContextNodes } from "./batch-response.mjs";
5
+ import { applyIncludedPage, backwardCursor, forwardCursor, } from "./batch-page-helpers.mjs";
6
+ /**
7
+ * Fetch remaining connection pages with a slim `@include` query. Independent
8
+ * connections that still have cursors are combined into one request per round.
9
+ */
10
+ export async function paginateBatchConnections(pr, repo, raw, opts, initialRateLimit) {
11
+ const dest = {
12
+ threads: [...raw.reviewThreads.nodes],
13
+ comments: [...raw.comments.nodes],
14
+ changesRequested: [...raw.changesRequestedReviews.nodes],
15
+ reviewSummaries: [...raw.reviewSummaries.nodes],
16
+ approvedReviews: [...raw.approvedReviews.nodes],
17
+ checks: [
18
+ ...requireContextNodes(raw.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? []),
19
+ ],
20
+ };
21
+ const firstOid = raw.commits.nodes[0]?.commit.oid;
22
+ let cursors = {
23
+ threads: backwardCursor(raw.reviewThreads),
24
+ comments: backwardCursor(raw.comments),
25
+ changesRequested: backwardCursor(raw.changesRequestedReviews),
26
+ reviewSummaries: backwardCursor(raw.reviewSummaries),
27
+ approvedReviews: opts.paginateApprovedReviews === true ? backwardCursor(raw.approvedReviews) : undefined,
28
+ checks: forwardCursor(raw.commits.nodes[0]?.commit.statusCheckRollup?.contexts),
29
+ };
30
+ let rateLimit = initialRateLimit;
31
+ let pageCount = 0;
32
+ while (Object.values(cursors).some((cursor) => cursor !== undefined)) {
33
+ if (rateLimit?.remaining === 0) {
34
+ throw new GitHubRequestError("GitHub GraphQL rate limit remaining is 0; pagination incomplete", { status: 403, rateLimit });
35
+ }
36
+ const res = await graphqlWithRateLimit(BATCH_PR_PAGE_QUERY, {
37
+ owner: repo.owner,
38
+ repo: repo.name,
39
+ pr,
40
+ includeThreads: cursors.threads !== undefined,
41
+ threadsCursor: cursors.threads ?? null,
42
+ includeComments: cursors.comments !== undefined,
43
+ commentsCursor: cursors.comments ?? null,
44
+ includeChangesRequested: cursors.changesRequested !== undefined,
45
+ changesRequestedCursor: cursors.changesRequested ?? null,
46
+ includeReviewSummaries: cursors.reviewSummaries !== undefined,
47
+ reviewSummariesCursor: cursors.reviewSummaries ?? null,
48
+ includeApprovedReviews: cursors.approvedReviews !== undefined,
49
+ approvedReviewsCursor: cursors.approvedReviews ?? null,
50
+ includeChecks: cursors.checks !== undefined,
51
+ checksCursor: cursors.checks ?? null,
52
+ });
53
+ rateLimit = res.rateLimit ?? rateLimit;
54
+ const pr2 = res.data.repository.pullRequest;
55
+ if (!pr2)
56
+ throw new Error(`PR #${pr} not found`);
57
+ const applied = applyIncludedPage(dest, pr2, cursors, firstOid, pageCount, pr);
58
+ cursors = applied.cursors;
59
+ pageCount = applied.pageCount;
60
+ }
61
+ return { ...dest, rateLimit };
62
+ }
@@ -0,0 +1,4 @@
1
+ import type { CheckRun } from "../types.mts";
2
+ import type { RawPr } from "./batch-raw-types.mts";
3
+ export declare function parseCheckSuitesComplete(raw: RawPr): boolean;
4
+ export declare function parseSuiteStartupFailures(raw: RawPr): CheckRun[];
@@ -0,0 +1,25 @@
1
+ export function parseCheckSuitesComplete(raw) {
2
+ return raw.commits.nodes[0]?.commit.checkSuites?.pageInfo.hasNextPage === false;
3
+ }
4
+ export function parseSuiteStartupFailures(raw) {
5
+ const suites = raw.commits.nodes[0]?.commit.checkSuites;
6
+ if (!suites)
7
+ return [];
8
+ return suites.nodes.flatMap((node) => {
9
+ if (node.conclusion !== "STARTUP_FAILURE" || node.workflowRun == null)
10
+ return [];
11
+ const runId = node.workflowRun.databaseId != null ? String(node.workflowRun.databaseId) : null;
12
+ const name = node.workflowRun.workflow?.name?.trim() || (runId ? `workflow run ${runId}` : "workflow run");
13
+ return [
14
+ {
15
+ name,
16
+ status: "COMPLETED",
17
+ conclusion: "STARTUP_FAILURE",
18
+ source: "startup_failure",
19
+ detailsUrl: node.workflowRun.url ?? "",
20
+ event: node.workflowRun.event,
21
+ runId,
22
+ },
23
+ ];
24
+ });
25
+ }
@@ -45,6 +45,7 @@ export function mapCheckRunNode(node) {
45
45
  ...(completedAtUnix !== undefined && { completedAtUnix }),
46
46
  ...(updatedAtUnix !== undefined && { updatedAtUnix }),
47
47
  ...(summary !== undefined && { summary }),
48
+ ...((node.annotations?.nodes.length ?? 0) > 0 && { hasAnnotations: true }),
48
49
  };
49
50
  }
50
51
  function extractCheckRunSummary(title, summary) {
@@ -0,0 +1,6 @@
1
+ import type { BranchRules, MergeQueueEntryStatus, StackStatus } from "../types.mts";
2
+ import type { RawBaseRef, RawPrMergeFields } from "./batch-raw-rules.mts";
3
+ export declare const EMPTY_BRANCH_RULES: BranchRules;
4
+ export declare function parseBranchRules(baseRef: RawBaseRef | null | undefined): BranchRules;
5
+ export declare function parseMergeQueueEntry(raw: RawPrMergeFields["mergeQueueEntry"]): MergeQueueEntryStatus | null;
6
+ export declare function parseStack(raw: RawPrMergeFields): StackStatus | null;
@@ -0,0 +1,122 @@
1
+ export const EMPTY_BRANCH_RULES = {
2
+ requiredApprovingReviewCount: 0,
3
+ requiresConversationResolution: false,
4
+ requiresCodeOwnerReviews: false,
5
+ requiresLastPushApproval: false,
6
+ requiresCommitSignatures: false,
7
+ requiresLinearHistory: false,
8
+ requiresStrictStatusChecks: false,
9
+ requiredStatusCheckContexts: [],
10
+ requiredDeploymentEnvironments: [],
11
+ requiresMergeQueue: false,
12
+ requiresWorkflows: false,
13
+ requiresCodeScanning: false,
14
+ };
15
+ export function parseBranchRules(baseRef) {
16
+ const rules = {
17
+ ...EMPTY_BRANCH_RULES,
18
+ requiredStatusCheckContexts: [],
19
+ requiredDeploymentEnvironments: [],
20
+ };
21
+ if (!baseRef)
22
+ return rules;
23
+ foldClassicProtection(rules, baseRef.branchProtectionRule);
24
+ for (const node of baseRef.rules?.nodes ?? [])
25
+ foldRulesetRule(rules, node);
26
+ return rules;
27
+ }
28
+ export function parseMergeQueueEntry(raw) {
29
+ if (!raw)
30
+ return null;
31
+ return {
32
+ position: raw.position,
33
+ state: raw.state,
34
+ estimatedTimeToMerge: raw.estimatedTimeToMerge,
35
+ };
36
+ }
37
+ export function parseStack(raw) {
38
+ if (!raw.stack)
39
+ return null;
40
+ return {
41
+ number: raw.stack.number,
42
+ size: raw.stack.size,
43
+ position: raw.stackEntry?.position ?? 0,
44
+ baseRefName: raw.stack.baseRefName,
45
+ };
46
+ }
47
+ function foldClassicProtection(rules, bp) {
48
+ if (!bp)
49
+ return;
50
+ if (bp.requiresApprovingReviews) {
51
+ rules.requiredApprovingReviewCount = Math.max(rules.requiredApprovingReviewCount, bp.requiredApprovingReviewCount ?? 0);
52
+ }
53
+ if (bp.requiresConversationResolution)
54
+ rules.requiresConversationResolution = true;
55
+ if (bp.requiresCodeOwnerReviews)
56
+ rules.requiresCodeOwnerReviews = true;
57
+ if (bp.requireLastPushApproval)
58
+ rules.requiresLastPushApproval = true;
59
+ if (bp.requiresCommitSignatures)
60
+ rules.requiresCommitSignatures = true;
61
+ if (bp.requiresLinearHistory)
62
+ rules.requiresLinearHistory = true;
63
+ if (bp.requiresStrictStatusChecks)
64
+ rules.requiresStrictStatusChecks = true;
65
+ if (bp.requiresStatusChecks) {
66
+ mergeUnique(rules.requiredStatusCheckContexts, bp.requiredStatusCheckContexts ?? []);
67
+ }
68
+ if (bp.requiresDeployments) {
69
+ mergeUnique(rules.requiredDeploymentEnvironments, bp.requiredDeploymentEnvironments ?? []);
70
+ }
71
+ }
72
+ function foldRulesetRule(rules, node) {
73
+ const p = node.parameters;
74
+ switch (node.type) {
75
+ case "PULL_REQUEST":
76
+ if (p?.requiredApprovingReviewCount != null) {
77
+ rules.requiredApprovingReviewCount = Math.max(rules.requiredApprovingReviewCount, p.requiredApprovingReviewCount);
78
+ }
79
+ if (p?.requiredReviewThreadResolution)
80
+ rules.requiresConversationResolution = true;
81
+ if (p?.requireCodeOwnerReview)
82
+ rules.requiresCodeOwnerReviews = true;
83
+ if (p?.requireLastPushApproval)
84
+ rules.requiresLastPushApproval = true;
85
+ break;
86
+ case "REQUIRED_REVIEW_THREAD_RESOLUTION":
87
+ rules.requiresConversationResolution = true;
88
+ break;
89
+ case "REQUIRED_STATUS_CHECKS":
90
+ if (p?.strictRequiredStatusChecksPolicy)
91
+ rules.requiresStrictStatusChecks = true;
92
+ mergeUnique(rules.requiredStatusCheckContexts, (p?.requiredStatusChecks ?? []).map((c) => c.context));
93
+ break;
94
+ case "REQUIRED_SIGNATURES":
95
+ rules.requiresCommitSignatures = true;
96
+ break;
97
+ case "REQUIRED_LINEAR_HISTORY":
98
+ rules.requiresLinearHistory = true;
99
+ break;
100
+ case "REQUIRED_DEPLOYMENTS":
101
+ mergeUnique(rules.requiredDeploymentEnvironments, p?.requiredDeploymentEnvironments ?? []);
102
+ break;
103
+ case "MERGE_QUEUE":
104
+ rules.requiresMergeQueue = true;
105
+ break;
106
+ case "WORKFLOWS":
107
+ case "REQUIRED_WORKFLOW_STATUS_CHECKS":
108
+ rules.requiresWorkflows = true;
109
+ break;
110
+ case "CODE_SCANNING":
111
+ rules.requiresCodeScanning = true;
112
+ break;
113
+ default:
114
+ break;
115
+ }
116
+ }
117
+ function mergeUnique(target, values) {
118
+ for (const value of values) {
119
+ if (value && !target.includes(value))
120
+ target.push(value);
121
+ }
122
+ }
@@ -1,6 +1,7 @@
1
1
  import { mapAuthorType, parseCreatedAt, mapStatusContextState, latestApprovedLogins, isReviewStale, mapCheckRunNode, } from "./batch-parser-helpers.mjs";
2
2
  import { buildPrActivitySummary } from "./activity.mjs";
3
3
  import { parseBranchProtection } from "./branch-protection.mjs";
4
+ import { parseBranchRules, parseMergeQueueEntry, parseStack } from "./batch-parsers-rules.mjs";
4
5
  function parseReviewNode(r) {
5
6
  const base = {
6
7
  id: r.id,
@@ -130,6 +131,11 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
130
131
  approvedReviews,
131
132
  checks,
132
133
  branchProtection: parseBranchProtection(raw),
134
+ branchRules: parseBranchRules(raw.baseRef),
135
+ isInMergeQueue: raw.isInMergeQueue ?? false,
136
+ isMergeQueueEnabled: raw.isMergeQueueEnabled ?? false,
137
+ mergeQueueEntry: parseMergeQueueEntry(raw.mergeQueueEntry),
138
+ stack: parseStack(raw),
133
139
  activity: buildPrActivitySummary(raw, comments, reviewThreads, reviewSummaries, allChangesRequestedReviews, approvedReviews),
134
140
  };
135
141
  }
@@ -0,0 +1,59 @@
1
+ export interface RawBranchProtectionRule {
2
+ requiresApprovingReviews: boolean;
3
+ requiredApprovingReviewCount: number;
4
+ requiresConversationResolution: boolean;
5
+ requiresStatusChecks: boolean;
6
+ requiredStatusCheckContexts: string[] | null;
7
+ requiresCodeOwnerReviews?: boolean;
8
+ requireLastPushApproval?: boolean;
9
+ requiresCommitSignatures?: boolean;
10
+ requiresLinearHistory?: boolean;
11
+ requiresStrictStatusChecks?: boolean;
12
+ requiresDeployments?: boolean;
13
+ requiredDeploymentEnvironments?: string[] | null;
14
+ }
15
+ interface RawMergeQueueEntry {
16
+ position: number;
17
+ state: string;
18
+ estimatedTimeToMerge: number | null;
19
+ }
20
+ interface RawStack {
21
+ number: number;
22
+ size: number;
23
+ baseRefName: string;
24
+ }
25
+ export interface RawRepositoryRule {
26
+ type: string;
27
+ parameters: RawRuleParameters | null;
28
+ }
29
+ interface RawRuleParameters {
30
+ requiredApprovingReviewCount?: number;
31
+ requiredReviewThreadResolution?: boolean;
32
+ requireCodeOwnerReview?: boolean;
33
+ requireLastPushApproval?: boolean;
34
+ strictRequiredStatusChecksPolicy?: boolean;
35
+ requiredStatusChecks?: Array<{
36
+ context: string;
37
+ }>;
38
+ requiredDeploymentEnvironments?: string[];
39
+ codeScanningTools?: Array<{
40
+ tool: string;
41
+ }>;
42
+ }
43
+ export interface RawBaseRef {
44
+ branchProtectionRule: RawBranchProtectionRule | null;
45
+ rules: {
46
+ nodes: RawRepositoryRule[];
47
+ } | null;
48
+ }
49
+ export interface RawPrMergeFields {
50
+ isInMergeQueue?: boolean;
51
+ isMergeQueueEnabled?: boolean;
52
+ mergeQueueEntry?: RawMergeQueueEntry | null;
53
+ stack?: RawStack | null;
54
+ stackEntry?: {
55
+ position: number;
56
+ } | null;
57
+ baseRef?: RawBaseRef | null;
58
+ }
59
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,10 +1,11 @@
1
1
  import type { CommentAuthorAssociation } from "../types/github.mts";
2
+ import type { RawPrMergeFields } from "./batch-raw-rules.mts";
2
3
  export interface RawBatchResponse {
3
4
  repository: {
4
5
  pullRequest: RawPr | null;
5
6
  } | null;
6
7
  }
7
- export interface RawPr {
8
+ export interface RawPr extends RawPrMergeFields {
8
9
  id: string;
9
10
  number: number;
10
11
  state: string;
@@ -18,9 +19,7 @@ export interface RawPr {
18
19
  nameWithOwner: string;
19
20
  } | null;
20
21
  baseRefName: string;
21
- baseRef: {
22
- branchProtectionRule: RawBranchProtectionRule | null;
23
- } | null;
22
+ baseRef: import("./batch-raw-rules.mts").RawBaseRef | null;
24
23
  reviewRequests: {
25
24
  nodes: Array<{
26
25
  requestedReviewer: {
@@ -79,6 +78,7 @@ export interface RawPr {
79
78
  commit: {
80
79
  oid: string;
81
80
  committedDate?: string;
81
+ checkSuites?: RawCheckSuites;
82
82
  statusCheckRollup: {
83
83
  contexts: {
84
84
  pageInfo: {
@@ -92,6 +92,22 @@ export interface RawPr {
92
92
  }>;
93
93
  };
94
94
  }
95
+ interface RawCheckSuites {
96
+ pageInfo: {
97
+ hasNextPage: boolean;
98
+ };
99
+ nodes: Array<{
100
+ conclusion: string | null;
101
+ workflowRun: {
102
+ databaseId: number | null;
103
+ event: string | null;
104
+ url: string | null;
105
+ workflow: {
106
+ name: string | null;
107
+ } | null;
108
+ } | null;
109
+ }>;
110
+ }
95
111
  interface RawAuthor {
96
112
  __typename?: string;
97
113
  login: string;
@@ -165,13 +181,6 @@ export interface RawReviewSummary {
165
181
  body: string;
166
182
  createdAt?: string;
167
183
  }
168
- interface RawBranchProtectionRule {
169
- requiresApprovingReviews: boolean;
170
- requiredApprovingReviewCount: number;
171
- requiresConversationResolution: boolean;
172
- requiresStatusChecks: boolean;
173
- requiredStatusCheckContexts: string[] | null;
174
- }
175
184
  export type RawContextNode = {
176
185
  __typename: "CheckRun";
177
186
  id: string;
@@ -183,6 +192,11 @@ export type RawContextNode = {
183
192
  startedAt?: string | null;
184
193
  title: string | null;
185
194
  summary: string | null;
195
+ annotations?: {
196
+ nodes: Array<{
197
+ message: string;
198
+ }>;
199
+ };
186
200
  checkSuite: {
187
201
  createdAt?: string;
188
202
  updatedAt?: string;
@@ -3,6 +3,8 @@ import type { BatchPrData } from "../types.mts";
3
3
  interface BatchResult {
4
4
  data: BatchPrData;
5
5
  rateLimit?: RateLimitInfo;
6
+ /** True when GraphQL returned a complete CheckSuite page; skip REST startup-failure fetch. */
7
+ checkSuitesComplete?: boolean;
6
8
  }
7
9
  interface FetchPrBatchOptions {
8
10
  /**