pr-shepherd 0.46.8 → 0.47.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 (37) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/api.d.mts +1 -1
  3. package/bin/commands/check-fingerprint.d.mts +8 -0
  4. package/bin/commands/check-fingerprint.mjs +69 -0
  5. package/bin/commands/check.d.mts +1 -0
  6. package/bin/commands/check.mjs +23 -3
  7. package/bin/commands/iterate/base.mjs +1 -0
  8. package/bin/commands/iterate/mark-ready.mjs +8 -0
  9. package/bin/commands/poll-progress.d.mts +11 -0
  10. package/bin/commands/poll-progress.mjs +52 -0
  11. package/bin/commands/poll-quota.d.mts +9 -0
  12. package/bin/commands/poll-quota.mjs +36 -0
  13. package/bin/commands/poll.mjs +64 -65
  14. package/bin/config/load.mjs +11 -1
  15. package/bin/github/batch-raw-rules.d.mts +4 -1
  16. package/bin/github/batch-raw-types.d.mts +14 -0
  17. package/bin/github/batch.d.mts +2 -0
  18. package/bin/github/batch.mjs +2 -0
  19. package/bin/github/fingerprint-fields.d.mts +58 -0
  20. package/bin/github/fingerprint-fields.mjs +39 -0
  21. package/bin/github/fingerprint.d.mts +35 -0
  22. package/bin/github/fingerprint.mjs +67 -0
  23. package/bin/github/gql/batch-pr.gql +25 -120
  24. package/bin/github/gql/commit-check-suites.gql +19 -0
  25. package/bin/github/gql/pr-fingerprint.gql +75 -0
  26. package/bin/github/gql/pr-merge-policy.gql +49 -0
  27. package/bin/github/merge-queue-checks.mjs +57 -30
  28. package/bin/github/queries.d.mts +2 -0
  29. package/bin/github/queries.mjs +4 -1
  30. package/bin/state/pr-fingerprint.d.mts +20 -0
  31. package/bin/state/pr-fingerprint.mjs +90 -0
  32. package/bin/types/iterate.d.mts +9 -0
  33. package/bin/types/report.d.mts +2 -0
  34. package/package.json +2 -2
  35. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  36. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  37. package/plugins/pr-shepherd/.mcp.json +1 -1
