pr-shepherd 0.54.1 → 0.55.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 (114) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -3
  3. package/bin/checks/unreported-required.d.mts +29 -0
  4. package/bin/checks/unreported-required.mjs +52 -0
  5. package/bin/classify/apply.d.mts +2 -0
  6. package/bin/classify/apply.mjs +31 -31
  7. package/bin/classify/rule-action.d.mts +8 -0
  8. package/bin/classify/rule-action.mjs +29 -0
  9. package/bin/classify/types.d.mts +1 -1
  10. package/bin/cli/help-command-pages.d.mts +2 -2
  11. package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
  12. package/bin/cli/help-iterate-poll-pages.mjs +6 -5
  13. package/bin/cli/help-top-page.d.mts +1 -1
  14. package/bin/cli/help-top-page.mjs +2 -2
  15. package/bin/cli/help.d.mts +3 -3
  16. package/bin/cli/iterate-formatter.mjs +29 -9
  17. package/bin/cli/iterate-instructions.mjs +6 -0
  18. package/bin/cli/iterate-lean.mjs +15 -0
  19. package/bin/cli/poll-summary-emitter.mjs +4 -1
  20. package/bin/cli/poll-summary-formatter.mjs +10 -3
  21. package/bin/cli/stack-overview.d.mts +48 -0
  22. package/bin/cli/stack-overview.mjs +138 -0
  23. package/bin/commands/check-fingerprint.mjs +2 -1
  24. package/bin/commands/check-status.d.mts +1 -1
  25. package/bin/commands/check-status.mjs +8 -3
  26. package/bin/commands/check-unreported.d.mts +26 -0
  27. package/bin/commands/check-unreported.mjs +73 -0
  28. package/bin/commands/check.mjs +44 -38
  29. package/bin/commands/iterate/base.mjs +9 -0
  30. package/bin/commands/iterate/escalate.mjs +4 -0
  31. package/bin/commands/iterate/index.mjs +69 -3
  32. package/bin/commands/iterate/merge-state.mjs +12 -12
  33. package/bin/commands/iterate/merge.d.mts +12 -2
  34. package/bin/commands/iterate/merge.mjs +32 -2
  35. package/bin/commands/iterate/unreported-required.d.mts +18 -0
  36. package/bin/commands/iterate/unreported-required.mjs +122 -0
  37. package/bin/commands/poll-summary.mjs +1 -0
  38. package/bin/commands/poll.mjs +9 -2
  39. package/bin/commands/ready-mergeability.d.mts +1 -1
  40. package/bin/commands/ready-mergeability.mjs +2 -2
  41. package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
  42. package/bin/commands/rule-auto-resolve-format.mjs +151 -0
  43. package/bin/commands/rule-auto-resolve.d.mts +21 -0
  44. package/bin/commands/rule-auto-resolve.mjs +113 -0
  45. package/bin/commands/stack-drain.d.mts +0 -5
  46. package/bin/commands/stack-drain.mjs +26 -16
  47. package/bin/commands/stack-merge-flag.d.mts +7 -0
  48. package/bin/commands/stack-merge-flag.mjs +6 -0
  49. package/bin/config/load.d.mts +3 -0
  50. package/bin/config/load.mjs +9 -2
  51. package/bin/config/merge-method.d.mts +27 -0
  52. package/bin/config/merge-method.mjs +56 -0
  53. package/bin/exit-codes.d.mts +1 -1
  54. package/bin/exit-codes.mjs +2 -1
  55. package/bin/github/batch-parse-suites.d.mts +3 -0
  56. package/bin/github/batch-parse-suites.mjs +15 -0
  57. package/bin/github/batch-parsers.d.mts +1 -1
  58. package/bin/github/batch-parsers.mjs +7 -0
  59. package/bin/github/batch-raw-types.d.mts +5 -0
  60. package/bin/github/batch.d.mts +3 -0
  61. package/bin/github/batch.mjs +9 -1
  62. package/bin/github/errors.d.mts +6 -0
  63. package/bin/github/errors.mjs +23 -5
  64. package/bin/github/fingerprint-fields.d.mts +1 -0
  65. package/bin/github/fingerprint-fields.mjs +1 -1
  66. package/bin/github/gql/batch-pr-page.gql +2 -0
  67. package/bin/github/gql/batch-pr.gql +5 -0
  68. package/bin/github/gql/commit-check-suites.gql +1 -0
  69. package/bin/github/gql/poll-stack-summary.gql +6 -0
  70. package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
  71. package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
  72. package/bin/github/gql/poll-summary-fragment.gql +16 -0
  73. package/bin/github/gql/pr-merge-policy.gql +1 -44
  74. package/bin/github/gql/ref-rules-query.gql +18 -0
  75. package/bin/github/gql/ref-rules.gql +46 -0
  76. package/bin/github/graphql-internal-retry.d.mts +1 -1
  77. package/bin/github/graphql-internal-retry.mjs +4 -3
  78. package/bin/github/merge-queue-checks.mjs +7 -3
  79. package/bin/github/merge-target-rules.d.mts +20 -0
  80. package/bin/github/merge-target-rules.mjs +54 -0
  81. package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
  82. package/bin/github/poll-summary-annotation-probe.mjs +88 -0
  83. package/bin/github/poll-summary-fingerprint.mjs +1 -1
  84. package/bin/github/poll-summary-projector.d.mts +1 -1
  85. package/bin/github/poll-summary-projector.mjs +19 -12
  86. package/bin/github/poll-summary-queue-removal.mjs +7 -3
  87. package/bin/github/poll-summary-raw.d.mts +16 -0
  88. package/bin/github/poll-summary-readiness.mjs +1 -0
  89. package/bin/github/poll-summary-route.mjs +5 -0
  90. package/bin/github/poll-summary-unreported.d.mts +6 -0
  91. package/bin/github/poll-summary-unreported.mjs +51 -0
  92. package/bin/github/poll-summary.d.mts +1 -0
  93. package/bin/github/poll-summary.mjs +28 -3
  94. package/bin/github/queries.d.mts +7 -0
  95. package/bin/github/queries.mjs +9 -2
  96. package/bin/github/queue-removal-freshness.d.mts +20 -5
  97. package/bin/github/queue-removal-freshness.mjs +37 -10
  98. package/bin/github/stack-read.d.mts +4 -0
  99. package/bin/github/stack-read.mjs +13 -1
  100. package/bin/mcp/server.mjs +6 -1
  101. package/bin/state/ci-retrigger.d.mts +25 -0
  102. package/bin/state/ci-retrigger.mjs +48 -0
  103. package/bin/types/escalate.d.mts +1 -1
  104. package/bin/types/github.d.mts +11 -0
  105. package/bin/types/iterate.d.mts +14 -3
  106. package/bin/types/poll-summary.d.mts +13 -0
  107. package/bin/types/report.d.mts +32 -1
  108. package/package.json +1 -1
  109. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  110. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  111. package/plugins/pr-shepherd/.mcp.json +1 -1
  112. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +2 -2
  113. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
  114. package/src/classify/types.mts +1 -1
