merge-steward 0.5.0 → 0.5.2
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/dist/github/shell-git.d.ts +1 -0
- package/dist/github/shell-git.js +18 -1
- package/dist/interfaces.d.ts +1 -0
- package/dist/reconciler.js +20 -0
- package/dist/service.d.ts +0 -1
- package/dist/service.js +2 -6
- package/dist/watch/QueueListView.js +1 -1
- package/dist/watch/index.js +8 -2
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ export declare class ShellGitOperations implements GitOperations, SpeculativeBra
|
|
|
8
8
|
private git;
|
|
9
9
|
fetch(remote?: string): Promise<void>;
|
|
10
10
|
headSha(branch: string): Promise<string>;
|
|
11
|
+
isAncestor(ancestor: string, descendant: string): Promise<boolean>;
|
|
11
12
|
rebase(branch: string, onto: string): Promise<RebaseResult>;
|
|
12
13
|
push(branch: string, force?: boolean): Promise<void>;
|
|
13
14
|
buildSpeculative(prBranch: string, baseBranch: string, specName: string): Promise<MergeResult>;
|
package/dist/github/shell-git.js
CHANGED
|
@@ -33,8 +33,25 @@ export class ShellGitOperations {
|
|
|
33
33
|
const result = await this.git(["rev-parse", branch]);
|
|
34
34
|
return result.stdout.trim();
|
|
35
35
|
}
|
|
36
|
+
async isAncestor(ancestor, descendant) {
|
|
37
|
+
const result = await this.git(["merge-base", "--is-ancestor", ancestor, descendant], { allowNonZero: true });
|
|
38
|
+
if (result.exitCode === 0)
|
|
39
|
+
return true;
|
|
40
|
+
if (result.exitCode === 1)
|
|
41
|
+
return false;
|
|
42
|
+
throw new Error(`git merge-base --is-ancestor failed: ${result.stderr || result.stdout}`);
|
|
43
|
+
}
|
|
36
44
|
async rebase(branch, onto) {
|
|
37
|
-
|
|
45
|
+
const remoteBranchRef = `refs/remotes/origin/${branch}`;
|
|
46
|
+
const remoteBranchExists = await this.git(["show-ref", "--verify", remoteBranchRef], { allowNonZero: true });
|
|
47
|
+
if (remoteBranchExists.exitCode === 0) {
|
|
48
|
+
// Always start from the freshly fetched remote branch tip so stale local branches
|
|
49
|
+
// cannot reintroduce old commits into queue processing.
|
|
50
|
+
await this.git(["checkout", "-B", branch, `origin/${branch}`]);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await this.git(["checkout", branch]);
|
|
54
|
+
}
|
|
38
55
|
const result = await this.git(["rebase", onto], { allowNonZero: true });
|
|
39
56
|
if (result.exitCode !== 0) {
|
|
40
57
|
await this.git(["rebase", "--abort"], { allowNonZero: true });
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { CIStatus, CheckResult, IncidentRecord, MergeResult, PRStatus, Queu
|
|
|
6
6
|
export interface GitOperations {
|
|
7
7
|
fetch(remote?: string): Promise<void>;
|
|
8
8
|
headSha(branch: string): Promise<string>;
|
|
9
|
+
isAncestor(ancestor: string, descendant: string): Promise<boolean>;
|
|
9
10
|
rebase(branch: string, onto: string): Promise<RebaseResult>;
|
|
10
11
|
push(branch: string, force?: boolean): Promise<void>;
|
|
11
12
|
}
|
package/dist/reconciler.js
CHANGED
|
@@ -154,6 +154,26 @@ async function performRebase(ctx, entry, baseSha) {
|
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
const headSha = result.newHeadSha ?? entry.headSha;
|
|
157
|
+
await ctx.git.fetch();
|
|
158
|
+
const latestRemoteHead = await ctx.git.headSha(ref(ctx, entry.branch));
|
|
159
|
+
const candidateKeepsLatestRemote = await ctx.git.isAncestor(latestRemoteHead, headSha);
|
|
160
|
+
if (!candidateKeepsLatestRemote) {
|
|
161
|
+
const detail = latestRemoteHead === entry.headSha
|
|
162
|
+
? `candidate diverged from remote head: expected ${entry.headSha.slice(0, 8)}, ` +
|
|
163
|
+
`latest ${latestRemoteHead.slice(0, 8)}, candidate ${headSha.slice(0, 8)}`
|
|
164
|
+
: `remote advanced during rebase: expected ${entry.headSha.slice(0, 8)}, ` +
|
|
165
|
+
`latest ${latestRemoteHead.slice(0, 8)}, candidate ${headSha.slice(0, 8)}`;
|
|
166
|
+
emit(ctx, entry, "branch_mismatch", { detail });
|
|
167
|
+
if (latestRemoteHead !== entry.headSha) {
|
|
168
|
+
ctx.store.updateHead(entry.id, latestRemoteHead);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
ctx.store.transition(entry.id, "queued", {
|
|
172
|
+
...CLEAN_CI,
|
|
173
|
+
}, `stale local branch diverged from ${latestRemoteHead.slice(0, 8)}`);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
157
177
|
await ctx.git.push(entry.branch, true);
|
|
158
178
|
emit(ctx, entry, "rebase_succeeded", { baseSha });
|
|
159
179
|
// Build speculative branch for downstream entries.
|
package/dist/service.d.ts
CHANGED
|
@@ -23,7 +23,6 @@ export declare class MergeStewardService {
|
|
|
23
23
|
private lastTickCompletedAt;
|
|
24
24
|
private lastTickOutcome;
|
|
25
25
|
private lastTickError;
|
|
26
|
-
private mainBrokenReported;
|
|
27
26
|
private currentQueueBlock;
|
|
28
27
|
constructor(config: StewardConfig, store: QueueStore, git: GitOperations, ci: CIRunner, github: GitHubPRApi, eviction: EvictionReporter, specBuilder: import("./interfaces.ts").SpeculativeBranchBuilder | null, logger: Logger);
|
|
29
28
|
/** Expose the GitHub client for webhook handler branch→PR lookups. */
|
package/dist/service.js
CHANGED
|
@@ -21,7 +21,6 @@ export class MergeStewardService {
|
|
|
21
21
|
lastTickCompletedAt = null;
|
|
22
22
|
lastTickOutcome = "idle";
|
|
23
23
|
lastTickError = null;
|
|
24
|
-
mainBrokenReported = false;
|
|
25
24
|
currentQueueBlock = null;
|
|
26
25
|
constructor(config, store, git, ci, github, eviction, specBuilder, logger) {
|
|
27
26
|
this.config = config;
|
|
@@ -264,16 +263,12 @@ export class MergeStewardService {
|
|
|
264
263
|
|| event.action === "fetch_started";
|
|
265
264
|
const level = isWarn ? "warn" : isDebug ? "debug" : "info";
|
|
266
265
|
this.logger[level]({ ...event }, `Queue: ${event.action} PR #${event.prNumber}`);
|
|
267
|
-
if (event.action === "main_broken" && !this.mainBrokenReported) {
|
|
268
|
-
this.mainBrokenReported = true;
|
|
269
|
-
}
|
|
270
266
|
if (event.action === "main_broken" && tickQueueBlockEvent === null) {
|
|
271
267
|
tickQueueBlockEvent = event;
|
|
272
268
|
}
|
|
273
269
|
},
|
|
274
270
|
});
|
|
275
271
|
this.currentQueueBlock = tickQueueBlockEvent ? await this.describeMainBroken(tickQueueBlockEvent) : null;
|
|
276
|
-
this.mainBrokenReported = false; // Reset after successful tick.
|
|
277
272
|
this.lastTickOutcome = "succeeded";
|
|
278
273
|
}
|
|
279
274
|
catch (error) {
|
|
@@ -309,9 +304,10 @@ export class MergeStewardService {
|
|
|
309
304
|
baseSha = event.baseSha ?? null;
|
|
310
305
|
}
|
|
311
306
|
}
|
|
307
|
+
const checkRef = event.baseSha ?? baseSha ?? baseRef;
|
|
312
308
|
let checks = [];
|
|
313
309
|
try {
|
|
314
|
-
checks = await this.github.listChecksForRef(
|
|
310
|
+
checks = await this.github.listChecksForRef(checkRef);
|
|
315
311
|
}
|
|
316
312
|
catch {
|
|
317
313
|
checks = [];
|
|
@@ -19,5 +19,5 @@ export function QueueListView({ entries, selectedEntryId, recentEvents, headEntr
|
|
|
19
19
|
const eventRows = Math.min(8, Math.max(4, rows - entries.length - CHROME_ROWS));
|
|
20
20
|
const displayedEvents = useMemo(() => recentEvents.slice(-eventRows), [eventRows, recentEvents]);
|
|
21
21
|
const queueBlockLabel = summarizeQueueBlock(queueBlock);
|
|
22
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [queueBlock && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", queueBlockLabel ?? "main is unhealthy", queueBlock.baseSha ? ` at ${truncate(queueBlock.baseSha, 10)}` : "", "."] }), _jsxs(Text, { dimColor: true, children: ["Head PR #", queueBlock.headPrNumber ?? "?", " will resume automatically once main recovers."] })] })), _jsx(Text, { dimColor: true, children: "
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [queueBlock && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", queueBlockLabel ?? "main is unhealthy", queueBlock.baseSha ? ` at ${truncate(queueBlock.baseSha, 10)}` : "", "."] }), _jsxs(Text, { dimColor: true, children: ["Head PR #", queueBlock.headPrNumber ?? "?", " will resume automatically once main recovers."] })] })), _jsx(Text, { dimColor: true, children: " sel head pos pr status retry age branch" }), entries.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue entries in this filter." })) : (entries.map((entry) => (_jsx(QueueRow, { entry: entry, selected: entry.id === selectedEntryId, branchWidth: branchWidth, isHead: entry.id === headEntryId, queueBlock: queueBlock }, entry.id)))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Recent Events" }), displayedEvents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue events yet." })) : (displayedEvents.map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: formatEventSummary(event) })] }, event.id ?? `${event.entryId}-${event.at}`))))] })] }));
|
|
23
23
|
}
|
package/dist/watch/index.js
CHANGED
|
@@ -21,6 +21,12 @@ export async function startWatch(configPath, initialPrNumber) {
|
|
|
21
21
|
const config = loadConfig(configPath);
|
|
22
22
|
const gatewayBase = resolveGatewayBaseUrl();
|
|
23
23
|
const baseUrl = `${gatewayBase}/repos/${config.repoId}`;
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
process.stderr.write("\u001b[?1049h\u001b[2J\u001b[H");
|
|
25
|
+
try {
|
|
26
|
+
const instance = render(createElement(App, { baseUrl, ...(initialPrNumber !== undefined ? { initialPrNumber } : {}) }), { stdout: process.stderr, stdin: process.stdin, patchConsole: false });
|
|
27
|
+
await instance.waitUntilExit();
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
process.stderr.write("\u001b[?1049l");
|
|
31
|
+
}
|
|
26
32
|
}
|