pr-shepherd 0.54.0 → 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 (126) 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-branch-segment.d.mts +3 -0
  17. package/bin/cli/iterate-branch-segment.mjs +13 -0
  18. package/bin/cli/iterate-formatter.mjs +34 -22
  19. package/bin/cli/iterate-instructions.mjs +6 -0
  20. package/bin/cli/iterate-lean.mjs +16 -0
  21. package/bin/cli/poll-summary-emitter.mjs +4 -1
  22. package/bin/cli/poll-summary-formatter.mjs +10 -3
  23. package/bin/cli/stack-overview.d.mts +48 -0
  24. package/bin/cli/stack-overview.mjs +138 -0
  25. package/bin/commands/check-fingerprint.mjs +2 -1
  26. package/bin/commands/check-status.d.mts +1 -1
  27. package/bin/commands/check-status.mjs +8 -3
  28. package/bin/commands/check-unreported.d.mts +26 -0
  29. package/bin/commands/check-unreported.mjs +73 -0
  30. package/bin/commands/check.mjs +45 -38
  31. package/bin/commands/iterate/base.mjs +9 -0
  32. package/bin/commands/iterate/conflicting-head-ci.d.mts +18 -0
  33. package/bin/commands/iterate/conflicting-head-ci.mjs +32 -0
  34. package/bin/commands/iterate/escalate.mjs +4 -0
  35. package/bin/commands/iterate/fix-code.mjs +30 -1
  36. package/bin/commands/iterate/index.mjs +69 -3
  37. package/bin/commands/iterate/merge-state.mjs +12 -12
  38. package/bin/commands/iterate/merge.d.mts +12 -2
  39. package/bin/commands/iterate/merge.mjs +32 -2
  40. package/bin/commands/iterate/native-stack-rebase.d.mts +8 -2
  41. package/bin/commands/iterate/native-stack-rebase.mjs +14 -4
  42. package/bin/commands/iterate/stack-trunk-conflict.d.mts +13 -0
  43. package/bin/commands/iterate/stack-trunk-conflict.mjs +51 -0
  44. package/bin/commands/iterate/unreported-required.d.mts +18 -0
  45. package/bin/commands/iterate/unreported-required.mjs +122 -0
  46. package/bin/commands/poll-summary.mjs +1 -0
  47. package/bin/commands/poll.mjs +9 -2
  48. package/bin/commands/ready-mergeability.d.mts +1 -1
  49. package/bin/commands/ready-mergeability.mjs +2 -2
  50. package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
  51. package/bin/commands/rule-auto-resolve-format.mjs +151 -0
  52. package/bin/commands/rule-auto-resolve.d.mts +21 -0
  53. package/bin/commands/rule-auto-resolve.mjs +113 -0
  54. package/bin/commands/stack-drain.d.mts +0 -5
  55. package/bin/commands/stack-drain.mjs +26 -16
  56. package/bin/commands/stack-merge-flag.d.mts +7 -0
  57. package/bin/commands/stack-merge-flag.mjs +6 -0
  58. package/bin/config/load.d.mts +3 -0
  59. package/bin/config/load.mjs +9 -2
  60. package/bin/config/merge-method.d.mts +27 -0
  61. package/bin/config/merge-method.mjs +56 -0
  62. package/bin/exit-codes.d.mts +1 -1
  63. package/bin/exit-codes.mjs +2 -1
  64. package/bin/github/batch-parse-suites.d.mts +5 -0
  65. package/bin/github/batch-parse-suites.mjs +20 -0
  66. package/bin/github/batch-parsers.d.mts +1 -1
  67. package/bin/github/batch-parsers.mjs +7 -0
  68. package/bin/github/batch-raw-types.d.mts +5 -0
  69. package/bin/github/batch.d.mts +5 -0
  70. package/bin/github/batch.mjs +10 -1
  71. package/bin/github/errors.d.mts +6 -0
  72. package/bin/github/errors.mjs +23 -5
  73. package/bin/github/fingerprint-fields.d.mts +1 -0
  74. package/bin/github/fingerprint-fields.mjs +1 -1
  75. package/bin/github/gql/batch-pr-page.gql +2 -0
  76. package/bin/github/gql/batch-pr.gql +5 -0
  77. package/bin/github/gql/commit-check-suites.gql +1 -0
  78. package/bin/github/gql/poll-stack-summary.gql +6 -0
  79. package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
  80. package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
  81. package/bin/github/gql/poll-summary-fragment.gql +16 -0
  82. package/bin/github/gql/pr-merge-policy.gql +1 -44
  83. package/bin/github/gql/ref-rules-query.gql +18 -0
  84. package/bin/github/gql/ref-rules.gql +46 -0
  85. package/bin/github/gql/upper-layer-conflict-target.gql +36 -0
  86. package/bin/github/graphql-internal-retry.d.mts +1 -1
  87. package/bin/github/graphql-internal-retry.mjs +4 -3
  88. package/bin/github/merge-queue-checks.mjs +7 -3
  89. package/bin/github/merge-target-rules.d.mts +20 -0
  90. package/bin/github/merge-target-rules.mjs +54 -0
  91. package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
  92. package/bin/github/poll-summary-annotation-probe.mjs +88 -0
  93. package/bin/github/poll-summary-fingerprint.mjs +1 -1
  94. package/bin/github/poll-summary-projector.d.mts +1 -1
  95. package/bin/github/poll-summary-projector.mjs +19 -12
  96. package/bin/github/poll-summary-queue-removal.mjs +7 -3
  97. package/bin/github/poll-summary-raw.d.mts +16 -0
  98. package/bin/github/poll-summary-readiness.mjs +1 -0
  99. package/bin/github/poll-summary-route.mjs +5 -0
  100. package/bin/github/poll-summary-unreported.d.mts +6 -0
  101. package/bin/github/poll-summary-unreported.mjs +51 -0
  102. package/bin/github/poll-summary.d.mts +1 -0
  103. package/bin/github/poll-summary.mjs +28 -3
  104. package/bin/github/queries.d.mts +12 -0
  105. package/bin/github/queries.mjs +14 -2
  106. package/bin/github/queue-removal-freshness.d.mts +20 -5
  107. package/bin/github/queue-removal-freshness.mjs +37 -10
  108. package/bin/github/stack-read.d.mts +4 -0
  109. package/bin/github/stack-read.mjs +13 -1
  110. package/bin/mcp/server.mjs +6 -1
  111. package/bin/state/ci-retrigger.d.mts +25 -0
  112. package/bin/state/ci-retrigger.mjs +48 -0
  113. package/bin/state/conflicting-head-seen.d.mts +13 -0
  114. package/bin/state/conflicting-head-seen.mjs +44 -0
  115. package/bin/types/escalate.d.mts +1 -1
  116. package/bin/types/github.d.mts +11 -0
  117. package/bin/types/iterate.d.mts +19 -3
  118. package/bin/types/poll-summary.d.mts +13 -0
  119. package/bin/types/report.d.mts +34 -1
  120. package/package.json +1 -1
  121. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  122. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  123. package/plugins/pr-shepherd/.mcp.json +1 -1
  124. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +2 -2
  125. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
  126. package/src/classify/types.mts +1 -1
