pr-shepherd 0.54.1 → 0.55.1

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 (121) 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 +32 -9
  17. package/bin/cli/iterate-instructions.mjs +6 -0
  18. package/bin/cli/iterate-lean.mjs +17 -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 +28 -0
  27. package/bin/commands/check-unreported.mjs +101 -0
  28. package/bin/commands/check.mjs +45 -38
  29. package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
  30. package/bin/commands/commit-suggestion-instruction.mjs +1 -1
  31. package/bin/commands/iterate/base.mjs +11 -0
  32. package/bin/commands/iterate/check-instructions.d.mts +1 -1
  33. package/bin/commands/iterate/check-instructions.mjs +1 -1
  34. package/bin/commands/iterate/escalate.mjs +4 -0
  35. package/bin/commands/iterate/index.mjs +69 -3
  36. package/bin/commands/iterate/merge-state.mjs +12 -12
  37. package/bin/commands/iterate/merge.d.mts +12 -2
  38. package/bin/commands/iterate/merge.mjs +32 -2
  39. package/bin/commands/iterate/unreported-required.d.mts +18 -0
  40. package/bin/commands/iterate/unreported-required.mjs +136 -0
  41. package/bin/commands/poll-summary.mjs +1 -0
  42. package/bin/commands/poll.mjs +9 -2
  43. package/bin/commands/ready-mergeability.d.mts +1 -1
  44. package/bin/commands/ready-mergeability.mjs +2 -2
  45. package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
  46. package/bin/commands/rule-auto-resolve-format.mjs +151 -0
  47. package/bin/commands/rule-auto-resolve.d.mts +21 -0
  48. package/bin/commands/rule-auto-resolve.mjs +113 -0
  49. package/bin/commands/shepherd-journal.d.mts +1 -1
  50. package/bin/commands/shepherd-journal.mjs +1 -1
  51. package/bin/commands/stack-drain.d.mts +0 -5
  52. package/bin/commands/stack-drain.mjs +26 -16
  53. package/bin/commands/stack-merge-flag.d.mts +7 -0
  54. package/bin/commands/stack-merge-flag.mjs +6 -0
  55. package/bin/config/load.d.mts +3 -0
  56. package/bin/config/load.mjs +9 -2
  57. package/bin/config/merge-method.d.mts +27 -0
  58. package/bin/config/merge-method.mjs +56 -0
  59. package/bin/exit-codes.d.mts +1 -1
  60. package/bin/exit-codes.mjs +2 -1
  61. package/bin/github/batch-parse-suites.d.mts +3 -0
  62. package/bin/github/batch-parse-suites.mjs +15 -0
  63. package/bin/github/batch-parsers.d.mts +1 -1
  64. package/bin/github/batch-parsers.mjs +7 -0
  65. package/bin/github/batch-raw-types.d.mts +5 -0
  66. package/bin/github/batch.d.mts +3 -0
  67. package/bin/github/batch.mjs +9 -1
  68. package/bin/github/errors.d.mts +6 -0
  69. package/bin/github/errors.mjs +23 -5
  70. package/bin/github/fingerprint-fields.d.mts +1 -0
  71. package/bin/github/fingerprint-fields.mjs +1 -1
  72. package/bin/github/gql/base-behind.gql +20 -0
  73. package/bin/github/gql/batch-pr-page.gql +2 -0
  74. package/bin/github/gql/batch-pr.gql +5 -0
  75. package/bin/github/gql/commit-check-suites.gql +1 -0
  76. package/bin/github/gql/poll-stack-summary.gql +6 -0
  77. package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
  78. package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
  79. package/bin/github/gql/poll-summary-fragment.gql +16 -0
  80. package/bin/github/gql/pr-merge-policy.gql +1 -44
  81. package/bin/github/gql/ref-rules-query.gql +18 -0
  82. package/bin/github/gql/ref-rules.gql +46 -0
  83. package/bin/github/graphql-internal-retry.d.mts +1 -1
  84. package/bin/github/graphql-internal-retry.mjs +4 -3
  85. package/bin/github/merge-queue-checks.mjs +7 -3
  86. package/bin/github/merge-target-rules.d.mts +28 -0
  87. package/bin/github/merge-target-rules.mjs +77 -0
  88. package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
  89. package/bin/github/poll-summary-annotation-probe.mjs +88 -0
  90. package/bin/github/poll-summary-fingerprint.mjs +1 -1
  91. package/bin/github/poll-summary-projector.d.mts +1 -1
  92. package/bin/github/poll-summary-projector.mjs +19 -12
  93. package/bin/github/poll-summary-queue-removal.mjs +7 -3
  94. package/bin/github/poll-summary-raw.d.mts +16 -0
  95. package/bin/github/poll-summary-readiness.mjs +1 -0
  96. package/bin/github/poll-summary-route.mjs +5 -0
  97. package/bin/github/poll-summary-unreported.d.mts +6 -0
  98. package/bin/github/poll-summary-unreported.mjs +51 -0
  99. package/bin/github/poll-summary.d.mts +1 -0
  100. package/bin/github/poll-summary.mjs +28 -3
  101. package/bin/github/queries.d.mts +9 -0
  102. package/bin/github/queries.mjs +11 -2
  103. package/bin/github/queue-removal-freshness.d.mts +20 -5
  104. package/bin/github/queue-removal-freshness.mjs +37 -10
  105. package/bin/github/stack-read.d.mts +4 -0
  106. package/bin/github/stack-read.mjs +13 -1
  107. package/bin/mcp/server.mjs +6 -1
  108. package/bin/state/ci-retrigger.d.mts +25 -0
  109. package/bin/state/ci-retrigger.mjs +48 -0
  110. package/bin/types/escalate.d.mts +1 -1
  111. package/bin/types/github.d.mts +11 -0
  112. package/bin/types/iterate.d.mts +16 -3
  113. package/bin/types/poll-summary.d.mts +13 -0
  114. package/bin/types/report.d.mts +34 -1
  115. package/package.json +1 -1
  116. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  117. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  118. package/plugins/pr-shepherd/.mcp.json +1 -1
  119. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +3 -3
  120. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
  121. package/src/classify/types.mts +1 -1
