pr-shepherd 0.51.1 → 0.52.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 +29 -18
- package/bin/cli/help-command-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.mjs +4 -3
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +3 -2
- package/bin/cli/help.d.mts +2 -2
- package/bin/cli/iterate-instructions.mjs +41 -0
- package/bin/cli/iterate-lean.mjs +1 -3
- package/bin/cli/poll-summary-emitter.mjs +2 -3
- package/bin/cli/poll-summary-formatter.mjs +12 -3
- package/bin/cli/runner.d.mts +1 -0
- package/bin/cli/runner.mjs +3 -1
- package/bin/commands/check.mjs +14 -4
- package/bin/commands/clean.mjs +7 -13
- package/bin/commands/iterate/check-instructions.d.mts +2 -2
- package/bin/commands/iterate/check-instructions.mjs +11 -7
- package/bin/commands/iterate/escalate.mjs +4 -13
- package/bin/commands/iterate/fix-code.d.mts +2 -0
- package/bin/commands/iterate/fix-code.mjs +20 -43
- package/bin/commands/iterate/helpers.d.mts +0 -1
- package/bin/commands/iterate/helpers.mjs +0 -15
- package/bin/commands/iterate/index.mjs +178 -11
- package/bin/commands/iterate/merge-state.mjs +35 -18
- package/bin/commands/iterate/native-stack-rebase.d.mts +34 -0
- package/bin/commands/iterate/native-stack-rebase.mjs +43 -0
- package/bin/commands/iterate/parent-first.d.mts +25 -0
- package/bin/commands/iterate/parent-first.mjs +70 -0
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +7 -12
- package/bin/commands/iterate/stale-ancestry.d.mts +22 -0
- package/bin/commands/iterate/stale-ancestry.mjs +40 -0
- package/bin/commands/iterate/stall.mjs +40 -3
- package/bin/commands/poll-progress.d.mts +5 -1
- package/bin/commands/poll-progress.mjs +7 -1
- package/bin/commands/poll-summary-instructions.d.mts +6 -1
- package/bin/commands/poll-summary-instructions.mjs +151 -107
- package/bin/commands/poll-summary.mjs +32 -6
- package/bin/commands/poll.mjs +3 -1
- package/bin/commands/ready-delay.d.mts +9 -4
- package/bin/commands/ready-delay.mjs +34 -20
- package/bin/commands/shepherd-journal.mjs +4 -1
- package/bin/commands/stack-drain.d.mts +35 -0
- package/bin/commands/stack-drain.mjs +129 -0
- package/bin/commands/stack-layer-readiness.d.mts +7 -0
- package/bin/commands/stack-layer-readiness.mjs +35 -0
- package/bin/commands/stack-stall.d.mts +14 -0
- package/bin/commands/stack-stall.mjs +69 -0
- package/bin/commands/stack-work.d.mts +32 -0
- package/bin/commands/stack-work.mjs +39 -0
- package/bin/config/load.d.mts +2 -0
- package/bin/config/load.mjs +10 -0
- package/bin/config.json +1 -0
- package/bin/exit-codes.d.mts +2 -0
- package/bin/exit-codes.mjs +2 -0
- package/bin/github/batch-parsers.mjs +1 -0
- package/bin/github/batch-raw-types.d.mts +2 -0
- package/bin/github/errors.d.mts +5 -0
- package/bin/github/errors.mjs +4 -0
- package/bin/github/gql/batch-pr.gql +1 -0
- package/bin/github/gql/poll-stack-summary.gql +8 -2
- package/bin/github/gql/poll-stack-topology.gql +38 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +34 -0
- package/bin/github/gql/poll-summary-check-page.gql +23 -0
- package/bin/github/gql/poll-summary-fragment.gql +53 -56
- package/bin/github/merge-queue-checks.mjs +10 -1
- package/bin/github/poll-summary-check-hydration.d.mts +12 -0
- package/bin/github/poll-summary-check-hydration.mjs +55 -0
- package/bin/github/poll-summary-fingerprint.d.mts +8 -0
- package/bin/github/poll-summary-fingerprint.mjs +48 -0
- package/bin/github/poll-summary-projector.mjs +70 -16
- package/bin/github/poll-summary-queue-removal.d.mts +5 -0
- package/bin/github/poll-summary-queue-removal.mjs +25 -0
- package/bin/github/poll-summary-raw.d.mts +52 -31
- package/bin/github/poll-summary-readiness.d.mts +6 -0
- package/bin/github/poll-summary-readiness.mjs +25 -0
- package/bin/github/poll-summary-route.mjs +8 -5
- package/bin/github/poll-summary.d.mts +3 -0
- package/bin/github/poll-summary.mjs +21 -73
- package/bin/github/queries.d.mts +7 -0
- package/bin/github/queries.mjs +9 -1
- package/bin/github/queue-removal-freshness.d.mts +16 -0
- package/bin/github/queue-removal-freshness.mjs +26 -0
- package/bin/github/stack-read.d.mts +34 -0
- package/bin/github/stack-read.mjs +92 -0
- package/bin/log/log-file.d.mts +1 -1
- package/bin/log/log-file.mjs +4 -17
- package/bin/state/base.d.mts +18 -1
- package/bin/state/base.mjs +65 -13
- package/bin/state/fix-attempts.d.mts +1 -1
- package/bin/state/fix-attempts.mjs +1 -1
- package/bin/state/graphql-quota-warnings.mjs +2 -6
- package/bin/state/iterate-stall.d.mts +7 -15
- package/bin/state/iterate-stall.mjs +6 -64
- package/bin/state/ready-receipts.d.mts +45 -0
- package/bin/state/ready-receipts.mjs +86 -0
- package/bin/state/rest-cache.d.mts +1 -1
- package/bin/state/rest-cache.mjs +1 -1
- package/bin/state/stack-stall.d.mts +16 -0
- package/bin/state/stack-stall.mjs +12 -0
- package/bin/state/stall-state-store.d.mts +37 -0
- package/bin/state/stall-state-store.mjs +74 -0
- package/bin/types/escalate.d.mts +2 -3
- package/bin/types/github.d.mts +2 -0
- package/bin/types/iterate.d.mts +2 -1
- package/bin/types/merge-requirements.d.mts +19 -0
- package/bin/types/poll-summary.d.mts +17 -2
- package/bin/types/report.d.mts +2 -0
- package/package.json +2 -2
- 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/bin/state/bot-cr-seen.d.mts +0 -51
- package/bin/state/bot-cr-seen.mjs +0 -100
package/bin/config.json
CHANGED
package/bin/exit-codes.d.mts
CHANGED
|
@@ -25,6 +25,8 @@ export declare const EXIT: Readonly<{
|
|
|
25
25
|
readonly CLOSED: 14;
|
|
26
26
|
/** Agent should run the emitted merge or queue command. */
|
|
27
27
|
readonly MERGE: 15;
|
|
28
|
+
/** Aggregate stack has PRs to shepherd through one-PR sessions. */
|
|
29
|
+
readonly SHEPHERD: 16;
|
|
28
30
|
/** Bad/unknown flag, unknown subcommand, missing required arg, invalid duration. */
|
|
29
31
|
readonly USAGE: 64;
|
|
30
32
|
/** Malformed caller data: bad `--require-sha`, `PRRC_*` IDs, bad repo string. */
|
package/bin/exit-codes.mjs
CHANGED
|
@@ -24,6 +24,8 @@ export const EXIT = Object.freeze({
|
|
|
24
24
|
CLOSED: 14,
|
|
25
25
|
/** Agent should run the emitted merge or queue command. */
|
|
26
26
|
MERGE: 15,
|
|
27
|
+
/** Aggregate stack has PRs to shepherd through one-PR sessions. */
|
|
28
|
+
SHEPHERD: 16,
|
|
27
29
|
/** Bad/unknown flag, unknown subcommand, missing required arg, invalid duration. */
|
|
28
30
|
USAGE: 64,
|
|
29
31
|
/** Malformed caller data: bad `--require-sha`, `PRRC_*` IDs, bad repo string. */
|
|
@@ -30,6 +30,8 @@ export interface RawPr extends RawPrMergeFields {
|
|
|
30
30
|
viewerPermission: string | null;
|
|
31
31
|
} | null;
|
|
32
32
|
baseRefName: string;
|
|
33
|
+
/** `PullRequest.baseRefOid`: the base commit GitHub recorded for this PR, not the branch's live tip. */
|
|
34
|
+
baseRefOid?: string;
|
|
33
35
|
baseRef: import("./batch-raw-rules.mts").RawBaseRef | null;
|
|
34
36
|
reviewRequests: {
|
|
35
37
|
nodes: Array<{
|
package/bin/github/errors.d.mts
CHANGED
|
@@ -10,6 +10,11 @@ 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
|
+
/** HTTP 200 GraphQL response whose `repository` field resolved to null. */
|
|
14
|
+
export declare function missingRepositoryError(repo: {
|
|
15
|
+
owner: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}): GitHubRequestError;
|
|
13
18
|
export declare class GitHubRequestError extends ShepherdError {
|
|
14
19
|
readonly status: number;
|
|
15
20
|
readonly rateLimit?: RateLimitInfo;
|
package/bin/github/errors.mjs
CHANGED
|
@@ -47,6 +47,10 @@ function classifyStatus(status, rateLimit, retryAfterSeconds, graphqlErrors) {
|
|
|
47
47
|
return EXIT.NOPERM;
|
|
48
48
|
return EXIT.UNAVAILABLE;
|
|
49
49
|
}
|
|
50
|
+
/** HTTP 200 GraphQL response whose `repository` field resolved to null. */
|
|
51
|
+
export function missingRepositoryError(repo) {
|
|
52
|
+
return new GitHubRequestError(`GitHub GraphQL response did not include repository ${repo.owner}/${repo.name} (not found or access denied)`, { status: 200 });
|
|
53
|
+
}
|
|
50
54
|
export class GitHubRequestError extends ShepherdError {
|
|
51
55
|
status;
|
|
52
56
|
rateLimit;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
query PollStackSummary(
|
|
1
|
+
query PollStackSummary(
|
|
2
|
+
$owner: String!
|
|
3
|
+
$repo: String!
|
|
4
|
+
$anchor: Int!
|
|
5
|
+
$first: Int!
|
|
6
|
+
$after: String
|
|
7
|
+
) {
|
|
2
8
|
_shepherdRateLimit: rateLimit {
|
|
3
9
|
cost
|
|
4
10
|
limit
|
|
@@ -15,7 +21,7 @@ query PollStackSummary($owner: String!, $repo: String!, $anchor: Int!, $after: S
|
|
|
15
21
|
number
|
|
16
22
|
size
|
|
17
23
|
baseRefName
|
|
18
|
-
entries(first:
|
|
24
|
+
entries(first: $first, after: $after) {
|
|
19
25
|
pageInfo {
|
|
20
26
|
hasNextPage
|
|
21
27
|
endCursor
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
query PollStackTopology($owner: String!, $repo: String!, $anchor: Int!, $after: String) {
|
|
2
|
+
_shepherdRateLimit: rateLimit {
|
|
3
|
+
cost
|
|
4
|
+
limit
|
|
5
|
+
nodeCount
|
|
6
|
+
remaining
|
|
7
|
+
resetAt
|
|
8
|
+
used
|
|
9
|
+
}
|
|
10
|
+
repository(owner: $owner, name: $repo) {
|
|
11
|
+
viewerCanAdminister
|
|
12
|
+
pullRequest(number: $anchor) {
|
|
13
|
+
stack {
|
|
14
|
+
id
|
|
15
|
+
number
|
|
16
|
+
size
|
|
17
|
+
baseRefName
|
|
18
|
+
entries(first: 50, after: $after) {
|
|
19
|
+
pageInfo {
|
|
20
|
+
hasNextPage
|
|
21
|
+
endCursor
|
|
22
|
+
}
|
|
23
|
+
nodes {
|
|
24
|
+
position
|
|
25
|
+
pullRequest {
|
|
26
|
+
number
|
|
27
|
+
state
|
|
28
|
+
headRefName
|
|
29
|
+
headRefOid
|
|
30
|
+
baseRefName
|
|
31
|
+
baseRefOid
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
fragment PollSummaryCheckContexts on StatusCheckRollupContextConnection {
|
|
2
|
+
totalCount
|
|
3
|
+
pageInfo {
|
|
4
|
+
hasPreviousPage
|
|
5
|
+
startCursor
|
|
6
|
+
}
|
|
7
|
+
nodes {
|
|
8
|
+
__typename
|
|
9
|
+
... on CheckRun {
|
|
10
|
+
id
|
|
11
|
+
name
|
|
12
|
+
status
|
|
13
|
+
conclusion
|
|
14
|
+
detailsUrl
|
|
15
|
+
annotations(first: 1) {
|
|
16
|
+
totalCount
|
|
17
|
+
}
|
|
18
|
+
checkSuite {
|
|
19
|
+
workflowRun {
|
|
20
|
+
databaseId
|
|
21
|
+
event
|
|
22
|
+
workflow {
|
|
23
|
+
databaseId
|
|
24
|
+
name
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
... on StatusContext {
|
|
30
|
+
context
|
|
31
|
+
state
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
query PollSummaryCheckPage($owner: String!, $repo: String!, $oid: GitObjectID!, $before: String!) {
|
|
2
|
+
_shepherdRateLimit: rateLimit {
|
|
3
|
+
cost
|
|
4
|
+
limit
|
|
5
|
+
nodeCount
|
|
6
|
+
remaining
|
|
7
|
+
resetAt
|
|
8
|
+
used
|
|
9
|
+
}
|
|
10
|
+
repository(owner: $owner, name: $repo) {
|
|
11
|
+
object(oid: $oid) {
|
|
12
|
+
__typename
|
|
13
|
+
... on Commit {
|
|
14
|
+
oid
|
|
15
|
+
statusCheckRollup {
|
|
16
|
+
contexts(last: 100, before: $before) {
|
|
17
|
+
...PollSummaryCheckContexts
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -3,6 +3,7 @@ fragment PollSummaryPr on PullRequest {
|
|
|
3
3
|
title
|
|
4
4
|
url
|
|
5
5
|
state
|
|
6
|
+
updatedAt
|
|
6
7
|
isDraft
|
|
7
8
|
viewerCanUpdate
|
|
8
9
|
headRefName
|
|
@@ -11,6 +12,26 @@ fragment PollSummaryPr on PullRequest {
|
|
|
11
12
|
baseRefOid
|
|
12
13
|
mergeable
|
|
13
14
|
mergeStateStatus
|
|
15
|
+
lifecycleEvents: timelineItems(
|
|
16
|
+
last: 1
|
|
17
|
+
itemTypes: [CLOSED_EVENT, REOPENED_EVENT, CONVERT_TO_DRAFT_EVENT, READY_FOR_REVIEW_EVENT]
|
|
18
|
+
) {
|
|
19
|
+
nodes {
|
|
20
|
+
__typename
|
|
21
|
+
... on ClosedEvent {
|
|
22
|
+
createdAt
|
|
23
|
+
}
|
|
24
|
+
... on ReopenedEvent {
|
|
25
|
+
createdAt
|
|
26
|
+
}
|
|
27
|
+
... on ConvertToDraftEvent {
|
|
28
|
+
createdAt
|
|
29
|
+
}
|
|
30
|
+
... on ReadyForReviewEvent {
|
|
31
|
+
createdAt
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
14
35
|
reviewDecision
|
|
15
36
|
reviewRequests(last: 50) {
|
|
16
37
|
nodes {
|
|
@@ -38,38 +59,39 @@ fragment PollSummaryPr on PullRequest {
|
|
|
38
59
|
}
|
|
39
60
|
}
|
|
40
61
|
isInMergeQueue
|
|
62
|
+
mergeQueueAdditions: timelineItems(last: 1, itemTypes: [ADDED_TO_MERGE_QUEUE_EVENT]) {
|
|
63
|
+
nodes {
|
|
64
|
+
... on AddedToMergeQueueEvent {
|
|
65
|
+
createdAt
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
mergeQueueRemovals: timelineItems(last: 1, itemTypes: [REMOVED_FROM_MERGE_QUEUE_EVENT]) {
|
|
70
|
+
nodes {
|
|
71
|
+
... on RemovedFromMergeQueueEvent {
|
|
72
|
+
id
|
|
73
|
+
reason
|
|
74
|
+
createdAt
|
|
75
|
+
actor {
|
|
76
|
+
login
|
|
77
|
+
}
|
|
78
|
+
beforeCommit {
|
|
79
|
+
oid
|
|
80
|
+
parents(first: 100) {
|
|
81
|
+
nodes {
|
|
82
|
+
oid
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
41
89
|
mergeQueueEntry {
|
|
42
90
|
headCommit {
|
|
91
|
+
oid
|
|
43
92
|
statusCheckRollup {
|
|
44
93
|
contexts(last: 100) {
|
|
45
|
-
|
|
46
|
-
pageInfo {
|
|
47
|
-
hasPreviousPage
|
|
48
|
-
}
|
|
49
|
-
nodes {
|
|
50
|
-
__typename
|
|
51
|
-
... on CheckRun {
|
|
52
|
-
id
|
|
53
|
-
name
|
|
54
|
-
status
|
|
55
|
-
conclusion
|
|
56
|
-
detailsUrl
|
|
57
|
-
checkSuite {
|
|
58
|
-
workflowRun {
|
|
59
|
-
databaseId
|
|
60
|
-
event
|
|
61
|
-
workflow {
|
|
62
|
-
databaseId
|
|
63
|
-
name
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
... on StatusContext {
|
|
69
|
-
context
|
|
70
|
-
state
|
|
71
|
-
}
|
|
72
|
-
}
|
|
94
|
+
...PollSummaryCheckContexts
|
|
73
95
|
}
|
|
74
96
|
}
|
|
75
97
|
}
|
|
@@ -161,36 +183,11 @@ fragment PollSummaryPr on PullRequest {
|
|
|
161
183
|
commits(last: 1) {
|
|
162
184
|
nodes {
|
|
163
185
|
commit {
|
|
186
|
+
oid
|
|
187
|
+
committedDate
|
|
164
188
|
statusCheckRollup {
|
|
165
189
|
contexts(last: 100) {
|
|
166
|
-
|
|
167
|
-
pageInfo {
|
|
168
|
-
hasPreviousPage
|
|
169
|
-
}
|
|
170
|
-
nodes {
|
|
171
|
-
__typename
|
|
172
|
-
... on CheckRun {
|
|
173
|
-
id
|
|
174
|
-
name
|
|
175
|
-
status
|
|
176
|
-
conclusion
|
|
177
|
-
detailsUrl
|
|
178
|
-
checkSuite {
|
|
179
|
-
workflowRun {
|
|
180
|
-
databaseId
|
|
181
|
-
event
|
|
182
|
-
workflow {
|
|
183
|
-
databaseId
|
|
184
|
-
name
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
... on StatusContext {
|
|
190
|
-
context
|
|
191
|
-
state
|
|
192
|
-
}
|
|
193
|
-
}
|
|
190
|
+
...PollSummaryCheckContexts
|
|
194
191
|
}
|
|
195
192
|
}
|
|
196
193
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { parseCreatedAt } from "./batch-parser-helpers.mjs";
|
|
1
2
|
import { graphql } from "./client.mjs";
|
|
3
|
+
import { queueRemovalAppliesToHead } from "./queue-removal-freshness.mjs";
|
|
2
4
|
import { requireContextNodes } from "./batch-response.mjs";
|
|
3
5
|
import { COMMIT_CHECK_CONTEXTS_QUERY } from "./queries.mjs";
|
|
4
6
|
function omittedCursorError(oid) {
|
|
@@ -60,8 +62,15 @@ function currentRemovalCommit(raw) {
|
|
|
60
62
|
if (addition && Date.parse(removal.createdAt) < Date.parse(addition.createdAt))
|
|
61
63
|
return undefined;
|
|
62
64
|
const parentOids = removal.beforeCommit.parents?.nodes.map((node) => node.oid);
|
|
63
|
-
|
|
65
|
+
const headCommittedAt = raw.commits?.nodes[0]?.commit.committedDate;
|
|
66
|
+
if (!queueRemovalAppliesToHead({
|
|
67
|
+
parentOids,
|
|
68
|
+
headOid: raw.headRefOid,
|
|
69
|
+
...(headCommittedAt && { headCommittedAtUnix: parseCreatedAt(headCommittedAt) }),
|
|
70
|
+
removedAtUnix: Math.floor(Date.parse(removal.createdAt) / 1000),
|
|
71
|
+
})) {
|
|
64
72
|
return undefined;
|
|
73
|
+
}
|
|
65
74
|
return removal.beforeCommit;
|
|
66
75
|
}
|
|
67
76
|
/** Hydrate all status contexts for the active or most recently removed queue commit. */
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RepoInfo } from "./client.mts";
|
|
2
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
3
|
+
/**
|
|
4
|
+
* Complete the compact summary's `last: 100` status-context windows in place.
|
|
5
|
+
*
|
|
6
|
+
* Without this, a PR with more than 100 contexts always reports incomplete
|
|
7
|
+
* checks, so it can never earn or match a READY receipt. Every summary fetch
|
|
8
|
+
* path runs this before anything fingerprints `commits`, so one-PR receipts and
|
|
9
|
+
* aggregate stack reads hash the same evidence. A window that cannot be
|
|
10
|
+
* completed stays incomplete, which already fails readiness closed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hydratePollSummaryChecks(raw: RawSummaryPr, repo: RepoInfo): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { graphqlWithRateLimit } from "./client.mjs";
|
|
2
|
+
import { POLL_SUMMARY_CHECK_PAGE_QUERY } from "./queries.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* Complete the compact summary's `last: 100` status-context windows in place.
|
|
5
|
+
*
|
|
6
|
+
* Without this, a PR with more than 100 contexts always reports incomplete
|
|
7
|
+
* checks, so it can never earn or match a READY receipt. Every summary fetch
|
|
8
|
+
* path runs this before anything fingerprints `commits`, so one-PR receipts and
|
|
9
|
+
* aggregate stack reads hash the same evidence. A window that cannot be
|
|
10
|
+
* completed stays incomplete, which already fails readiness closed.
|
|
11
|
+
*/
|
|
12
|
+
export async function hydratePollSummaryChecks(raw, repo) {
|
|
13
|
+
const headCommit = raw.commits.nodes[0]?.commit;
|
|
14
|
+
if (headCommit)
|
|
15
|
+
await hydrateCommitContexts(headCommit, repo);
|
|
16
|
+
const queueCommit = raw.mergeQueueEntry?.headCommit;
|
|
17
|
+
if (queueCommit)
|
|
18
|
+
await hydrateCommitContexts(queueCommit, repo);
|
|
19
|
+
}
|
|
20
|
+
async function hydrateCommitContexts(commit, repo) {
|
|
21
|
+
const contexts = commit.statusCheckRollup?.contexts;
|
|
22
|
+
if (!contexts)
|
|
23
|
+
return;
|
|
24
|
+
const nodes = [...contexts.nodes];
|
|
25
|
+
let pageInfo = contexts.pageInfo;
|
|
26
|
+
while (pageInfo.hasPreviousPage && pageInfo.startCursor) {
|
|
27
|
+
const older = await fetchOlderContexts(commit.oid, pageInfo.startCursor, repo);
|
|
28
|
+
if (!older)
|
|
29
|
+
break;
|
|
30
|
+
nodes.unshift(...older.nodes);
|
|
31
|
+
pageInfo = older.pageInfo;
|
|
32
|
+
}
|
|
33
|
+
// A window that shifted between pages can repeat or skip contexts; the
|
|
34
|
+
// first page's total is the snapshot the hydrated list must account for.
|
|
35
|
+
const complete = !pageInfo.hasPreviousPage && nodes.length === contexts.totalCount;
|
|
36
|
+
commit.statusCheckRollup = {
|
|
37
|
+
contexts: {
|
|
38
|
+
totalCount: contexts.totalCount,
|
|
39
|
+
pageInfo: { hasPreviousPage: !complete },
|
|
40
|
+
nodes,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function fetchOlderContexts(oid, before, repo) {
|
|
45
|
+
const { data } = await graphqlWithRateLimit(POLL_SUMMARY_CHECK_PAGE_QUERY, {
|
|
46
|
+
owner: repo.owner,
|
|
47
|
+
repo: repo.name,
|
|
48
|
+
oid,
|
|
49
|
+
before,
|
|
50
|
+
});
|
|
51
|
+
const object = data.repository?.object;
|
|
52
|
+
if (object?.__typename !== "Commit" || object.oid !== oid)
|
|
53
|
+
return null;
|
|
54
|
+
return object.statusCheckRollup?.contexts ?? null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
2
|
+
/**
|
|
3
|
+
* Bind receipts to the review and head-commit CI evidence shared by one-PR
|
|
4
|
+
* completion and stack reconciliation. Queue entry, queue checks, and GitHub's
|
|
5
|
+
* computed merge state are checked separately: including them here would
|
|
6
|
+
* invalidate a READY receipt merely because a ready PR entered the queue.
|
|
7
|
+
*/
|
|
8
|
+
export declare function fingerprintRawSummaryPr(raw: RawSummaryPr): string | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Bind receipts to the review and head-commit CI evidence shared by one-PR
|
|
4
|
+
* completion and stack reconciliation. Queue entry, queue checks, and GitHub's
|
|
5
|
+
* computed merge state are checked separately: including them here would
|
|
6
|
+
* invalidate a READY receipt merely because a ready PR entered the queue.
|
|
7
|
+
*/
|
|
8
|
+
export function fingerprintRawSummaryPr(raw) {
|
|
9
|
+
if (!raw.updatedAt || !raw.headRefOid || !raw.baseRefOid)
|
|
10
|
+
return null;
|
|
11
|
+
const evidence = {
|
|
12
|
+
number: raw.number,
|
|
13
|
+
state: raw.state,
|
|
14
|
+
lifecycleEvents: raw.lifecycleEvents,
|
|
15
|
+
isDraft: raw.isDraft,
|
|
16
|
+
headRefOid: raw.headRefOid,
|
|
17
|
+
baseRefOid: raw.baseRefOid,
|
|
18
|
+
baseRefName: raw.baseRefName,
|
|
19
|
+
reviewDecision: raw.reviewDecision,
|
|
20
|
+
reviewRequests: raw.reviewRequests,
|
|
21
|
+
latestReviews: raw.latestReviews,
|
|
22
|
+
comments: hideBodies(raw.comments),
|
|
23
|
+
reviews: hideBodies(raw.reviews),
|
|
24
|
+
reviewThreads: {
|
|
25
|
+
...raw.reviewThreads,
|
|
26
|
+
nodes: raw.reviewThreads.nodes.map((thread) => ({
|
|
27
|
+
...thread,
|
|
28
|
+
rootComments: thread.rootComments && hideBodies(thread.rootComments),
|
|
29
|
+
comments: hideBodies(thread.comments),
|
|
30
|
+
})),
|
|
31
|
+
},
|
|
32
|
+
commits: {
|
|
33
|
+
...raw.commits,
|
|
34
|
+
nodes: raw.commits.nodes.map((node) => ({
|
|
35
|
+
...node,
|
|
36
|
+
commit: { ...node.commit, committedDate: undefined },
|
|
37
|
+
})),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
return createHash("sha256").update(JSON.stringify(evidence)).digest("hex");
|
|
41
|
+
}
|
|
42
|
+
/** Hidden bodies are not readiness evidence: bots keep editing hidden notices after a PR settles. */
|
|
43
|
+
function hideBodies(connection) {
|
|
44
|
+
return {
|
|
45
|
+
...connection,
|
|
46
|
+
nodes: connection.nodes.map((node) => (node.isMinimized ? { ...node, body: undefined } : node)),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -3,8 +3,12 @@ import { updateReadyDelay } from "../commands/ready-delay.mjs";
|
|
|
3
3
|
import { formatPrUrl } from "../pr-reference.mjs";
|
|
4
4
|
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
5
5
|
import { loadSeenMap } from "../state/seen-comments.mjs";
|
|
6
|
+
import { isReadyReceiptCurrent, readReadyReceipt } from "../state/ready-receipts.mjs";
|
|
6
7
|
import { summarizePollSummaryChecks } from "./poll-summary-checks.mjs";
|
|
7
8
|
import { summarizePollSummaryReview } from "./poll-summary-review.mjs";
|
|
9
|
+
import { fingerprintRawSummaryPr } from "./poll-summary-fingerprint.mjs";
|
|
10
|
+
import { currentQueueRemovalEvent } from "./poll-summary-queue-removal.mjs";
|
|
11
|
+
import { isCurrentSummaryReady } from "./poll-summary-readiness.mjs";
|
|
8
12
|
import { normalizePollSummaryState, routePollSummary } from "./poll-summary-route.mjs";
|
|
9
13
|
export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdminister = false) {
|
|
10
14
|
const repoName = `${repo.owner}/${repo.name}`;
|
|
@@ -12,19 +16,24 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
12
16
|
const checks = summarizePollSummaryChecks(raw);
|
|
13
17
|
const review = await summarizePollSummaryReview(raw, seen, viewerCanAdminister);
|
|
14
18
|
const blockingReviewerInProgress = detectBlockingReviewer(raw);
|
|
19
|
+
const removalEvent = currentQueueRemovalEvent(raw);
|
|
15
20
|
let { action, reasons } = routePollSummary(raw, checks, review, opts);
|
|
16
21
|
let remainingSeconds;
|
|
17
|
-
if (raw.isDraft &&
|
|
22
|
+
if (raw.isDraft &&
|
|
23
|
+
blockingReviewerInProgress &&
|
|
24
|
+
(action === "mark_ready" || reasons.includes("draft-auto-mark-ready-disabled"))) {
|
|
18
25
|
action = "wait";
|
|
19
26
|
reasons = ["blocking-reviewer-in-progress"];
|
|
20
27
|
}
|
|
21
28
|
const appearsReady = reasons.includes("appears-ready");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
if (opts.stackPrNumber === undefined) {
|
|
30
|
+
const readyDelaySeconds = opts.readyDelaySeconds ?? (loadConfig().watch?.readyDelayMinutes ?? 10) * 60;
|
|
31
|
+
const readyState = await updateReadyDelay(raw.number, appearsReady, readyDelaySeconds, repo.owner, repo.name, { headSha: raw.headRefOid });
|
|
32
|
+
if (appearsReady && !readyState.shouldCancel) {
|
|
33
|
+
action = "wait";
|
|
34
|
+
reasons = ["ready-delay"];
|
|
35
|
+
remainingSeconds = readyState.remainingSeconds;
|
|
36
|
+
}
|
|
28
37
|
}
|
|
29
38
|
const stack = raw.stack
|
|
30
39
|
? {
|
|
@@ -34,6 +43,32 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
34
43
|
baseRefName: raw.stack.baseRefName,
|
|
35
44
|
}
|
|
36
45
|
: undefined;
|
|
46
|
+
const fingerprint = opts.stackPrNumber !== undefined ? fingerprintRawSummaryPr(raw) : null;
|
|
47
|
+
const receipt = fingerprint
|
|
48
|
+
? await readReadyReceipt({ owner: repo.owner, repo: repo.name, pr: raw.number })
|
|
49
|
+
: null;
|
|
50
|
+
// A queued PR's target branch can advance as earlier queue entries merge.
|
|
51
|
+
// Keep the pre-enqueue base binding for the receipt comparison while the
|
|
52
|
+
// merge group itself supplies the current mergeability evidence.
|
|
53
|
+
const receiptFingerprint = raw.isInMergeQueue && receipt
|
|
54
|
+
? fingerprintRawSummaryPr({ ...raw, baseRefOid: receipt.baseRefOid })
|
|
55
|
+
: fingerprint;
|
|
56
|
+
const currentReady = isCurrentSummaryReady(raw, checks, review, {
|
|
57
|
+
allowQueuedProgress: opts.stackPrNumber !== undefined && raw.isInMergeQueue,
|
|
58
|
+
});
|
|
59
|
+
const readyReceipt = receiptFingerprint !== null &&
|
|
60
|
+
isReadyReceiptCurrent(receipt, {
|
|
61
|
+
headRefOid: raw.headRefOid,
|
|
62
|
+
baseRefOid: raw.isInMergeQueue && receipt ? receipt.baseRefOid : raw.baseRefOid,
|
|
63
|
+
readinessFingerprint: receiptFingerprint,
|
|
64
|
+
status: currentReady ? "READY" : "PENDING",
|
|
65
|
+
isDraft: raw.isDraft,
|
|
66
|
+
});
|
|
67
|
+
const queueRemoval = removalEvent?.id && readyReceipt && receipt?.acknowledgedQueueRemovalId === removalEvent.id
|
|
68
|
+
? undefined
|
|
69
|
+
: removalEvent
|
|
70
|
+
? projectQueueRemoval(removalEvent)
|
|
71
|
+
: undefined;
|
|
37
72
|
return {
|
|
38
73
|
pr: raw.number,
|
|
39
74
|
repo: repoName,
|
|
@@ -50,15 +85,29 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
50
85
|
baseRefName: raw.baseRefName,
|
|
51
86
|
...(raw.isDraft && { isDraft: true }),
|
|
52
87
|
...(raw.isInMergeQueue && { isInMergeQueue: true }),
|
|
88
|
+
...(queueRemoval && { queueRemoval }),
|
|
53
89
|
...(blockingReviewerInProgress && { blockingReviewerInProgress: true }),
|
|
54
90
|
...(remainingSeconds !== undefined && { remainingSeconds }),
|
|
55
91
|
...(Object.keys(checks).length > 0 && { checks }),
|
|
56
92
|
...(Object.keys(review).length > 0 && { review }),
|
|
57
93
|
...(stack && { stack }),
|
|
58
|
-
...(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
94
|
+
...(readyReceipt && { readyReceipt: true }),
|
|
95
|
+
...((opts.stackPrNumber !== undefined && raw.state === "OPEN") ||
|
|
96
|
+
(!["wait", "cancel"].includes(action) &&
|
|
97
|
+
!(opts.stackPrNumber !== undefined && raw.stack && action === "merge"))
|
|
98
|
+
? pollCommandFields(repoName, raw.number, raw.isDraft, opts)
|
|
99
|
+
: {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function projectQueueRemoval(removal) {
|
|
103
|
+
const removalTime = Date.parse(removal.createdAt);
|
|
104
|
+
const parents = removal.beforeCommit?.parents?.nodes.map((parent) => parent.oid) ?? [];
|
|
105
|
+
return {
|
|
106
|
+
reason: removal.reason,
|
|
107
|
+
createdAtUnix: Math.floor(removalTime / 1000),
|
|
108
|
+
...(removal.actor?.login && { actor: removal.actor.login }),
|
|
109
|
+
...(removal.beforeCommit?.oid && { beforeCommitOid: removal.beforeCommit.oid }),
|
|
110
|
+
beforeCommitParentOids: parents,
|
|
62
111
|
};
|
|
63
112
|
}
|
|
64
113
|
function detectBlockingReviewer(raw) {
|
|
@@ -67,16 +116,21 @@ function detectBlockingReviewer(raw) {
|
|
|
67
116
|
return ((raw.reviewRequests?.nodes ?? []).some((request) => matches(request.requestedReviewer)) ||
|
|
68
117
|
(raw.latestReviews?.nodes ?? []).some((review) => review.state === "PENDING" && matches(review.author)));
|
|
69
118
|
}
|
|
70
|
-
function
|
|
71
|
-
const
|
|
72
|
-
|
|
119
|
+
function pollCommandFields(repo, pr, isDraft, opts) {
|
|
120
|
+
const autoMarkReadyDisabled = opts.noAutoMarkReady || loadConfig().actions.autoMarkReady === false;
|
|
121
|
+
const boundedDraft = isDraft && autoMarkReadyDisabled;
|
|
122
|
+
const args = boundedDraft
|
|
123
|
+
? [formatPrUrl(repo, pr), "--timeout", "1s", "--debounce", "0s", "--no-auto-mark-ready"]
|
|
124
|
+
: [formatPrUrl(repo, pr), "--until-terminal"];
|
|
125
|
+
if (opts.merge && opts.stackPrNumber === undefined)
|
|
73
126
|
args.push("--merge");
|
|
74
127
|
if (opts.readyDelaySeconds !== undefined)
|
|
75
128
|
args.push("--ready-delay", `${opts.readyDelaySeconds}s`);
|
|
76
129
|
if (opts.stallTimeoutSeconds !== undefined) {
|
|
77
130
|
args.push("--stall-timeout", `${opts.stallTimeoutSeconds}s`);
|
|
78
131
|
}
|
|
79
|
-
if (opts.noAutoMarkReady)
|
|
132
|
+
if (opts.noAutoMarkReady && !boundedDraft)
|
|
80
133
|
args.push("--no-auto-mark-ready");
|
|
81
|
-
|
|
134
|
+
const pollCommand = buildPrShepherdCommand(args).text;
|
|
135
|
+
return boundedDraft ? { pollCommand, pollProbe: true } : { pollCommand };
|
|
82
136
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
2
|
+
type QueueRemovalEvent = NonNullable<RawSummaryPr["mergeQueueRemovals"]>["nodes"][number];
|
|
3
|
+
/** Latest queue removal that still applies to this PR head, not an older attempt. */
|
|
4
|
+
export declare function currentQueueRemovalEvent(raw: RawSummaryPr): QueueRemovalEvent | null;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { queueRemovalAppliesToHead } from "./queue-removal-freshness.mjs";
|
|
2
|
+
import { parseCreatedAt } from "./batch-parser-helpers.mjs";
|
|
3
|
+
/** Latest queue removal that still applies to this PR head, not an older attempt. */
|
|
4
|
+
export function currentQueueRemovalEvent(raw) {
|
|
5
|
+
const removal = raw.mergeQueueRemovals?.nodes[0];
|
|
6
|
+
if (!removal || raw.isInMergeQueue)
|
|
7
|
+
return null;
|
|
8
|
+
const removalTime = Date.parse(removal.createdAt);
|
|
9
|
+
if (!Number.isFinite(removalTime))
|
|
10
|
+
return null;
|
|
11
|
+
const addition = raw.mergeQueueAdditions?.nodes[0];
|
|
12
|
+
if (addition && Date.parse(addition.createdAt) > removalTime)
|
|
13
|
+
return null;
|
|
14
|
+
const parents = removal.beforeCommit?.parents?.nodes.map((parent) => parent.oid);
|
|
15
|
+
const headCommittedAt = raw.commits?.nodes[0]?.commit.committedDate;
|
|
16
|
+
if (!queueRemovalAppliesToHead({
|
|
17
|
+
parentOids: parents,
|
|
18
|
+
headOid: raw.headRefOid,
|
|
19
|
+
...(headCommittedAt && { headCommittedAtUnix: parseCreatedAt(headCommittedAt) }),
|
|
20
|
+
removedAtUnix: Math.floor(removalTime / 1000),
|
|
21
|
+
})) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return removal;
|
|
25
|
+
}
|