merge-steward 0.14.0 → 0.14.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/db/schema.js +2 -0
- package/dist/db/sqlite-store.d.ts +1 -1
- package/dist/db/sqlite-store.js +11 -2
- package/dist/reconciler-merge.js +2 -0
- package/dist/service-queue.js +16 -0
- package/dist/store.d.ts +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/watch/DetailView.js +1 -1
- package/dist/watch/ProjectDetailView.js +3 -3
- package/dist/watch/format.d.ts +10 -1
- package/dist/watch/format.js +37 -3
- package/dist/watch/state-visualization.js +9 -1
- package/package.json +1 -1
package/dist/db/schema.js
CHANGED
|
@@ -24,6 +24,7 @@ export function ensureSchema(connection) {
|
|
|
24
24
|
spec_branch TEXT,
|
|
25
25
|
spec_sha TEXT,
|
|
26
26
|
spec_based_on TEXT,
|
|
27
|
+
wait_detail TEXT,
|
|
27
28
|
post_merge_status TEXT,
|
|
28
29
|
post_merge_sha TEXT,
|
|
29
30
|
post_merge_summary TEXT,
|
|
@@ -73,6 +74,7 @@ export function ensureSchema(connection) {
|
|
|
73
74
|
ensureColumn(connection, "queue_entries", "post_merge_sha", "TEXT");
|
|
74
75
|
ensureColumn(connection, "queue_entries", "post_merge_summary", "TEXT");
|
|
75
76
|
ensureColumn(connection, "queue_entries", "post_merge_checked_at", "TEXT");
|
|
77
|
+
ensureColumn(connection, "queue_entries", "wait_detail", "TEXT");
|
|
76
78
|
connection.exec(`
|
|
77
79
|
UPDATE queue_entries
|
|
78
80
|
SET post_merge_status = 'pending'
|
|
@@ -12,7 +12,7 @@ export declare class SqliteStore implements QueueStore {
|
|
|
12
12
|
listActive(repoId: string): QueueEntry[];
|
|
13
13
|
listAll(repoId: string): QueueEntry[];
|
|
14
14
|
insert(entry: QueueEntry): void;
|
|
15
|
-
transition(entryId: string, to: QueueEntryStatus, patch?: Partial<Pick<QueueEntry, "headSha" | "baseSha" | "ciRunId" | "ciRetries" | "retryAttempts" | "lastFailedBaseSha" | "specBranch" | "specSha" | "specBasedOn" | "postMergeStatus" | "postMergeSha" | "postMergeSummary" | "postMergeCheckedAt">>, detail?: string): void;
|
|
15
|
+
transition(entryId: string, to: QueueEntryStatus, patch?: Partial<Pick<QueueEntry, "headSha" | "baseSha" | "ciRunId" | "ciRetries" | "retryAttempts" | "lastFailedBaseSha" | "specBranch" | "specSha" | "specBasedOn" | "waitDetail" | "postMergeStatus" | "postMergeSha" | "postMergeSummary" | "postMergeCheckedAt">>, detail?: string): void;
|
|
16
16
|
dequeue(entryId: string): void;
|
|
17
17
|
updateHead(entryId: string, newHeadSha: string): void;
|
|
18
18
|
insertIncident(incident: IncidentRecord): void;
|
package/dist/db/sqlite-store.js
CHANGED
|
@@ -22,6 +22,7 @@ function mapEntry(row) {
|
|
|
22
22
|
specBranch: row.spec_branch === null || row.spec_branch === undefined ? null : String(row.spec_branch),
|
|
23
23
|
specSha: row.spec_sha === null || row.spec_sha === undefined ? null : String(row.spec_sha),
|
|
24
24
|
specBasedOn: row.spec_based_on === null || row.spec_based_on === undefined ? null : String(row.spec_based_on),
|
|
25
|
+
waitDetail: row.wait_detail === null || row.wait_detail === undefined ? null : String(row.wait_detail),
|
|
25
26
|
postMergeStatus: row.post_merge_status === null || row.post_merge_status === undefined
|
|
26
27
|
? null
|
|
27
28
|
: String(row.post_merge_status),
|
|
@@ -122,10 +123,10 @@ export class SqliteStore {
|
|
|
122
123
|
this.conn.prepare(`INSERT INTO queue_entries
|
|
123
124
|
(id, repo_id, pr_number, branch, head_sha, base_sha, status, position,
|
|
124
125
|
priority, generation, ci_run_id, ci_retries, retry_attempts,
|
|
125
|
-
max_retries, last_failed_base_sha, issue_key,
|
|
126
|
+
max_retries, last_failed_base_sha, issue_key, wait_detail,
|
|
126
127
|
post_merge_status, post_merge_sha, post_merge_summary, post_merge_checked_at,
|
|
127
128
|
enqueued_at, updated_at)
|
|
128
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(entry.id, entry.repoId, entry.prNumber, entry.branch, entry.headSha, entry.baseSha, entry.status, entry.position, entry.priority, entry.generation, entry.ciRunId, entry.ciRetries, entry.retryAttempts, entry.maxRetries, entry.lastFailedBaseSha, entry.issueKey ?? null, entry.postMergeStatus ?? null, entry.postMergeSha ?? null, entry.postMergeSummary ?? null, entry.postMergeCheckedAt ?? null, entry.enqueuedAt, entry.updatedAt);
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(entry.id, entry.repoId, entry.prNumber, entry.branch, entry.headSha, entry.baseSha, entry.status, entry.position, entry.priority, entry.generation, entry.ciRunId, entry.ciRetries, entry.retryAttempts, entry.maxRetries, entry.lastFailedBaseSha, entry.issueKey ?? null, entry.waitDetail ?? null, entry.postMergeStatus ?? null, entry.postMergeSha ?? null, entry.postMergeSummary ?? null, entry.postMergeCheckedAt ?? null, entry.enqueuedAt, entry.updatedAt);
|
|
129
130
|
this.writeEvent(entry.id, null, entry.status);
|
|
130
131
|
})();
|
|
131
132
|
}
|
|
@@ -173,6 +174,13 @@ export class SqliteStore {
|
|
|
173
174
|
sets.push("spec_based_on = ?");
|
|
174
175
|
values.push(patch.specBasedOn);
|
|
175
176
|
}
|
|
177
|
+
if (patch?.waitDetail !== undefined) {
|
|
178
|
+
sets.push("wait_detail = ?");
|
|
179
|
+
values.push(patch.waitDetail);
|
|
180
|
+
}
|
|
181
|
+
else if (from !== to) {
|
|
182
|
+
sets.push("wait_detail = NULL");
|
|
183
|
+
}
|
|
176
184
|
if (patch?.postMergeStatus !== undefined) {
|
|
177
185
|
sets.push("post_merge_status = ?");
|
|
178
186
|
values.push(patch.postMergeStatus);
|
|
@@ -210,6 +218,7 @@ export class SqliteStore {
|
|
|
210
218
|
ci_run_id = NULL, ci_retries = 0, retry_attempts = 0,
|
|
211
219
|
last_failed_base_sha = NULL,
|
|
212
220
|
spec_branch = NULL, spec_sha = NULL, spec_based_on = NULL,
|
|
221
|
+
wait_detail = NULL,
|
|
213
222
|
post_merge_status = NULL, post_merge_sha = NULL, post_merge_summary = NULL, post_merge_checked_at = NULL,
|
|
214
223
|
updated_at = ?
|
|
215
224
|
WHERE id = ?`).run(newHeadSha, newGen, isoNow(), entryId);
|
package/dist/reconciler-merge.js
CHANGED
|
@@ -17,6 +17,7 @@ export async function mergeHead(ctx, entry) {
|
|
|
17
17
|
? "required approval missing"
|
|
18
18
|
: `review gate not satisfied (${prStatus.reviewDecision ?? "unknown"})`;
|
|
19
19
|
emit(ctx, entry, "merge_waiting_approval", { detail });
|
|
20
|
+
ctx.store.transition(entry.id, "merging", { waitDetail: detail }, detail);
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
23
|
if (prStatus.headSha !== entry.headSha) {
|
|
@@ -55,6 +56,7 @@ export async function mergeHead(ctx, entry) {
|
|
|
55
56
|
// Throwing the spec away here would force a rebuild + re-run of
|
|
56
57
|
// CI that produces the same tree — wasting the speculation.
|
|
57
58
|
emit(ctx, entry, "merge_waiting_main", { detail: "main checks still pending, holding merge" });
|
|
59
|
+
ctx.store.transition(entry.id, "merging", { waitDetail: "main checks still pending, holding merge" }, "main checks still pending, holding merge");
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
60
62
|
if (mainStatus === "fail") {
|
package/dist/service-queue.js
CHANGED
|
@@ -7,6 +7,16 @@ function matchGlob(pattern, value) {
|
|
|
7
7
|
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
8
8
|
return regex.test(value);
|
|
9
9
|
}
|
|
10
|
+
function getLatestEvictedEntry(entries, prNumber) {
|
|
11
|
+
const evicted = entries.filter((entry) => entry.prNumber === prNumber && entry.status === "evicted");
|
|
12
|
+
evicted.sort((left, right) => {
|
|
13
|
+
const updatedDelta = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
|
|
14
|
+
if (updatedDelta !== 0)
|
|
15
|
+
return updatedDelta;
|
|
16
|
+
return right.position - left.position;
|
|
17
|
+
});
|
|
18
|
+
return evicted[0];
|
|
19
|
+
}
|
|
10
20
|
export class MergeStewardQueueCommands {
|
|
11
21
|
config;
|
|
12
22
|
policy;
|
|
@@ -48,6 +58,7 @@ export class MergeStewardQueueCommands {
|
|
|
48
58
|
specBranch: null,
|
|
49
59
|
specSha: null,
|
|
50
60
|
specBasedOn: null,
|
|
61
|
+
waitDetail: null,
|
|
51
62
|
postMergeStatus: null,
|
|
52
63
|
postMergeSha: null,
|
|
53
64
|
postMergeSummary: null,
|
|
@@ -100,6 +111,11 @@ export class MergeStewardQueueCommands {
|
|
|
100
111
|
this.logger.debug({ prNumber }, "PR already queued, skipping admission");
|
|
101
112
|
return false;
|
|
102
113
|
}
|
|
114
|
+
const latestEvicted = getLatestEvictedEntry(this.store.listAll(this.config.repoId), prNumber);
|
|
115
|
+
if (latestEvicted?.headSha === headSha) {
|
|
116
|
+
this.logger.debug({ prNumber, headSha, evictedEntryId: latestEvicted.id }, "PR head matches latest evicted entry, skipping admission until a new push");
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
103
119
|
try {
|
|
104
120
|
const status = await this.github.getStatus(prNumber);
|
|
105
121
|
if (!status.reviewApproved) {
|
package/dist/store.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface QueueStore {
|
|
|
11
11
|
listActive(repoId: string): QueueEntry[];
|
|
12
12
|
listAll(repoId: string): QueueEntry[];
|
|
13
13
|
insert(entry: QueueEntry): void;
|
|
14
|
-
transition(entryId: string, to: QueueEntryStatus, patch?: Partial<Pick<QueueEntry, "headSha" | "baseSha" | "ciRunId" | "ciRetries" | "retryAttempts" | "lastFailedBaseSha" | "specBranch" | "specSha" | "specBasedOn" | "postMergeStatus" | "postMergeSha" | "postMergeSummary" | "postMergeCheckedAt">>, detail?: string): void;
|
|
14
|
+
transition(entryId: string, to: QueueEntryStatus, patch?: Partial<Pick<QueueEntry, "headSha" | "baseSha" | "ciRunId" | "ciRetries" | "retryAttempts" | "lastFailedBaseSha" | "specBranch" | "specSha" | "specBasedOn" | "waitDetail" | "postMergeStatus" | "postMergeSha" | "postMergeSummary" | "postMergeCheckedAt">>, detail?: string): void;
|
|
15
15
|
dequeue(entryId: string): void;
|
|
16
16
|
updateHead(entryId: string, newHeadSha: string): void;
|
|
17
17
|
insertIncident(incident: IncidentRecord): void;
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface QueueEntry {
|
|
|
38
38
|
specSha: string | null;
|
|
39
39
|
/** Entry ID of the previous entry this spec branch is based on. Null if based on main. */
|
|
40
40
|
specBasedOn: string | null;
|
|
41
|
+
/** Optional human-facing wait reason when the entry is blocked inside its current state. */
|
|
42
|
+
waitDetail?: string | null;
|
|
41
43
|
/** Post-merge verification status for the landed commit on main. */
|
|
42
44
|
postMergeStatus: PostMergeStatus | null;
|
|
43
45
|
/** Commit SHA used when checking post-merge status. */
|
package/dist/watch/DetailView.js
CHANGED
|
@@ -17,7 +17,7 @@ export function DetailView({ detail, isHead, activeIndex, activeCount, headPrNum
|
|
|
17
17
|
headPrNumber,
|
|
18
18
|
queueBlock,
|
|
19
19
|
});
|
|
20
|
-
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) }), _jsx(Text, { dimColor: true, children: `· ${nextStepLabel(entry.status, entry)}` })] }), _jsxs(Box, { gap: 2, children: [_jsx(Text, { dimColor: true, children: entry.branch }), _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: "tested as" }), _jsx(Text, { children: entry.specBranch }), _jsx(Text, { dimColor: true, children: `(${shortSha(entry.specSha)} ← ${entry.specBasedOn ? "PR ahead" : "main"})` })] })), entry.retryAttempts > 0 && (_jsxs(Text, { color: "yellow", children: ["retry ", entry.retryAttempts, "/", entry.maxRetries] })), isHead && queueBlock && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", summarizeQueueBlock(queueBlock) ?? `waiting for ${queueBlock.baseBranch} checks`, "."] }), _jsx(Text, { dimColor: true, children: queueBlock.missingRequiredChecks.length > 0
|
|
20
|
+
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, entry), children: humanStatus(entry.status, entry) }), _jsx(Text, { dimColor: true, children: `· ${nextStepLabel(entry.status, entry)}` })] }), _jsxs(Box, { gap: 2, children: [_jsx(Text, { dimColor: true, children: entry.branch }), _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: "tested as" }), _jsx(Text, { children: entry.specBranch }), _jsx(Text, { dimColor: true, children: `(${shortSha(entry.specSha)} ← ${entry.specBasedOn ? "PR ahead" : "main"})` })] })), entry.retryAttempts > 0 && (_jsxs(Text, { color: "yellow", children: ["retry ", entry.retryAttempts, "/", entry.maxRetries] })), isHead && queueBlock && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", summarizeQueueBlock(queueBlock) ?? `waiting for ${queueBlock.baseBranch} checks`, "."] }), _jsx(Text, { dimColor: true, children: queueBlock.missingRequiredChecks.length > 0
|
|
21
21
|
? `Fix ${queueBlock.baseBranch} first, then reconcile this repo to resume the queue.`
|
|
22
22
|
: `Will resume automatically once ${queueBlock.baseBranch} is healthy.` })] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "GitHub Policy" }), _jsxs(Text, { dimColor: true, children: ["Required checks: ", githubPolicy.requiredChecks.length > 0 ? githubPolicy.requiredChecks.join(", ") : "(none)"] }), githubPolicy.fetchedAt ? _jsxs(Text, { dimColor: true, children: ["Fetched: ", githubPolicy.fetchedAt] }) : null, githubPolicy.lastRefreshReason ? (_jsxs(Text, { dimColor: true, children: ["Last refresh: ", githubPolicy.lastRefreshReason, githubPolicy.lastRefreshChanged === null ? "" : githubPolicy.lastRefreshChanged ? " (changed)" : " (unchanged)"] })) : null] }), _jsx(EntryStateGraph, { main: graph.main, exits: graph.exits }), _jsx(ExternalRepairObservation, { observations: observations }), incidents.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "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}`))))] })] }));
|
|
23
23
|
}
|
|
@@ -75,7 +75,7 @@ export function ProjectDetailView({ repo, selectedEntryId, detail, filter, }) {
|
|
|
75
75
|
return `#${entry.prNumber}${ci.icon}`;
|
|
76
76
|
}).join(" ")
|
|
77
77
|
: "empty";
|
|
78
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: repo.repoId }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: health.color, children:
|
|
78
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: repo.repoId }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: health.color, children: health.label })] }), _jsx(Text, { dimColor: true, children: ` ${projectStatsSummary(snapshot, compact)}` }), compact ? null : _jsx(Text, { dimColor: true, children: runtimeSummary(snapshot) }), _jsx(Text, { dimColor: true, children: compact
|
|
79
79
|
? `Queue: ${truncate(chainText, width - 10)}`
|
|
80
80
|
: `Queue: main -> ${truncate(chainText, width - 18)}` }), compact ? null : queueBlockSummary ? _jsx(Text, { color: "yellow", children: queueBlockSummary }) : null, compact ? null : (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["GitHub required checks: ", snapshot.githubPolicy.requiredChecks.length > 0 ? snapshot.githubPolicy.requiredChecks.join(", ") : "(none)"] }), snapshot.githubPolicy.fetchedAt ? (_jsxs(Text, { dimColor: true, children: ["Policy fetched ", relativeTime(snapshot.githubPolicy.fetchedAt), " ago", snapshot.githubPolicy.lastRefreshReason
|
|
81
81
|
? ` via ${snapshot.githubPolicy.lastRefreshReason}${snapshot.githubPolicy.lastRefreshChanged === null ? "" : snapshot.githubPolicy.lastRefreshChanged ? " (changed)" : " (unchanged)"}`
|
|
@@ -86,8 +86,8 @@ export function ProjectDetailView({ repo, selectedEntryId, detail, filter, }) {
|
|
|
86
86
|
const statusColor = entry.status === "evicted" ? "red" : ci.color;
|
|
87
87
|
const baseStatusText = compact ? compactEntryStatus(entry.status) : humanStatus(entry.status, entry);
|
|
88
88
|
const statusText = entry.status === "merged" ? `${baseStatusText}: ${postMergeStatusLine(entry)}` : baseStatusText;
|
|
89
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? "cyan" : "gray", children: isSelected ? ">" : " " }), _jsx(Text, { bold: true, children: `#${entry.prNumber}` }), entry.issueKey ? _jsx(Text, { children: ` ${entry.issueKey}` }) : null, _jsx(Text, { children: ` ` }), _jsx(Text, { color: ci.color, children: ci.icon }), _jsx(Text, { color: statusColor, children: ` ${statusText}` }), compact ? null : (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ` updated ${relativeTime(entry.updatedAt)} ago` }), isHead ? _jsx(Text, { dimColor: true, children: ` · ${humanStatus(entry.status, entry)} head` }) : null] }))] }), (isSelected || entry.status === "evicted" || (isHead && queueBlockSummary)) && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children:
|
|
89
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? "cyan" : "gray", children: isSelected ? ">" : " " }), _jsx(Text, { bold: true, children: `#${entry.prNumber}` }), entry.issueKey ? _jsx(Text, { children: ` ${entry.issueKey}` }) : null, _jsx(Text, { children: ` ` }), _jsx(Text, { color: ci.color, children: ci.icon }), _jsx(Text, { color: statusColor, children: ` ${statusText}` }), compact ? null : (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ` updated ${relativeTime(entry.updatedAt)} ago` }), isHead ? _jsx(Text, { dimColor: true, children: ` · ${humanStatus(entry.status, entry)} head` }) : null] }))] }), (isSelected || entry.status === "evicted" || (isHead && queueBlockSummary)) && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: describeEntry(entry, { isHead, queueBlockSummary }) }) }))] }, entry.id));
|
|
90
90
|
}))] }), selectedEntry ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: `Selected PR #${selectedEntry.prNumber}` }), compact ? (_jsx(Text, { dimColor: true, children: `head ${shortSha(selectedEntry.headSha)} · base ${shortSha(selectedEntry.baseSha)}` })) : (_jsxs(Text, { dimColor: true, children: [selectedEntry.branch, " \u00B7 head ", shortSha(selectedEntry.headSha), " \u00B7 base ", shortSha(selectedEntry.baseSha)] })), compact ? null : _jsx(Text, { dimColor: true, children: `Test branch: ${specChainLabel(selectedEntry, snapshot.entries)}` }), selectedEntry.status === "merged" ? (_jsx(Text, { dimColor: true, children: `Post-merge: ${postMergeStatusLine(selectedEntry)}${selectedEntry.postMergeCheckedAt ? ` · ${relativeTime(selectedEntry.postMergeCheckedAt)} ago` : ""}` })) : null, detail?.incidents.length
|
|
91
91
|
? detail.incidents.slice(-2).map((incident) => (_jsxs(Text, { color: "red", children: [relativeTime(incident.at), " ago \u00B7 ", incident.failureClass, " \u00B7 ", incident.outcome] }, incident.id)))
|
|
92
|
-
: null] })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: detail ? "Selected PR Activity" : "Recent Project Activity" }), visibleEvents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue events yet." })) : (visibleEvents.map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children:
|
|
92
|
+
: null] })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: detail ? "Selected PR Activity" : "Recent Project Activity" }), visibleEvents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue events yet." })) : (visibleEvents.map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: formatEventNarrative(event, selectedEntry ? { prNumber: selectedEntry.prNumber } : {}) })] }, "id" in event && event.id ? String(event.id) : `${event.entryId}-${event.at}`))))] })] }));
|
|
93
93
|
}
|
package/dist/watch/format.d.ts
CHANGED
|
@@ -2,10 +2,17 @@ import type { CheckResult, PostMergeStatus, QueueBlockState, QueueEntryStatus, Q
|
|
|
2
2
|
export declare function isPendingMainVerification(block: QueueBlockState | null | undefined): boolean;
|
|
3
3
|
export declare function shortSha(value: string | null | undefined): string;
|
|
4
4
|
export declare function relativeTime(iso: string | null | undefined): string;
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function mergeWaitState(entry: {
|
|
6
|
+
status: QueueEntryStatus;
|
|
7
|
+
waitDetail?: string | null | undefined;
|
|
8
|
+
} | undefined): "approval" | "main" | null;
|
|
9
|
+
export declare function statusColor(status: QueueEntryStatus, entry?: {
|
|
10
|
+
waitDetail?: string | null | undefined;
|
|
11
|
+
}): "yellow" | "cyan" | "green" | "red" | "gray";
|
|
6
12
|
export declare function humanStatus(status: QueueEntryStatus, entry?: {
|
|
7
13
|
lastFailedBaseSha: string | null;
|
|
8
14
|
specBranch: string | null;
|
|
15
|
+
waitDetail?: string | null | undefined;
|
|
9
16
|
}): string;
|
|
10
17
|
export declare function queueProgress(status: QueueEntryStatus): {
|
|
11
18
|
current: number;
|
|
@@ -14,6 +21,7 @@ export declare function queueProgress(status: QueueEntryStatus): {
|
|
|
14
21
|
export declare function nextStepLabel(status: QueueEntryStatus, entry?: {
|
|
15
22
|
lastFailedBaseSha: string | null;
|
|
16
23
|
specBasedOn: string | null;
|
|
24
|
+
waitDetail?: string | null | undefined;
|
|
17
25
|
}): string;
|
|
18
26
|
/** Describe the spec chain for a queue entry. */
|
|
19
27
|
export declare function specChainLabel(entry: {
|
|
@@ -44,6 +52,7 @@ export declare function summarizeCheckNames(checks: CheckResult[], limit?: numbe
|
|
|
44
52
|
export declare function ciStatusIcon(entry: {
|
|
45
53
|
status: QueueEntryStatus;
|
|
46
54
|
ciRunId: string | null;
|
|
55
|
+
waitDetail?: string | null | undefined;
|
|
47
56
|
postMergeStatus?: PostMergeStatus | null;
|
|
48
57
|
}): {
|
|
49
58
|
icon: string;
|
package/dist/watch/format.js
CHANGED
|
@@ -35,14 +35,28 @@ export function relativeTime(iso) {
|
|
|
35
35
|
const days = Math.floor(hours / 24);
|
|
36
36
|
return `${days}d`;
|
|
37
37
|
}
|
|
38
|
-
export function
|
|
38
|
+
export function mergeWaitState(entry) {
|
|
39
|
+
if (!entry || entry.status !== "merging" || !entry.waitDetail) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const detail = entry.waitDetail.toLowerCase();
|
|
43
|
+
if (detail.includes("approval") || detail.includes("review")) {
|
|
44
|
+
return "approval";
|
|
45
|
+
}
|
|
46
|
+
if (detail.includes("main")) {
|
|
47
|
+
return "main";
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
export function statusColor(status, entry) {
|
|
39
52
|
switch (status) {
|
|
40
53
|
case "queued":
|
|
41
54
|
return "yellow";
|
|
42
55
|
case "preparing_head":
|
|
43
56
|
case "validating":
|
|
44
|
-
case "merging":
|
|
45
57
|
return "cyan";
|
|
58
|
+
case "merging":
|
|
59
|
+
return mergeWaitState(entry ? { status, waitDetail: entry.waitDetail } : undefined) ? "yellow" : "cyan";
|
|
46
60
|
case "merged":
|
|
47
61
|
return "green";
|
|
48
62
|
case "evicted":
|
|
@@ -62,6 +76,12 @@ export function humanStatus(status, entry) {
|
|
|
62
76
|
case "validating":
|
|
63
77
|
return "testing";
|
|
64
78
|
case "merging":
|
|
79
|
+
if (mergeWaitState(entry ? { status, waitDetail: entry.waitDetail } : undefined) === "approval") {
|
|
80
|
+
return "waiting for approval";
|
|
81
|
+
}
|
|
82
|
+
if (mergeWaitState(entry ? { status, waitDetail: entry.waitDetail } : undefined) === "main") {
|
|
83
|
+
return "waiting for main";
|
|
84
|
+
}
|
|
65
85
|
return "merging";
|
|
66
86
|
case "merged":
|
|
67
87
|
return "merged";
|
|
@@ -99,6 +119,12 @@ export function nextStepLabel(status, entry) {
|
|
|
99
119
|
? "CI running, tested together with PRs ahead"
|
|
100
120
|
: "CI running on combined changes";
|
|
101
121
|
case "merging":
|
|
122
|
+
if (mergeWaitState(entry ? { status, waitDetail: entry.waitDetail } : undefined) === "approval") {
|
|
123
|
+
return "waiting for GitHub approval before landing on main";
|
|
124
|
+
}
|
|
125
|
+
if (mergeWaitState(entry ? { status, waitDetail: entry.waitDetail } : undefined) === "main") {
|
|
126
|
+
return "waiting for main checks to settle before landing";
|
|
127
|
+
}
|
|
102
128
|
return "landing on main";
|
|
103
129
|
case "merged":
|
|
104
130
|
return "landed on main";
|
|
@@ -179,6 +205,13 @@ export function formatEventNarrative(event, options = {}) {
|
|
|
179
205
|
return withDetail(`${prPrefix}started running CI.`, event.detail);
|
|
180
206
|
}
|
|
181
207
|
if (event.toStatus === "merging") {
|
|
208
|
+
const detail = event.detail?.toLowerCase() ?? "";
|
|
209
|
+
if (detail.includes("approval") || detail.includes("review")) {
|
|
210
|
+
return withDetail(`${prPrefix}is waiting for approval before merging to main.`, event.detail);
|
|
211
|
+
}
|
|
212
|
+
if (detail.includes("main")) {
|
|
213
|
+
return withDetail(`${prPrefix}is waiting for main verification before merging.`, event.detail);
|
|
214
|
+
}
|
|
182
215
|
return withDetail(`${prPrefix}passed CI and started merging to main.`, event.detail);
|
|
183
216
|
}
|
|
184
217
|
if (event.toStatus === "merged") {
|
|
@@ -270,7 +303,8 @@ export function ciStatusIcon(entry) {
|
|
|
270
303
|
icon: postMergeSymbol(entry.postMergeStatus),
|
|
271
304
|
color: postMergeColor(entry.postMergeStatus),
|
|
272
305
|
};
|
|
273
|
-
case "merging":
|
|
306
|
+
case "merging":
|
|
307
|
+
return mergeWaitState(entry) ? { icon: QUEUE_SYMBOLS.inProgress, color: "yellow" } : { icon: "\u2713", color: "cyan" };
|
|
274
308
|
case "evicted": return { icon: "\u2717", color: "red" }; // ✗
|
|
275
309
|
case "dequeued": return { icon: "\u2012", color: "gray" }; // ‒
|
|
276
310
|
case "validating":
|
|
@@ -108,7 +108,15 @@ export function buildExternalRepairObservations(detail, options) {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
else if (entry.status === "merging") {
|
|
111
|
-
|
|
111
|
+
if (entry.waitDetail?.toLowerCase().includes("approval") || entry.waitDetail?.toLowerCase().includes("review")) {
|
|
112
|
+
observations.push({ tone: "warn", text: "CI passed. Merge is blocked until GitHub approval returns." });
|
|
113
|
+
}
|
|
114
|
+
else if (entry.waitDetail?.toLowerCase().includes("main")) {
|
|
115
|
+
observations.push({ tone: "info", text: "CI passed. Waiting for main verification before landing." });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
observations.push({ tone: "info", text: "CI passed. Landing on main." });
|
|
119
|
+
}
|
|
112
120
|
}
|
|
113
121
|
return observations.slice(0, 4);
|
|
114
122
|
}
|