@@ -1,8 +1,10 @@
1
1
  import { mapAuthorType, parseCreatedAt, latestApprovedLogins, isReviewStale, } from "./batch-parser-helpers.mjs";
2
2
  import { parseCheckNodes } from "./batch-parse-checks.mjs";
3
+ import { headPushUnixFromCheckNodes } from "./queue-removal-freshness.mjs";
3
4
  import { requireContextNodes } from "./batch-response.mjs";
4
5
  import { buildPrActivitySummary } from "./activity.mjs";
5
6
  import { parseBranchProtection } from "./branch-protection.mjs";
7
+ import { readAllowedMergeMethods } from "../config/merge-method.mjs";
6
8
  import { parseAutoMergeRequest, parseBranchRules, parseLatestMergeQueueRemoval, parseMergeQueueEntry, parseStack, } from "./batch-parsers-rules.mjs";
7
9
  function parseReviewNode(r) {
8
10
  const base = {
@@ -13,6 +15,7 @@ function parseReviewNode(r) {
13
15
  viewerCanMinimize: "viewerCanMinimize" in r && r.viewerCanMinimize === true,
14
16
  body: r.body,
15
17
  createdAtUnix: r.createdAt ? parseCreatedAt(r.createdAt) : 0,
18
+ ...("url" in r && r.url ? { url: r.url } : {}),
16
19
  };
17
20
  if ("commit" in r && r.commit?.oid) {
18
21
  base.commitOid = r.commit.oid;
@@ -100,9 +103,11 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
100
103
  const mergeQueueChecks = parseCheckNodes(queueHead?.statusCheckRollup
101
104
  ? requireContextNodes(queueHead.statusCheckRollup.contexts.nodes)
102
105
  : undefined, queueHead?.oid);
106
+ const headPushedAtUnix = headPushUnixFromCheckNodes(rawCheckNodes);
103
107
  const removedMergeQueueChecks = parseCheckNodes(removedQueueHead?.statusCheckRollup
104
108
  ? requireContextNodes(removedQueueHead.statusCheckRollup.contexts.nodes)
105
109
  : undefined, removedQueueHead?.oid);
110
+ const allowedMergeMethods = readAllowedMergeMethods(repository);
106
111
  return {
107
112
  nodeId: raw.id,
108
113
  number: raw.number,
@@ -112,6 +117,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
112
117
  mergeStateStatus: raw.mergeStateStatus,
113
118
  reviewDecision: (raw.reviewDecision ?? null),
114
119
  headRefOid: raw.headRefOid,
120
+ ...(headPushedAtUnix !== undefined && { headPushedAtUnix }),
115
121
  headRefName: raw.headRefName,
116
122
  headRepoWithOwner: raw.headRepository?.nameWithOwner ?? null,
117
123
  viewerAuthorization: {
@@ -135,6 +141,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
135
141
  approvedReviews,
136
142
  checks,
137
143
  branchProtection: parseBranchProtection(raw),
144
+ ...(allowedMergeMethods && { allowedMergeMethods }),
138
145
  branchRules: parseBranchRules(raw.baseRef),
139
146
  isInMergeQueue: raw.isInMergeQueue ?? false,
140
147
  isMergeQueueEnabled: raw.isMergeQueueEnabled ?? false,
@@ -7,6 +7,9 @@ export interface RawBatchResponse {
7
7
  repository: {
8
8
  viewerPermission: string | null;
9
9
  viewerCanAdminister: boolean;
10
+ mergeCommitAllowed?: boolean;
11
+ squashMergeAllowed?: boolean;
12
+ rebaseMergeAllowed?: boolean;
10
13
  pullRequest: RawPr | null;
11
14
  } | null;
12
15
  }
@@ -118,6 +121,7 @@ interface RawCheckSuites {
118
121
  };
119
122
  nodes: Array<{
120
123
  id?: string;
124
+ status?: string | null;
121
125
  conclusion: string | null;
122
126
  workflowRun: {
123
127
  databaseId: number | null;
@@ -208,6 +212,7 @@ export interface RawReviewSummary {
208
212
  author: RawAuthor | null;
209
213
  body: string;
210
214
  createdAt?: string;
215
+ url?: string;
211
216
  }
212
217
  export type RawContextNode = {
213
218
  __typename: "CheckRun";
@@ -1,4 +1,5 @@
1
1
  import { type RateLimitInfo, type RepoInfo } from "./client.mts";
2
+ import type { WorkflowSuiteSnapshot } from "../checks/unreported-required.mts";
2
3
  import type { BatchPrData } from "../types.mts";
3
4
  import { type PrFingerprint } from "./fingerprint.mts";
4
5
  interface BatchResult {
@@ -9,6 +10,8 @@ interface BatchResult {
9
10
  checkSuitesComplete?: boolean;
10
11
  /** True when that complete page listed no check suites on the head commit. */
11
12
  headCheckSuitesEmpty?: true;
13
+ /** Actions workflow suites on the head, excluding apps that have no workflow run. */
14
+ headWorkflowSuites?: WorkflowSuiteSnapshot[];
12
15
  }
13
16
  interface FetchPrBatchOptions {
14
17
  /**
@@ -2,7 +2,7 @@ import { graphqlWithRateLimit } from "./client.mjs";
2
2
  import { hydrateThreadCommentPages } from "./thread-comments.mjs";
3
3
  import { BATCH_PR_QUERY } from "./queries.mjs";
4
4
  import { parseRawPr } from "./batch-parsers.mjs";
5
- import { parseCheckSuitesComplete, parseHeadCheckSuitesEmpty, parseSuiteStartupFailures, } from "./batch-parse-suites.mjs";
5
+ import { parseCheckSuitesComplete, parseHeadCheckSuitesEmpty, parseHeadWorkflowSuites, parseSuiteStartupFailures, } from "./batch-parse-suites.mjs";
6
6
  import { mergeStartupFailureChecks } from "../checks/startup-failures.mjs";
7
7
  import { paginateBatchConnections } from "./batch-page.mjs";
8
8
  import { requireRawPr } from "./batch-response.mjs";
@@ -22,6 +22,9 @@ export async function fetchPrBatch(pr, repo, opts = {}) {
22
22
  const paged = await paginateBatchConnections(pr, repo, raw, opts, result.rateLimit);
23
23
  const rawThreadPages = await hydrateThreadCommentPages(paged.threads);
24
24
  const data = parseRawPr(raw, rawThreadPages, paged.comments, paged.changesRequested, paged.reviewSummaries, paged.approvedReviews, paged.checks, result.data.repository);
25
+ const viewerLogin = result.data.viewer?.login;
26
+ if (viewerLogin)
27
+ data.viewerLogin = viewerLogin;
25
28
  data.checks = mergeStartupFailureChecks(data.checks, parseSuiteStartupFailures(raw));
26
29
  return {
27
30
  data,
@@ -29,5 +32,10 @@ export async function fetchPrBatch(pr, repo, opts = {}) {
29
32
  rateLimit: paged.rateLimit ?? result.rateLimit,
30
33
  ...(parseCheckSuitesComplete(raw) && { checkSuitesComplete: true }),
31
34
  ...(parseHeadCheckSuitesEmpty(raw) && { headCheckSuitesEmpty: true }),
35
+ ...workflowSuites(raw),
32
36
  };
33
37
  }
38
+ function workflowSuites(raw) {
39
+ const suites = parseHeadWorkflowSuites(raw);
40
+ return suites.length > 0 ? { headWorkflowSuites: suites } : {};
41
+ }
@@ -10,6 +10,12 @@ export interface GitHubGraphQlError {
10
10
  }
11
11
  /** GitHub GraphQL engine crash: HTTP 200, `data: null`, INTERNAL type/code or message. */
12
12
  export declare function isRetryableGraphQlInternal(graphqlErrors?: GitHubGraphQlError[]): boolean;
13
+ /**
14
+ * GitHub refused the query for size or load (`Resource limits for this query
15
+ * exceeded`). The same document can succeed on a later attempt, and a smaller
16
+ * page can succeed when the full one cannot.
17
+ */
18
+ export declare function isRetryableGraphQlResourceLimit(graphqlErrors?: GitHubGraphQlError[]): boolean;
13
19
  /** HTTP 200 GraphQL response whose `repository` field resolved to null. */
14
20
  export declare function missingRepositoryError(repo: {
15
21
  owner: string;
@@ -6,8 +6,9 @@ import { EXIT, ShepherdError } from "../exit-codes.mjs";
6
6
  // GraphQL error messages themselves.
7
7
  const GRAPHQL_PERMISSION_ERROR = /resource not accessible/i;
8
8
  const GRAPHQL_INTERNAL_MESSAGE = /something went wrong while executing your query/i;
9
- function isInternalToken(value) {
10
- return typeof value === "string" && value.toUpperCase() === "INTERNAL";
9
+ const GRAPHQL_RESOURCE_LIMIT_MESSAGE = /resource limits for this query exceeded/i;
10
+ function isToken(value, token) {
11
+ return typeof value === "string" && value.toUpperCase() === token;
11
12
  }
12
13
  function extensionsCode(extensions) {
13
14
  if (typeof extensions !== "object" || extensions === null || Array.isArray(extensions)) {
@@ -23,13 +24,29 @@ export function isRetryableGraphQlInternal(graphqlErrors) {
23
24
  if (!graphqlErrors?.length)
24
25
  return false;
25
26
  return graphqlErrors.some((error) => {
26
- if (isInternalToken(error.type))
27
+ if (isToken(error.type, "INTERNAL"))
27
28
  return true;
28
- if (isInternalToken(extensionsCode(error.extensions)))
29
+ if (isToken(extensionsCode(error.extensions), "INTERNAL"))
29
30
  return true;
30
31
  return GRAPHQL_INTERNAL_MESSAGE.test(error.message);
31
32
  });
32
33
  }
34
+ /**
35
+ * GitHub refused the query for size or load (`Resource limits for this query
36
+ * exceeded`). The same document can succeed on a later attempt, and a smaller
37
+ * page can succeed when the full one cannot.
38
+ */
39
+ export function isRetryableGraphQlResourceLimit(graphqlErrors) {
40
+ if (!graphqlErrors?.length)
41
+ return false;
42
+ return graphqlErrors.some((error) => {
43
+ if (isToken(error.type, "RESOURCE_LIMITS_EXCEEDED"))
44
+ return true;
45
+ if (isToken(extensionsCode(error.extensions), "RESOURCE_LIMITS_EXCEEDED"))
46
+ return true;
47
+ return GRAPHQL_RESOURCE_LIMIT_MESSAGE.test(error.message);
48
+ });
49
+ }
33
50
  function classifyStatus(status, rateLimit, retryAfterSeconds, graphqlErrors) {
34
51
  // Retry signals take priority over everything else: GitHub's secondary rate limit
35
52
  // returns 403 with a Retry-After header, which is a transient throttle — not the
@@ -40,7 +57,8 @@ function classifyStatus(status, rateLimit, retryAfterSeconds, graphqlErrors) {
40
57
  status >= 500 ||
41
58
  retryAfterSeconds !== undefined ||
42
59
  rateLimitExhausted ||
43
- isRetryableGraphQlInternal(graphqlErrors)) {
60
+ isRetryableGraphQlInternal(graphqlErrors) ||
61
+ isRetryableGraphQlResourceLimit(graphqlErrors)) {
44
62
  return EXIT.TEMPFAIL;
45
63
  }
46
64
  if (status === 401 || status === 403 || hasPermissionError(graphqlErrors))
@@ -5,6 +5,7 @@ export interface FingerprintSuites {
5
5
  };
6
6
  nodes?: Array<{
7
7
  id?: string;
8
+ status?: string | null;
8
9
  conclusion: string | null;
9
10
  workflowRun?: {
10
11
  databaseId: number | null;
@@ -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
  };
@@ -0,0 +1,20 @@
1
+ # Commits on the PR base that the head branch does not contain.
2
+ # Fetched only when required checks never reported, so a BLOCKED merge state
3
+ # cannot hide a behind base.
4
+ query BaseBehind($owner: String!, $repo: String!, $qualifiedName: String!, $headRef: String!) {
5
+ _shepherdRateLimit: rateLimit {
6
+ cost
7
+ limit
8
+ nodeCount
9
+ remaining
10
+ resetAt
11
+ used
12
+ }
13
+ repository(owner: $owner, name: $repo) {
14
+ ref(qualifiedName: $qualifiedName) {
15
+ compare(headRef: $headRef) {
16
+ behindBy
17
+ }
18
+ }
19
+ }
20
+ }
@@ -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,28 @@
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>;
21
+ /**
22
+ * Commits on the PR base that `headRef` does not contain.
23
+ * Pass the head commit OID. A fork's branch name can exist on the base
24
+ * repository, or fail to resolve, and either result hides a real behind count.
25
+ * `mergeStateStatus` stays `BLOCKED` when a conversation or an expected check
26
+ * is also open, so this compare is the behind count that status hides.
27
+ */
28
+ export declare function loadBaseBehindBy(owner: string, name: string, baseRefName: string, headRef: string): Promise<number>;
@@ -0,0 +1,77 @@
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 { BASE_BEHIND_QUERY, 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
+ /**
38
+ * Commits on the PR base that `headRef` does not contain.
39
+ * Pass the head commit OID. A fork's branch name can exist on the base
40
+ * repository, or fail to resolve, and either result hides a real behind count.
41
+ * `mergeStateStatus` stays `BLOCKED` when a conversation or an expected check
42
+ * is also open, so this compare is the behind count that status hides.
43
+ */
44
+ export async function loadBaseBehindBy(owner, name, baseRefName, headRef) {
45
+ const qualifiedName = `refs/heads/${baseRefName}`;
46
+ const { data } = await graphqlWithRateLimit(BASE_BEHIND_QUERY, {
47
+ owner,
48
+ repo: name,
49
+ qualifiedName,
50
+ headRef,
51
+ });
52
+ if (!data.repository)
53
+ throw missingRepositoryError({ owner, name });
54
+ const ref = data.repository.ref;
55
+ if (!ref) {
56
+ throw new ShepherdError(`Branch ${qualifiedName} was not found in ${owner}/${name}`, EXIT.TEMPFAIL);
57
+ }
58
+ return ref.compare?.behindBy ?? 0;
59
+ }
60
+ async function fetchRefRules(owner, name, qualifiedName, headRef) {
61
+ const { data } = await graphqlWithRateLimit(REF_RULES_QUERY, {
62
+ owner,
63
+ repo: name,
64
+ qualifiedName,
65
+ headRef,
66
+ });
67
+ if (!data.repository)
68
+ throw missingRepositoryError({ owner, name });
69
+ const ref = data.repository.ref;
70
+ if (!ref) {
71
+ throw new ShepherdError(`Branch ${qualifiedName} was not found in ${owner}/${name}`, EXIT.TEMPFAIL);
72
+ }
73
+ return {
74
+ contexts: parseBranchRules(ref).requiredStatusCheckContexts,
75
+ behindBy: ref.compare?.behindBy ?? 0,
76
+ };
77
+ }