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
|
@@ -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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RawBaseRef } from "./batch-raw-rules.mts";
|
|
1
2
|
export interface RawAuthor {
|
|
2
3
|
__typename?: string;
|
|
3
4
|
login: string;
|
|
@@ -29,9 +30,11 @@ type RawCheckContext = {
|
|
|
29
30
|
totalCount: number;
|
|
30
31
|
};
|
|
31
32
|
checkSuite: {
|
|
33
|
+
createdAt?: string | null;
|
|
32
34
|
workflowRun: {
|
|
33
35
|
databaseId?: string | number;
|
|
34
36
|
event: string;
|
|
37
|
+
createdAt?: string | null;
|
|
35
38
|
workflow?: {
|
|
36
39
|
name: string;
|
|
37
40
|
databaseId: string | number;
|
|
@@ -60,11 +63,23 @@ export interface RawSummaryCommit {
|
|
|
60
63
|
oid: string;
|
|
61
64
|
committedDate?: string;
|
|
62
65
|
statusCheckRollup: RawCheckRollup | null;
|
|
66
|
+
checkSuites?: {
|
|
67
|
+
nodes: Array<{
|
|
68
|
+
status?: string | null;
|
|
69
|
+
conclusion: string | null;
|
|
70
|
+
workflowRun: {
|
|
71
|
+
event: string | null;
|
|
72
|
+
} | null;
|
|
73
|
+
}>;
|
|
74
|
+
} | null;
|
|
63
75
|
}
|
|
64
76
|
export interface RawSummaryPr {
|
|
65
77
|
number: number;
|
|
66
78
|
title: string;
|
|
67
79
|
url: string;
|
|
80
|
+
author?: {
|
|
81
|
+
login: string;
|
|
82
|
+
} | null;
|
|
68
83
|
state: string;
|
|
69
84
|
updatedAt?: string;
|
|
70
85
|
lifecycleEvents?: {
|
|
@@ -79,6 +94,7 @@ export interface RawSummaryPr {
|
|
|
79
94
|
headRefOid: string;
|
|
80
95
|
baseRefOid: string;
|
|
81
96
|
baseRefName: string;
|
|
97
|
+
baseRef?: RawBaseRef | null;
|
|
82
98
|
mergeable: string;
|
|
83
99
|
mergeStateStatus: string;
|
|
84
100
|
reviewDecision: string | null;
|
|
@@ -21,5 +21,6 @@ export function isCurrentSummaryReady(raw, checks, review, options = {}) {
|
|
|
21
21
|
(checks.failing ?? 0) === 0 &&
|
|
22
22
|
(sourceChecks.failing ?? 0) === 0 &&
|
|
23
23
|
(sourceChecks.inProgress ?? 0) === 0 &&
|
|
24
|
+
(checks.unreportedRequired?.length ?? 0) === 0 &&
|
|
24
25
|
(review.actionable ?? 0) === 0);
|
|
25
26
|
}
|
|
@@ -16,6 +16,11 @@ export function routePollSummary(raw, checks, review, opts) {
|
|
|
16
16
|
}
|
|
17
17
|
return { action: "fix_code", reasons: ["review-work"] };
|
|
18
18
|
}
|
|
19
|
+
if ((checks.unreportedRequired?.length ?? 0) > 0 &&
|
|
20
|
+
checks.actionsWorkflowInProgress !== true &&
|
|
21
|
+
(checks.inProgress ?? 0) === 0) {
|
|
22
|
+
return { action: "fix_code", reasons: ["unreported-required-checks"] };
|
|
23
|
+
}
|
|
19
24
|
if ((checks.inProgress ?? 0) > 0 ||
|
|
20
25
|
raw.mergeable === "UNKNOWN" ||
|
|
21
26
|
raw.mergeStateStatus === "UNKNOWN" ||
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PollSummaryChecks } from "../types.mts";
|
|
2
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
3
|
+
/** Required contexts from the open layer whose base is the stack trunk. */
|
|
4
|
+
export declare function trunkRequiredContexts(ordered: readonly RawSummaryPr[]): string[] | undefined;
|
|
5
|
+
/** Record missing required contexts. A running Actions workflow does not clear the names. */
|
|
6
|
+
export declare function applyUnreportedRequiredChecks(raw: RawSummaryPr, checks: PollSummaryChecks, mergeTargetContexts: readonly string[] | undefined): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { loadConfig } from "../config/load.mjs";
|
|
2
|
+
import { actionsWorkflowInProgress, selectMergeTargetContexts, unreportedRequiredContexts, } from "../checks/unreported-required.mjs";
|
|
3
|
+
import { parseBranchRules } from "./batch-parsers-rules.mjs";
|
|
4
|
+
/** Required contexts from the open layer whose base is the stack trunk. */
|
|
5
|
+
export function trunkRequiredContexts(ordered) {
|
|
6
|
+
const trunk = ordered.find((pr) => pr.stack)?.stack?.baseRefName;
|
|
7
|
+
if (!trunk)
|
|
8
|
+
return undefined;
|
|
9
|
+
const bottom = ordered.find((pr) => pr.state === "OPEN" && pr.baseRefName === trunk);
|
|
10
|
+
if (!bottom)
|
|
11
|
+
return undefined;
|
|
12
|
+
return parseBranchRules(bottom.baseRef).requiredStatusCheckContexts;
|
|
13
|
+
}
|
|
14
|
+
/** Record missing required contexts. A running Actions workflow does not clear the names. */
|
|
15
|
+
export function applyUnreportedRequiredChecks(raw, checks, mergeTargetContexts) {
|
|
16
|
+
const trunk = raw.stack?.baseRefName;
|
|
17
|
+
const required = selectMergeTargetContexts({
|
|
18
|
+
localContexts: parseBranchRules(raw.baseRef).requiredStatusCheckContexts,
|
|
19
|
+
...(mergeTargetContexts !== undefined && { trunkContexts: mergeTargetContexts }),
|
|
20
|
+
baseRefName: raw.baseRefName,
|
|
21
|
+
...(trunk !== undefined && mergeTargetContexts !== undefined && { trunkRefName: trunk }),
|
|
22
|
+
});
|
|
23
|
+
const missing = unreportedRequiredContexts(required, reportedNames(raw));
|
|
24
|
+
if (missing.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
checks.unreportedRequired = missing;
|
|
27
|
+
const suites = raw.commits.nodes[0]?.commit.checkSuites?.nodes ?? [];
|
|
28
|
+
if (actionsWorkflowInProgress(suites, new Set(loadConfig().checks.ciTriggerEvents))) {
|
|
29
|
+
checks.actionsWorkflowInProgress = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function reportedNames(raw) {
|
|
33
|
+
const names = new Set();
|
|
34
|
+
const rollups = [
|
|
35
|
+
raw.commits.nodes[0]?.commit.statusCheckRollup,
|
|
36
|
+
raw.mergeQueueEntry?.headCommit?.statusCheckRollup,
|
|
37
|
+
];
|
|
38
|
+
for (const rollup of rollups) {
|
|
39
|
+
for (const node of rollup?.contexts.nodes ?? []) {
|
|
40
|
+
if (!node)
|
|
41
|
+
continue;
|
|
42
|
+
const name = node.__typename === "CheckRun" ? node.name : node.context;
|
|
43
|
+
if (typeof name !== "string")
|
|
44
|
+
continue;
|
|
45
|
+
const trimmed = name.trim();
|
|
46
|
+
if (trimmed)
|
|
47
|
+
names.add(trimmed);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return names;
|
|
51
|
+
}
|
|
@@ -5,6 +5,7 @@ export interface FetchedPollSummary {
|
|
|
5
5
|
selection: PollSummarySelection;
|
|
6
6
|
prs: PollSummaryItem[];
|
|
7
7
|
stackAncestry?: PollSummaryStackAncestry[];
|
|
8
|
+
allowedMergeMethods?: import("../config/merge-method.mts").MergeMethod[];
|
|
8
9
|
}
|
|
9
10
|
export declare function fetchPollSummary(opts: PollSummaryCommandOptions, repo: RepoInfo): Promise<FetchedPollSummary>;
|
|
10
11
|
/** Fresh, read-only snapshot used to bind a one-PR READY receipt to stack routing. */
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
2
2
|
import { graphqlWithRateLimit } from "./client.mjs";
|
|
3
|
-
import { missingRepositoryError } from "./errors.mjs";
|
|
3
|
+
import { GitHubRequestError, isRetryableGraphQlResourceLimit, missingRepositoryError, } from "./errors.mjs";
|
|
4
4
|
import { hydratePollSummaryChecks } from "./poll-summary-check-hydration.mjs";
|
|
5
5
|
import { summarizePollSummaryPr } from "./poll-summary-projector.mjs";
|
|
6
|
+
import { trunkRequiredContexts } from "./poll-summary-unreported.mjs";
|
|
6
7
|
import { POLL_STACK_SUMMARY_QUERY, POLL_SUMMARY_FRAGMENT } from "./queries.mjs";
|
|
7
8
|
import { MAX_STACK_ENTRIES_PER_PAGE, readStack, readStackTopology, stackAncestryGaps, } from "./stack-read.mjs";
|
|
8
9
|
const MAX_EXPLICIT_PRS_PER_QUERY = 50;
|
|
@@ -62,16 +63,40 @@ async function fetchExplicitChunk(prs, repo) {
|
|
|
62
63
|
async function fetchStackSummary(opts, repo) {
|
|
63
64
|
const anchor = opts.stackPrNumber;
|
|
64
65
|
const topology = await readStackTopology(anchor, repo);
|
|
65
|
-
const { stackNumber, stackSize, viewerCanAdminister, ordered } = await
|
|
66
|
+
const { stackNumber, stackSize, viewerLogin, viewerCanAdminister, ordered, allowedMergeMethods } = await readStackSummary(anchor, repo, topology.stackSize);
|
|
66
67
|
for (const pr of ordered)
|
|
67
68
|
await hydratePollSummaryChecks(pr, repo);
|
|
68
69
|
const stackAncestry = stackAncestryGaps(ordered);
|
|
70
|
+
const required = trunkRequiredContexts(ordered);
|
|
69
71
|
return {
|
|
70
72
|
selection: { kind: "stack", anchor, stackNumber, stackSize },
|
|
71
|
-
prs: await Promise.all(ordered.map((pr) => summarizePollSummaryPr(pr, repo, opts, viewerCanAdminister))),
|
|
73
|
+
prs: await Promise.all(ordered.map((pr) => summarizePollSummaryPr(pr, repo, opts, viewerCanAdminister, required, viewerLogin))),
|
|
72
74
|
...(stackAncestry.length > 0 && { stackAncestry }),
|
|
75
|
+
...(allowedMergeMethods && { allowedMergeMethods }),
|
|
73
76
|
};
|
|
74
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* A wide check matrix can blow GitHub's per-query resource limit on the full
|
|
80
|
+
* page. Halve `first` down to one entry and let `readStack` follow `after`.
|
|
81
|
+
* The shared summary fragment stays intact: one layer is the same shape as an
|
|
82
|
+
* explicit summary, and check hydration still completes windows past 100.
|
|
83
|
+
*/
|
|
84
|
+
async function readStackSummary(anchor, repo, stackSize) {
|
|
85
|
+
let first = Math.min(stackSize, MAX_STACK_ENTRIES_PER_PAGE);
|
|
86
|
+
for (;;) {
|
|
87
|
+
try {
|
|
88
|
+
return await readStack(POLL_STACK_SUMMARY_QUERY, anchor, repo, { first });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (!(err instanceof GitHubRequestError) ||
|
|
92
|
+
!isRetryableGraphQlResourceLimit(err.graphqlErrors) ||
|
|
93
|
+
first <= 1) {
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
first = Math.floor(first / 2);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
75
100
|
function deduplicate(values) {
|
|
76
101
|
return [...new Set(values)];
|
|
77
102
|
}
|
package/bin/github/queries.d.mts
CHANGED
|
@@ -14,11 +14,20 @@ export declare const BATCH_PR_PAGE_QUERY: string;
|
|
|
14
14
|
export declare const PR_FINGERPRINT_QUERY: string;
|
|
15
15
|
/** Compact per-PR fields shared by explicit-list and native-stack summary queries. */
|
|
16
16
|
export declare const POLL_SUMMARY_FRAGMENT: string;
|
|
17
|
+
/** Trunk branch rules plus how far `headRef` is behind that branch. */
|
|
18
|
+
export declare const REF_RULES_QUERY: string;
|
|
19
|
+
/** How far a non-stack head is behind its PR base. One compare, no rules payload. */
|
|
20
|
+
export declare const BASE_BEHIND_QUERY: string;
|
|
17
21
|
/**
|
|
18
22
|
* Pages older status contexts for one commit with the same node selection as
|
|
19
23
|
* the compact summary, so hydrated and first-page nodes fingerprint alike.
|
|
20
24
|
*/
|
|
21
25
|
export declare const POLL_SUMMARY_CHECK_PAGE_QUERY: string;
|
|
26
|
+
/**
|
|
27
|
+
* Annotation totals for one commit, used only when a summary layer otherwise
|
|
28
|
+
* looks ready. `$before` pages older than the newest 100 contexts.
|
|
29
|
+
*/
|
|
30
|
+
export declare const POLL_SUMMARY_ANNOTATION_PROBE_QUERY: string;
|
|
22
31
|
/** Discovers and summarizes every entry in one native GitHub pull-request stack. */
|
|
23
32
|
export declare const POLL_STACK_SUMMARY_QUERY: string;
|
|
24
33
|
/** Reads only native-stack membership and the refs that link adjacent entries. */
|
package/bin/github/queries.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
const gql = (name) => readFileSync(join(import.meta.dirname, "gql", name), "utf8");
|
|
12
|
-
const withSharedFragments = (query) => `${gql("pr-merge-policy.gql")}\n${gql("commit-check-suites.gql")}\n${query}`;
|
|
12
|
+
const withSharedFragments = (query) => `${gql("ref-rules.gql")}\n${gql("pr-merge-policy.gql")}\n${gql("commit-check-suites.gql")}\n${query}`;
|
|
13
13
|
/** The primary batch query that fetches CI + comments + merge status in one round-trip. */
|
|
14
14
|
export const BATCH_PR_QUERY = withSharedFragments(gql("batch-pr.gql"));
|
|
15
15
|
/** Slim @include follow-up for outstanding batch-query connections. */
|
|
@@ -18,12 +18,21 @@ export const BATCH_PR_PAGE_QUERY = gql("batch-pr-page.gql");
|
|
|
18
18
|
export const PR_FINGERPRINT_QUERY = withSharedFragments(gql("pr-fingerprint.gql"));
|
|
19
19
|
const POLL_SUMMARY_CHECK_CONTEXTS_FRAGMENT = gql("poll-summary-check-contexts.gql");
|
|
20
20
|
/** Compact per-PR fields shared by explicit-list and native-stack summary queries. */
|
|
21
|
-
export const POLL_SUMMARY_FRAGMENT = `${POLL_SUMMARY_CHECK_CONTEXTS_FRAGMENT}\n${gql("poll-summary-fragment.gql")}`;
|
|
21
|
+
export const POLL_SUMMARY_FRAGMENT = `${gql("ref-rules.gql")}\n${POLL_SUMMARY_CHECK_CONTEXTS_FRAGMENT}\n${gql("poll-summary-fragment.gql")}`;
|
|
22
|
+
/** Trunk branch rules plus how far `headRef` is behind that branch. */
|
|
23
|
+
export const REF_RULES_QUERY = `${gql("ref-rules.gql")}\n${gql("ref-rules-query.gql")}`;
|
|
24
|
+
/** How far a non-stack head is behind its PR base. One compare, no rules payload. */
|
|
25
|
+
export const BASE_BEHIND_QUERY = gql("base-behind.gql");
|
|
22
26
|
/**
|
|
23
27
|
* Pages older status contexts for one commit with the same node selection as
|
|
24
28
|
* the compact summary, so hydrated and first-page nodes fingerprint alike.
|
|
25
29
|
*/
|
|
26
30
|
export const POLL_SUMMARY_CHECK_PAGE_QUERY = `${POLL_SUMMARY_CHECK_CONTEXTS_FRAGMENT}\n${gql("poll-summary-check-page.gql")}`;
|
|
31
|
+
/**
|
|
32
|
+
* Annotation totals for one commit, used only when a summary layer otherwise
|
|
33
|
+
* looks ready. `$before` pages older than the newest 100 contexts.
|
|
34
|
+
*/
|
|
35
|
+
export const POLL_SUMMARY_ANNOTATION_PROBE_QUERY = gql("poll-summary-annotation-probe.gql");
|
|
27
36
|
/** Discovers and summarizes every entry in one native GitHub pull-request stack. */
|
|
28
37
|
export const POLL_STACK_SUMMARY_QUERY = `${POLL_SUMMARY_FRAGMENT}\n${gql("poll-stack-summary.gql")}`;
|
|
29
38
|
/** Reads only native-stack membership and the refs that link adjacent entries. */
|
|
@@ -1,16 +1,31 @@
|
|
|
1
|
+
type PushCheckNode = {
|
|
2
|
+
__typename?: string;
|
|
3
|
+
checkSuite?: {
|
|
4
|
+
createdAt?: string | null;
|
|
5
|
+
workflowRun?: {
|
|
6
|
+
event?: string | null;
|
|
7
|
+
createdAt?: string | null;
|
|
8
|
+
} | null;
|
|
9
|
+
} | null;
|
|
10
|
+
};
|
|
1
11
|
/**
|
|
2
12
|
* Whether a removed merge-queue commit still belongs to the current PR head.
|
|
3
13
|
*
|
|
4
14
|
* A merge-commit queue lists that head as a parent. Squash and rebase queues
|
|
5
|
-
* build a one-parent commit on the base
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
15
|
+
* build a one-parent commit on the base, so parent identity cannot show a
|
|
16
|
+
* later push. Missing parents are unverifiable: GitHub keeps returning the
|
|
17
|
+
* latest removal after the synthetic commit is gone. A one-parent removal is
|
|
18
|
+
* stale once this head reached the PR after the removal. That time is the
|
|
19
|
+
* earliest pull_request check on the head, with the head's committer time as
|
|
20
|
+
* the fallback when no check time is available.
|
|
10
21
|
*/
|
|
11
22
|
export declare function queueRemovalAppliesToHead(input: {
|
|
12
23
|
parentOids: readonly string[] | null | undefined;
|
|
13
24
|
headOid: string;
|
|
14
25
|
headCommittedAtUnix?: number;
|
|
26
|
+
headPushedAtUnix?: number;
|
|
15
27
|
removedAtUnix?: number;
|
|
16
28
|
}): boolean;
|
|
29
|
+
/** Earliest pull_request check-suite time on the head commit, when one exists. */
|
|
30
|
+
export declare function headPushUnixFromCheckNodes(nodes: readonly (PushCheckNode | null)[] | null | undefined): number | undefined;
|
|
31
|
+
export {};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { parseCreatedAt } from "./batch-parser-helpers.mjs";
|
|
2
|
+
const PULL_REQUEST_EVENTS = new Set(["pull_request", "pull_request_target"]);
|
|
1
3
|
/**
|
|
2
4
|
* Whether a removed merge-queue commit still belongs to the current PR head.
|
|
3
5
|
*
|
|
4
6
|
* A merge-commit queue lists that head as a parent. Squash and rebase queues
|
|
5
|
-
* build a one-parent commit on the base
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* build a one-parent commit on the base, so parent identity cannot show a
|
|
8
|
+
* later push. Missing parents are unverifiable: GitHub keeps returning the
|
|
9
|
+
* latest removal after the synthetic commit is gone. A one-parent removal is
|
|
10
|
+
* stale once this head reached the PR after the removal. That time is the
|
|
11
|
+
* earliest pull_request check on the head, with the head's committer time as
|
|
12
|
+
* the fallback when no check time is available.
|
|
10
13
|
*/
|
|
11
14
|
export function queueRemovalAppliesToHead(input) {
|
|
12
15
|
const parents = input.parentOids ?? [];
|
|
@@ -16,11 +19,35 @@ export function queueRemovalAppliesToHead(input) {
|
|
|
16
19
|
return true;
|
|
17
20
|
if (parents.length > 1)
|
|
18
21
|
return false;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const removedAt = input.removedAtUnix;
|
|
23
|
+
if (removedAt === undefined || removedAt <= 0)
|
|
24
|
+
return true;
|
|
25
|
+
const pushedAt = input.headPushedAtUnix;
|
|
26
|
+
if (pushedAt !== undefined && pushedAt > 0)
|
|
27
|
+
return pushedAt <= removedAt;
|
|
28
|
+
const committedAt = input.headCommittedAtUnix;
|
|
29
|
+
if (committedAt !== undefined && committedAt > removedAt)
|
|
23
30
|
return false;
|
|
24
|
-
}
|
|
25
31
|
return true;
|
|
26
32
|
}
|
|
33
|
+
/** Earliest pull_request check-suite time on the head commit, when one exists. */
|
|
34
|
+
export function headPushUnixFromCheckNodes(nodes) {
|
|
35
|
+
let earliest;
|
|
36
|
+
for (const node of nodes ?? []) {
|
|
37
|
+
if (node?.__typename !== "CheckRun")
|
|
38
|
+
continue;
|
|
39
|
+
const run = node.checkSuite?.workflowRun;
|
|
40
|
+
const event = run?.event;
|
|
41
|
+
if (event == null || !PULL_REQUEST_EVENTS.has(event))
|
|
42
|
+
continue;
|
|
43
|
+
const raw = node.checkSuite?.createdAt || run?.createdAt;
|
|
44
|
+
if (!raw)
|
|
45
|
+
continue;
|
|
46
|
+
const at = parseCreatedAt(raw);
|
|
47
|
+
if (at <= 0)
|
|
48
|
+
continue;
|
|
49
|
+
if (earliest === undefined || at < earliest)
|
|
50
|
+
earliest = at;
|
|
51
|
+
}
|
|
52
|
+
return earliest;
|
|
53
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MergeMethod } from "../config/merge-method.mts";
|
|
1
2
|
import type { PollSummaryStackAncestry } from "../types.mts";
|
|
2
3
|
import { type RepoInfo } from "./client.mts";
|
|
3
4
|
/** GitHub's `first` ceiling for one native-stack entries page. */
|
|
@@ -15,9 +16,12 @@ export interface StackMemberRefs {
|
|
|
15
16
|
export interface StackRead<Pr extends StackMemberRefs> {
|
|
16
17
|
stackNumber: number;
|
|
17
18
|
stackSize: number;
|
|
19
|
+
viewerLogin: string | null;
|
|
18
20
|
viewerCanAdminister: boolean;
|
|
19
21
|
/** Every member, bottom-to-top, each observed in the same paged read. */
|
|
20
22
|
ordered: Pr[];
|
|
23
|
+
/** Present when the query selected repository merge settings, including an empty list. */
|
|
24
|
+
allowedMergeMethods?: MergeMethod[];
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
27
|
* Page through one native stack and fail closed on anything short of the full
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readAllowedMergeMethods } from "../config/merge-method.mjs";
|
|
1
2
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
2
3
|
import { graphqlWithRateLimit } from "./client.mjs";
|
|
3
4
|
import { missingRepositoryError } from "./errors.mjs";
|
|
@@ -15,13 +16,17 @@ export async function readStack(query, anchor, repo, variables = {}) {
|
|
|
15
16
|
let stackNumber = 0;
|
|
16
17
|
let stackSize = 0;
|
|
17
18
|
let viewerCanAdminister = false;
|
|
19
|
+
let viewerLogin = null;
|
|
20
|
+
let allowedMergeMethods;
|
|
18
21
|
const entries = [];
|
|
19
22
|
do {
|
|
20
23
|
const response = await graphqlWithRateLimit(query, { owner: repo.owner, repo: repo.name, anchor, after, ...variables });
|
|
24
|
+
viewerLogin = response.data.viewer?.login ?? viewerLogin;
|
|
21
25
|
const repository = response.data.repository;
|
|
22
26
|
if (!repository)
|
|
23
27
|
throw missingRepositoryError(repo);
|
|
24
28
|
viewerCanAdminister = repository.viewerCanAdminister;
|
|
29
|
+
allowedMergeMethods = readAllowedMergeMethods(repository) ?? allowedMergeMethods;
|
|
25
30
|
if (!repository.pullRequest) {
|
|
26
31
|
throw new ShepherdError(`PR #${anchor} not found`, EXIT.UNAVAILABLE);
|
|
27
32
|
}
|
|
@@ -59,7 +64,14 @@ export async function readStack(query, anchor, repo, variables = {}) {
|
|
|
59
64
|
const ordered = [...unique.values()]
|
|
60
65
|
.sort((left, right) => left.position - right.position)
|
|
61
66
|
.map((entry) => entry.pullRequest);
|
|
62
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
stackNumber,
|
|
69
|
+
stackSize,
|
|
70
|
+
viewerLogin,
|
|
71
|
+
viewerCanAdminister,
|
|
72
|
+
ordered,
|
|
73
|
+
...(allowedMergeMethods && { allowedMergeMethods }),
|
|
74
|
+
};
|
|
63
75
|
}
|
|
64
76
|
/** Membership and linking refs only — no per-PR CI or review hydration. */
|
|
65
77
|
export function readStackTopology(anchor, repo) {
|
package/bin/mcp/server.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { isRepositoryQualifiedPrReference, normalizeRepositoryIdentity, parsePrR
|
|
|
7
7
|
import { formatJournalResult } from "../cli/journal-formatter.mjs";
|
|
8
8
|
import { formatCommitSuggestionResult, formatSuggestionPatchesResult, formatIterateResult, formatMarkFilesAsViewedResult, formatMutateResult, projectIterateLean, } from "../cli/formatters.mjs";
|
|
9
9
|
import { formatPollSummaryResult } from "../cli/poll-summary-formatter.mjs";
|
|
10
|
+
import { projectStackOverview } from "../cli/stack-overview.mjs";
|
|
10
11
|
import { formatCliError, serializeGitHubRequestErrorDetails } from "../cli/error-format.mjs";
|
|
11
12
|
import { errorToExitCode, EXIT } from "../exit-codes.mjs";
|
|
12
13
|
const QUALIFIED_PR_ERROR = "pr must be a GitHub pull-request URL or an owner/repo#number reference";
|
|
@@ -104,7 +105,11 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
104
105
|
};
|
|
105
106
|
return runTool(() => runIterateSelector(shepherd, requireRepositoryQualifiedIterate(input)), (result) => isPollSummary(result)
|
|
106
107
|
? formatPollSummaryResult(result)
|
|
107
|
-
: formatIterateResult(result, opts), (result) => isPollSummary(result)
|
|
108
|
+
: formatIterateResult(result, opts), (result) => isPollSummary(result)
|
|
109
|
+
? result.selection.kind === "stack"
|
|
110
|
+
? projectStackOverview(result)
|
|
111
|
+
: result
|
|
112
|
+
: projectIterateLean(result, opts));
|
|
108
113
|
});
|
|
109
114
|
server.registerTool("apply", {
|
|
110
115
|
description: "Apply ordered review, journal, and file-view operations after prevalidation; explicit requests rely on GitHub's mutation response.",
|