pr-shepherd 0.54.1 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +29 -9
- package/bin/cli/iterate-instructions.mjs +6 -0
- package/bin/cli/iterate-lean.mjs +15 -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 +26 -0
- package/bin/commands/check-unreported.mjs +73 -0
- package/bin/commands/check.mjs +44 -38
- package/bin/commands/iterate/base.mjs +9 -0
- 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 +122 -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/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/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 +20 -0
- package/bin/github/merge-target-rules.mjs +54 -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 +7 -0
- package/bin/github/queries.mjs +9 -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 +14 -3
- package/bin/types/poll-summary.d.mts +13 -0
- package/bin/types/report.d.mts +32 -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 +2 -2
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
- package/src/classify/types.mts +1 -1
|
@@ -4,7 +4,7 @@ export function suiteFingerprint(suites) {
|
|
|
4
4
|
return { checkSuiteConclusions: "", checkSuitesComplete: false };
|
|
5
5
|
return {
|
|
6
6
|
checkSuiteConclusions: (suites.nodes ?? [])
|
|
7
|
-
.map((node) => `${node.id ?? node.workflowRun?.databaseId ?? ""}:${node.conclusion ?? ""}`)
|
|
7
|
+
.map((node) => `${node.id ?? node.workflowRun?.databaseId ?? ""}:${node.status ?? ""}:${node.conclusion ?? ""}`)
|
|
8
8
|
.join(","),
|
|
9
9
|
checkSuitesComplete: suites.pageInfo?.hasNextPage === false,
|
|
10
10
|
};
|
|
@@ -128,6 +128,7 @@ query BatchPrPage(
|
|
|
128
128
|
}
|
|
129
129
|
body
|
|
130
130
|
createdAt
|
|
131
|
+
url
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
approvedReviews: reviews(states: APPROVED, last: 50, before: $approvedReviewsCursor)
|
|
@@ -147,6 +148,7 @@ query BatchPrPage(
|
|
|
147
148
|
}
|
|
148
149
|
body
|
|
149
150
|
createdAt
|
|
151
|
+
url
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
commits(last: 1) @include(if: $includeChecks) {
|
|
@@ -15,6 +15,9 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
|
15
15
|
repository(owner: $owner, name: $repo) {
|
|
16
16
|
viewerPermission
|
|
17
17
|
viewerCanAdminister
|
|
18
|
+
mergeCommitAllowed
|
|
19
|
+
squashMergeAllowed
|
|
20
|
+
rebaseMergeAllowed
|
|
18
21
|
pullRequest(number: $pr) {
|
|
19
22
|
id
|
|
20
23
|
number
|
|
@@ -220,6 +223,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
|
220
223
|
}
|
|
221
224
|
body
|
|
222
225
|
createdAt
|
|
226
|
+
url
|
|
223
227
|
}
|
|
224
228
|
}
|
|
225
229
|
allReviews: reviews(last: 100) {
|
|
@@ -245,6 +249,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
|
245
249
|
}
|
|
246
250
|
body
|
|
247
251
|
createdAt
|
|
252
|
+
url
|
|
248
253
|
}
|
|
249
254
|
}
|
|
250
255
|
commits(last: 1) {
|
|
@@ -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,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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
?
|
|
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
|
|
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
|
-
...(
|
|
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;
|