@@ -4,7 +4,7 @@ export function suiteFingerprint(suites) {
4
4
  return { checkSuiteConclusions: "", checkSuitesComplete: false };
5
5
  return {
6
6
  checkSuiteConclusions: (suites.nodes ?? [])
7
- .map((node) => `${node.id ?? node.workflowRun?.databaseId ?? ""}:${node.conclusion ?? ""}`)
7
+ .map((node) => `${node.id ?? node.workflowRun?.databaseId ?? ""}:${node.status ?? ""}:${node.conclusion ?? ""}`)
8
8
  .join(","),
9
9
  checkSuitesComplete: suites.pageInfo?.hasNextPage === false,
10
10
  };
@@ -128,6 +128,7 @@ query BatchPrPage(
128
128
  }
129
129
  body
130
130
  createdAt
131
+ url
131
132
  }
132
133
  }
133
134
  approvedReviews: reviews(states: APPROVED, last: 50, before: $approvedReviewsCursor)
@@ -147,6 +148,7 @@ query BatchPrPage(
147
148
  }
148
149
  body
149
150
  createdAt
151
+ url
150
152
  }
151
153
  }
152
154
  commits(last: 1) @include(if: $includeChecks) {
@@ -15,6 +15,9 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
15
15
  repository(owner: $owner, name: $repo) {
16
16
  viewerPermission
17
17
  viewerCanAdminister
18
+ mergeCommitAllowed
19
+ squashMergeAllowed
20
+ rebaseMergeAllowed
18
21
  pullRequest(number: $pr) {
19
22
  id
20
23
  number
@@ -220,6 +223,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
220
223
  }
221
224
  body
222
225
  createdAt
226
+ url
223
227
  }
224
228
  }