@@ -0,0 +1,58 @@
1
+ import type { RawBaseRef } from "./batch-raw-rules.mts";
2
+ export interface FingerprintSuites {
3
+ pageInfo?: {
4
+ hasNextPage: boolean;
5
+ };
6
+ nodes?: Array<{
7
+ id?: string;
8
+ conclusion: string | null;
9
+ workflowRun?: {
10
+ databaseId: number | null;
11
+ } | null;
12
+ }>;
13
+ }
14
+ export interface FingerprintComment {
15
+ id: string;
16
+ updatedAt?: string;
17
+ }
18
+ export declare function suiteFingerprint(suites: FingerprintSuites | undefined): {
19
+ checkSuiteConclusions: string;
20
+ checkSuitesComplete: boolean;
21
+ };
22
+ export declare function mergePolicyFingerprint(raw: {
23
+ isMergeQueueEnabled?: boolean;
24
+ baseRef?: RawBaseRef | null;
25
+ }): string;
26
+ export declare function commentRevisions(nodes: FingerprintComment[]): string;
27
+ export declare function threadCommentRevisions(nodes: Array<{
28
+ id: string;
29
+ comments?: {
30
+ totalCount?: number;
31
+ nodes: Array<{
32
+ id: string;
33
+ updatedAt?: string;
34
+ }>;
35
+ };
36
+ }>): string;
37
+ export declare function hasMultiCommentThreads(nodes: Array<{
38
+ comments?: {
39
+ totalCount?: number;
40
+ };
41
+ }>): boolean;
42
+ export declare function rulesComplete(baseRef?: {
43
+ rules?: {
44
+ pageInfo?: {
45
+ hasNextPage: boolean;
46
+ };
47
+ } | null;
48
+ } | null): boolean;
49
+ export declare function stackKey(raw: {
50
+ stack?: {
51
+ number: number;
52
+ size: number;
53
+ baseRefName: string;
54
+ } | null;
55
+ stackEntry?: {
56
+ position: number;
57
+ } | null;
58
+ }): string;
@@ -0,0 +1,39 @@
1
+ import { parseBranchRules } from "./batch-parsers-rules.mjs";
2
+ export function suiteFingerprint(suites) {
3
+ if (suites === undefined)
4
+ return { checkSuiteConclusions: "", checkSuitesComplete: false };
5
+ return {
6
+ checkSuiteConclusions: (suites.nodes ?? [])
7
+ .map((node) => `${node.id ?? node.workflowRun?.databaseId ?? ""}:${node.conclusion ?? ""}`)
8
+ .join(","),
9
+ checkSuitesComplete: suites.pageInfo?.hasNextPage === false,
10
+ };
11
+ }
12
+ export function mergePolicyFingerprint(raw) {
13
+ return JSON.stringify({
14
+ isMergeQueueEnabled: Boolean(raw.isMergeQueueEnabled),
15
+ rules: parseBranchRules(raw.baseRef),
16
+ });
17
+ }
18
+ export function commentRevisions(nodes) {
19
+ return nodes.map((node) => `${node.id}:${node.updatedAt ?? ""}`).join(",");
20
+ }
21
+ export function threadCommentRevisions(nodes) {
22
+ return nodes
23
+ .map((thread) => {
24
+ const last = thread.comments?.nodes.at(-1);
25
+ return `${thread.id}:${last?.id ?? ""}:${last?.updatedAt ?? ""}`;
26
+ })
27
+ .join(",");
28
+ }
29
+ export function hasMultiCommentThreads(nodes) {
30
+ return nodes.some((thread) => (thread.comments?.totalCount ?? 0) > 1);
31
+ }
32
+ export function rulesComplete(baseRef) {
33
+ return baseRef?.rules?.pageInfo?.hasNextPage !== true;
34
+ }
35
+ export function stackKey(raw) {
36
+ if (!raw.stack)
37
+ return "";
38
+ return `${raw.stack.number}:${raw.stack.size}:${raw.stackEntry?.position ?? 0}:${raw.stack.baseRefName}`;
39
+ }
@@ -0,0 +1,35 @@
1
+ import type { RepoInfo } from "./client.mts";
2
+ import type { RawPr } from "./batch-raw-types.mts";
3
+ export interface PrFingerprint {
4
+ headRefOid: string;
5
+ updatedAt: string;
6
+ state: string;
7
+ isDraft: boolean;
8
+ mergeable: string;
9
+ mergeStateStatus: string;
10
+ reviewDecision: string | null;
11
+ isInMergeQueue: boolean;
12
+ isMergeQueueEnabled: boolean;
13
+ mergePolicy: string;
14
+ commentCount: number;
15
+ commentRevisions: string;
16
+ threadCount: number;
17
+ reviewCount: number;
18
+ reviewRevisions: string;
19
+ latestCommentId: string | null;
20
+ latestThreadId: string | null;
21
+ latestReviewId: string | null;
22
+ checkRollupState: string | null;
23
+ checkSuiteConclusions: string;
24
+ checkSuitesComplete: boolean;
25
+ viewerCanUpdate: boolean;
26
+ viewerPermission: string | null;
27
+ viewerLogin: string | null;
28
+ stackKey: string;
29
+ threadCommentRevisions: string;
30
+ rulesComplete: boolean;
31
+ hasMultiCommentThreads: boolean;
32
+ }
33
+ export declare function fingerprintFromRaw(raw: RawPr, viewerPermission?: string | null, viewerLogin?: string | null): PrFingerprint;
34
+ export declare function fingerprintsEqual(left: PrFingerprint, right: PrFingerprint): boolean;
35
+ export declare function fetchPrFingerprint(pr: number, repo: RepoInfo): Promise<PrFingerprint>;
@@ -0,0 +1,67 @@
1
+ import { graphqlWithRateLimit } from "./client.mjs";
2
+ import { PR_FINGERPRINT_QUERY } from "./queries.mjs";
3
+ import { GitHubRequestError } from "./errors.mjs";
4
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
5
+ import { commentRevisions, mergePolicyFingerprint, rulesComplete, stackKey, suiteFingerprint, hasMultiCommentThreads, threadCommentRevisions, } from "./fingerprint-fields.mjs";
6
+ function coreFingerprint(raw, counts, viewer) {
7
+ return {
8
+ headRefOid: raw.headRefOid,
9
+ updatedAt: raw.updatedAt ?? "",
10
+ state: raw.state,
11
+ isDraft: raw.isDraft,
12
+ mergeable: raw.mergeable,
13
+ mergeStateStatus: raw.mergeStateStatus,
14
+ reviewDecision: raw.reviewDecision,
15
+ isInMergeQueue: Boolean(raw.isInMergeQueue),
16
+ isMergeQueueEnabled: Boolean(raw.isMergeQueueEnabled),
17
+ mergePolicy: mergePolicyFingerprint(raw),
18
+ commentCount: raw.comments.totalCount ?? raw.comments.nodes.length,
19
+ commentRevisions: commentRevisions(raw.comments.nodes),
20
+ threadCount: raw.reviewThreads.totalCount ?? raw.reviewThreads.nodes.length,
21
+ reviewCount: counts.reviewCount,
22
+ reviewRevisions: counts.reviewRevisions,
23
+ latestCommentId: raw.comments.nodes.at(-1)?.id ?? null,
24
+ latestThreadId: raw.reviewThreads.nodes.at(-1)?.id ?? null,
25
+ latestReviewId: counts.latestReviewId,
26
+ checkRollupState: raw.commits.nodes[0]?.commit.statusCheckRollup?.state ?? null,
27
+ ...suiteFingerprint(raw.commits.nodes[0]?.commit.checkSuites),
28
+ viewerCanUpdate: raw.viewerCanUpdate === true,
29
+ viewerPermission: viewer.permission,
30
+ viewerLogin: viewer.login,
31
+ stackKey: stackKey(raw),
32
+ threadCommentRevisions: threadCommentRevisions(raw.reviewThreads.nodes),
33
+ rulesComplete: rulesComplete(raw.baseRef),
34
+ hasMultiCommentThreads: hasMultiCommentThreads(raw.reviewThreads.nodes),
35
+ };
36
+ }
37
+ export function fingerprintFromRaw(raw, viewerPermission = null, viewerLogin = null) {
38
+ return coreFingerprint(raw, {
39
+ reviewCount: raw.allReviews?.totalCount ?? 0,
40
+ latestReviewId: raw.allReviews?.nodes?.at(-1)?.id ?? null,
41
+ reviewRevisions: commentRevisions(raw.allReviews?.nodes ?? []),
42
+ }, { permission: viewerPermission, login: viewerLogin });
43
+ }
44
+ export function fingerprintsEqual(left, right) {
45
+ return JSON.stringify(left) === JSON.stringify(right);
46
+ }
47
+ export async function fetchPrFingerprint(pr, repo) {
48
+ const result = await graphqlWithRateLimit(PR_FINGERPRINT_QUERY, {
49
+ owner: repo.owner,
50
+ repo: repo.name,
51
+ pr,
52
+ });
53
+ if (!result.data.repository) {
54
+ throw new GitHubRequestError(`GitHub GraphQL response did not include repository ${repo.owner}/${repo.name} (not found or access denied)`, { status: 200 });
55
+ }
56
+ const raw = result.data.repository.pullRequest;
57
+ if (!raw)
58
+ throw new ShepherdError(`PR #${pr} not found`, EXIT.UNAVAILABLE);
59
+ return coreFingerprint(raw, {
60
+ reviewCount: raw.reviews.totalCount,
61
+ latestReviewId: raw.reviews.nodes.at(-1)?.id ?? null,
62
+ reviewRevisions: commentRevisions(raw.reviews.nodes),
63
+ }, {
64
+ permission: result.data.repository.viewerPermission,
65
+ login: result.data.viewer?.login ?? null,
66
+ });
67
+ }
@@ -9,12 +9,16 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
9
9
  resetAt
