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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/bin/checks/unreported-required.d.mts +29 -0
- package/bin/checks/unreported-required.mjs +52 -0
- package/bin/classify/apply.d.mts +2 -0
- package/bin/classify/apply.mjs +31 -31
- package/bin/classify/rule-action.d.mts +8 -0
- package/bin/classify/rule-action.mjs +29 -0
- package/bin/classify/types.d.mts +1 -1
- package/bin/cli/help-command-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +6 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +3 -3
- package/bin/cli/iterate-formatter.mjs +32 -9
- package/bin/cli/iterate-instructions.mjs +6 -0
- package/bin/cli/iterate-lean.mjs +17 -0
- package/bin/cli/poll-summary-emitter.mjs +4 -1
- package/bin/cli/poll-summary-formatter.mjs +10 -3
- package/bin/cli/stack-overview.d.mts +48 -0
- package/bin/cli/stack-overview.mjs +138 -0
- package/bin/commands/check-fingerprint.mjs +2 -1
- package/bin/commands/check-status.d.mts +1 -1
- package/bin/commands/check-status.mjs +8 -3
- package/bin/commands/check-unreported.d.mts +28 -0
- package/bin/commands/check-unreported.mjs +101 -0
- package/bin/commands/check.mjs +45 -38
- package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
- package/bin/commands/commit-suggestion-instruction.mjs +1 -1
- package/bin/commands/iterate/base.mjs +11 -0
- package/bin/commands/iterate/check-instructions.d.mts +1 -1
- package/bin/commands/iterate/check-instructions.mjs +1 -1
- package/bin/commands/iterate/escalate.mjs +4 -0
- package/bin/commands/iterate/index.mjs +69 -3
- package/bin/commands/iterate/merge-state.mjs +12 -12
- package/bin/commands/iterate/merge.d.mts +12 -2
- package/bin/commands/iterate/merge.mjs +32 -2
- package/bin/commands/iterate/unreported-required.d.mts +18 -0
- package/bin/commands/iterate/unreported-required.mjs +136 -0
- package/bin/commands/poll-summary.mjs +1 -0
- package/bin/commands/poll.mjs +9 -2
- package/bin/commands/ready-mergeability.d.mts +1 -1
- package/bin/commands/ready-mergeability.mjs +2 -2
- package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
- package/bin/commands/rule-auto-resolve-format.mjs +151 -0
- package/bin/commands/rule-auto-resolve.d.mts +21 -0
- package/bin/commands/rule-auto-resolve.mjs +113 -0
- package/bin/commands/shepherd-journal.d.mts +1 -1
- package/bin/commands/shepherd-journal.mjs +1 -1
- package/bin/commands/stack-drain.d.mts +0 -5
- package/bin/commands/stack-drain.mjs +26 -16
- package/bin/commands/stack-merge-flag.d.mts +7 -0
- package/bin/commands/stack-merge-flag.mjs +6 -0
- package/bin/config/load.d.mts +3 -0
- package/bin/config/load.mjs +9 -2
- package/bin/config/merge-method.d.mts +27 -0
- package/bin/config/merge-method.mjs +56 -0
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +2 -1
- package/bin/github/batch-parse-suites.d.mts +3 -0
- package/bin/github/batch-parse-suites.mjs +15 -0
- package/bin/github/batch-parsers.d.mts +1 -1
- package/bin/github/batch-parsers.mjs +7 -0
- package/bin/github/batch-raw-types.d.mts +5 -0
- package/bin/github/batch.d.mts +3 -0
- package/bin/github/batch.mjs +9 -1
- package/bin/github/errors.d.mts +6 -0
- package/bin/github/errors.mjs +23 -5
- package/bin/github/fingerprint-fields.d.mts +1 -0
- package/bin/github/fingerprint-fields.mjs +1 -1
- package/bin/github/gql/base-behind.gql +20 -0
- package/bin/github/gql/batch-pr-page.gql +2 -0
- package/bin/github/gql/batch-pr.gql +5 -0
- package/bin/github/gql/commit-check-suites.gql +1 -0
- package/bin/github/gql/poll-stack-summary.gql +6 -0
- package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
- package/bin/github/gql/poll-summary-fragment.gql +16 -0
- package/bin/github/gql/pr-merge-policy.gql +1 -44
- package/bin/github/gql/ref-rules-query.gql +18 -0
- package/bin/github/gql/ref-rules.gql +46 -0
- package/bin/github/graphql-internal-retry.d.mts +1 -1
- package/bin/github/graphql-internal-retry.mjs +4 -3
- package/bin/github/merge-queue-checks.mjs +7 -3
- package/bin/github/merge-target-rules.d.mts +28 -0
- package/bin/github/merge-target-rules.mjs +77 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
- package/bin/github/poll-summary-annotation-probe.mjs +88 -0
- package/bin/github/poll-summary-fingerprint.mjs +1 -1
- package/bin/github/poll-summary-projector.d.mts +1 -1
- package/bin/github/poll-summary-projector.mjs +19 -12
- package/bin/github/poll-summary-queue-removal.mjs +7 -3
- package/bin/github/poll-summary-raw.d.mts +16 -0
- package/bin/github/poll-summary-readiness.mjs +1 -0
- package/bin/github/poll-summary-route.mjs +5 -0
- package/bin/github/poll-summary-unreported.d.mts +6 -0
- package/bin/github/poll-summary-unreported.mjs +51 -0
- package/bin/github/poll-summary.d.mts +1 -0
- package/bin/github/poll-summary.mjs +28 -3
- package/bin/github/queries.d.mts +9 -0
- package/bin/github/queries.mjs +11 -2
- package/bin/github/queue-removal-freshness.d.mts +20 -5
- package/bin/github/queue-removal-freshness.mjs +37 -10
- package/bin/github/stack-read.d.mts +4 -0
- package/bin/github/stack-read.mjs +13 -1
- package/bin/mcp/server.mjs +6 -1
- package/bin/state/ci-retrigger.d.mts +25 -0
- package/bin/state/ci-retrigger.mjs +48 -0
- package/bin/types/escalate.d.mts +1 -1
- package/bin/types/github.d.mts +11 -0
- package/bin/types/iterate.d.mts +16 -3
- package/bin/types/poll-summary.d.mts +13 -0
- package/bin/types/report.d.mts +34 -1
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +3 -3
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
- 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";
|
package/bin/github/batch.d.mts
CHANGED
|
@@ -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
|
/**
|
package/bin/github/batch.mjs
CHANGED
|
@@ -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
|
+
}
|
package/bin/github/errors.d.mts
CHANGED
|
@@ -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;
|
package/bin/github/errors.mjs
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
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 (
|
|
27
|
+
if (isToken(error.type, "INTERNAL"))
|
|
27
28
|
return true;
|
|
28
|
-
if (
|
|
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))
|
|
@@ -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) {
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
...(
|
|
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
|
+
}
|