@@ -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
+ }
@@ -0,0 +1,36 @@
1
+ query UpperLayerConflictTarget($owner: String!, $name: String!, $number: Int!, $headRef: String!) {
2
+ _shepherdRateLimit: rateLimit {
3
+ cost
4
+ limit
5
+ nodeCount
6
+ remaining
7
+ resetAt
8
+ used
9
+ }
10
+ repository(owner: $owner, name: $name) {
11
+ pullRequest(number: $number) {
12
+ # compare() is invoked on the PR base. behindBy is commits on that base
13
+ # that the head does not contain — the same direction as REST base...head.
14
+ baseRef {
15
+ compare(headRef: $headRef) {
16
+ behindBy
17
+ }
18
+ }
19
+ stack {
20
+ entries(first: 100) {
21
+ pageInfo {
22
+ hasNextPage
23
+ }
24
+ nodes {
25
+ position
26
+ pullRequest {
27
+ number
28
+ state
29
+ baseRefName
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
@@ -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;
@@ -1,3 +1,4 @@
1
+ import type { RawBaseRef } from "./batch-raw-rules.mts";
1
2
  export interface RawAuthor {
2
3
  __typename?: string;
3
4
  login: string;
@@ -29,9 +30,11 @@ type RawCheckContext = {
29
30
  totalCount: number;
30
31
  };
31
32
  checkSuite: {
33
+ createdAt?: string | null;
32
34
  workflowRun: {
33
35
  databaseId?: string | number;
34
36
  event: string;
37
+ createdAt?: string | null;
35
38
  workflow?: {
36
39
  name: string;
37
40
  databaseId: string | number;
@@ -60,11 +63,23 @@ export interface RawSummaryCommit {
60
63
  oid: string;
61
64
  committedDate?: string;
62
65
  statusCheckRollup: RawCheckRollup | null;
66
+ checkSuites?: {
67
+ nodes: Array<{
68
+ status?: string | null;
69
+ conclusion: string | null;
70
+ workflowRun: {
71
+ event: string | null;
72
+ } | null;
73
+ }>;
74
+ } | null;
63
75
  }
64
76
  export interface RawSummaryPr {
65
77
  number: number;
66
78
  title: string;
67
79
  url: string;
80
+ author?: {
81
+ login: string;
82
+ } | null;
68
83
  state: string;
69
84
  updatedAt?: string;
70
85
  lifecycleEvents?: {
@@ -79,6 +94,7 @@ export interface RawSummaryPr {
79
94
  headRefOid: string;
80
95
  baseRefOid: string;
81
96
  baseRefName: string;
97
+ baseRef?: RawBaseRef | null;
82
98
  mergeable: string;
83
99
  mergeStateStatus: string;
84
100
  reviewDecision: string | null;
@@ -21,5 +21,6 @@ export function isCurrentSummaryReady(raw, checks, review, options = {}) {
21
21
  (checks.failing ?? 0) === 0 &&
22
22
  (sourceChecks.failing ?? 0) === 0 &&
23
23
  (sourceChecks.inProgress ?? 0) === 0 &&
24
+ (checks.unreportedRequired?.length ?? 0) === 0 &&
24
25
  (review.actionable ?? 0) === 0);
25
26
  }
@@ -16,6 +16,11 @@ export function routePollSummary(raw, checks, review, opts) {
16
16
  }
17
17
  return { action: "fix_code", reasons: ["review-work"] };
18
18
  }
19
+ if ((checks.unreportedRequired?.length ?? 0) > 0 &&
20
+ checks.actionsWorkflowInProgress !== true &&
21
+ (checks.inProgress ?? 0) === 0) {
22
+ return { action: "fix_code", reasons: ["unreported-required-checks"] };
23
+ }
19
24
  if ((checks.inProgress ?? 0) > 0 ||
20
25
  raw.mergeable === "UNKNOWN" ||
21
26
  raw.mergeStateStatus === "UNKNOWN" ||
@@ -0,0 +1,6 @@
1
+ import type { PollSummaryChecks } from "../types.mts";
2
+ import type { RawSummaryPr } from "./poll-summary-raw.mts";
3
+ /** Required contexts from the open layer whose base is the stack trunk. */
4
+ export declare function trunkRequiredContexts(ordered: readonly RawSummaryPr[]): string[] | undefined;
5
+ /** Record missing required contexts. A running Actions workflow does not clear the names. */
6
+ export declare function applyUnreportedRequiredChecks(raw: RawSummaryPr, checks: PollSummaryChecks, mergeTargetContexts: readonly string[] | undefined): void;