225
229
  allReviews: reviews(last: 100) {
@@ -245,6 +249,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
245
249
  }
246
250
  body
247
251
  createdAt
252
+ url
248
253
  }
249
254
  }
250
255
  commits(last: 1) {
@@ -5,6 +5,7 @@ fragment CommitCheckSuites on Commit {
5
5
  }
6
6
  nodes {
7
7
  id
8
+ status
8
9
  conclusion
9
10
  workflowRun {
10
11
  databaseId
@@ -13,8 +13,14 @@ query PollStackSummary(
13
13
  resetAt
14
14
  used
15
15
  }
16
+ viewer {
17
+ login
18
+ }
16
19
  repository(owner: $owner, name: $repo) {
17
20
  viewerCanAdminister
21
+ mergeCommitAllowed
22
+ squashMergeAllowed
23
+ rebaseMergeAllowed
18
24
  pullRequest(number: $anchor) {
19
25
  stack {
20
26
  id
@@ -0,0 +1,32 @@
1
+ query PollSummaryAnnotationProbe(
2
+ $owner: String!
3
+ $repo: String!
4
+ $oid: GitObjectID!
5
+ $before: String
6
+ ) {
7
+ repository(owner: $owner, name: $repo) {
8
+ object(oid: $oid) {
9
+ __typename
10
+ ... on Commit {
11
+ oid
12
+ statusCheckRollup {
13
+ contexts(last: 100, before: $before) {
14
+ pageInfo {
15
+ hasPreviousPage
16
+ startCursor
17
+ }
18
+ nodes {
19
+ __typename
20
+ ... on CheckRun {
21
+ id
22
+ annotations(first: 1) {
23
+ totalCount
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
@@ -12,13 +12,12 @@ fragment PollSummaryCheckContexts on StatusCheckRollupContextConnection {
12
12
  status
13
13
  conclusion
14
14
  detailsUrl
15
- annotations(first: 1) {
16
- totalCount
17
- }
18
15
  checkSuite {
16
+ createdAt
19
17
  workflowRun {
20
18
  databaseId
21
19
  event
20
+ createdAt
22
21
  workflow {
23
22
  databaseId
24
23
  name
@@ -2,6 +2,10 @@ fragment PollSummaryPr on PullRequest {
2
2
  number
3
3
  title
4
4
  url
5
+ author {
6
+ __typename
7
+ login
8
+ }
5
9
  state
6
10
  updatedAt
7
11
  isDraft
@@ -10,6 +14,9 @@ fragment PollSummaryPr on PullRequest {
10
14
  headRefOid
11
15
  baseRefName
12
16
  baseRefOid
17
+ baseRef {
18
+ ...RefRules
19
+ }
13
20
  mergeable
14
21
  mergeStateStatus
15
22
  lifecycleEvents: timelineItems(
@@ -185,6 +192,15 @@ fragment PollSummaryPr on PullRequest {
185
192
  commit {
186
193
  oid
187
194
  committedDate
195
+ checkSuites(first: 50) {
196
+ nodes {
197
+ status
198
+ conclusion
199
+ workflowRun {
200
+ event
201
+ }
202
+ }
203
+ }
188
204
  statusCheckRollup {
189
205
  contexts(last: 100) {
190
206
  ...PollSummaryCheckContexts
@@ -1,49 +1,6 @@
1
1
  fragment PrMergePolicy on PullRequest {
2
2
  isMergeQueueEnabled
3
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
- }
4
+ ...RefRules
48
5
  }
49
6
  }
@@ -0,0 +1,18 @@
1
+ query RefRules($owner: String!, $repo: String!, $qualifiedName: String!, $headRef: String!) {
2
+ _shepherdRateLimit: rateLimit {
3
+ cost
4
+ limit
5
+ nodeCount
6
+ remaining
7
+ resetAt
8
+ used
9
+ }
10
+ repository(owner: $owner, name: $repo) {
11
+ ref(qualifiedName: $qualifiedName) {
12
+ ...RefRules
13
+ compare(headRef: $headRef) {
14
+ behindBy
15
+ }
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,46 @@
1
+ fragment RefRules on Ref {
2
+ branchProtectionRule {
3
+ requiresApprovingReviews
4
+ requiredApprovingReviewCount
5
+ requiresConversationResolution
6
+ requiresCodeOwnerReviews
7
+ requireLastPushApproval
8
+ requiresCommitSignatures
9
+ requiresLinearHistory
10
+ requiresStatusChecks
11
+ requiredStatusCheckContexts
12
+ requiresStrictStatusChecks
13
+ requiresDeployments
14
+ requiredDeploymentEnvironments
15
+ }
16
+ rules(first: 100) {
17
+ pageInfo {
18
+ hasNextPage
19
+ }
20
+ nodes {
21
+ type
22
+ parameters {
23
+ ... on PullRequestParameters {
24
+ requiredApprovingReviewCount
25
+ requiredReviewThreadResolution
26
+ requireCodeOwnerReview
27
+ requireLastPushApproval
28
+ }
29
+ ... on RequiredStatusChecksParameters {
30
+ strictRequiredStatusChecksPolicy
31
+ requiredStatusChecks {
32
+ context
33
+ }
34
+ }
35
+ ... on RequiredDeploymentsParameters {
36
+ requiredDeploymentEnvironments
37
+ }
38
+ ... on CodeScanningParameters {
39
+ codeScanningTools {
40
+ tool
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
@@ -1,2 +1,2 @@
1
- /** Retry GitHub GraphQL engine crashes (HTTP 200, data: null, INTERNAL) on reads. */
1
+ /** Retry GitHub GraphQL engine crashes and resource-limit failures on reads. */
2
2
  export declare function withGraphQlInternalRetry<T>(document: string, run: () => Promise<T>): Promise<T>;
@@ -1,5 +1,5 @@
1
1
  import { sleep } from "../util/sleep.mjs";
2
- import { GitHubRequestError, isRetryableGraphQlInternal } from "./errors.mjs";
2
+ import { GitHubRequestError, isRetryableGraphQlInternal, isRetryableGraphQlResourceLimit, } from "./errors.mjs";
3
3
  const GRAPHQL_INTERNAL_RETRY_DELAYS = [500, 1500];
4
4
  /** True when the root operation is a mutation. Anonymous `{...}` is a query. */
5
5
  function isGraphQlMutationDocument(document) {
@@ -25,7 +25,7 @@ function hasServerBackoff(err) {
25
25
  return true;
26
26
  return err.rateLimit !== undefined && err.rateLimit.remaining <= 0;
27
27
  }
28
- /** Retry GitHub GraphQL engine crashes (HTTP 200, data: null, INTERNAL) on reads. */
28
+ /** Retry GitHub GraphQL engine crashes and resource-limit failures on reads. */
29
29
  export async function withGraphQlInternalRetry(document, run) {
30
30
  let lastErr;
31
31
  for (let attempt = 1; attempt <= GRAPHQL_INTERNAL_RETRY_DELAYS.length + 1; attempt++) {
@@ -34,7 +34,8 @@ export async function withGraphQlInternalRetry(document, run) {
34
34
  }
35
35
  catch (err) {
36
36
  if (!(err instanceof GitHubRequestError) ||
37
- !isRetryableGraphQlInternal(err.graphqlErrors) ||
37
+ !(isRetryableGraphQlInternal(err.graphqlErrors) ||
38
+ isRetryableGraphQlResourceLimit(err.graphqlErrors)) ||
38
39
  isGraphQlMutationDocument(document) ||
39
40
  hasServerBackoff(err)) {
40
41
  throw err;
@@ -1,6 +1,6 @@
1
1
  import { parseCreatedAt } from "./batch-parser-helpers.mjs";
2
2
  import { graphql } from "./client.mjs";
3
- import { queueRemovalAppliesToHead } from "./queue-removal-freshness.mjs";
3
+ import { headPushUnixFromCheckNodes, queueRemovalAppliesToHead, } from "./queue-removal-freshness.mjs";
4
4
  import { requireContextNodes } from "./batch-response.mjs";
5
5
  import { COMMIT_CHECK_CONTEXTS_QUERY } from "./queries.mjs";
6
6
  function omittedCursorError(oid) {
@@ -62,11 +62,15 @@ function currentRemovalCommit(raw) {
62
62
  if (addition && Date.parse(removal.createdAt) < Date.parse(addition.createdAt))
63
63
  return undefined;
64
64
  const parentOids = removal.beforeCommit.parents?.nodes.map((node) => node.oid);
65
- const headCommittedAt = raw.commits?.nodes[0]?.commit.committedDate;
65
+ const headCommit = raw.commits?.nodes[0]?.commit;
66
+ const headPushedAtUnix = headPushUnixFromCheckNodes(headCommit?.statusCheckRollup?.contexts.nodes);
66
67
  if (!queueRemovalAppliesToHead({
67
68
  parentOids,
68
69
  headOid: raw.headRefOid,
69
- ...(headCommittedAt && { headCommittedAtUnix: parseCreatedAt(headCommittedAt) }),
70
+ ...(headCommit?.committedDate && {
71
+ headCommittedAtUnix: parseCreatedAt(headCommit.committedDate),
72
+ }),
73
+ ...(headPushedAtUnix !== undefined && { headPushedAtUnix }),
70
74
  removedAtUnix: Math.floor(Date.parse(removal.createdAt) / 1000),
71
75
  })) {
72
76
  return undefined;
@@ -0,0 +1,20 @@
1
+ export interface MergeTargetStatus {
2
+ contexts: string[];
3
+ trunkBehindBy?: number;
4
+ stackBottomPr?: number;
5
+ }
6
+ /**
7
+ * Required status contexts for the branch GitHub actually merges into.
8
+ * A native stack uses the trunk ref, and `behindBy` is that trunk against the bottom open layer.
9
+ */
10
+ export declare function loadMergeTargetStatus(input: {
11
+ owner: string;
12
+ name: string;
13
+ pr: number;
14
+ baseRefName: string;
15
+ headRefName: string;
16
+ localContexts: readonly string[];
17
+ stack?: {
18
+ baseRefName: string;
19
+ } | null;
20
+ }): Promise<MergeTargetStatus>;
@@ -0,0 +1,54 @@
1
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
2
+ import { parseBranchRules } from "./batch-parsers-rules.mjs";
3
+ import { graphqlWithRateLimit } from "./client.mjs";
4
+ import { missingRepositoryError } from "./errors.mjs";
5
+ import { REF_RULES_QUERY } from "./queries.mjs";
6
+ import { readStackTopology } from "./stack-read.mjs";
7
+ /**
8
+ * Required status contexts for the branch GitHub actually merges into.
9
+ * A native stack uses the trunk ref, and `behindBy` is that trunk against the bottom open layer.
10
+ */
11
+ export async function loadMergeTargetStatus(input) {
12
+ const trunk = input.stack?.baseRefName;
13
+ if (!trunk)
14
+ return { contexts: [...input.localContexts] };
15
+ let headRef = input.headRefName;
16
+ let stackBottomPr = input.pr;
17
+ if (trunk !== input.baseRefName) {
18
+ const bottom = await bottomOpenLayer(input.pr, { owner: input.owner, name: input.name }, trunk);
19
+ headRef = bottom.headRefName;
20
+ stackBottomPr = bottom.number;
21
+ }
22
+ const loaded = await fetchRefRules(input.owner, input.name, `refs/heads/${trunk}`, headRef);
23
+ return {
24
+ contexts: loaded.contexts,
25
+ ...(loaded.behindBy > 0 && { trunkBehindBy: loaded.behindBy }),
26
+ stackBottomPr,
27
+ };
28
+ }
29
+ async function bottomOpenLayer(pr, repo, trunk) {
30
+ const topology = await readStackTopology(pr, repo);
31
+ const bottom = topology.ordered.find((member) => member.state === "OPEN" && member.baseRefName === trunk);
32
+ if (!bottom) {
33
+ throw new ShepherdError(`Native stack for PR #${pr} has no open layer based on ${trunk}`, EXIT.TEMPFAIL);
34
+ }
35
+ return bottom;
36
+ }
37
+ async function fetchRefRules(owner, name, qualifiedName, headRef) {
38
+ const { data } = await graphqlWithRateLimit(REF_RULES_QUERY, {
39
+ owner,
40
+ repo: name,
41
+ qualifiedName,
42
+ headRef,
43
+ });
44
+ if (!data.repository)
45
+ throw missingRepositoryError({ owner, name });
46
+ const ref = data.repository.ref;
47
+ if (!ref) {
48
+ throw new ShepherdError(`Branch ${qualifiedName} was not found in ${owner}/${name}`, EXIT.TEMPFAIL);
49
+ }
50
+ return {
51
+ contexts: parseBranchRules(ref).requiredStatusCheckContexts,
52
+ behindBy: ref.compare?.behindBy ?? 0,
53
+ };
54
+ }
@@ -0,0 +1,19 @@
1
+ import { type RepoInfo } from "./client.mts";
2
+ import type { RawSummaryPr } from "./poll-summary-raw.mts";
3
+ /**
4
+ * True when a ready layer's annotation probe failed or stopped short of every
5
+ * check page. Callers must not store or compare a fingerprint of that snapshot:
6
+ * it is missing annotation totals a later successful probe would include.
7
+ */
8
+ export declare function annotationProbeUnavailable(pr: RawSummaryPr): boolean;
9
+ /** Stored hash when the probe missed, so a later complete probe can still match. */
10
+ export declare function readyFingerprint(raw: RawSummaryPr, stored: string | null): string | null;
11
+ /**
12
+ * Annotation totals are omitted from the always-on summary. A layer that
13
+ * otherwise looks ready loads them once so a READY receipt still changes when
14
+ * a check gains an annotation.
15
+ */
16
+ export declare function hydrateReadyAnnotationProbe(pr: RawSummaryPr, repo: RepoInfo, review: {
17
+ actionable?: number;
18
+ incomplete?: true;
19
+ }): Promise<void>;
@@ -0,0 +1,88 @@
1
+ import { graphqlWithRateLimit } from "./client.mjs";
2
+ import { isCurrentSummaryReady } from "./poll-summary-readiness.mjs";
3
+ import { summarizePollSummaryChecks } from "./poll-summary-checks.mjs";
4
+ import { fingerprintRawSummaryPr } from "./poll-summary-fingerprint.mjs";
5
+ import { POLL_SUMMARY_ANNOTATION_PROBE_QUERY } from "./queries.mjs";
6
+ const PROBE_UNAVAILABLE = Symbol.for("prShepherd.annotationProbeUnavailable");
7
+ /**
8
+ * True when a ready layer's annotation probe failed or stopped short of every
9
+ * check page. Callers must not store or compare a fingerprint of that snapshot:
10
+ * it is missing annotation totals a later successful probe would include.
11
+ */
12
+ export function annotationProbeUnavailable(pr) {
13
+ return Boolean(pr[PROBE_UNAVAILABLE]);
14
+ }
15
+ /** Stored hash when the probe missed, so a later complete probe can still match. */
16
+ export function readyFingerprint(raw, stored) {
17
+ if (annotationProbeUnavailable(raw))
18
+ return stored;
19
+ return fingerprintRawSummaryPr(raw);
20
+ }
21
+ /**
22
+ * Annotation totals are omitted from the always-on summary. A layer that
23
+ * otherwise looks ready loads them once so a READY receipt still changes when
24
+ * a check gains an annotation.
25
+ */
26
+ export async function hydrateReadyAnnotationProbe(pr, repo, review) {
27
+ const checks = summarizePollSummaryChecks(pr);
28
+ if (!isCurrentSummaryReady(pr, checks, review))
29
+ return;
30
+ if (!(await hydrateCommitAnnotations(pr, repo))) {
31
+ pr[PROBE_UNAVAILABLE] = true;
32
+ }
33
+ }
34
+ async function hydrateCommitAnnotations(pr, repo) {
35
+ const commit = pr.commits.nodes[0]?.commit;
36
+ const nodes = commit?.statusCheckRollup?.contexts.nodes;
37
+ if (!commit || !nodes?.some((node) => node.__typename === "CheckRun" && node.id))
38
+ return true;
39
+ const pages = await collectProbePages(commit.oid, repo);
40
+ if (!pages)
41
+ return false;
42
+ const probed = new Map(pages.flatMap((node) => node.__typename === "CheckRun" && node.id ? [[node.id, node.annotations]] : []));
43
+ for (const node of nodes) {
44
+ if (node.__typename !== "CheckRun" || !node.id)
45
+ continue;
46
+ const annotations = probed.get(node.id);
47
+ if (annotations)
48
+ node.annotations = annotations;
49
+ }
50
+ return true;
51
+ }
52
+ /** Every check page, oldest last. Null when a page cannot be trusted. */
53
+ async function collectProbePages(oid, repo) {
54
+ const pages = [];
55
+ let before = null;
56
+ const seen = new Set();
57
+ for (;;) {
58
+ const contexts = await fetchProbePage(oid, repo, before);
59
+ if (!contexts)
60
+ return null;
61
+ pages.push(...contexts.nodes);
62
+ const pageInfo = contexts.pageInfo;
63
+ if (!pageInfo?.hasPreviousPage)
64
+ return pages;
65
+ const cursor = pageInfo.startCursor;
66
+ if (!cursor || seen.has(cursor))
67
+ return null;
68
+ seen.add(cursor);
69
+ before = cursor;
70
+ }
71
+ }
72
+ async function fetchProbePage(oid, repo, before) {
73
+ try {
74
+ const { data } = await graphqlWithRateLimit(POLL_SUMMARY_ANNOTATION_PROBE_QUERY, {
75
+ owner: repo.owner,
76
+ repo: repo.name,
77
+ oid,
78
+ before,
79
+ });
80
+ const object = data.repository?.object;
81
+ if (object?.__typename !== "Commit" || object.oid !== oid)
82
+ return null;
83
+ return object.statusCheckRollup?.contexts ?? null;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
@@ -33,7 +33,7 @@ export function fingerprintRawSummaryPr(raw) {
33
33
  ...raw.commits,
34
34
  nodes: raw.commits.nodes.map((node) => ({
35
35
  ...node,
36
- commit: { ...node.commit, committedDate: undefined },
36
+ commit: { ...node.commit, committedDate: undefined, checkSuites: undefined },
37
37
  })),
38
38
  },
39
39
  };
@@ -1,4 +1,4 @@
1
1
  import type { PollSummaryCommandOptions, PollSummaryItem } from "../types.mts";
2
2
  import type { RepoInfo } from "./client.mts";
3
3
  import type { RawSummaryPr } from "./poll-summary-raw.mts";
4
- export declare function summarizePollSummaryPr(raw: RawSummaryPr, repo: RepoInfo, opts: PollSummaryCommandOptions, viewerCanAdminister?: boolean): Promise<PollSummaryItem>;
4
+ export declare function summarizePollSummaryPr(raw: RawSummaryPr, repo: RepoInfo, opts: PollSummaryCommandOptions, viewerCanAdminister?: boolean, mergeTargetContexts?: readonly string[], viewerLogin?: string | null): Promise<PollSummaryItem>;
@@ -6,15 +6,17 @@ import { loadSeenMap } from "../state/seen-comments.mjs";
6
6
  import { isReadyReceiptCurrent, readReadyReceipt } from "../state/ready-receipts.mjs";
7
7
  import { summarizePollSummaryChecks } from "./poll-summary-checks.mjs";
8
8
  import { summarizePollSummaryReview } from "./poll-summary-review.mjs";
9
- import { fingerprintRawSummaryPr } from "./poll-summary-fingerprint.mjs";
10
9
  import { currentQueueRemovalEvent } from "./poll-summary-queue-removal.mjs";
11
10
  import { isCurrentSummaryReady } from "./poll-summary-readiness.mjs";
12
11
  import { applyOpenCheckBlockers } from "./poll-summary-check-blockers.mjs";
13
12
  import { normalizePollSummaryState, routePollSummary } from "./poll-summary-route.mjs";
14
- export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdminister = false) {
13
+ import { hydrateReadyAnnotationProbe, readyFingerprint } from "./poll-summary-annotation-probe.mjs";
14
+ import { applyUnreportedRequiredChecks } from "./poll-summary-unreported.mjs";
15
+ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdminister = false, mergeTargetContexts, viewerLogin = null) {
15
16
  const repoName = `${repo.owner}/${repo.name}`;
16
17
  const seen = await loadSeenMap({ owner: repo.owner, repo: repo.name, pr: raw.number });
17
18
  const checks = summarizePollSummaryChecks(raw);
19
+ applyUnreportedRequiredChecks(raw, checks, mergeTargetContexts);
18
20
  const review = await summarizePollSummaryReview(raw, seen, viewerCanAdminister);
19
21
  const blockingReviewerInProgress = detectBlockingReviewer(raw);
20
22
  const removalEvent = currentQueueRemovalEvent(raw);
@@ -44,15 +46,17 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
44
46
  baseRefName: raw.stack.baseRefName,
45
47
  }
46
48
  : undefined;
47
- const fingerprint = opts.stackPrNumber !== undefined ? fingerprintRawSummaryPr(raw) : null;
48
- const receipt = fingerprint
49
- ? await readReadyReceipt({ owner: repo.owner, repo: repo.name, pr: raw.number })
50
- : null;
51
- // A queued PR's target branch can advance as earlier queue entries merge.
52
- // Keep the pre-enqueue base binding for the receipt comparison while the
53
- // merge group itself supplies the current mergeability evidence.
49
+ if (opts.stackPrNumber !== undefined)
50
+ await hydrateReadyAnnotationProbe(raw, repo, review);
51
+ const receipt = opts.stackPrNumber === undefined
52
+ ? null
53
+ : await readReadyReceipt({ owner: repo.owner, repo: repo.name, pr: raw.number });
54
+ const fingerprint = opts.stackPrNumber === undefined
55
+ ? null
56
+ : readyFingerprint(raw, receipt?.readinessFingerprint ?? null);
57
+ // A queued PR's base can advance. Keep the receipt base while the probe is complete.
54
58
  const receiptFingerprint = raw.isInMergeQueue && receipt
55
- ? fingerprintRawSummaryPr({ ...raw, baseRefOid: receipt.baseRefOid })
59
+ ? readyFingerprint({ ...raw, baseRefOid: receipt.baseRefOid }, receipt.readinessFingerprint)
56
60
  : fingerprint;
57
61
  const currentReady = isCurrentSummaryReady(raw, checks, review, {
58
62
  allowQueuedProgress: opts.stackPrNumber !== undefined && raw.isInMergeQueue,
@@ -78,6 +82,10 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
78
82
  repo: repoName,
79
83
  title: raw.title,
80
84
  url: raw.url || formatPrUrl(repoName, raw.number),
85
+ ...(raw.author?.login && { authorLogin: raw.author.login }),
86
+ ...(raw.author?.login &&
87
+ viewerLogin !== null &&
88
+ raw.author.login.toLowerCase() === viewerLogin.toLowerCase() && { owned: true }),
81
89
  action,
82
90
  reasons,
83
91
  state: normalizePollSummaryState(raw.state),
@@ -131,9 +139,8 @@ function pollCommandFields(repo, pr, isDraft, opts) {
131
139
  args.push("--merge");
132
140
  if (opts.readyDelaySeconds !== undefined)
133
141
  args.push("--ready-delay", `${opts.readyDelaySeconds}s`);
134
- if (opts.stallTimeoutSeconds !== undefined) {
142
+ if (opts.stallTimeoutSeconds !== undefined)
135
143
  args.push("--stall-timeout", `${opts.stallTimeoutSeconds}s`);
136
- }
137
144
  if (opts.noAutoMarkReady && !boundedDraft)
138
145
  args.push("--no-auto-mark-ready");
139
146
  const pollCommand = buildPrShepherdCommand(args).text;
@@ -1,4 +1,4 @@
1
- import { queueRemovalAppliesToHead } from "./queue-removal-freshness.mjs";
1
+ import { headPushUnixFromCheckNodes, queueRemovalAppliesToHead, } from "./queue-removal-freshness.mjs";
2
2
  import { parseCreatedAt } from "./batch-parser-helpers.mjs";
3
3
  /** Latest queue removal that still applies to this PR head, not an older attempt. */
4
4
  export function currentQueueRemovalEvent(raw) {
@@ -12,11 +12,15 @@ export function currentQueueRemovalEvent(raw) {
12
12
  if (addition && Date.parse(addition.createdAt) > removalTime)
13
13
  return null;
14
14
  const parents = removal.beforeCommit?.parents?.nodes.map((parent) => parent.oid);
15
- const headCommittedAt = raw.commits?.nodes[0]?.commit.committedDate;
15
+ const headCommit = raw.commits?.nodes[0]?.commit;
16
+ const headPushedAtUnix = headPushUnixFromCheckNodes(headCommit?.statusCheckRollup?.contexts.nodes);
16
17
  if (!queueRemovalAppliesToHead({
17
18
  parentOids: parents,
18
19
  headOid: raw.headRefOid,
19
- ...(headCommittedAt && { headCommittedAtUnix: parseCreatedAt(headCommittedAt) }),
20
+ ...(headCommit?.committedDate && {
21
+ headCommittedAtUnix: parseCreatedAt(headCommit.committedDate),
22
+ }),
23
+ ...(headPushedAtUnix !== undefined && { headPushedAtUnix }),
20
24
  removedAtUnix: Math.floor(removalTime / 1000),
21
25
  })) {
22
26
  return null;