merge-steward 0.6.0 → 0.7.0
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/watch/DetailView.js
CHANGED
|
@@ -18,5 +18,5 @@ export function DetailView({ detail, isHead, activeIndex, activeCount, headPrNum
|
|
|
18
18
|
queueBlock,
|
|
19
19
|
});
|
|
20
20
|
const pipeline = queueProgress(entry.status);
|
|
21
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 2, children: [_jsxs(Text, { bold: true, children: ["#", entry.prNumber] }), entry.issueKey ? _jsx(Text, { children: entry.issueKey }) : null, _jsx(Text, { color: statusColor(entry.status), children: humanStatus(entry.status) }), _jsxs(Text, { dimColor: true, children: ["pos ", entry.position] }), _jsxs(Text, { dimColor: true, children: ["
|
|
21
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 2, children: [_jsxs(Text, { bold: true, children: ["#", entry.prNumber] }), entry.issueKey ? _jsx(Text, { children: entry.issueKey }) : null, _jsx(Text, { color: statusColor(entry.status), children: humanStatus(entry.status, entry) }), _jsxs(Text, { dimColor: true, children: ["pos ", entry.position] }), _jsxs(Text, { dimColor: true, children: ["retry ", entry.retryAttempts, "/", entry.maxRetries] })] }), _jsx(Text, { children: entry.branch }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { dimColor: true, children: ["head ", shortSha(entry.headSha)] }), _jsxs(Text, { dimColor: true, children: ["base ", shortSha(entry.baseSha)] })] }), entry.specBranch && (_jsxs(Box, { gap: 2, children: [_jsx(Text, { dimColor: true, children: "spec" }), _jsx(Text, { children: entry.specBranch }), _jsx(Text, { dimColor: true, children: shortSha(entry.specSha) }), _jsx(Text, { dimColor: true, children: "\u2190" }), _jsx(Text, { dimColor: true, children: entry.specBasedOn ? `entry ${shortSha(entry.specBasedOn)}` : "main" })] })), _jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "progress" }), _jsx(Text, { children: progressBar(pipeline.current, pipeline.total, 12) }), _jsx(Text, { dimColor: true, children: nextStepLabel(entry.status, entry) })] }), isHead && queueBlock && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", summarizeQueueBlock(queueBlock) ?? "main branch is unhealthy", "."] }), _jsxs(Text, { dimColor: true, children: [queueBlock.baseBranch, queueBlock.baseSha ? ` @ ${shortSha(queueBlock.baseSha)}` : ""] }), _jsxs(Text, { dimColor: true, children: ["Head PR #", queueBlock.headPrNumber ?? entry.prNumber, " will resume automatically once main recovers."] })] })), entry.maxRetries > 0 && (_jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "retry" }), _jsx(Text, { children: progressBar(entry.retryAttempts, entry.maxRetries, 10) }), _jsxs(Text, { dimColor: true, children: [entry.retryAttempts, "/", entry.maxRetries] })] })), _jsx(EntryStateGraph, { main: graph.main, exits: graph.exits }), _jsx(ExternalRepairObservation, { observations: observations }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Incidents" }), incidents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No incidents." })) : (incidents.map((incident) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(incident.at).padStart(4, " ") }), _jsx(Text, { color: "red", children: incident.failureClass }), _jsx(Text, { dimColor: true, children: incident.outcome })] }, incident.id))))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Events" }), events.length === 0 ? (_jsx(Text, { dimColor: true, children: "No events yet." })) : (events.slice(-16).map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: formatEntryEvent(event) })] }, event.id ?? `${event.entryId}-${event.at}`))))] })] }));
|
|
22
22
|
}
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { Box, Text, useStdout } from "ink";
|
|
4
|
-
import { formatEventSummary, humanStatus, nextStepLabel, progressBar, queueProgress, relativeTime, statusColor, summarizeQueueBlock, truncate } from "./format.js";
|
|
4
|
+
import { formatEventSummary, humanStatus, nextStepLabel, progressBar, queueProgress, relativeTime, specChainLabel, statusColor, summarizeQueueBlock, truncate } from "./format.js";
|
|
5
5
|
const ENTRY_ROW_HEIGHT = 2;
|
|
6
6
|
const CHROME_ROWS = 13;
|
|
7
|
-
function QueueRow({ entry, selected, infoWidth, isHead, queueBlock, }) {
|
|
7
|
+
function QueueRow({ entry, selected, infoWidth, isHead, queueBlock, allEntries, }) {
|
|
8
8
|
const retryText = `${entry.retryAttempts}/${entry.maxRetries}`;
|
|
9
9
|
const ciText = entry.ciRetries > 0 ? `CI retries ${entry.ciRetries}` : null;
|
|
10
10
|
const blockedOnMain = isHead && queueBlock?.reason === "main_broken" && queueBlock.headPrNumber === entry.prNumber;
|
|
11
|
-
const renderedStatus = blockedOnMain ? "blocked by broken main" : humanStatus(entry.status);
|
|
12
|
-
const renderedColor = blockedOnMain
|
|
11
|
+
const renderedStatus = blockedOnMain ? "blocked by broken main" : humanStatus(entry.status, entry);
|
|
12
|
+
const renderedColor = blockedOnMain
|
|
13
|
+
? "red"
|
|
14
|
+
: entry.status === "preparing_head" && entry.lastFailedBaseSha ? "yellow"
|
|
15
|
+
: statusColor(entry.status);
|
|
13
16
|
const progress = queueProgress(entry.status);
|
|
14
|
-
const
|
|
17
|
+
const specLabel = entry.specBranch
|
|
18
|
+
? specChainLabel(entry, allEntries)
|
|
19
|
+
: truncate(entry.branch, Math.max(12, infoWidth - 34));
|
|
15
20
|
const nextStep = blockedOnMain
|
|
16
21
|
? summarizeQueueBlock(queueBlock) ?? "waiting for main to recover"
|
|
17
|
-
: nextStepLabel(entry.status);
|
|
18
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : "gray", children: selected ? "›" : " " }), _jsx(Text, { color: blockedOnMain ? "red" : isHead ? "green" : "gray", children: blockedOnMain ? "!" : isHead ? "#" : " " }), _jsx(Text, { bold: true, children: ` #${entry.prNumber}` }), entry.issueKey ? _jsx(Text, { children: ` ${entry.issueKey}` }) : null, _jsx(Text, { dimColor: true, children: ` pos ${entry.position}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(entry.updatedAt)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: renderedColor, children: renderedStatus })] }), _jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: progressBar(progress.current, progress.total, 8) }), _jsx(Text, { dimColor: true, children:
|
|
22
|
+
: nextStepLabel(entry.status, entry);
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : "gray", children: selected ? "›" : " " }), _jsx(Text, { color: blockedOnMain ? "red" : isHead ? "green" : "gray", children: blockedOnMain ? "!" : isHead ? "#" : " " }), _jsx(Text, { bold: true, children: ` #${entry.prNumber}` }), entry.issueKey ? _jsx(Text, { children: ` ${entry.issueKey}` }) : null, _jsx(Text, { dimColor: true, children: ` pos ${entry.position}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(entry.updatedAt)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: renderedColor, children: renderedStatus })] }), _jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: progressBar(progress.current, progress.total, 8) }), _jsx(Text, { dimColor: true, children: specLabel }), _jsx(Text, { dimColor: true, children: "|" }), _jsx(Text, { dimColor: true, children: nextStep }), _jsx(Text, { dimColor: true, children: ` | retry ${retryText}` }), ciText ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "|" }), _jsx(Text, { dimColor: true, children: ciText })] })) : null] })] }));
|
|
19
24
|
}
|
|
20
25
|
export function QueueListView({ entries, selectedEntryId, recentEvents, headEntryId, queueBlock, }) {
|
|
21
26
|
const { stdout } = useStdout();
|
|
@@ -25,5 +30,5 @@ export function QueueListView({ entries, selectedEntryId, recentEvents, headEntr
|
|
|
25
30
|
const eventRows = Math.min(8, Math.max(4, rows - (entries.length * ENTRY_ROW_HEIGHT) - CHROME_ROWS));
|
|
26
31
|
const displayedEvents = useMemo(() => recentEvents.slice(-eventRows), [eventRows, recentEvents]);
|
|
27
32
|
const queueBlockLabel = summarizeQueueBlock(queueBlock);
|
|
28
|
-
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."] })] })), 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, infoWidth: infoWidth, 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}`))))] })] }));
|
|
33
|
+
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."] })] })), 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, infoWidth: infoWidth, isHead: entry.id === headEntryId, queueBlock: queueBlock, allEntries: entries }, 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}`))))] })] }));
|
|
29
34
|
}
|
package/dist/watch/format.d.ts
CHANGED
|
@@ -2,12 +2,28 @@ import type { CheckResult, QueueBlockState, QueueEntryStatus, QueueEventRecord,
|
|
|
2
2
|
export declare function shortSha(value: string | null | undefined): string;
|
|
3
3
|
export declare function relativeTime(iso: string | null | undefined): string;
|
|
4
4
|
export declare function statusColor(status: QueueEntryStatus): "yellow" | "cyan" | "green" | "red" | "gray";
|
|
5
|
-
export declare function humanStatus(status: QueueEntryStatus
|
|
5
|
+
export declare function humanStatus(status: QueueEntryStatus, entry?: {
|
|
6
|
+
lastFailedBaseSha: string | null;
|
|
7
|
+
specBranch: string | null;
|
|
8
|
+
}): string;
|
|
6
9
|
export declare function queueProgress(status: QueueEntryStatus): {
|
|
7
10
|
current: number;
|
|
8
11
|
total: number;
|
|
9
12
|
};
|
|
10
|
-
export declare function nextStepLabel(status: QueueEntryStatus
|
|
13
|
+
export declare function nextStepLabel(status: QueueEntryStatus, entry?: {
|
|
14
|
+
lastFailedBaseSha: string | null;
|
|
15
|
+
specBasedOn: string | null;
|
|
16
|
+
}): string;
|
|
17
|
+
/** Describe the spec chain for a queue entry. */
|
|
18
|
+
export declare function specChainLabel(entry: {
|
|
19
|
+
specBranch: string | null;
|
|
20
|
+
specBasedOn: string | null;
|
|
21
|
+
specSha: string | null;
|
|
22
|
+
}, allEntries: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
prNumber: number;
|
|
25
|
+
specBranch: string | null;
|
|
26
|
+
}>): string;
|
|
11
27
|
export declare function runtimeLabel(runtime: QueueRuntimeStatus): string;
|
|
12
28
|
export declare function formatEventSummary(event: QueueEventSummary): string;
|
|
13
29
|
export declare function formatEntryEvent(event: QueueEventRecord): string;
|
package/dist/watch/format.js
CHANGED
|
@@ -38,12 +38,14 @@ export function statusColor(status) {
|
|
|
38
38
|
return "gray";
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
export function humanStatus(status) {
|
|
41
|
+
export function humanStatus(status, entry) {
|
|
42
42
|
switch (status) {
|
|
43
43
|
case "queued":
|
|
44
44
|
return "queued";
|
|
45
45
|
case "preparing_head":
|
|
46
|
-
|
|
46
|
+
if (entry?.lastFailedBaseSha)
|
|
47
|
+
return "retry-gated";
|
|
48
|
+
return "building spec";
|
|
47
49
|
case "validating":
|
|
48
50
|
return "running CI";
|
|
49
51
|
case "merging":
|
|
@@ -71,16 +73,18 @@ export function queueProgress(status) {
|
|
|
71
73
|
return { current: 4, total: 4 };
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
|
-
export function nextStepLabel(status) {
|
|
76
|
+
export function nextStepLabel(status, entry) {
|
|
75
77
|
switch (status) {
|
|
76
78
|
case "queued":
|
|
77
79
|
return "waiting for head-of-line turn";
|
|
78
80
|
case "preparing_head":
|
|
79
|
-
|
|
81
|
+
if (entry?.lastFailedBaseSha)
|
|
82
|
+
return "waiting for base to advance";
|
|
83
|
+
return "building cumulative spec branch";
|
|
80
84
|
case "validating":
|
|
81
|
-
return "waiting for CI
|
|
85
|
+
return "waiting for CI on spec branch";
|
|
82
86
|
case "merging":
|
|
83
|
-
return "
|
|
87
|
+
return "pushing spec to main";
|
|
84
88
|
case "merged":
|
|
85
89
|
return "landed on main";
|
|
86
90
|
case "evicted":
|
|
@@ -89,6 +93,18 @@ export function nextStepLabel(status) {
|
|
|
89
93
|
return "removed manually";
|
|
90
94
|
}
|
|
91
95
|
}
|
|
96
|
+
/** Describe the spec chain for a queue entry. */
|
|
97
|
+
export function specChainLabel(entry, allEntries) {
|
|
98
|
+
if (!entry.specBranch)
|
|
99
|
+
return "no spec yet";
|
|
100
|
+
const parent = entry.specBasedOn
|
|
101
|
+
? allEntries.find((e) => e.id === entry.specBasedOn)
|
|
102
|
+
: null;
|
|
103
|
+
const base = parent
|
|
104
|
+
? `#${parent.prNumber}`
|
|
105
|
+
: "main";
|
|
106
|
+
return `${shortSha(entry.specSha)} \u2190 ${base}`;
|
|
107
|
+
}
|
|
92
108
|
export function runtimeLabel(runtime) {
|
|
93
109
|
if (runtime.tickInProgress) {
|
|
94
110
|
return "running";
|
|
@@ -96,19 +96,25 @@ export function buildExternalRepairObservations(detail, options) {
|
|
|
96
96
|
if (entry.lastFailedBaseSha) {
|
|
97
97
|
observations.push({
|
|
98
98
|
tone: "warn",
|
|
99
|
-
text: `
|
|
99
|
+
text: `Retry-gated: conflict on base ${entry.lastFailedBaseSha.slice(0, 7)}. Waiting for base to advance before rebuilding spec.`,
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
else if (entry.status === "validating") {
|
|
103
|
+
const specNote = entry.specBranch
|
|
104
|
+
? `CI running on spec branch ${entry.specBranch}.`
|
|
105
|
+
: "Waiting on CI for the spec branch.";
|
|
106
|
+
const cascadeNote = entry.specBasedOn
|
|
107
|
+
? " Will merge automatically when head clears (cascade)."
|
|
108
|
+
: "";
|
|
103
109
|
observations.push({
|
|
104
110
|
tone: "info",
|
|
105
|
-
text:
|
|
111
|
+
text: `${specNote}${cascadeNote}`,
|
|
106
112
|
});
|
|
107
113
|
}
|
|
108
114
|
else if (entry.status === "merging") {
|
|
109
115
|
observations.push({
|
|
110
116
|
tone: "info",
|
|
111
|
-
text: "
|
|
117
|
+
text: "CI passed; pushing spec branch to main (fast-forward).",
|
|
112
118
|
});
|
|
113
119
|
}
|
|
114
120
|
if (entry.generation > 0) {
|