10
10
  used
11
11
  }
12
+ viewer {
13
+ login
14
+ }
12
15
  repository(owner: $owner, name: $repo) {
13
16
  viewerPermission
14
17
  viewerCanAdminister
15
18
  pullRequest(number: $pr) {
16
19
  id
17
20
  number
21
+ updatedAt
18
22
  state
19
23
  isDraft
20
24
  viewerDidAuthor
@@ -32,7 +36,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
32
36
  }
33
37
  baseRefName
34
38
  isInMergeQueue
35
- isMergeQueueEnabled
39
+ ...PrMergePolicy
36
40
  autoMergeRequest {
37
41
  enabledAt
38
42
  mergeMethod
@@ -49,7 +53,8 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
49
53
  login
50
54
  }
51
55
  headCommit {
52
- ...QueueCheckCommit
56
+ oid
57
+ committedDate
53
58
  }
54
59
  }
55
60
  mergeQueueAdditions: timelineItems(last: 1, itemTypes: [ADDED_TO_MERGE_QUEUE_EVENT]) {
@@ -68,7 +73,13 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
68
73
  login
69
74
  }
70
75
  beforeCommit {
71
- ...QueueCheckCommit
76
+ oid
77
+ committedDate
78
+ parents(first: 100) {
79
+ nodes {
80
+ oid
81
+ }
82
+ }
72
83
  }
