pr-shepherd 0.53.1 → 0.54.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 +1 -1
- package/bin/cli/args.mjs +3 -0
- package/bin/cli/check-blocker-handler.d.mts +2 -0
- package/bin/cli/check-blocker-handler.mjs +82 -0
- package/bin/cli/help-command-pages.d.mts +22 -3
- package/bin/cli/help-command-pages.mjs +21 -2
- package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.mjs +1 -1
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +3 -1
- package/bin/cli/help.d.mts +23 -4
- package/bin/cli/help.mjs +2 -0
- package/bin/cli/iterate-branch-segment.d.mts +3 -0
- package/bin/cli/iterate-branch-segment.mjs +13 -0
- package/bin/cli/iterate-formatter.mjs +5 -13
- package/bin/cli/iterate-lean.mjs +1 -0
- package/bin/cli/poll-handler.mjs +5 -1
- package/bin/cli-parser.mjs +4 -0
- package/bin/commands/apply-check-blocker.d.mts +22 -0
- package/bin/commands/apply-check-blocker.mjs +36 -0
- package/bin/commands/check-annotations.d.mts +1 -1
- package/bin/commands/check-annotations.mjs +27 -17
- package/bin/commands/check-blocker-ref.d.mts +3 -0
- package/bin/commands/check-blocker-ref.mjs +50 -0
- package/bin/commands/check.mjs +1 -0
- package/bin/commands/iterate/check-blocker-gate.d.mts +18 -0
- package/bin/commands/iterate/check-blocker-gate.mjs +115 -0
- package/bin/commands/iterate/check-instructions.d.mts +2 -0
- package/bin/commands/iterate/check-instructions.mjs +4 -0
- package/bin/commands/iterate/conflicting-head-ci.d.mts +18 -0
- package/bin/commands/iterate/conflicting-head-ci.mjs +32 -0
- package/bin/commands/iterate/fix-code.d.mts +2 -0
- package/bin/commands/iterate/fix-code.mjs +61 -16
- package/bin/commands/iterate/index.mjs +22 -8
- package/bin/commands/iterate/native-stack-rebase.d.mts +8 -2
- package/bin/commands/iterate/native-stack-rebase.mjs +14 -4
- package/bin/commands/iterate/stack-trunk-conflict.d.mts +13 -0
- package/bin/commands/iterate/stack-trunk-conflict.mjs +51 -0
- package/bin/commands/poll-quota.d.mts +2 -0
- package/bin/commands/poll-quota.mjs +25 -12
- package/bin/commands/poll-rate-limit-cancel.d.mts +35 -0
- package/bin/commands/poll-rate-limit-cancel.mjs +101 -0
- package/bin/commands/poll-rate-limit-delay.d.mts +1 -0
- package/bin/commands/poll-rate-limit-delay.mjs +7 -0
- package/bin/commands/poll-rate-limit-wait.d.mts +14 -0
- package/bin/commands/poll-rate-limit-wait.mjs +125 -0
- package/bin/commands/poll-summary.mjs +21 -9
- package/bin/commands/poll.mjs +22 -17
- package/bin/config/load.d.mts +2 -0
- package/bin/config/load.mjs +24 -2
- package/bin/config.json +1 -0
- package/bin/github/batch-parse-suites.d.mts +2 -0
- package/bin/github/batch-parse-suites.mjs +5 -0
- package/bin/github/batch.d.mts +2 -0
- package/bin/github/batch.mjs +2 -1
- package/bin/github/check-annotation-cache.d.mts +8 -0
- package/bin/github/check-annotation-cache.mjs +23 -0
- package/bin/github/check-annotation-pages.d.mts +11 -0
- package/bin/github/check-annotation-pages.mjs +36 -0
- package/bin/github/check-annotation-shape.d.mts +21 -0
- package/bin/github/check-annotation-shape.mjs +50 -0
- package/bin/github/check-annotations-batch.d.mts +17 -0
- package/bin/github/check-annotations-batch.mjs +92 -0
- package/bin/github/check-annotations.d.mts +8 -11
- package/bin/github/check-annotations.mjs +14 -103
- package/bin/github/gql/batch-pr.gql +1 -1
- package/bin/github/gql/check-run-annotations-batch.gql +41 -0
- package/bin/github/gql/upper-layer-conflict-target.gql +36 -0
- package/bin/github/pagination.d.mts +1 -1
- package/bin/github/pagination.mjs +1 -1
- package/bin/github/poll-summary-check-blockers.d.mts +16 -0
- package/bin/github/poll-summary-check-blockers.mjs +25 -0
- package/bin/github/poll-summary-checks.d.mts +2 -0
- package/bin/github/poll-summary-checks.mjs +35 -20
- package/bin/github/poll-summary-projector.mjs +5 -0
- package/bin/github/queries.d.mts +11 -0
- package/bin/github/queries.mjs +11 -0
- package/bin/state/check-blockers.d.mts +30 -0
- package/bin/state/check-blockers.mjs +88 -0
- package/bin/state/conflicting-head-seen.d.mts +13 -0
- package/bin/state/conflicting-head-seen.mjs +44 -0
- package/bin/types/iterate.d.mts +5 -0
- package/bin/types/report.d.mts +2 -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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchCheckRunAnnotationsBatch } from "../github/check-annotations-batch.mjs";
|
|
2
|
+
import { pollRateLimitRetryAfterMs } from "./poll-quota.mjs";
|
|
2
3
|
function shouldFetchCheckAnnotations(check) {
|
|
3
4
|
return check.id != null && check.status === "COMPLETED" && check.hasAnnotations === true;
|
|
4
5
|
}
|
|
@@ -58,30 +59,39 @@ export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber,
|
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
async function attachUnseenCheckAnnotations(checks, seenMap, prNumber, cacheOpts) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
return checksWithAnnotations;
|
|
67
|
-
}
|
|
68
|
-
async function attachForCheck(check, seenMap, prNumber, cacheOpts) {
|
|
69
|
-
if (check.id == null)
|
|
70
|
-
return check;
|
|
71
|
-
let annotations;
|
|
62
|
+
if (checks.length === 0)
|
|
63
|
+
return checks;
|
|
64
|
+
const ids = checks.flatMap((check) => (check.id == null ? [] : [check.id]));
|
|
65
|
+
let batch;
|
|
72
66
|
try {
|
|
73
|
-
|
|
67
|
+
batch = await fetchCheckRunAnnotationsBatch(ids, cacheOpts);
|
|
74
68
|
}
|
|
75
69
|
catch (err) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
if (pollRateLimitRetryAfterMs(err) !== null)
|
|
71
|
+
throw err;
|
|
72
|
+
writeAnnotationFailureSummary(prNumber, ids.length, err);
|
|
73
|
+
return checks;
|
|
79
74
|
}
|
|
80
|
-
const
|
|
75
|
+
const firstFailure = batch.failures[0];
|
|
76
|
+
if (firstFailure !== undefined) {
|
|
77
|
+
writeAnnotationFailureSummary(prNumber, batch.failures.length, firstFailure.error);
|
|
78
|
+
}
|
|
79
|
+
return checks.map((check) => withUnseenAnnotations(check, batch.annotations, seenMap));
|
|
80
|
+
}
|
|
81
|
+
function withUnseenAnnotations(check, annotations, seenMap) {
|
|
82
|
+
const fetched = check.id == null ? undefined : annotations.get(check.id);
|
|
83
|
+
if (fetched === undefined)
|
|
84
|
+
return check;
|
|
85
|
+
const unseen = fetched.filter((annotation) => !seenMap.has(annotation.id));
|
|
81
86
|
if (unseen.length === 0)
|
|
82
87
|
return check;
|
|
83
88
|
return { ...check, annotations: unseen };
|
|
84
89
|
}
|
|
90
|
+
function writeAnnotationFailureSummary(prNumber, count, err) {
|
|
91
|
+
const noun = count === 1 ? "check" : "checks";
|
|
92
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
93
|
+
process.stderr.write(`pr-shepherd: annotation fetch failed for ${count} ${noun} on PR #${prNumber} (ignored): ${message}\n`);
|
|
94
|
+
}
|
|
85
95
|
export function annotationMarkerBody(a) {
|
|
86
96
|
return JSON.stringify({
|
|
87
97
|
path: a.path,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const NAME = /^[A-Za-z0-9_.-]+$/;
|
|
2
|
+
const SHORTHAND = /^(?:(issue):)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#([1-9]\d*)$/;
|
|
3
|
+
/** Parse `--blocked-by`. Returns null for anything other than the four accepted forms. */
|
|
4
|
+
export function parseBlockedByRef(raw) {
|
|
5
|
+
const text = raw.trim();
|
|
6
|
+
const short = SHORTHAND.exec(text);
|
|
7
|
+
if (short?.[2] && short[3] && short[4]) {
|
|
8
|
+
const number = Number(short[4]);
|
|
9
|
+
if (!Number.isSafeInteger(number))
|
|
10
|
+
return null;
|
|
11
|
+
return {
|
|
12
|
+
owner: short[2],
|
|
13
|
+
name: short[3],
|
|
14
|
+
number,
|
|
15
|
+
kind: short[1] === "issue" ? "issue" : "pull",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return parseUrl(text);
|
|
19
|
+
}
|
|
20
|
+
function parseUrl(text) {
|
|
21
|
+
let url;
|
|
22
|
+
try {
|
|
23
|
+
url = new URL(text);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
29
|
+
return null;
|
|
30
|
+
if (url.hostname !== "github.com" && url.hostname !== "www.github.com")
|
|
31
|
+
return null;
|
|
32
|
+
if (url.search !== "" || url.hash !== "")
|
|
33
|
+
return null;
|
|
34
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
35
|
+
if (parts.length !== 4)
|
|
36
|
+
return null;
|
|
37
|
+
const [owner, name, type, num] = parts;
|
|
38
|
+
if (!owner || !name || !num || !NAME.test(owner) || !NAME.test(name))
|
|
39
|
+
return null;
|
|
40
|
+
if (!/^[1-9]\d*$/.test(num))
|
|
41
|
+
return null;
|
|
42
|
+
const number = Number(num);
|
|
43
|
+
if (!Number.isSafeInteger(number))
|
|
44
|
+
return null;
|
|
45
|
+
if (type === "pull")
|
|
46
|
+
return { owner, name, number, kind: "pull" };
|
|
47
|
+
if (type === "issues")
|
|
48
|
+
return { owner, name, number, kind: "issue" };
|
|
49
|
+
return null;
|
|
50
|
+
}
|
package/bin/commands/check.mjs
CHANGED
|
@@ -259,6 +259,7 @@ export async function runCheck(opts) {
|
|
|
259
259
|
status,
|
|
260
260
|
baseBranch: batchData.baseRefName,
|
|
261
261
|
...(batchData.baseRefOid && { baseRefOid: batchData.baseRefOid }),
|
|
262
|
+
...(result.headCheckSuitesEmpty && { headCheckSuitesEmpty: true }),
|
|
262
263
|
mergeStatus,
|
|
263
264
|
checks: {
|
|
264
265
|
passing: merged.passing,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IterateResult } from "../../types.mts";
|
|
2
|
+
export interface CheckBlockerGate {
|
|
3
|
+
deferredNames: ReadonlySet<string>;
|
|
4
|
+
releasedNames: ReadonlySet<string>;
|
|
5
|
+
/** `owner/name#number` for blockers that are still open. */
|
|
6
|
+
openBlockers: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
interface StateKey {
|
|
9
|
+
owner: string;
|
|
10
|
+
repo: string;
|
|
11
|
+
pr: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveCheckBlockerGate(key: StateKey, failing: readonly {
|
|
14
|
+
name: string;
|
|
15
|
+
}[], mergeStatus?: string): Promise<CheckBlockerGate | null>;
|
|
16
|
+
/** Append the open blocker to a WAIT log. Other actions are unchanged. */
|
|
17
|
+
export declare function annotateBlockedWait(result: IterateResult, gate: CheckBlockerGate | null): IterateResult;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { graphql } from "../../github/client.mjs";
|
|
2
|
+
import { pollRateLimitRetryAfterMs } from "../poll-quota.mjs";
|
|
3
|
+
import { clearCheckBlocker, formatCheckBlockerRef, readCheckBlockers, } from "../../state/check-blockers.mjs";
|
|
4
|
+
const RATE_LIMIT = "cost limit nodeCount remaining resetAt used";
|
|
5
|
+
const PULL_QUERY = `query CheckBlockerPull($owner: String!, $name: String!, $number: Int!) {
|
|
6
|
+
_shepherdRateLimit: rateLimit { ${RATE_LIMIT} }
|
|
7
|
+
repository(owner: $owner, name: $name) {
|
|
8
|
+
pullRequest(number: $number) { state merged }
|
|
9
|
+
}
|
|
10
|
+
}`;
|
|
11
|
+
const ISSUE_QUERY = `query CheckBlockerIssue($owner: String!, $name: String!, $number: Int!) {
|
|
12
|
+
_shepherdRateLimit: rateLimit { ${RATE_LIMIT} }
|
|
13
|
+
repository(owner: $owner, name: $name) {
|
|
14
|
+
issue(number: $number) { state }
|
|
15
|
+
}
|
|
16
|
+
}`;
|
|
17
|
+
/**
|
|
18
|
+
* Match failing checks to stored blockers. No records, or none of them name a
|
|
19
|
+
* failing check, skips GitHub. Rate-limit errors propagate; other lookup
|
|
20
|
+
* failures leave that check as a normal failure.
|
|
21
|
+
*/
|
|
22
|
+
/** Behind or conflicting branches still need the update-branch instruction. */
|
|
23
|
+
const KEEP_RELEASED_STATUS = new Set(["BEHIND", "CONFLICTS", "DIRTY", "UNKNOWN"]);
|
|
24
|
+
export async function resolveCheckBlockerGate(key, failing, mergeStatus) {
|
|
25
|
+
const records = await readCheckBlockers(key);
|
|
26
|
+
if (records.length === 0 || failing.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
const failingNames = new Set(failing.map((check) => check.name));
|
|
29
|
+
const matched = records.filter((record) => failingNames.has(record.checkName));
|
|
30
|
+
if (matched.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
const lookups = new Map();
|
|
33
|
+
for (const record of matched) {
|
|
34
|
+
const id = blockerKey(record.blocker);
|
|
35
|
+
if (lookups.has(id))
|
|
36
|
+
continue;
|
|
37
|
+
lookups.set(id, await lookupBlocker(record.blocker));
|
|
38
|
+
}
|
|
39
|
+
const deferredNames = new Set();
|
|
40
|
+
const releasedNames = new Set();
|
|
41
|
+
const openBlockers = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const record of matched) {
|
|
44
|
+
const status = lookups.get(blockerKey(record.blocker));
|
|
45
|
+
if (status === "deferred") {
|
|
46
|
+
deferredNames.add(record.checkName);
|
|
47
|
+
const label = formatCheckBlockerRef(record.blocker);
|
|
48
|
+
if (!seen.has(label)) {
|
|
49
|
+
seen.add(label);
|
|
50
|
+
openBlockers.push(label);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (status === "released") {
|
|
54
|
+
if (mergeStatus !== undefined && !KEEP_RELEASED_STATUS.has(mergeStatus)) {
|
|
55
|
+
// The branch is already current, so update-branch would no-op and a
|
|
56
|
+
// later unrelated failure must be a normal check again.
|
|
57
|
+
await clearCheckBlocker(key, record.checkName);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
releasedNames.add(record.checkName);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (deferredNames.size === 0 && releasedNames.size === 0)
|
|
65
|
+
return null;
|
|
66
|
+
return { deferredNames, releasedNames, openBlockers };
|
|
67
|
+
}
|
|
68
|
+
/** Append the open blocker to a WAIT log. Other actions are unchanged. */
|
|
69
|
+
export function annotateBlockedWait(result, gate) {
|
|
70
|
+
if (result.action !== "wait" || gate === null || gate.openBlockers.length === 0)
|
|
71
|
+
return result;
|
|
72
|
+
const note = `blocked by ${gate.openBlockers.join(", ")}`;
|
|
73
|
+
if (result.log.includes(note))
|
|
74
|
+
return result;
|
|
75
|
+
return { ...result, log: `${result.log} — ${note}` };
|
|
76
|
+
}
|
|
77
|
+
async function lookupBlocker(blocker) {
|
|
78
|
+
try {
|
|
79
|
+
const vars = { owner: blocker.owner, name: blocker.name, number: blocker.number };
|
|
80
|
+
if (blocker.kind === "issue") {
|
|
81
|
+
const { data } = await graphql(ISSUE_QUERY, vars);
|
|
82
|
+
const issue = data.repository?.issue;
|
|
83
|
+
if (!issue)
|
|
84
|
+
return ignore(blocker, "not found");
|
|
85
|
+
if (issue.state === "OPEN")
|
|
86
|
+
return "deferred";
|
|
87
|
+
if (issue.state === "CLOSED")
|
|
88
|
+
return "released";
|
|
89
|
+
return ignore(blocker, `unexpected state ${issue.state}`);
|
|
90
|
+
}
|
|
91
|
+
const { data } = await graphql(PULL_QUERY, vars);
|
|
92
|
+
const pull = data.repository?.pullRequest;
|
|
93
|
+
if (!pull)
|
|
94
|
+
return ignore(blocker, "not found");
|
|
95
|
+
if (pull.state === "OPEN" && pull.merged !== true)
|
|
96
|
+
return "deferred";
|
|
97
|
+
if (pull.state === "MERGED" || pull.state === "CLOSED" || pull.merged === true) {
|
|
98
|
+
return "released";
|
|
99
|
+
}
|
|
100
|
+
return ignore(blocker, `unexpected state ${pull.state}`);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (pollRateLimitRetryAfterMs(err) !== null)
|
|
104
|
+
throw err;
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
return ignore(blocker, message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function ignore(blocker, message) {
|
|
110
|
+
process.stderr.write(`pr-shepherd: check blocker lookup failed for ${formatCheckBlockerRef(blocker)} (ignored): ${message}\n`);
|
|
111
|
+
return "ignored";
|
|
112
|
+
}
|
|
113
|
+
function blockerKey(blocker) {
|
|
114
|
+
return `${blocker.kind}:${formatCheckBlockerRef(blocker)}`;
|
|
115
|
+
}
|
|
@@ -33,4 +33,6 @@ export declare function buildRepeatedWorkflowBranchRecoveryInstructions(baseBran
|
|
|
33
33
|
export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand): string[];
|
|
34
34
|
/** Build the CI-triage pointer; the skill limits follow-up actions to included evidence. */
|
|
35
35
|
export declare function buildFailingCheckInstructions(checks: AgentCheck[]): string[];
|
|
36
|
+
/** Update the PR branch after an external blocker merges or closes. Never rerun that job. */
|
|
37
|
+
export declare function buildReleasedBlockerInstruction(prNumber: number): string;
|
|
36
38
|
export declare function buildFixCompletionInstruction(checks: AgentCheck[], hasConflicts?: boolean, hasShaGatedReviewMutations?: boolean, pushesRewrittenStack?: boolean): string;
|
|
@@ -80,6 +80,10 @@ export function buildFailingCheckInstructions(checks) {
|
|
|
80
80
|
}
|
|
81
81
|
return instructions;
|
|
82
82
|
}
|
|
83
|
+
/** Update the PR branch after an external blocker merges or closes. Never rerun that job. */
|
|
84
|
+
export function buildReleasedBlockerInstruction(prNumber) {
|
|
85
|
+
return `Update this PR branch from its base with \`gh pr update-branch ${prNumber} --rebase\`. Do not rerun the job; a rerun retests the old merge ref.`;
|
|
86
|
+
}
|
|
83
87
|
export function buildFixCompletionInstruction(checks, hasConflicts = false, hasShaGatedReviewMutations = false, pushesRewrittenStack = false) {
|
|
84
88
|
const push = pushesRewrittenStack
|
|
85
89
|
? "push the rewritten stack with `gh stack push`"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function conflictingHeadCiNote(input: {
|
|
2
|
+
hasConflicts: boolean;
|
|
3
|
+
headCheckSuitesEmpty: boolean;
|
|
4
|
+
checkRunCount: number;
|
|
5
|
+
/** Unix seconds when Shepherd first saw this head. Omit when that time is unknown. */
|
|
6
|
+
firstSeenAtUnix: number | null | undefined;
|
|
7
|
+
nowMs: number;
|
|
8
|
+
}): string | undefined;
|
|
9
|
+
export declare function countReportedChecks(checks: {
|
|
10
|
+
passing: readonly unknown[];
|
|
11
|
+
failing: readonly unknown[];
|
|
12
|
+
inProgress: readonly unknown[];
|
|
13
|
+
skipped: readonly unknown[];
|
|
14
|
+
filtered: readonly unknown[];
|
|
15
|
+
ignored?: readonly unknown[];
|
|
16
|
+
}): number;
|
|
17
|
+
/** Place the missing-CI note with the conflict step, ahead of later repair lines. */
|
|
18
|
+
export declare function insertConflictingHeadCiNote(instructions: string[], note: string | undefined): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** How long a conflicting head may sit with no CI before Shepherd says none will start. */
|
|
2
|
+
const CONFLICTING_HEAD_CI_GRACE_MS = 2 * 60 * 1000;
|
|
3
|
+
const NO_CI_NOTE = "GitHub did not start pull_request workflows for this conflicting head: no check suites or workflow runs have appeared for at least 2 minutes. Missing CI is a consequence of the conflict, not an outage, and pushing this same head again will not start them.";
|
|
4
|
+
export function conflictingHeadCiNote(input) {
|
|
5
|
+
if (!input.hasConflicts || !input.headCheckSuitesEmpty || input.checkRunCount > 0) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
if (typeof input.firstSeenAtUnix !== "number")
|
|
9
|
+
return undefined;
|
|
10
|
+
const ageMs = input.nowMs - input.firstSeenAtUnix * 1000;
|
|
11
|
+
if (!Number.isFinite(ageMs) || ageMs < CONFLICTING_HEAD_CI_GRACE_MS)
|
|
12
|
+
return undefined;
|
|
13
|
+
return NO_CI_NOTE;
|
|
14
|
+
}
|
|
15
|
+
export function countReportedChecks(checks) {
|
|
16
|
+
return (checks.passing.length +
|
|
17
|
+
checks.failing.length +
|
|
18
|
+
checks.inProgress.length +
|
|
19
|
+
checks.skipped.length +
|
|
20
|
+
checks.filtered.length +
|
|
21
|
+
(checks.ignored?.length ?? 0));
|
|
22
|
+
}
|
|
23
|
+
/** Place the missing-CI note with the conflict step, ahead of later repair lines. */
|
|
24
|
+
export function insertConflictingHeadCiNote(instructions, note) {
|
|
25
|
+
if (!note)
|
|
26
|
+
return;
|
|
27
|
+
const conflictAt = instructions.findIndex((step) => step.includes("merge conflicts"));
|
|
28
|
+
if (conflictAt >= 0)
|
|
29
|
+
instructions.splice(conflictAt + 1, 0, note);
|
|
30
|
+
else
|
|
31
|
+
instructions.unshift(note);
|
|
32
|
+
}
|
|
@@ -19,6 +19,8 @@ interface HandleFixCodeContext {
|
|
|
19
19
|
editedSummaries: Review[];
|
|
20
20
|
surfacedApprovals: Review[];
|
|
21
21
|
botUsernames: NormalizedBotUsernames;
|
|
22
|
+
/** Failing checks whose external blocker has merged or closed. */
|
|
23
|
+
releasedCheckNames?: ReadonlySet<string>;
|
|
22
24
|
ruleAutoResolveThreadIds?: string[];
|
|
23
25
|
/** Verified stack-repair guidance, when ancestry is stale. */
|
|
24
26
|
repairInstructions?: string[];
|
|
@@ -6,7 +6,11 @@ import { checkEscalateTriggers, validateBaseBranch, buildEscalateSuggestion, bui
|
|
|
6
6
|
import { buildResolveCommand } from "./classify.mjs";
|
|
7
7
|
import { buildThreadMutationRouting, threadHasAuthorizedMutation, } from "./thread-mutation-routing.mjs";
|
|
8
8
|
import { buildFixInstructions } from "./render.mjs";
|
|
9
|
+
import { buildReleasedBlockerInstruction } from "./check-instructions.mjs";
|
|
9
10
|
import { buildNativeStackLayerRebase } from "./native-stack-rebase.mjs";
|
|
11
|
+
import { lookupUpperLayerTrunkConflict } from "./stack-trunk-conflict.mjs";
|
|
12
|
+
import { conflictingHeadCiNote, countReportedChecks, insertConflictingHeadCiNote, } from "./conflicting-head-ci.mjs";
|
|
13
|
+
import { conflictingHeadFirstSeenUnix } from "../../state/conflicting-head-seen.mjs";
|
|
10
14
|
import { applyStallGuard } from "./stall.mjs";
|
|
11
15
|
import { annotationMarkerBody, checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
12
16
|
import { threadTranscriptBody } from "../../threads/transcript.mjs";
|
|
@@ -14,6 +18,7 @@ import { isHumanAuthor, isConfiguredBotAuthor } from "../../comments/authors.mjs
|
|
|
14
18
|
import { canRerunWorkflows } from "../../checks/conclusions.mjs";
|
|
15
19
|
import { loadConfig } from "../../config/load.mjs";
|
|
16
20
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
21
|
+
const EMPTY_RELEASED = new Set();
|
|
17
22
|
function checkRequiresHumanFollowUp(check) {
|
|
18
23
|
if (check.rerunCommand)
|
|
19
24
|
return false;
|
|
@@ -68,7 +73,7 @@ function pendingReviewCommands(resolveCommand, resolveOnlyCommand) {
|
|
|
68
73
|
return Object.keys(pending).length > 0 ? pending : undefined;
|
|
69
74
|
}
|
|
70
75
|
export async function handleFixCode(ctx) {
|
|
71
|
-
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, ruleAutoResolveThreadIds, repairInstructions, } = ctx;
|
|
76
|
+
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, releasedCheckNames = EMPTY_RELEASED, ruleAutoResolveThreadIds, repairInstructions, } = ctx;
|
|
72
77
|
const prReference = formatPrUrl(report.repo, prNumber);
|
|
73
78
|
const failingChecks = report.checks.failing;
|
|
74
79
|
const annotatedExtra = checksWithActionableAnnotations(report).filter((c) => c.category !== "failing");
|
|
@@ -158,18 +163,22 @@ export async function handleFixCode(ctx) {
|
|
|
158
163
|
? [c.runId]
|
|
159
164
|
: []));
|
|
160
165
|
const initialAttemptRunIds = new Set(failingChecks.flatMap((c) => (c.runId !== null && c.runAttempt === 1 ? [c.runId] : [])));
|
|
161
|
-
const failingAgentChecks = toAgentChecks(failingChecks).map((c) =>
|
|
162
|
-
c.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
166
|
+
const failingAgentChecks = toAgentChecks(failingChecks).map((c) => {
|
|
167
|
+
if (releasedCheckNames.has(c.name))
|
|
168
|
+
return c;
|
|
169
|
+
return rerunAuthorized &&
|
|
170
|
+
c.runId &&
|
|
171
|
+
actionsRunIds.has(c.runId) &&
|
|
172
|
+
// GitHub increments run_attempt after every rerun. Recommend at most one rerun by limiting
|
|
173
|
+
// the command to the original attempt; missing attempt metadata is denied conservatively.
|
|
174
|
+
initialAttemptRunIds.has(c.runId) &&
|
|
175
|
+
// ACTION_REQUIRED means the run is paused pending manual workflow approval; rerunning does
|
|
176
|
+
// not grant that approval, so no rerun command applies.
|
|
177
|
+
c.conclusion !== "ACTION_REQUIRED" &&
|
|
178
|
+
!inProgressWorkflowRunIds.has(c.runId)
|
|
179
|
+
? { ...c, rerunCommand: `gh run rerun ${c.runId} -R ${report.repo}` }
|
|
180
|
+
: c;
|
|
181
|
+
});
|
|
173
182
|
const checks = [
|
|
174
183
|
...failingAgentChecks,
|
|
175
184
|
...toAgentChecks(annotatedExtra).map((c) => ({ ...c, annotationOnly: true })),
|
|
@@ -184,7 +193,9 @@ export async function handleFixCode(ctx) {
|
|
|
184
193
|
const inProgressRunIds = [];
|
|
185
194
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
186
195
|
const belongsToActiveWorkflowRun = (check) => check.runId !== null && inProgressWorkflowRunIds.has(check.runId);
|
|
187
|
-
const manualFollowUpChecks = failingAgentChecks.filter((check) => !
|
|
196
|
+
const manualFollowUpChecks = failingAgentChecks.filter((check) => !releasedCheckNames.has(check.name) &&
|
|
197
|
+
!belongsToActiveWorkflowRun(check) &&
|
|
198
|
+
checkRequiresHumanFollowUp(check));
|
|
188
199
|
const exhaustedAttempts = failingAgentChecks.filter((check) => check.runAttempt !== undefined && check.runAttempt > 1);
|
|
189
200
|
const manualExhaustedAttempts = exhaustedAttempts.filter((check) => manualFollowUpChecks.includes(check));
|
|
190
201
|
const hasBehindBaseRecovery = isBehind && manualExhaustedAttempts.length > 0;
|
|
@@ -201,7 +212,9 @@ export async function handleFixCode(ctx) {
|
|
|
201
212
|
report.threads.firstLook.length > 0 ||
|
|
202
213
|
report.comments.firstLook.length > 0 ||
|
|
203
214
|
checks.some((check) => (check.annotations?.length ?? 0) > 0) ||
|
|
204
|
-
failingAgentChecks.some((check) =>
|
|
215
|
+
failingAgentChecks.some((check) => releasedCheckNames.has(check.name) ||
|
|
216
|
+
belongsToActiveWorkflowRun(check) ||
|
|
217
|
+
!checkRequiresHumanFollowUp(check));
|
|
205
218
|
if (manualFollowUpChecks.length > 0 && !hasAutonomousWork) {
|
|
206
219
|
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(failingAgentChecks);
|
|
207
220
|
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
@@ -272,17 +285,49 @@ export async function handleFixCode(ctx) {
|
|
|
272
285
|
}
|
|
273
286
|
const firstLookThreads = report.threads.firstLook;
|
|
274
287
|
const firstLookComments = report.comments.firstLook;
|
|
288
|
+
const stack = report.mergeStatus.mergeRequirements?.stack;
|
|
289
|
+
const headRef = report.headSha;
|
|
290
|
+
// Only an upper layer can be dirty against trunk while already containing its parent.
|
|
291
|
+
const trunkConflict = hasConflicts && stack && headRef && baseLookup.branch !== stack.baseRefName
|
|
292
|
+
? await lookupUpperLayerTrunkConflict({
|
|
293
|
+
owner: repoOwner,
|
|
294
|
+
name: repoName,
|
|
295
|
+
pr: prNumber,
|
|
296
|
+
headRef,
|
|
297
|
+
trunk: stack.baseRefName,
|
|
298
|
+
})
|
|
299
|
+
: undefined;
|
|
275
300
|
// Conflicts, and a behind branch whose rerun already failed, both ask for a branch update.
|
|
276
301
|
const stackRebase = hasConflicts || (isBehind && exhaustedAttempts.length > 0)
|
|
277
|
-
? buildNativeStackLayerRebase(report.repo, { number: prNumber, baseBranch: baseLookup.branch },
|
|
302
|
+
? buildNativeStackLayerRebase(report.repo, { number: prNumber, baseBranch: baseLookup.branch }, stack, trunkConflict)
|
|
278
303
|
: undefined;
|
|
279
304
|
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);
|
|
305
|
+
if (failingAgentChecks.some((check) => releasedCheckNames.has(check.name))) {
|
|
306
|
+
const completion = instructions.pop();
|
|
307
|
+
instructions.push(buildReleasedBlockerInstruction(prNumber));
|
|
308
|
+
if (completion !== undefined)
|
|
309
|
+
instructions.push(completion);
|
|
310
|
+
}
|
|
280
311
|
if (repairInstructions && repairInstructions.length > 0) {
|
|
281
312
|
instructions.unshift(...repairInstructions);
|
|
282
313
|
}
|
|
314
|
+
const checkRunCount = countReportedChecks(report.checks);
|
|
315
|
+
const suitesEmpty = report.headCheckSuitesEmpty === true;
|
|
316
|
+
const nowMs = Date.now();
|
|
317
|
+
const firstSeenAtUnix = hasConflicts && suitesEmpty && checkRunCount === 0 && report.headSha
|
|
318
|
+
? await conflictingHeadFirstSeenUnix({ owner: repoOwner, repo: repoName, pr: prNumber }, report.headSha, nowMs)
|
|
319
|
+
: undefined;
|
|
320
|
+
insertConflictingHeadCiNote(instructions, conflictingHeadCiNote({
|
|
321
|
+
hasConflicts,
|
|
322
|
+
headCheckSuitesEmpty: suitesEmpty,
|
|
323
|
+
checkRunCount,
|
|
324
|
+
firstSeenAtUnix,
|
|
325
|
+
nowMs,
|
|
326
|
+
}));
|
|
283
327
|
const prospectiveResult = {
|
|
284
328
|
...base,
|
|
285
329
|
baseBranch: baseLookup.branch,
|
|
330
|
+
...(trunkConflict && { stackTrunkConflict: trunkConflict.trunk }),
|
|
286
331
|
action: "fix_code",
|
|
287
332
|
fix: {
|
|
288
333
|
threads,
|
|
@@ -24,6 +24,7 @@ import { isCurrentSummaryReady } from "../../github/poll-summary-readiness.mjs";
|
|
|
24
24
|
import { clearReadyReceipt, isReadyReceiptCurrent, readReadyReceipt, writeReadyReceipt, } from "../../state/ready-receipts.mjs";
|
|
25
25
|
import { stackDraftHold } from "./parent-first.mjs";
|
|
26
26
|
import { findStaleNativeStackAncestry } from "./stale-ancestry.mjs";
|
|
27
|
+
import { annotateBlockedWait, resolveCheckBlockerGate } from "./check-blocker-gate.mjs";
|
|
27
28
|
export function runIterate(opts) {
|
|
28
29
|
return withIterateApiUsage(opts, () => runIterateCore(opts));
|
|
29
30
|
}
|
|
@@ -58,6 +59,17 @@ async function runIterateCore(opts) {
|
|
|
58
59
|
await clearStallState(stallKey);
|
|
59
60
|
return buildTerminalCancelResult(report);
|
|
60
61
|
}
|
|
62
|
+
const blockerGate = await resolveCheckBlockerGate({ owner: repoOwner, repo: repoName, pr: prNumber }, report.checks.failing, report.mergeStatus.status);
|
|
63
|
+
const deferredNames = blockerGate?.deferredNames;
|
|
64
|
+
const reportForWork = deferredNames === undefined || deferredNames.size === 0
|
|
65
|
+
? report
|
|
66
|
+
: {
|
|
67
|
+
...report,
|
|
68
|
+
checks: {
|
|
69
|
+
...report.checks,
|
|
70
|
+
failing: report.checks.failing.filter((check) => !deferredNames.has(check.name)),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
61
73
|
const { minimizeIds, selfMinimizeIds, firstLookSummaries, editedSummaries, surfacedApprovals } = classifyReviewSummaries({
|
|
62
74
|
firstLook: report.firstLookSummaries,
|
|
63
75
|
seen: report.reviewSummaries,
|
|
@@ -86,7 +98,7 @@ async function runIterateCore(opts) {
|
|
|
86
98
|
report.comments.actionable.length > 0 ||
|
|
87
99
|
(report.comments.minimizeIds?.length ?? 0) > 0 ||
|
|
88
100
|
report.changesRequestedReviews.length > 0 ||
|
|
89
|
-
hasCheckDrivenActionableWork(
|
|
101
|
+
hasCheckDrivenActionableWork(reportForWork.checks, report.mergeStatus.status) ||
|
|
90
102
|
reviewSummaryIds.length > 0 ||
|
|
91
103
|
firstLookSummaries.length > 0 ||
|
|
92
104
|
editedSummaries.length > 0 ||
|
|
@@ -109,20 +121,20 @@ async function runIterateCore(opts) {
|
|
|
109
121
|
// current already proves the delay elapsed for this exact head, base, and
|
|
110
122
|
// readiness evidence, so a rerun (e.g. with --merge) does not wait again.
|
|
111
123
|
const readyState = await updateReadyDelay(report.pr, isCleanReadyState, readyDelaySeconds, repoOwner, repoName, { headSha, alreadyElapsed: receiptCurrent });
|
|
112
|
-
const base = buildIterateBase(
|
|
124
|
+
const base = buildIterateBase(reportForWork, readyState);
|
|
113
125
|
// Checks (including merge-queue synthetic-commit checks) and hard conflicts are signals
|
|
114
126
|
// GitHub itself is already acting on — the queue will eject the PR for these regardless of
|
|
115
127
|
// what Shepherd does, so they always surface immediately. Only review threads/comments/
|
|
116
128
|
// changes-requested reviews/review summaries — the categories that would otherwise cause a
|
|
117
129
|
// Shepherd-initiated push while the PR sits safely in the queue — are eligible for deferral.
|
|
118
|
-
const checkDrivenActionableWork = hasCheckDrivenActionableWork(
|
|
130
|
+
const checkDrivenActionableWork = hasCheckDrivenActionableWork(reportForWork.checks, report.mergeStatus.status);
|
|
119
131
|
const deferWhileQueued = opts.merge === true &&
|
|
120
132
|
report.mergeQueue?.inQueue === true &&
|
|
121
133
|
config.actions.workWhileQueued !== true;
|
|
122
134
|
if (hasActionableWork && !(deferWhileQueued && !checkDrivenActionableWork)) {
|
|
123
135
|
return handleFixCode({
|
|
124
136
|
base,
|
|
125
|
-
report,
|
|
137
|
+
report: reportForWork,
|
|
126
138
|
opts: { ...opts, prNumber, neverCancelRuns },
|
|
127
139
|
headSha,
|
|
128
140
|
stallKey,
|
|
@@ -135,6 +147,7 @@ async function runIterateCore(opts) {
|
|
|
135
147
|
editedSummaries,
|
|
136
148
|
surfacedApprovals,
|
|
137
149
|
botUsernames,
|
|
150
|
+
releasedCheckNames: blockerGate?.releasedNames,
|
|
138
151
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
139
152
|
});
|
|
140
153
|
}
|
|
@@ -151,11 +164,11 @@ async function runIterateCore(opts) {
|
|
|
151
164
|
minimizeApprovals: config.iterate.minimizeApprovals,
|
|
152
165
|
});
|
|
153
166
|
if (mergeStateResult)
|
|
154
|
-
return mergeStateResult;
|
|
167
|
+
return annotateBlockedWait(mergeStateResult, blockerGate);
|
|
155
168
|
if (staleAncestry) {
|
|
156
169
|
return handleFixCode({
|
|
157
170
|
base,
|
|
158
|
-
report,
|
|
171
|
+
report: reportForWork,
|
|
159
172
|
opts: { ...opts, prNumber, neverCancelRuns },
|
|
160
173
|
headSha,
|
|
161
174
|
stallKey,
|
|
@@ -168,6 +181,7 @@ async function runIterateCore(opts) {
|
|
|
168
181
|
editedSummaries,
|
|
169
182
|
surfacedApprovals,
|
|
170
183
|
botUsernames,
|
|
184
|
+
releasedCheckNames: blockerGate?.releasedNames,
|
|
171
185
|
repairInstructions: staleAncestry.instructions,
|
|
172
186
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
173
187
|
});
|
|
@@ -191,7 +205,7 @@ async function runIterateCore(opts) {
|
|
|
191
205
|
shouldCancel: false,
|
|
192
206
|
log: `WAIT: PR #${base.pr} reached ready-delay but its stack readiness receipt could not be persisted`,
|
|
193
207
|
};
|
|
194
|
-
return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, receiptWait,
|
|
208
|
+
return annotateBlockedWait(await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, receiptWait, reportForWork, reviewSummaryIds), blockerGate);
|
|
195
209
|
}
|
|
196
210
|
await clearReadyDelay(report.pr, repoOwner, repoName);
|
|
197
211
|
await clearStallState(stallKey);
|
|
@@ -213,7 +227,7 @@ async function runIterateCore(opts) {
|
|
|
213
227
|
log: buildWaitLog(base),
|
|
214
228
|
...(hold && { stackDraftHold: hold }),
|
|
215
229
|
};
|
|
216
|
-
return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, wait,
|
|
230
|
+
return annotateBlockedWait(await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, wait, reportForWork, reviewSummaryIds), blockerGate);
|
|
217
231
|
}
|
|
218
232
|
async function recordReadyReceipt(key, report) {
|
|
219
233
|
if (report.status !== "READY" ||
|
|
@@ -7,6 +7,8 @@ export type NativeStackRebaseStart = {
|
|
|
7
7
|
parentBranch: string;
|
|
8
8
|
} | {
|
|
9
9
|
bottomPr: number;
|
|
10
|
+
} | {
|
|
11
|
+
trunk: string;
|
|
10
12
|
};
|
|
11
13
|
/**
|
|
12
14
|
* One gh-stack rebase step. A native stack layer must not be rebased or merged from its base
|
|
@@ -22,12 +24,16 @@ export declare function buildNativeStackRebaseInstruction(repo: string, stackNum
|
|
|
22
24
|
* workflow keeps failing); undefined outside a stack.
|
|
23
25
|
* A layer whose PR targets the stack's trunk (`stack.baseRefName`) is the bottom open layer —
|
|
24
26
|
* position 1, or a higher layer GitHub retargeted after every layer below it merged. Any other
|
|
25
|
-
* layer is an upper layer whose parent is its own PR base branch.
|
|
27
|
+
* layer is an upper layer whose parent is its own PR base branch. `trunkConflict` means that
|
|
28
|
+
* upper layer already contains its parent, so the rebase starts at the bottom open layer.
|
|
26
29
|
*/
|
|
27
30
|
export declare function buildNativeStackLayerRebase(repo: string, pr: {
|
|
28
31
|
number: number;
|
|
29
32
|
baseBranch: string;
|
|
30
|
-
}, stack: StackStatus | undefined
|
|
33
|
+
}, stack: StackStatus | undefined, trunkConflict?: {
|
|
34
|
+
trunk: string;
|
|
35
|
+
bottomPr?: number;
|
|
36
|
+
}): string | undefined;
|
|
31
37
|
/** Point at the conflicts, and at the stack rebase when the PR is a native stack layer. */
|
|
32
38
|
export declare function buildConflictInstruction(stackRebase: string | undefined): string;
|
|
33
39
|
/** Push a conflict resolution or branch refresh: one branch, or the whole rewritten native stack. */
|
|
@@ -12,7 +12,9 @@ export function buildNativeStackRebaseInstruction(repo, stackNumber, start) {
|
|
|
12
12
|
`check out the parent stack branch \`${start.parentBranch}\``,
|
|
13
13
|
"gh stack rebase --upstack --no-trunk",
|
|
14
14
|
]
|
|
15
|
-
:
|
|
15
|
+
: "bottomPr" in start
|
|
16
|
+
? [`check out the head branch of PR #${start.bottomPr}`, "gh stack rebase"]
|
|
17
|
+
: [`check out the bottom open layer whose base is \`${start.trunk}\``, "gh stack rebase"];
|
|
16
18
|
const prepare = `if \`gh stack\` does not track stack #${stackNumber} locally, import it with \`gh stack checkout ${stackNumber}\`, then confirm every layer's local branch is at its PR's head commit — a stale local layer would overwrite that PR's newer commits on push`;
|
|
17
19
|
return `From a clean checkout of \`${repo}\`, ${prepare}. Then ${checkout} and run \`${command}\`; if it stops on a conflict, resolve it and run \`gh stack rebase --continue\`.`;
|
|
18
20
|
}
|
|
@@ -21,12 +23,20 @@ export function buildNativeStackRebaseInstruction(repo, stackNumber, start) {
|
|
|
21
23
|
* workflow keeps failing); undefined outside a stack.
|
|
22
24
|
* A layer whose PR targets the stack's trunk (`stack.baseRefName`) is the bottom open layer —
|
|
23
25
|
* position 1, or a higher layer GitHub retargeted after every layer below it merged. Any other
|
|
24
|
-
* layer is an upper layer whose parent is its own PR base branch.
|
|
26
|
+
* layer is an upper layer whose parent is its own PR base branch. `trunkConflict` means that
|
|
27
|
+
* upper layer already contains its parent, so the rebase starts at the bottom open layer.
|
|
25
28
|
*/
|
|
26
|
-
export function buildNativeStackLayerRebase(repo, pr, stack) {
|
|
29
|
+
export function buildNativeStackLayerRebase(repo, pr, stack, trunkConflict) {
|
|
27
30
|
if (!stack)
|
|
28
31
|
return undefined;
|
|
29
|
-
|
|
32
|
+
const start = trunkConflict
|
|
33
|
+
? trunkConflict.bottomPr !== undefined
|
|
34
|
+
? { bottomPr: trunkConflict.bottomPr }
|
|
35
|
+
: { trunk: trunkConflict.trunk }
|
|
36
|
+
: pr.baseBranch === stack.baseRefName
|
|
37
|
+
? { bottomPr: pr.number }
|
|
38
|
+
: { parentBranch: pr.baseBranch };
|
|
39
|
+
return buildNativeStackRebaseInstruction(repo, stack.number, start);
|
|
30
40
|
}
|
|
31
41
|
/** Point at the conflicts, and at the stack rebase when the PR is a native stack layer. */
|
|
32
42
|
export function buildConflictInstruction(stackRebase) {
|