pr-shepherd 0.56.5 → 0.56.6
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/bin/commands/iterate/fix-code.mjs +15 -11
- package/bin/commands/iterate/native-stack-rebase.d.mts +6 -5
- package/bin/commands/iterate/native-stack-rebase.mjs +13 -7
- package/bin/commands/iterate/stack-trunk-conflict.d.mts +4 -2
- package/bin/commands/iterate/stack-trunk-conflict.mjs +27 -18
- package/bin/github/gql/upper-layer-conflict-target.gql +0 -15
- package/bin/github/merge-target-rules.d.mts +4 -0
- package/bin/github/merge-target-rules.mjs +9 -11
- package/bin/github/poll-summary-unreported.d.mts +3 -2
- package/bin/github/poll-summary-unreported.mjs +9 -4
- package/bin/github/poll-summary.mjs +1 -1
- package/bin/github/queries.d.mts +2 -2
- package/bin/github/queries.mjs +2 -2
- package/bin/github/stack-read.d.mts +2 -0
- package/bin/github/stack-read.mjs +13 -0
- 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
|
@@ -301,16 +301,20 @@ export async function handleFixCode(ctx) {
|
|
|
301
301
|
baseRefOid: report.baseRefOid,
|
|
302
302
|
})
|
|
303
303
|
: [];
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
304
|
+
// A bottom open layer can still retain its merged parent's PR base. Its update starts
|
|
305
|
+
// at trunk regardless of that base comparison; true upper layers first check the parent.
|
|
306
|
+
const trunkConflict = hasConflicts && stack && report.stackBottomPr === prNumber
|
|
307
|
+
? { trunk: stack.baseRefName, bottomPr: prNumber }
|
|
308
|
+
: hasConflicts && stack && headRef && baseLookup.branch !== stack.baseRefName
|
|
309
|
+
? await lookupUpperLayerTrunkConflict({
|
|
310
|
+
owner: repoOwner,
|
|
311
|
+
name: repoName,
|
|
312
|
+
pr: prNumber,
|
|
313
|
+
headRef,
|
|
314
|
+
trunk: stack.baseRefName,
|
|
315
|
+
bottomPr: report.stackBottomPr,
|
|
316
|
+
})
|
|
317
|
+
: undefined;
|
|
314
318
|
const requeue = buildRemovedQueueRecovery(report, failingAgentChecks, opts.merge);
|
|
315
319
|
const queueRemovalAcknowledgment = buildStackQueueRemovalAcknowledgment(report, failingAgentChecks);
|
|
316
320
|
const queueEjection = !currentEjectionCommit(report, failingAgentChecks)
|
|
@@ -323,7 +327,7 @@ export async function handleFixCode(ctx) {
|
|
|
323
327
|
// Conflicts, a behind branch whose rerun already failed, and a queue ejection that may be
|
|
324
328
|
// reproduced on the latest base all print the branch update route.
|
|
325
329
|
const stackRebase = hasConflicts || (isBehind && exhaustedAttempts.length > 0) || queueEjection
|
|
326
|
-
? buildNativeStackLayerRebase(report.repo, { number: prNumber, baseBranch: baseLookup.branch }, stack, trunkConflict)
|
|
330
|
+
? buildNativeStackLayerRebase(report.repo, { number: prNumber, baseBranch: baseLookup.branch }, stack, trunkConflict, report.stackBottomPr)
|
|
327
331
|
: undefined;
|
|
328
332
|
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, report.viewerAuthorization?.viewerCanUpdate === true, exhaustedAttempts.length > 0, stackRebase, queueEjection);
|
|
329
333
|
if (failingAgentChecks.some((check) => releasedCheckNames.has(check.name))) {
|
|
@@ -22,10 +22,11 @@ export declare function buildNativeStackRebaseInstruction(repo: string, stackNum
|
|
|
22
22
|
/**
|
|
23
23
|
* The stack-aware branch update for a native stack layer (a conflict, or a behind branch whose
|
|
24
24
|
* workflow keeps failing); undefined outside a stack.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* upper layer already contains its parent, so the rebase starts
|
|
25
|
+
* The first open layer in validated stack order is the bottom open layer, even when GitHub
|
|
26
|
+
* retains its merged parent's branch as its PR base. Without that topology, a PR base matching
|
|
27
|
+
* the stack trunk also identifies a bottom layer. Other layers rebase from their PR base.
|
|
28
|
+
* `trunkConflict` means an upper layer already contains its parent, so the rebase starts
|
|
29
|
+
* at the bottom open layer.
|
|
29
30
|
*/
|
|
30
31
|
export declare function buildNativeStackLayerRebase(repo: string, pr: {
|
|
31
32
|
number: number;
|
|
@@ -33,7 +34,7 @@ export declare function buildNativeStackLayerRebase(repo: string, pr: {
|
|
|
33
34
|
}, stack: StackStatus | undefined, trunkConflict?: {
|
|
34
35
|
trunk: string;
|
|
35
36
|
bottomPr?: number;
|
|
36
|
-
}): string | undefined;
|
|
37
|
+
}, stackBottomPr?: number): string | undefined;
|
|
37
38
|
/** Point at the conflicts, and at the stack rebase when the PR is a native stack layer. */
|
|
38
39
|
export declare function buildConflictInstruction(stackRebase: string | undefined): string;
|
|
39
40
|
/**
|
|
@@ -15,26 +15,32 @@ export function buildNativeStackRebaseInstruction(repo, stackNumber, start) {
|
|
|
15
15
|
]
|
|
16
16
|
: "bottomPr" in start
|
|
17
17
|
? [`check out the head branch of PR #${start.bottomPr}`, "gh stack rebase"]
|
|
18
|
-
: [
|
|
18
|
+
: [
|
|
19
|
+
`check out the bottom open layer of the stack to rebase onto \`${start.trunk}\``,
|
|
20
|
+
"gh stack rebase",
|
|
21
|
+
];
|
|
19
22
|
const prepare = `if \`gh stack\` does not track stack #${stackNumber} locally, import it with \`gh stack checkout ${stackNumber}\``;
|
|
20
23
|
return `From a clean checkout of \`${repo}\`, ${prepare}. Then ${checkout} and run \`${command}\`. ${playbookPointer("Branch update")}`;
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
26
|
* The stack-aware branch update for a native stack layer (a conflict, or a behind branch whose
|
|
24
27
|
* workflow keeps failing); undefined outside a stack.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* upper layer already contains its parent, so the rebase starts
|
|
28
|
+
* The first open layer in validated stack order is the bottom open layer, even when GitHub
|
|
29
|
+
* retains its merged parent's branch as its PR base. Without that topology, a PR base matching
|
|
30
|
+
* the stack trunk also identifies a bottom layer. Other layers rebase from their PR base.
|
|
31
|
+
* `trunkConflict` means an upper layer already contains its parent, so the rebase starts
|
|
32
|
+
* at the bottom open layer.
|
|
29
33
|
*/
|
|
30
|
-
export function buildNativeStackLayerRebase(repo, pr, stack, trunkConflict) {
|
|
34
|
+
export function buildNativeStackLayerRebase(repo, pr, stack, trunkConflict, stackBottomPr) {
|
|
31
35
|
if (!stack)
|
|
32
36
|
return undefined;
|
|
33
37
|
const start = trunkConflict
|
|
34
38
|
? trunkConflict.bottomPr !== undefined
|
|
35
39
|
? { bottomPr: trunkConflict.bottomPr }
|
|
36
40
|
: { trunk: trunkConflict.trunk }
|
|
37
|
-
:
|
|
41
|
+
: (stackBottomPr !== undefined
|
|
42
|
+
? pr.number === stackBottomPr
|
|
43
|
+
: pr.baseBranch === stack.baseRefName)
|
|
38
44
|
? { bottomPr: pr.number }
|
|
39
45
|
: { parentBranch: pr.baseBranch };
|
|
40
46
|
return buildNativeStackRebaseInstruction(repo, stack.number, start);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** A bottom open layer, or an upper layer that already contains its parent, updates from trunk. */
|
|
2
2
|
export interface UpperLayerTrunkConflict {
|
|
3
3
|
trunk: string;
|
|
4
|
-
/**
|
|
4
|
+
/** First open layer after a verified merged prefix. Omitted when topology is unavailable. */
|
|
5
5
|
bottomPr?: number;
|
|
6
6
|
}
|
|
7
7
|
export declare function lookupUpperLayerTrunkConflict(input: {
|
|
@@ -10,4 +10,6 @@ export declare function lookupUpperLayerTrunkConflict(input: {
|
|
|
10
10
|
pr: number;
|
|
11
11
|
headRef: string;
|
|
12
12
|
trunk: string;
|
|
13
|
+
/** Reuse the bottom layer already resolved for the report, avoiding a topology fetch. */
|
|
14
|
+
bottomPr?: number;
|
|
13
15
|
}): Promise<UpperLayerTrunkConflict | undefined>;
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { graphql } from "../../github/client.mjs";
|
|
2
|
+
import { readStackTopology, verifiedBottomOpenLayer } from "../../github/stack-read.mjs";
|
|
2
3
|
import { UPPER_LAYER_CONFLICT_TARGET_QUERY } from "../../github/queries.mjs";
|
|
3
4
|
import { pollRateLimitRetryAfterMs } from "../poll-quota.mjs";
|
|
4
|
-
/** Lowest-position open layer whose base is the stack trunk, if this page contains one. */
|
|
5
|
-
function selectBottomOpenLayer(nodes, trunk) {
|
|
6
|
-
const onTrunk = nodes.flatMap((node) => {
|
|
7
|
-
const pull = node?.pullRequest;
|
|
8
|
-
if (!node || !pull || pull.state !== "OPEN" || pull.baseRefName !== trunk)
|
|
9
|
-
return [];
|
|
10
|
-
return [{ position: node.position, number: pull.number }];
|
|
11
|
-
});
|
|
12
|
-
onTrunk.sort((a, b) => a.position - b.position);
|
|
13
|
-
return onTrunk[0]?.number;
|
|
14
|
-
}
|
|
15
5
|
export async function lookupUpperLayerTrunkConflict(input) {
|
|
6
|
+
let bottomPr = input.bottomPr;
|
|
7
|
+
if (bottomPr === undefined) {
|
|
8
|
+
let topology;
|
|
9
|
+
try {
|
|
10
|
+
topology = await readStackTopology(input.pr, { owner: input.owner, name: input.name });
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (pollRateLimitRetryAfterMs(err) !== null)
|
|
14
|
+
throw err;
|
|
15
|
+
ignore(input.pr, err instanceof Error ? err.message : String(err));
|
|
16
|
+
}
|
|
17
|
+
if (topology) {
|
|
18
|
+
try {
|
|
19
|
+
bottomPr = verifiedBottomOpenLayer(topology.ordered, input.pr).number;
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
ignore(input.pr, err instanceof Error ? err.message : String(err));
|
|
23
|
+
// A base comparison cannot prove that a closed or unverified predecessor merged.
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Merged lower layers need not cause GitHub to retarget this PR to trunk. Even when its
|
|
29
|
+
// head is behind that old parent branch, the first open layer rebases the stack onto trunk.
|
|
30
|
+
if (bottomPr === input.pr)
|
|
31
|
+
return { trunk: input.trunk, bottomPr };
|
|
16
32
|
let data;
|
|
17
33
|
try {
|
|
18
34
|
({ data } = await graphql(UPPER_LAYER_CONFLICT_TARGET_QUERY, {
|
|
@@ -37,13 +53,6 @@ export async function lookupUpperLayerTrunkConflict(input) {
|
|
|
37
53
|
// behindBy > 0: the layer is still behind its parent. Keep the parent rebase.
|
|
38
54
|
if (behindBy !== 0)
|
|
39
55
|
return undefined;
|
|
40
|
-
const entries = pull.stack?.entries;
|
|
41
|
-
const bottomPr = entries ? selectBottomOpenLayer(entries.nodes, input.trunk) : undefined;
|
|
42
|
-
if (bottomPr === undefined) {
|
|
43
|
-
ignore(input.pr, entries?.pageInfo.hasNextPage
|
|
44
|
-
? "stack entry page truncated before the trunk layer"
|
|
45
|
-
: "bottom open layer not found");
|
|
46
|
-
}
|
|
47
56
|
return { trunk: input.trunk, ...(bottomPr !== undefined && { bottomPr }) };
|
|
48
57
|
}
|
|
49
58
|
function ignore(pr, message) {
|
|
@@ -16,21 +16,6 @@ query UpperLayerConflictTarget($owner: String!, $name: String!, $number: Int!, $
|
|
|
16
16
|
behindBy
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
stack {
|
|
20
|
-
entries(first: 100) {
|
|
21
|
-
pageInfo {
|
|
22
|
-
hasNextPage
|
|
23
|
-
}
|
|
24
|
-
nodes {
|
|
25
|
-
position
|
|
26
|
-
pullRequest {
|
|
27
|
-
number
|
|
28
|
-
state
|
|
29
|
-
baseRefName
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
19
|
}
|
|
35
20
|
}
|
|
36
21
|
}
|
|
@@ -27,3 +27,7 @@ export declare function loadMergeTargetStatus(input: {
|
|
|
27
27
|
* is also open, so this compare is the behind count that status hides.
|
|
28
28
|
*/
|
|
29
29
|
export declare function loadBaseBehindBy(owner: string, name: string, baseRefName: string, headRef: string): Promise<number>;
|
|
30
|
+
export declare function loadRefRules(owner: string, name: string, qualifiedName: string, headRef: string): Promise<{
|
|
31
|
+
contexts: string[];
|
|
32
|
+
behindBy: number;
|
|
33
|
+
}>;
|
|
@@ -3,7 +3,7 @@ import { parseBranchRules } from "./batch-parsers-rules.mjs";
|
|
|
3
3
|
import { graphqlWithRateLimit } from "./client.mjs";
|
|
4
4
|
import { missingRepositoryError } from "./errors.mjs";
|
|
5
5
|
import { BASE_BEHIND_QUERY, REF_RULES_QUERY } from "./queries.mjs";
|
|
6
|
-
import { readStackTopology } from "./stack-read.mjs";
|
|
6
|
+
import { readStackTopology, verifiedBottomOpenLayer } from "./stack-read.mjs";
|
|
7
7
|
/**
|
|
8
8
|
* Required status contexts for the branch GitHub actually merges into.
|
|
9
9
|
* A native stack uses the trunk ref, and `behindBy` is that trunk against the bottom open layer.
|
|
@@ -15,24 +15,22 @@ export async function loadMergeTargetStatus(input, context) {
|
|
|
15
15
|
let headRef = input.headRefName;
|
|
16
16
|
let stackBottomPr = input.pr;
|
|
17
17
|
if (trunk !== input.baseRefName) {
|
|
18
|
-
const bottom = await bottomOpenLayer(input.pr, { owner: input.owner, name: input.name },
|
|
19
|
-
headRef = bottom.
|
|
18
|
+
const bottom = await bottomOpenLayer(input.pr, { owner: input.owner, name: input.name }, context);
|
|
19
|
+
headRef = bottom.headRefOid;
|
|
20
20
|
stackBottomPr = bottom.number;
|
|
21
21
|
}
|
|
22
|
-
const loaded = await
|
|
22
|
+
const loaded = await loadRefRules(input.owner, input.name, `refs/heads/${trunk}`, headRef);
|
|
23
23
|
return {
|
|
24
24
|
contexts: loaded.contexts,
|
|
25
25
|
...(loaded.behindBy > 0 && { trunkBehindBy: loaded.behindBy }),
|
|
26
26
|
stackBottomPr,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
async function bottomOpenLayer(pr, repo,
|
|
29
|
+
async function bottomOpenLayer(pr, repo, context) {
|
|
30
30
|
const topology = await (context?.readStackTopology(pr, repo) ?? readStackTopology(pr, repo));
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
return bottom;
|
|
31
|
+
// GitHub can retain a merged parent's branch as the lowest open layer's
|
|
32
|
+
// base. Stack order, rather than retargeting, determines the review target.
|
|
33
|
+
return verifiedBottomOpenLayer(topology.ordered, pr);
|
|
36
34
|
}
|
|
37
35
|
/**
|
|
38
36
|
* Commits on the PR base that `headRef` does not contain.
|
|
@@ -57,7 +55,7 @@ export async function loadBaseBehindBy(owner, name, baseRefName, headRef) {
|
|
|
57
55
|
}
|
|
58
56
|
return ref.compare?.behindBy ?? 0;
|
|
59
57
|
}
|
|
60
|
-
async function
|
|
58
|
+
export async function loadRefRules(owner, name, qualifiedName, headRef) {
|
|
61
59
|
const { data } = await graphqlWithRateLimit(REF_RULES_QUERY, {
|
|
62
60
|
owner,
|
|
63
61
|
repo: name,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { RepoInfo } from "./client.mts";
|
|
1
2
|
import type { PollSummaryChecks } from "../types.mts";
|
|
2
3
|
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
3
|
-
/**
|
|
4
|
-
export declare function trunkRequiredContexts(ordered: readonly RawSummaryPr[]): string[] | undefined
|
|
4
|
+
/** Current trunk rules may remain available on a merged layer's base ref. */
|
|
5
|
+
export declare function trunkRequiredContexts(ordered: readonly RawSummaryPr[], repo: RepoInfo): Promise<string[] | undefined>;
|
|
5
6
|
/** Record missing required contexts. A running Actions workflow does not clear the names. */
|
|
6
7
|
export declare function applyUnreportedRequiredChecks(raw: RawSummaryPr, checks: PollSummaryChecks, mergeTargetContexts: readonly string[] | undefined): void;
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { loadConfig } from "../config/load.mjs";
|
|
2
2
|
import { actionsWorkflowInProgress, selectMergeTargetContexts, unreportedRequiredContexts, } from "../checks/unreported-required.mjs";
|
|
3
3
|
import { parseBranchRules } from "./batch-parsers-rules.mjs";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { loadRefRules } from "./merge-target-rules.mjs";
|
|
5
|
+
/** Current trunk rules may remain available on a merged layer's base ref. */
|
|
6
|
+
export async function trunkRequiredContexts(ordered, repo) {
|
|
6
7
|
const trunk = ordered.find((pr) => pr.stack)?.stack?.baseRefName;
|
|
7
8
|
if (!trunk)
|
|
8
9
|
return undefined;
|
|
9
|
-
const bottom = ordered.find((pr) => pr.state === "OPEN"
|
|
10
|
+
const bottom = ordered.find((pr) => pr.state === "OPEN");
|
|
10
11
|
if (!bottom)
|
|
11
12
|
return undefined;
|
|
12
|
-
|
|
13
|
+
const trunkLayer = ordered.find((pr) => pr.baseRefName === trunk && pr.baseRef != null);
|
|
14
|
+
if (trunkLayer)
|
|
15
|
+
return parseBranchRules(trunkLayer.baseRef).requiredStatusCheckContexts;
|
|
16
|
+
const loaded = await loadRefRules(repo.owner, repo.name, `refs/heads/${trunk}`, bottom.headRefOid);
|
|
17
|
+
return loaded.contexts;
|
|
13
18
|
}
|
|
14
19
|
/** Record missing required contexts. A running Actions workflow does not clear the names. */
|
|
15
20
|
export function applyUnreportedRequiredChecks(raw, checks, mergeTargetContexts) {
|
|
@@ -72,7 +72,7 @@ async function fetchStackSummary(opts, repo) {
|
|
|
72
72
|
await refreshUnknownSummaryMergeability(pr, repo);
|
|
73
73
|
}
|
|
74
74
|
const stackAncestry = stackAncestryGaps(ordered);
|
|
75
|
-
const required = trunkRequiredContexts(ordered);
|
|
75
|
+
const required = await trunkRequiredContexts(ordered, repo);
|
|
76
76
|
return {
|
|
77
77
|
selection: { kind: "stack", anchor, stackNumber, stackSize },
|
|
78
78
|
prs: await Promise.all(ordered.map((pr) => summarizePollSummaryPr(pr, repo, opts, viewerCanAdminister, required, viewerLogin))),
|
package/bin/github/queries.d.mts
CHANGED
|
@@ -35,8 +35,8 @@ export declare const POLL_STACK_SUMMARY_QUERY: string;
|
|
|
35
35
|
/** Reads only native-stack membership and the refs that link adjacent entries. */
|
|
36
36
|
export declare const POLL_STACK_TOPOLOGY_QUERY: string;
|
|
37
37
|
/**
|
|
38
|
-
* Whether a conflicting upper stack layer already contains its PR base
|
|
39
|
-
*
|
|
38
|
+
* Whether a conflicting upper stack layer already contains its PR base.
|
|
39
|
+
* Bottom-layer identity comes from the validated stack topology. Not part of BatchPr.
|
|
40
40
|
*/
|
|
41
41
|
export declare const UPPER_LAYER_CONFLICT_TARGET_QUERY: string;
|
|
42
42
|
/** Merged PRs whose head may be the stale base of a non-stack conflict. */
|
package/bin/github/queries.mjs
CHANGED
|
@@ -40,8 +40,8 @@ export const POLL_STACK_SUMMARY_QUERY = `${POLL_SUMMARY_FRAGMENT}\n${gql("poll-s
|
|
|
40
40
|
/** Reads only native-stack membership and the refs that link adjacent entries. */
|
|
41
41
|
export const POLL_STACK_TOPOLOGY_QUERY = gql("poll-stack-topology.gql");
|
|
42
42
|
/**
|
|
43
|
-
* Whether a conflicting upper stack layer already contains its PR base
|
|
44
|
-
*
|
|
43
|
+
* Whether a conflicting upper stack layer already contains its PR base.
|
|
44
|
+
* Bottom-layer identity comes from the validated stack topology. Not part of BatchPr.
|
|
45
45
|
*/
|
|
46
46
|
export const UPPER_LAYER_CONFLICT_TARGET_QUERY = gql("upper-layer-conflict-target.gql");
|
|
47
47
|
/** Merged PRs whose head may be the stale base of a non-stack conflict. */
|
|
@@ -31,6 +31,8 @@ export interface StackRead<Pr extends StackMemberRefs> {
|
|
|
31
31
|
export declare function readStack<Pr extends StackMemberRefs>(query: string, anchor: number, repo: RepoInfo, variables?: Record<string, unknown>): Promise<StackRead<Pr>>;
|
|
32
32
|
/** Membership and linking refs only — no per-PR CI or review hydration. */
|
|
33
33
|
export declare function readStackTopology(anchor: number, repo: RepoInfo): Promise<StackRead<StackMemberRefs>>;
|
|
34
|
+
/** The first open member can replace merged lower layers only after a fully merged prefix. */
|
|
35
|
+
export declare function verifiedBottomOpenLayer<Pr extends StackMemberRefs>(ordered: readonly Pr[], anchor: number): Pr;
|
|
34
36
|
/**
|
|
35
37
|
* Adjacent entries, both open, whose child base no longer matches the parent
|
|
36
38
|
* head. Both sides come from the same read, so each gap was observed together.
|
|
@@ -77,6 +77,19 @@ export async function readStack(query, anchor, repo, variables = {}) {
|
|
|
77
77
|
export function readStackTopology(anchor, repo) {
|
|
78
78
|
return readStack(POLL_STACK_TOPOLOGY_QUERY, anchor, repo);
|
|
79
79
|
}
|
|
80
|
+
/** The first open member can replace merged lower layers only after a fully merged prefix. */
|
|
81
|
+
export function verifiedBottomOpenLayer(ordered, anchor) {
|
|
82
|
+
const index = ordered.findIndex((member) => member.state === "OPEN");
|
|
83
|
+
const bottom = ordered[index];
|
|
84
|
+
if (!bottom) {
|
|
85
|
+
throw new ShepherdError(`Native stack for PR #${anchor} has no open layer`, EXIT.TEMPFAIL);
|
|
86
|
+
}
|
|
87
|
+
const predecessor = ordered.slice(0, index).find((member) => member.state !== "MERGED");
|
|
88
|
+
if (predecessor) {
|
|
89
|
+
throw new ShepherdError(`Native stack for PR #${anchor} has PR #${predecessor.number} in state ${predecessor.state} before open PR #${bottom.number}; cannot verify the bottom open layer`, EXIT.TEMPFAIL);
|
|
90
|
+
}
|
|
91
|
+
return bottom;
|
|
92
|
+
}
|
|
80
93
|
/**
|
|
81
94
|
* Adjacent entries, both open, whose child base no longer matches the parent
|
|
82
95
|
* head. Both sides come from the same read, so each gap was observed together.
|
package/package.json
CHANGED