73
84
  }
74
85
  }
@@ -81,49 +92,6 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
81
92
  stackEntry {
82
93
  position
83
94
  }
84
- baseRef {
85
- branchProtectionRule {
86
- requiresApprovingReviews
87
- requiredApprovingReviewCount
88
- requiresConversationResolution
89
- requiresCodeOwnerReviews
90
- requireLastPushApproval
91
- requiresCommitSignatures
92
- requiresLinearHistory
93
- requiresStatusChecks
94
- requiredStatusCheckContexts
95
- requiresStrictStatusChecks
96
- requiresDeployments
97
- requiredDeploymentEnvironments
98
- }
99
- rules(first: 100) {
100
- nodes {
101
- type
102
- parameters {
103
- ... on PullRequestParameters {
104
- requiredApprovingReviewCount
105
- requiredReviewThreadResolution
106
- requireCodeOwnerReview
107
- requireLastPushApproval
108
- }
109
- ... on RequiredStatusChecksParameters {
110
- strictRequiredStatusChecksPolicy
111
- requiredStatusChecks {
112
- context
113
- }
114
- }
115
- ... on RequiredDeploymentsParameters {
116
- requiredDeploymentEnvironments
117
- }
118
- ... on CodeScanningParameters {
119
- codeScanningTools {
120
- tool
121
- }
122
- }
123
- }
124
- }
125
- }
126
- }
127
95
  # Not paginated — capped at 50; PRs with more pending reviewers truncate silently.
128
96
  reviewRequests(last: 50) {
129
97
  nodes {
@@ -153,6 +121,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
153
121
  }
154
122
  }
155
123
  reviewThreads(last: 100) {
124
+ totalCount
156
125
  pageInfo {
157
126
  hasPreviousPage
158
127
  startCursor
@@ -189,11 +158,13 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
189
158
  line
190
159
  startLine
191
160
  createdAt
161
+ updatedAt
192
162
  }
193
163
  }
194
164
  }
195
165
  }
196
166
  comments(last: 100) {
167
+ totalCount
197
168
  pageInfo {
198
169
  hasPreviousPage
199
170
  startCursor
@@ -210,6 +181,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
210
181
  }
211
182
  body
212
183
  createdAt
184
+ updatedAt
213
185
  }
214
186
  }
215
187
  changesRequestedReviews: reviews(states: CHANGES_REQUESTED, last: 50) {
@@ -249,8 +221,12 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
249
221
  createdAt
250
222
  }
251
223
  }
