merge-steward 0.12.0 → 0.13.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/db/schema.js +29 -0
- package/dist/db/sqlite-store.d.ts +1 -1
- package/dist/db/sqlite-store.js +27 -2
- package/dist/reconciler-merge.js +12 -1
- package/dist/reconciler-post-merge.d.ts +8 -0
- package/dist/reconciler-post-merge.js +85 -0
- package/dist/reconciler-sanitize.js +12 -1
- package/dist/reconciler.js +38 -0
- package/dist/service-queue.js +10 -1
- package/dist/store.d.ts +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/watch/OverviewView.js +1 -1
- package/dist/watch/ProjectDetailView.js +5 -4
- package/dist/watch/QueueListView.js +7 -4
- package/dist/watch/dashboard-model.js +5 -1
- package/dist/watch/format.d.ts +7 -1
- package/dist/watch/format.js +49 -4
- package/package.json +1 -1
package/dist/db/schema.js
CHANGED
|
@@ -24,6 +24,10 @@ export function ensureSchema(connection) {
|
|
|
24
24
|
spec_branch TEXT,
|
|
25
25
|
spec_sha TEXT,
|
|
26
26
|
spec_based_on TEXT,
|
|
27
|
+
post_merge_status TEXT,
|
|
28
|
+
post_merge_sha TEXT,
|
|
29
|
+
post_merge_summary TEXT,
|
|
30
|
+
post_merge_checked_at TEXT,
|
|
27
31
|
enqueued_at TEXT NOT NULL,
|
|
28
32
|
updated_at TEXT NOT NULL
|
|
29
33
|
)
|
|
@@ -64,6 +68,16 @@ export function ensureSchema(connection) {
|
|
|
64
68
|
connection.exec(`
|
|
65
69
|
CREATE INDEX IF NOT EXISTS idx_queue_events_entry
|
|
66
70
|
ON queue_events(entry_id, id)
|
|
71
|
+
`);
|
|
72
|
+
ensureColumn(connection, "queue_entries", "post_merge_status", "TEXT");
|
|
73
|
+
ensureColumn(connection, "queue_entries", "post_merge_sha", "TEXT");
|
|
74
|
+
ensureColumn(connection, "queue_entries", "post_merge_summary", "TEXT");
|
|
75
|
+
ensureColumn(connection, "queue_entries", "post_merge_checked_at", "TEXT");
|
|
76
|
+
connection.exec(`
|
|
77
|
+
UPDATE queue_entries
|
|
78
|
+
SET post_merge_status = 'pending'
|
|
79
|
+
WHERE status = 'merged'
|
|
80
|
+
AND post_merge_status IS NULL
|
|
67
81
|
`);
|
|
68
82
|
// Must match TERMINAL_STATUSES in types.ts: merged, evicted, dequeued
|
|
69
83
|
connection.exec(`
|
|
@@ -72,3 +86,18 @@ export function ensureSchema(connection) {
|
|
|
72
86
|
WHERE status NOT IN ('merged', 'evicted', 'dequeued')
|
|
73
87
|
`);
|
|
74
88
|
}
|
|
89
|
+
function hasColumn(connection, table, column) {
|
|
90
|
+
const rows = connection.prepare(`PRAGMA table_info(${table})`).all();
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
if (String(row.name) === column) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
function ensureColumn(connection, table, column, type) {
|
|
99
|
+
if (hasColumn(connection, table, column)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
connection.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
103
|
+
}
|
|
@@ -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">>, detail?: string): 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;
|
|
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,12 @@ 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
|
+
postMergeStatus: row.post_merge_status === null || row.post_merge_status === undefined
|
|
26
|
+
? null
|
|
27
|
+
: String(row.post_merge_status),
|
|
28
|
+
postMergeSha: row.post_merge_sha === null || row.post_merge_sha === undefined ? null : String(row.post_merge_sha),
|
|
29
|
+
postMergeSummary: row.post_merge_summary === null || row.post_merge_summary === undefined ? null : String(row.post_merge_summary),
|
|
30
|
+
postMergeCheckedAt: row.post_merge_checked_at === null || row.post_merge_checked_at === undefined ? null : String(row.post_merge_checked_at),
|
|
25
31
|
enqueuedAt: String(row.enqueued_at),
|
|
26
32
|
updatedAt: String(row.updated_at),
|
|
27
33
|
};
|
|
@@ -116,8 +122,10 @@ export class SqliteStore {
|
|
|
116
122
|
this.conn.prepare(`INSERT INTO queue_entries
|
|
117
123
|
(id, repo_id, pr_number, branch, head_sha, base_sha, status, position,
|
|
118
124
|
priority, generation, ci_run_id, ci_retries, retry_attempts,
|
|
119
|
-
max_retries, last_failed_base_sha, issue_key,
|
|
120
|
-
|
|
125
|
+
max_retries, last_failed_base_sha, issue_key,
|
|
126
|
+
post_merge_status, post_merge_sha, post_merge_summary, post_merge_checked_at,
|
|
127
|
+
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);
|
|
121
129
|
this.writeEvent(entry.id, null, entry.status);
|
|
122
130
|
})();
|
|
123
131
|
}
|
|
@@ -165,6 +173,22 @@ export class SqliteStore {
|
|
|
165
173
|
sets.push("spec_based_on = ?");
|
|
166
174
|
values.push(patch.specBasedOn);
|
|
167
175
|
}
|
|
176
|
+
if (patch?.postMergeStatus !== undefined) {
|
|
177
|
+
sets.push("post_merge_status = ?");
|
|
178
|
+
values.push(patch.postMergeStatus);
|
|
179
|
+
}
|
|
180
|
+
if (patch?.postMergeSha !== undefined) {
|
|
181
|
+
sets.push("post_merge_sha = ?");
|
|
182
|
+
values.push(patch.postMergeSha);
|
|
183
|
+
}
|
|
184
|
+
if (patch?.postMergeSummary !== undefined) {
|
|
185
|
+
sets.push("post_merge_summary = ?");
|
|
186
|
+
values.push(patch.postMergeSummary);
|
|
187
|
+
}
|
|
188
|
+
if (patch?.postMergeCheckedAt !== undefined) {
|
|
189
|
+
sets.push("post_merge_checked_at = ?");
|
|
190
|
+
values.push(patch.postMergeCheckedAt);
|
|
191
|
+
}
|
|
168
192
|
this.conn.prepare(`UPDATE queue_entries SET ${sets.join(", ")} WHERE id = ?`).run(...values, entryId);
|
|
169
193
|
this.writeEvent(entryId, from, to, detail);
|
|
170
194
|
})();
|
|
@@ -186,6 +210,7 @@ export class SqliteStore {
|
|
|
186
210
|
ci_run_id = NULL, ci_retries = 0, retry_attempts = 0,
|
|
187
211
|
last_failed_base_sha = NULL,
|
|
188
212
|
spec_branch = NULL, spec_sha = NULL, spec_based_on = NULL,
|
|
213
|
+
post_merge_status = NULL, post_merge_sha = NULL, post_merge_summary = NULL, post_merge_checked_at = NULL,
|
|
189
214
|
updated_at = ?
|
|
190
215
|
WHERE id = ?`).run(newHeadSha, newGen, isoNow(), entryId);
|
|
191
216
|
this.writeEvent(entryId, from, "queued", `updateHead: generation ${newGen}`);
|
package/dist/reconciler-merge.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CLEAN_CI, CLEAN_SPEC, emit, isBudgetExhausted, ref } from "./reconciler-core.js";
|
|
2
2
|
import { cleanupSpec, evictEntry, invalidateDownstream } from "./reconciler-evict.js";
|
|
3
|
+
import { verifyPostMergeStatus } from "./reconciler-post-merge.js";
|
|
3
4
|
export async function mergeHead(ctx, entry) {
|
|
4
5
|
emit(ctx, entry, "merge_revalidating");
|
|
5
6
|
const prStatus = await ctx.github.getStatus(entry.prNumber);
|
|
@@ -99,7 +100,17 @@ export async function mergeHead(ctx, entry) {
|
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
emit(ctx, entry, "merge_succeeded");
|
|
102
|
-
|
|
103
|
+
const verificationResult = await verifyPostMergeStatus(ctx, {
|
|
104
|
+
...entry,
|
|
105
|
+
postMergeSha: entry.specSha ?? entry.headSha,
|
|
106
|
+
});
|
|
107
|
+
ctx.store.transition(entry.id, "merged", {
|
|
108
|
+
...CLEAN_SPEC,
|
|
109
|
+
postMergeStatus: verificationResult.postMergeStatus,
|
|
110
|
+
postMergeSha: verificationResult.postMergeSha,
|
|
111
|
+
postMergeSummary: verificationResult.postMergeSummary,
|
|
112
|
+
postMergeCheckedAt: new Date().toISOString(),
|
|
113
|
+
}, `spec pushed to main; ${verificationResult.postMergeSummary}`);
|
|
103
114
|
await cleanupSpec(ctx, entry);
|
|
104
115
|
try {
|
|
105
116
|
await ctx.github.deleteBranch(entry.prNumber);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PostMergeStatus, QueueEntry } from "./types.ts";
|
|
2
|
+
import type { ReconcileContext } from "./reconciler-core.ts";
|
|
3
|
+
export interface PostMergeVerificationResult {
|
|
4
|
+
postMergeStatus: PostMergeStatus;
|
|
5
|
+
postMergeSummary: string;
|
|
6
|
+
postMergeSha: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function verifyPostMergeStatus(ctx: ReconcileContext, entry: QueueEntry): Promise<PostMergeVerificationResult>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
function normalizeCheckName(name) {
|
|
2
|
+
return name.trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
function joinItems(items) {
|
|
5
|
+
if (items.length === 0) {
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
return items.slice(0, 5).join(", ");
|
|
9
|
+
}
|
|
10
|
+
function evaluateChecks(requiredChecks, checks) {
|
|
11
|
+
const isPassingConclusion = (conclusion) => conclusion === "success";
|
|
12
|
+
if (requiredChecks.length > 0) {
|
|
13
|
+
const byName = new Map(checks.map((check) => [normalizeCheckName(check.name), check]));
|
|
14
|
+
const failed = [];
|
|
15
|
+
const pending = [];
|
|
16
|
+
for (const required of requiredChecks) {
|
|
17
|
+
const check = byName.get(normalizeCheckName(required));
|
|
18
|
+
if (!check) {
|
|
19
|
+
pending.push(required);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (check.conclusion === "pending") {
|
|
23
|
+
pending.push(check.name);
|
|
24
|
+
}
|
|
25
|
+
else if (!isPassingConclusion(check.conclusion)) {
|
|
26
|
+
failed.push(check.name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (failed.length > 0) {
|
|
30
|
+
return {
|
|
31
|
+
postMergeStatus: "fail",
|
|
32
|
+
summary: failed.length === 1 ? `check failed: ${failed[0]}` : `checks failed: ${joinItems(failed)}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (pending.length > 0) {
|
|
36
|
+
return {
|
|
37
|
+
postMergeStatus: "pending",
|
|
38
|
+
summary: pending.length === 1 ? `check pending: ${pending[0]}` : `checks pending: ${joinItems(pending)}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { postMergeStatus: "pass", summary: "all required checks passed" };
|
|
42
|
+
}
|
|
43
|
+
const pending = checks.filter((check) => check.conclusion === "pending").map((check) => check.name);
|
|
44
|
+
const failed = checks.filter((check) => !isPassingConclusion(check.conclusion)).map((check) => check.name);
|
|
45
|
+
if (checks.length === 0) {
|
|
46
|
+
return { postMergeStatus: "unknown", summary: "no checks found yet" };
|
|
47
|
+
}
|
|
48
|
+
if (failed.length > 0) {
|
|
49
|
+
return {
|
|
50
|
+
postMergeStatus: "fail",
|
|
51
|
+
summary: failed.length === 1 ? `check failed: ${failed[0]}` : `checks failed: ${joinItems(failed)}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (pending.length > 0) {
|
|
55
|
+
return {
|
|
56
|
+
postMergeStatus: "pending",
|
|
57
|
+
summary: pending.length === 1 ? `check pending: ${pending[0]}` : `checks pending: ${joinItems(pending)}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { postMergeStatus: "pass", summary: "all checks passed" };
|
|
61
|
+
}
|
|
62
|
+
export async function verifyPostMergeStatus(ctx, entry) {
|
|
63
|
+
const postMergeSha = entry.postMergeSha ?? entry.specSha ?? entry.headSha;
|
|
64
|
+
const requiredChecks = ctx.policy.getRequiredChecks();
|
|
65
|
+
if (!postMergeSha) {
|
|
66
|
+
return {
|
|
67
|
+
postMergeStatus: "unknown",
|
|
68
|
+
postMergeSummary: "post-merge SHA is unknown",
|
|
69
|
+
postMergeSha: entry.headSha,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
let checks;
|
|
73
|
+
try {
|
|
74
|
+
checks = await ctx.github.listChecksForRef(postMergeSha);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
checks = [];
|
|
78
|
+
}
|
|
79
|
+
const evaluation = evaluateChecks(requiredChecks, checks);
|
|
80
|
+
return {
|
|
81
|
+
postMergeStatus: evaluation.postMergeStatus,
|
|
82
|
+
postMergeSummary: evaluation.summary,
|
|
83
|
+
postMergeSha,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CLEAN_SPEC, emit } from "./reconciler-core.js";
|
|
2
2
|
import { cleanupSpec } from "./reconciler-evict.js";
|
|
3
|
+
import { verifyPostMergeStatus } from "./reconciler-post-merge.js";
|
|
3
4
|
export async function sanitizeEntry(ctx, entry) {
|
|
4
5
|
const canonical = ctx.store.getEntryByPR(ctx.repoId, entry.prNumber);
|
|
5
6
|
if (canonical && canonical.id !== entry.id) {
|
|
@@ -13,11 +14,21 @@ export async function sanitizeEntry(ctx, entry) {
|
|
|
13
14
|
try {
|
|
14
15
|
const prStatus = await ctx.github.getStatus(entry.prNumber);
|
|
15
16
|
if (prStatus.merged) {
|
|
17
|
+
const verification = await verifyPostMergeStatus(ctx, {
|
|
18
|
+
...entry,
|
|
19
|
+
postMergeSha: entry.headSha,
|
|
20
|
+
});
|
|
16
21
|
emit(ctx, entry, "merge_external", {
|
|
17
22
|
detail: `PR #${entry.prNumber} already merged on GitHub (detected in sanitize)`,
|
|
18
23
|
});
|
|
19
24
|
await cleanupSpec(ctx, entry);
|
|
20
|
-
ctx.store.transition(entry.id, "merged",
|
|
25
|
+
ctx.store.transition(entry.id, "merged", {
|
|
26
|
+
...CLEAN_SPEC,
|
|
27
|
+
postMergeStatus: verification.postMergeStatus,
|
|
28
|
+
postMergeSha: verification.postMergeSha,
|
|
29
|
+
postMergeSummary: verification.postMergeSummary,
|
|
30
|
+
postMergeCheckedAt: new Date().toISOString(),
|
|
31
|
+
}, "merged externally (sanitize)");
|
|
21
32
|
return true;
|
|
22
33
|
}
|
|
23
34
|
if (!prStatus.mergeable && !prStatus.merged) {
|
package/dist/reconciler.js
CHANGED
|
@@ -6,6 +6,7 @@ import { checkValidation } from "./reconciler-validate.js";
|
|
|
6
6
|
import { mergeHead } from "./reconciler-merge.js";
|
|
7
7
|
import { cleanupSpec } from "./reconciler-evict.js";
|
|
8
8
|
import { INVALIDATION_PATCH } from "./invalidation.js";
|
|
9
|
+
import { verifyPostMergeStatus } from "./reconciler-post-merge.js";
|
|
9
10
|
// ─── Main reconcile loop ────────────────────────────────────────
|
|
10
11
|
export async function reconcile(ctx) {
|
|
11
12
|
const allActive = ctx.store.listActive(ctx.repoId);
|
|
@@ -74,5 +75,42 @@ export async function reconcile(ctx) {
|
|
|
74
75
|
throw wrapped;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
await verifyMergedEntriesPostPush(ctx);
|
|
79
|
+
}
|
|
80
|
+
async function verifyMergedEntriesPostPush(ctx) {
|
|
81
|
+
const mergedEntries = ctx.store.listAll(ctx.repoId).filter((entry) => entry.status === "merged");
|
|
82
|
+
for (const entry of mergedEntries) {
|
|
83
|
+
if (entry.postMergeStatus === "pass" || entry.postMergeStatus === "fail")
|
|
84
|
+
continue;
|
|
85
|
+
const postMergeSha = entry.postMergeSha ?? entry.specSha ?? entry.headSha;
|
|
86
|
+
if (!postMergeSha) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
emit(ctx, entry, "post_merge_verification_started", {
|
|
90
|
+
detail: `post-merge check on ${postMergeSha.slice(0, 8)}`,
|
|
91
|
+
});
|
|
92
|
+
const verification = await verifyPostMergeStatus(ctx, entry);
|
|
93
|
+
emit(ctx, entry, "post_merge_verification_completed", {
|
|
94
|
+
detail: `post-merge checks ${verification.postMergeStatus}: ${verification.postMergeSummary}`,
|
|
95
|
+
});
|
|
96
|
+
const now = new Date().toISOString();
|
|
97
|
+
const previousStatus = entry.postMergeStatus ?? "pending";
|
|
98
|
+
if (previousStatus !== verification.postMergeStatus
|
|
99
|
+
|| entry.postMergeSummary !== verification.postMergeSummary
|
|
100
|
+
|| entry.postMergeCheckedAt === null
|
|
101
|
+
|| entry.postMergeCheckedAt === undefined) {
|
|
102
|
+
ctx.store.transition(entry.id, "merged", {
|
|
103
|
+
postMergeStatus: verification.postMergeStatus,
|
|
104
|
+
postMergeSha: verification.postMergeSha,
|
|
105
|
+
postMergeSummary: verification.postMergeSummary,
|
|
106
|
+
postMergeCheckedAt: now,
|
|
107
|
+
}, `post-merge verification: ${verification.postMergeStatus}`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
ctx.store.transition(entry.id, "merged", {
|
|
111
|
+
postMergeCheckedAt: now,
|
|
112
|
+
}, "post-merge verification");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
77
115
|
}
|
|
78
116
|
// phase implementations live in dedicated modules now
|
package/dist/service-queue.js
CHANGED
|
@@ -48,6 +48,10 @@ export class MergeStewardQueueCommands {
|
|
|
48
48
|
specBranch: null,
|
|
49
49
|
specSha: null,
|
|
50
50
|
specBasedOn: null,
|
|
51
|
+
postMergeStatus: null,
|
|
52
|
+
postMergeSha: null,
|
|
53
|
+
postMergeSummary: null,
|
|
54
|
+
postMergeCheckedAt: null,
|
|
51
55
|
enqueuedAt: new Date().toISOString(),
|
|
52
56
|
updatedAt: new Date().toISOString(),
|
|
53
57
|
};
|
|
@@ -156,7 +160,12 @@ export class MergeStewardQueueCommands {
|
|
|
156
160
|
acknowledgeExternalMerge(prNumber) {
|
|
157
161
|
const entry = this.store.getEntryByPR(this.config.repoId, prNumber);
|
|
158
162
|
if (entry) {
|
|
159
|
-
this.store.transition(entry.id, "merged"
|
|
163
|
+
this.store.transition(entry.id, "merged", {
|
|
164
|
+
postMergeStatus: "pending",
|
|
165
|
+
postMergeSha: entry.headSha,
|
|
166
|
+
postMergeSummary: "external merge detected, verification pending",
|
|
167
|
+
postMergeCheckedAt: new Date().toISOString(),
|
|
168
|
+
});
|
|
160
169
|
this.invalidateDownstreamOf(entry);
|
|
161
170
|
this.logger.info({ prNumber, entryId: entry.id }, "External merge acknowledged");
|
|
162
171
|
}
|
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">>, detail?: string): 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;
|
|
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
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export type QueueEntryStatus = "queued" | "preparing_head" | "validating" | "merging" | "evicted" | "merged" | "dequeued";
|
|
15
15
|
export declare const TERMINAL_STATUSES: QueueEntryStatus[];
|
|
16
|
+
export type PostMergeStatus = "pending" | "pass" | "fail" | "unknown";
|
|
16
17
|
export interface QueueEntry {
|
|
17
18
|
id: string;
|
|
18
19
|
repoId: string;
|
|
@@ -37,6 +38,14 @@ export interface QueueEntry {
|
|
|
37
38
|
specSha: string | null;
|
|
38
39
|
/** Entry ID of the previous entry this spec branch is based on. Null if based on main. */
|
|
39
40
|
specBasedOn: string | null;
|
|
41
|
+
/** Post-merge verification status for the landed commit on main. */
|
|
42
|
+
postMergeStatus: PostMergeStatus | null;
|
|
43
|
+
/** Commit SHA used when checking post-merge status. */
|
|
44
|
+
postMergeSha: string | null;
|
|
45
|
+
/** Optional one-line summary of post-merge checks. */
|
|
46
|
+
postMergeSummary: string | null;
|
|
47
|
+
/** Timestamp of the most recent post-merge verification attempt. */
|
|
48
|
+
postMergeCheckedAt: string | null;
|
|
40
49
|
enqueuedAt: string;
|
|
41
50
|
updatedAt: string;
|
|
42
51
|
}
|
|
@@ -181,7 +190,7 @@ export interface QueueConfig {
|
|
|
181
190
|
flakyRetries: number;
|
|
182
191
|
pollIntervalMs: number;
|
|
183
192
|
}
|
|
184
|
-
export type ReconcileAction = "promoted" | "fetch_started" | "main_broken" | "branch_mismatch" | "spec_build_started" | "spec_build_succeeded" | "spec_build_conflict" | "ci_triggered" | "ci_pending" | "ci_passed" | "ci_failed" | "ci_flaky_retry" | "merge_revalidating" | "merge_succeeded" | "merge_rejected" | "merge_external" | "policy_changed" | "evicted" | "invalidated" | "retry_gated" | "budget_exhausted" | "merge_waiting_approval" | "sanitized_closed" | "sanitized_duplicate" | "branch_unreachable";
|
|
193
|
+
export type ReconcileAction = "promoted" | "fetch_started" | "main_broken" | "branch_mismatch" | "spec_build_started" | "spec_build_succeeded" | "spec_build_conflict" | "ci_triggered" | "ci_pending" | "ci_passed" | "ci_failed" | "ci_flaky_retry" | "merge_revalidating" | "merge_succeeded" | "merge_rejected" | "merge_external" | "policy_changed" | "evicted" | "invalidated" | "retry_gated" | "budget_exhausted" | "merge_waiting_approval" | "sanitized_closed" | "sanitized_duplicate" | "post_merge_verification_started" | "post_merge_verification_completed" | "branch_unreachable";
|
|
185
194
|
export interface ReconcileEvent {
|
|
186
195
|
at: string;
|
|
187
196
|
entryId: string;
|
|
@@ -39,6 +39,6 @@ export function OverviewView({ repos, selectedRepoId, gatewayError }) {
|
|
|
39
39
|
}).join(" ")
|
|
40
40
|
: compact ? "empty" : "queue is empty"
|
|
41
41
|
: "queue data unavailable";
|
|
42
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: repo.repoId === selectedRepoId ? "cyan" : "gray", children: repo.repoId === selectedRepoId ? "
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: repo.repoId === selectedRepoId ? "cyan" : "gray", children: repo.repoId === selectedRepoId ? ">" : " " }), _jsx(Text, { bold: true, children: truncate(repo.repoId, idWidth) }), compact ? null : _jsx(Text, { dimColor: true, children: ` ${truncate(repo.repoFullName, 28)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: health.color, children: compact ? health.label.slice(0, 5) : health.label }), _jsx(Text, { dimColor: true, children: ` ${projectStatsSummary(repo.snapshot, compact)}` })] }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: `Queue: ${truncate(chainText, chainWidth)}` }) }), compact ? null : (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: health.color, children: truncate(queueBlockSummary ?? health.detail, 110) }) }))] }, repo.repoId));
|
|
43
43
|
}) }))] }));
|
|
44
44
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text, useStdout } from "ink";
|
|
3
3
|
import { buildDisplayEntries } from "./display-filter.js";
|
|
4
4
|
import { describeEntry, getChainEntries, getRepoHealth, projectStatsSummary, runtimeSummary } from "./dashboard-model.js";
|
|
5
|
-
import { ciStatusIcon, formatEventNarrative, humanStatus, relativeTime, shortSha, specChainLabel, summarizeQueueBlock, truncate } from "./format.js";
|
|
5
|
+
import { ciStatusIcon, formatEventNarrative, humanStatus, postMergeStatusLine, relativeTime, shortSha, specChainLabel, summarizeQueueBlock, truncate } from "./format.js";
|
|
6
6
|
function clampWindowStart(selectedIndex, itemCount, maxItems) {
|
|
7
7
|
if (itemCount <= maxItems) {
|
|
8
8
|
return 0;
|
|
@@ -84,9 +84,10 @@ export function ProjectDetailView({ repo, selectedEntryId, detail, filter, }) {
|
|
|
84
84
|
const isHead = snapshot.summary.headEntryId === entry.id;
|
|
85
85
|
const isSelected = entry.id === selectedEntryId;
|
|
86
86
|
const statusColor = entry.status === "evicted" ? "red" : ci.color;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
const baseStatusText = compact ? compactEntryStatus(entry.status) : humanStatus(entry.status, entry);
|
|
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: truncate(describeEntry(entry, { isHead, queueBlockSummary }), compact ? width - 4 : 110) }) }))] }, entry.id));
|
|
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
|
|
90
91
|
? detail.incidents.slice(-2).map((incident) => (_jsxs(Text, { color: "red", children: [relativeTime(incident.at), " ago \u00B7 ", incident.failureClass, " \u00B7 ", incident.outcome] }, incident.id)))
|
|
91
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: truncate(formatEventNarrative(event), compact ? Math.max(12, width - 8) : 106) })] }, "id" in event && event.id ? String(event.id) : `${event.entryId}-${event.at}`))))] })] }));
|
|
92
93
|
}
|
|
@@ -3,14 +3,17 @@ import { useMemo } from "react";
|
|
|
3
3
|
import { Box, Text, useStdout } from "ink";
|
|
4
4
|
import { TERMINAL_STATUSES } from "../types.js";
|
|
5
5
|
import { buildChainEntries } from "./display-filter.js";
|
|
6
|
-
import { ciStatusIcon, formatEventSummary, humanStatus, isPendingMainVerification, nextStepLabel, relativeTime, statusColor, summarizeQueueBlock, truncate } from "./format.js";
|
|
6
|
+
import { ciStatusIcon, formatEventSummary, humanStatus, isPendingMainVerification, nextStepLabel, postMergeStatusLine, relativeTime, statusColor, summarizeQueueBlock, truncate } from "./format.js";
|
|
7
7
|
const CHROME_ROWS = 13;
|
|
8
8
|
function QueueRow({ entry, selected, isHead, queueBlock, }) {
|
|
9
9
|
const isTerminal = TERMINAL_STATUSES.includes(entry.status);
|
|
10
10
|
if (isTerminal) {
|
|
11
11
|
const icon = entry.status === "merged" ? "\u2713" : "\u2717";
|
|
12
12
|
const iconColor = entry.status === "merged" ? "green" : "red";
|
|
13
|
-
|
|
13
|
+
const statusSuffix = entry.status === "merged"
|
|
14
|
+
? ` \u00b7 ${postMergeStatusLine(entry)}`
|
|
15
|
+
: "";
|
|
16
|
+
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: ` #${entry.prNumber}` }), entry.issueKey ? _jsx(Text, { dimColor: true, children: ` ${entry.issueKey}` }) : null, _jsx(Text, { dimColor: true, children: ` ${relativeTime(entry.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: iconColor, children: `${icon} ${humanStatus(entry.status)}${statusSuffix}` })] }));
|
|
14
17
|
}
|
|
15
18
|
const blockedOnMain = isHead && queueBlock?.reason === "main_broken" && queueBlock.headPrNumber === entry.prNumber;
|
|
16
19
|
const pendingMainVerification = blockedOnMain && isPendingMainVerification(queueBlock);
|
|
@@ -25,7 +28,7 @@ function QueueRow({ entry, selected, isHead, queueBlock, }) {
|
|
|
25
28
|
: nextStepLabel(entry.status, entry);
|
|
26
29
|
// Only show retry counter when retries have actually happened.
|
|
27
30
|
const retryNote = entry.retryAttempts > 0 ? ` \u00b7 retry ${entry.retryAttempts}/${entry.maxRetries}` : "";
|
|
28
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : "gray", children: selected ? "
|
|
31
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : "gray", children: selected ? ">" : " " }), _jsx(Text, { ...(isHead ? { color: "green" } : {}), bold: true, children: ` #${entry.prNumber}` }), entry.issueKey ? _jsx(Text, { children: ` ${entry.issueKey}` }) : null, _jsx(Text, { dimColor: true, children: ` ${relativeTime(entry.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: color, children: status }), _jsx(Text, { dimColor: true, children: ` \u00b7 ${nextStep}${retryNote}` })] }));
|
|
29
32
|
}
|
|
30
33
|
export function QueueListView({ entries, allEntries, selectedEntryId, recentEvents, headEntryId, queueBlock, }) {
|
|
31
34
|
const { stdout } = useStdout();
|
|
@@ -38,7 +41,7 @@ export function QueueListView({ entries, allEntries, selectedEntryId, recentEven
|
|
|
38
41
|
const chainEntries = useMemo(() => buildChainEntries(allEntries), [allEntries]);
|
|
39
42
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [chainEntries.length > 0 && (_jsxs(Box, { marginBottom: 1, gap: 0, children: [_jsx(Text, { dimColor: true, children: "main" }), chainEntries.map((entry) => {
|
|
40
43
|
const ci = ciStatusIcon(entry);
|
|
41
|
-
return (_jsxs(Box, { gap: 0, children: [_jsx(Text, { dimColor: true, children: " \u2500 " }), _jsxs(Text, { bold: true, children: ["#", entry.prNumber] }), _jsx(Text, { color: ci.color, children:
|
|
44
|
+
return (_jsxs(Box, { gap: 0, children: [_jsx(Text, { dimColor: true, children: " \u2500 " }), _jsxs(Text, { bold: true, children: ["#", entry.prNumber] }), _jsx(Text, { color: ci.color, children: ci.icon })] }, entry.id));
|
|
42
45
|
})] })), queueBlock && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", queueBlockLabel ?? "waiting for main checks", queueBlock.baseSha ? ` at ${truncate(queueBlock.baseSha, 10)}` : "", "."] }), _jsx(Text, { dimColor: true, children: queueBlock.missingRequiredChecks.length > 0
|
|
43
46
|
? `Head PR #${queueBlock.headPrNumber ?? "?"} is waiting for an operator fix on ${queueBlock.baseBranch}.`
|
|
44
47
|
: `Head PR #${queueBlock.headPrNumber ?? "?"} will resume automatically once main is healthy.` })] })), 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, 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}`))))] })] }));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildChainEntries, buildDisplayEntries } from "./display-filter.js";
|
|
2
|
-
import { formatDuration, humanStatus, nextStepLabel, relativeTime, summarizeQueueBlock } from "./format.js";
|
|
2
|
+
import { formatDuration, humanStatus, nextStepLabel, postMergeStatusLine, relativeTime, summarizeQueueBlock } from "./format.js";
|
|
3
3
|
const RECENT_EVICTION_MS = 60 * 60 * 1_000;
|
|
4
4
|
const STUCK_ENTRY_MS = 25 * 60 * 1_000;
|
|
5
5
|
const OFFLINE_STALE_MS = 10 * 1_000;
|
|
@@ -285,6 +285,10 @@ export function describeEntry(entry, options) {
|
|
|
285
285
|
if (options.isHead && options.queueBlockSummary) {
|
|
286
286
|
return options.queueBlockSummary;
|
|
287
287
|
}
|
|
288
|
+
if (entry.status === "merged") {
|
|
289
|
+
return `Landed on main. ${postMergeStatusLine(entry)}`
|
|
290
|
+
+ (entry.postMergeCheckedAt ? ` (${relativeTime(entry.postMergeCheckedAt)} ago).` : ".");
|
|
291
|
+
}
|
|
288
292
|
if (entry.status === "queued") {
|
|
289
293
|
if (entry.position > 1) {
|
|
290
294
|
return `Waiting behind ${entry.position - 1} earlier PRs.`;
|
package/dist/watch/format.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckResult, QueueBlockState, QueueEntryStatus, QueueEventRecord, QueueEventSummary, QueueRuntimeStatus } from "../types.ts";
|
|
1
|
+
import type { CheckResult, PostMergeStatus, QueueBlockState, QueueEntryStatus, QueueEventRecord, QueueEventSummary, QueueRuntimeStatus } from "../types.ts";
|
|
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;
|
|
@@ -32,11 +32,17 @@ export declare function formatEventNarrative(event: QueueEventRecord | QueueEven
|
|
|
32
32
|
export declare function formatDuration(ms: number): string;
|
|
33
33
|
export declare function progressBar(current: number, total: number, width: number): string;
|
|
34
34
|
export declare function truncate(value: string, width: number): string;
|
|
35
|
+
export declare function postMergeLabel(status: PostMergeStatus | null | undefined): string;
|
|
36
|
+
export declare function postMergeStatusLine(entry: {
|
|
37
|
+
postMergeStatus?: PostMergeStatus | null;
|
|
38
|
+
postMergeSummary?: string | null;
|
|
39
|
+
}): string;
|
|
35
40
|
export declare function summarizeCheckNames(checks: CheckResult[], limit?: number): string;
|
|
36
41
|
/** CI status icon for the spec chain summary line. */
|
|
37
42
|
export declare function ciStatusIcon(entry: {
|
|
38
43
|
status: QueueEntryStatus;
|
|
39
44
|
ciRunId: string | null;
|
|
45
|
+
postMergeStatus?: PostMergeStatus | null;
|
|
40
46
|
}): {
|
|
41
47
|
icon: string;
|
|
42
48
|
color: string;
|
package/dist/watch/format.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
const QUEUE_SYMBOLS = {
|
|
2
|
+
inProgress: "\u25cf",
|
|
3
|
+
checkPassed: "\u2713",
|
|
4
|
+
checkFailed: "\u2717",
|
|
5
|
+
checkPending: "\u25cf",
|
|
6
|
+
checkUnknown: "\u25cb",
|
|
7
|
+
};
|
|
1
8
|
export function isPendingMainVerification(block) {
|
|
2
9
|
return Boolean(block
|
|
3
10
|
&& block.failingChecks.length === 0
|
|
@@ -211,6 +218,40 @@ export function truncate(value, width) {
|
|
|
211
218
|
return value.slice(0, width);
|
|
212
219
|
return `${value.slice(0, width - 1)}…`;
|
|
213
220
|
}
|
|
221
|
+
function postMergeSymbol(status) {
|
|
222
|
+
if (status === "pass")
|
|
223
|
+
return QUEUE_SYMBOLS.checkPassed;
|
|
224
|
+
if (status === "fail")
|
|
225
|
+
return QUEUE_SYMBOLS.checkFailed;
|
|
226
|
+
if (status === "pending")
|
|
227
|
+
return QUEUE_SYMBOLS.checkPending;
|
|
228
|
+
return QUEUE_SYMBOLS.checkUnknown;
|
|
229
|
+
}
|
|
230
|
+
function postMergeColor(status) {
|
|
231
|
+
if (status === "pass")
|
|
232
|
+
return "green";
|
|
233
|
+
if (status === "fail")
|
|
234
|
+
return "red";
|
|
235
|
+
if (status === "pending")
|
|
236
|
+
return "yellow";
|
|
237
|
+
return "gray";
|
|
238
|
+
}
|
|
239
|
+
export function postMergeLabel(status) {
|
|
240
|
+
if (status === "pass")
|
|
241
|
+
return "checks passed";
|
|
242
|
+
if (status === "fail")
|
|
243
|
+
return "checks failed";
|
|
244
|
+
if (status === "pending")
|
|
245
|
+
return "checks running";
|
|
246
|
+
return "checks unknown";
|
|
247
|
+
}
|
|
248
|
+
export function postMergeStatusLine(entry) {
|
|
249
|
+
const status = postMergeLabel(entry.postMergeStatus);
|
|
250
|
+
if (!entry.postMergeSummary) {
|
|
251
|
+
return status;
|
|
252
|
+
}
|
|
253
|
+
return `${status}: ${entry.postMergeSummary}`;
|
|
254
|
+
}
|
|
214
255
|
export function summarizeCheckNames(checks, limit = 3) {
|
|
215
256
|
const names = [...new Set(checks.map((check) => check.name))];
|
|
216
257
|
if (names.length === 0)
|
|
@@ -222,17 +263,21 @@ export function summarizeCheckNames(checks, limit = 3) {
|
|
|
222
263
|
/** CI status icon for the spec chain summary line. */
|
|
223
264
|
export function ciStatusIcon(entry) {
|
|
224
265
|
switch (entry.status) {
|
|
225
|
-
case "merged":
|
|
266
|
+
case "merged":
|
|
267
|
+
return {
|
|
268
|
+
icon: postMergeSymbol(entry.postMergeStatus),
|
|
269
|
+
color: postMergeColor(entry.postMergeStatus),
|
|
270
|
+
};
|
|
226
271
|
case "merging": return { icon: "\u2713", color: "cyan" }; // ✓
|
|
227
272
|
case "evicted": return { icon: "\u2717", color: "red" }; // ✗
|
|
228
273
|
case "dequeued": return { icon: "\u2012", color: "gray" }; // ‒
|
|
229
274
|
case "validating":
|
|
230
|
-
return entry.ciRunId ? { icon:
|
|
275
|
+
return entry.ciRunId ? { icon: QUEUE_SYMBOLS.inProgress, color: "cyan" } : { icon: QUEUE_SYMBOLS.checkUnknown, color: "gray" };
|
|
231
276
|
case "preparing_head":
|
|
232
|
-
return { icon:
|
|
277
|
+
return { icon: QUEUE_SYMBOLS.checkUnknown, color: "yellow" };
|
|
233
278
|
case "queued":
|
|
234
279
|
default:
|
|
235
|
-
return { icon:
|
|
280
|
+
return { icon: QUEUE_SYMBOLS.checkUnknown, color: "gray" };
|
|
236
281
|
}
|
|
237
282
|
}
|
|
238
283
|
export function summarizeQueueBlock(block) {
|