252
- allReviews: reviews(last: 1) {
224
+ allReviews: reviews(last: 100) {
253
225
  totalCount
226
+ nodes {
227
+ id
228
+ updatedAt
229
+ }
254
230
  }
255
231
  approvedReviews: reviews(states: APPROVED, last: 50) {
256
232
  pageInfo {
@@ -276,23 +252,9 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
276
252
  commit {
277
253
  oid
278
254
  committedDate
279
- checkSuites(first: 50) {
280
- pageInfo {
281
- hasNextPage
282
- }
283
- nodes {
284
- conclusion
285
- workflowRun {
286
- databaseId
287
- event
288
- url
289
- workflow {
290
- name
291
- }
292
- }
293
- }
294
- }
255
+ ...CommitCheckSuites
295
256
  statusCheckRollup {
257
+ state
296
258
  contexts(first: 100) {
297
259
  pageInfo {
298
260
  hasNextPage
@@ -345,60 +307,3 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
345
307
  }
346
308
  }
347
309
  }
348
-
349
- fragment QueueCheckCommit on Commit {
350
- oid
351
- committedDate
352
- parents(first: 100) {
353
- nodes {
354
- oid
355
- }
356
- }
357
- statusCheckRollup {
358
- contexts(first: 100) {
359
- pageInfo {
360
- hasNextPage
361
- endCursor
362
- }
363
- nodes {
364
- __typename
365
- ... on CheckRun {
366
- id
367
- name
368
- status
369
- conclusion
370
- detailsUrl
371
- completedAt
372
- startedAt
373
- title
374
- summary
375
- annotations(first: 1) {
376
- nodes {
377
- message
378
- }
379
- }
380
- checkSuite {
381
- createdAt
382
- updatedAt
383
- workflowRun {
384
- event
385
- createdAt
386
- updatedAt
387
- workflow {
388
- name
389
- databaseId
390
- }
391
- }
392
- }
393
- }
394
- ... on StatusContext {
395
- context
396
- state
397
- createdAt
398
- targetUrl
399
- description
400
- }
401
- }
402
- }
403
- }
404
- }
@@ -0,0 +1,19 @@
1
+ fragment CommitCheckSuites on Commit {
2
+ checkSuites(first: 50) {
3
+ pageInfo {
4
+ hasNextPage
5
+ }
6
+ nodes {
7
+ id
8
+ conclusion
9
+ workflowRun {
10
+ databaseId
11
+ event
12
+ url
13
+ workflow {
14
+ name
15
+ }
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,75 @@
1
+ # Cheap preflight. Cost is 1. Used to skip BatchPr when GitHub state is unchanged.
2
+ query PrFingerprint($owner: String!, $repo: String!, $pr: Int!) {
3
+ _shepherdRateLimit: rateLimit {
4
+ cost
5
+ limit
6
+ nodeCount
7
+ remaining
8
+ resetAt
9
+ used
10
+ }
11
+ viewer {
12
+ login
13
+ }
14
+ repository(owner: $owner, name: $repo) {
15
+ viewerPermission
16
+ pullRequest(number: $pr) {
17
+ updatedAt
18
+ state
19
+ isDraft
20
+ viewerCanUpdate
21
+ headRefOid
22
+ mergeable
23
+ mergeStateStatus
24
+ reviewDecision
25
+ isInMergeQueue
26
+ stack {
27
+ number
28
+ size
29
+ baseRefName
30
+ }
31
+ stackEntry {
32
+ position
33
+ }
34
+ ...PrMergePolicy
35
+ comments(last: 100) {
36
+ totalCount
37
+ nodes {
38
+ id
39
+ updatedAt
40
+ }
41
+ }
42
+ reviewThreads(last: 20) {
43
+ totalCount
44
+ nodes {
45
+ id
46
+ comments(last: 1) {
47
+ totalCount
48
+ nodes {
49
+ id
50
+ updatedAt
51
+ }
52
+ }
53
+ }
54
+ }
55
+ reviews(last: 100) {
56
+ totalCount
57
+ nodes {
58
+ id
59
+ updatedAt
60
+ }
61
+ }
62
+ commits(last: 1) {
63
+ nodes {
64
+ commit {
65
+ oid
66
+ statusCheckRollup {
67
+ state
68
+ }
69
+ ...CommitCheckSuites
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,49 @@
1
+ fragment PrMergePolicy on PullRequest {
2
+ isMergeQueueEnabled
3
+ baseRef {
4
+ branchProtectionRule {
5
+ requiresApprovingReviews
6
+ requiredApprovingReviewCount
7
+ requiresConversationResolution
8
+ requiresCodeOwnerReviews
9
+ requireLastPushApproval
10
+ requiresCommitSignatures
11
+ requiresLinearHistory
12
+ requiresStatusChecks
13
+ requiredStatusCheckContexts
14
+ requiresStrictStatusChecks
15
+ requiresDeployments
16
+ requiredDeploymentEnvironments
17
+ }
18
+ rules(first: 100) {
19
+ pageInfo {
20
+ hasNextPage
21
+ }
22
+ nodes {
23
+ type
24
+ parameters {
25
+ ... on PullRequestParameters {
26
+ requiredApprovingReviewCount
27
+ requiredReviewThreadResolution
28
+ requireCodeOwnerReview
29
+ requireLastPushApproval
30
+ }
31
+ ... on RequiredStatusChecksParameters {
32
+ strictRequiredStatusChecksPolicy
33
+ requiredStatusChecks {
34
+ context
35
+ }
36
+ }
37
+ ... on RequiredDeploymentsParameters {
38
+ requiredDeploymentEnvironments
39
+ }
40
+ ... on CodeScanningParameters {
41
+ codeScanningTools {
42
+ tool
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
@@ -1,46 +1,73 @@
1
1
  import { graphql } from "./client.mjs";
2
2
  import { requireContextNodes } from "./batch-response.mjs";
3
3
  import { COMMIT_CHECK_CONTEXTS_QUERY } from "./queries.mjs";
4
- async function hydrateCommitContexts(commit, repo) {
5
- const contexts = commit.statusCheckRollup?.contexts;
6
- if (!contexts)
7
- return;
8
- contexts.nodes = requireContextNodes(contexts.nodes);
9
- if (contexts.pageInfo.hasNextPage && !contexts.pageInfo.endCursor) {
10
- throw new Error(`Merge queue check pagination interrupted: GitHub omitted the next cursor for ${commit.oid}. Retry.`);
4
+ function omittedCursorError(oid) {
5
+ return new Error(`Merge queue check pagination interrupted: GitHub omitted the next cursor for ${oid}. Retry.`);
6
+ }
7
+ function nextPageCursor(contexts, oid) {
8
+ if (!contexts.pageInfo.hasNextPage)
9
+ return undefined;
10
+ if (!contexts.pageInfo.endCursor)
11
+ throw omittedCursorError(oid);
12
+ return contexts.pageInfo.endCursor;
13
+ }
14
+ function initialQueueCursor(existing, oid) {
15
+ if (!existing)
16
+ return null;
17
+ return nextPageCursor(existing, oid);
18
+ }
19
+ async function fetchQueuePage(oid, repo, cursor) {
20
+ const result = await graphql(COMMIT_CHECK_CONTEXTS_QUERY, {
21
+ owner: repo.owner,
22
+ repo: repo.name,
23
+ oid,
24
+ ...(cursor !== null && { cursor }),
25
+ });
26
+ const object = result.data.repository?.object;
27
+ if (object?.__typename !== "Commit" || object.oid !== oid) {
28
+ if (cursor === null)
29
+ return null;
30
+ throw new Error(`Merge queue check pagination interrupted: commit ${oid} disappeared or changed. Retry.`);
11
31
  }
12
- let cursor = contexts.pageInfo.hasNextPage ? contexts.pageInfo.endCursor : null;
13
- while (cursor) {
32
+ return object.statusCheckRollup?.contexts ?? null;
33
+ }
34
+ async function hydrateCommitContexts(commit, repo) {
35
+ const existing = commit.statusCheckRollup?.contexts;
36
+ const nodes = existing ? [...requireContextNodes(existing.nodes)] : [];
37
+ let cursor = initialQueueCursor(existing, commit.oid);
38
+ while (cursor !== undefined) {
14
39
  // eslint-disable-next-line no-await-in-loop
15
- const result = await graphql(COMMIT_CHECK_CONTEXTS_QUERY, {
16
- owner: repo.owner,
17
- repo: repo.name,
18
- oid: commit.oid,
19
- cursor,
20
- });
21
- const object = result.data.repository?.object;
22
- if (object?.__typename !== "Commit" || object.oid !== commit.oid) {
23
- throw new Error(`Merge queue check pagination interrupted: commit ${commit.oid} disappeared or changed. Retry.`);
24
- }
25
- const next = object.statusCheckRollup?.contexts;
40
+ const next = await fetchQueuePage(commit.oid, repo, cursor);
26
41
  if (!next) {
42
+ if (cursor === null) {
43
+ cursor = undefined;
44
+ continue;
45
+ }
27
46
  throw new Error(`Merge queue check pagination interrupted: statusCheckRollup disappeared for ${commit.oid}. Retry.`);
28
47
  }
29
- contexts.nodes.push(...requireContextNodes(next.nodes));
30
- contexts.pageInfo = next.pageInfo;
31
- cursor = next.pageInfo.hasNextPage ? next.pageInfo.endCursor : null;
32
- if (next.pageInfo.hasNextPage && !cursor) {
33
- throw new Error(`Merge queue check pagination interrupted: GitHub omitted the next cursor for ${commit.oid}. Retry.`);
34
- }
48
+ nodes.push(...requireContextNodes(next.nodes));
49
+ cursor = nextPageCursor(next, commit.oid);
35
50
  }
51
+ commit.statusCheckRollup = {
52
+ contexts: { pageInfo: { hasNextPage: false, endCursor: null }, nodes },
53
+ };
54
+ }
55
+ function currentRemovalCommit(raw) {
56
+ const removal = raw.mergeQueueRemovals?.nodes[0];
57
+ const addition = raw.mergeQueueAdditions?.nodes[0];
58
+ if (!removal?.beforeCommit)
59
+ return undefined;
60
+ if (addition && Date.parse(removal.createdAt) < Date.parse(addition.createdAt))
61
+ return undefined;
62
+ const parentOids = removal.beforeCommit.parents?.nodes.map((node) => node.oid);
63
+ if (!parentOids?.includes(raw.headRefOid))
64
+ return undefined;
65
+ return removal.beforeCommit;
36
66
  }
37
67
  /** Hydrate all status contexts for the active or most recently removed queue commit. */
38
68
  export async function hydrateMergeQueueChecks(raw, repo) {
39
69
  const active = raw.mergeQueueEntry?.headCommit;
40
- const removal = raw.mergeQueueRemovals?.nodes[0];
41
- const addition = raw.mergeQueueAdditions?.nodes[0];
42
- const removalIsCurrent = Boolean(removal && (!addition || Date.parse(removal.createdAt) >= Date.parse(addition.createdAt)));
43
- const removed = removalIsCurrent ? removal?.beforeCommit : undefined;
70
+ const removed = currentRemovalCommit(raw);
44
71
  if (active)
45
72
  await hydrateCommitContexts(active, repo);
46
73
  if (removed && removed.oid !== active?.oid)
@@ -10,6 +10,8 @@
10
10
  export declare const BATCH_PR_QUERY: string;
11
11
  /** Slim @include follow-up for outstanding batch-query connections. */
12
12
  export declare const BATCH_PR_PAGE_QUERY: string;
13
+ /** Cheap PR fingerprint used to skip an unchanged BatchPr snapshot. */
14
+ export declare const PR_FINGERPRINT_QUERY: string;
13
15
  /** PR head fields plus a single review thread for `commit-suggestion`. */
14
16
  export declare const SUGGESTION_THREADS_QUERY: string;
15
17
  /** Fetches additional comments for a single review thread when its nested connection paginates. */