merge-steward 0.21.0 → 0.22.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/sqlite-store.d.ts +1 -0
- package/dist/db/sqlite-store.js +15 -0
- package/dist/reconciler-main-status.d.ts +8 -0
- package/dist/reconciler-main-status.js +17 -0
- package/dist/reconciler-merge.js +13 -4
- package/dist/reconciler-prepare.js +9 -2
- package/dist/service-queue.d.ts +3 -0
- package/dist/service-queue.js +41 -0
- package/dist/service.d.ts +1 -0
- package/dist/service.js +3 -0
- package/dist/store.d.ts +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/watch/QueueListView.js +7 -6
- package/dist/watch/dashboard-model.js +2 -2
- package/dist/watch/format.d.ts +4 -0
- package/dist/watch/format.js +9 -0
- package/dist/watch/state-visualization.js +2 -1
- package/dist/webhook-handler.js +7 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ export declare class SqliteStore implements QueueStore {
|
|
|
15
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
|
+
updatePriority(entryId: string, priority: number, detail?: string): void;
|
|
18
19
|
insertIncident(incident: IncidentRecord): void;
|
|
19
20
|
listIncidents(entryId: string): IncidentRecord[];
|
|
20
21
|
getIncident(incidentId: string): IncidentRecord | undefined;
|
package/dist/db/sqlite-store.js
CHANGED
|
@@ -227,6 +227,21 @@ export class SqliteStore {
|
|
|
227
227
|
this.writeEvent(entryId, from, "queued", `updateHead: generation ${newGen}`);
|
|
228
228
|
})();
|
|
229
229
|
}
|
|
230
|
+
updatePriority(entryId, priority, detail) {
|
|
231
|
+
this.conn.transaction(() => {
|
|
232
|
+
const current = this.conn.prepare("SELECT status, priority FROM queue_entries WHERE id = ?").get(entryId);
|
|
233
|
+
if (!current)
|
|
234
|
+
return;
|
|
235
|
+
const from = String(current.status);
|
|
236
|
+
if (Number(current.priority) === priority)
|
|
237
|
+
return;
|
|
238
|
+
this.conn.prepare(`UPDATE queue_entries SET
|
|
239
|
+
priority = ?,
|
|
240
|
+
updated_at = ?
|
|
241
|
+
WHERE id = ?`).run(priority, isoNow(), entryId);
|
|
242
|
+
this.writeEvent(entryId, from, from, detail ?? `priority updated to ${priority}`);
|
|
243
|
+
})();
|
|
244
|
+
}
|
|
230
245
|
insertIncident(incident) {
|
|
231
246
|
this.conn.prepare(`INSERT INTO queue_incidents (id, entry_id, at, failure_class, context_json, outcome)
|
|
232
247
|
VALUES (?, ?, ?, ?, ?, ?)`).run(incident.id, incident.entryId, incident.at, incident.failureClass, JSON.stringify(incident.context), incident.outcome);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CIStatus } from "./types.ts";
|
|
2
|
+
import type { ReconcileContext } from "./reconciler-core.ts";
|
|
3
|
+
export interface EffectiveMainStatus {
|
|
4
|
+
status: CIStatus;
|
|
5
|
+
trustedMergedEntryId?: string | undefined;
|
|
6
|
+
trustedMergedPrNumber?: number | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare function getEffectiveMainStatus(ctx: ReconcileContext, baseSha: string): Promise<EffectiveMainStatus>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export async function getEffectiveMainStatus(ctx, baseSha) {
|
|
2
|
+
const status = await ctx.ci.getMainStatus?.(ctx.baseBranch) ?? "pass";
|
|
3
|
+
if (status !== "pending") {
|
|
4
|
+
return { status };
|
|
5
|
+
}
|
|
6
|
+
const trustedMergedEntry = ctx.store.listAll(ctx.repoId).find((entry) => entry.status === "merged"
|
|
7
|
+
&& entry.postMergeStatus === "pass"
|
|
8
|
+
&& entry.postMergeSha === baseSha);
|
|
9
|
+
if (!trustedMergedEntry) {
|
|
10
|
+
return { status };
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
status: "pass",
|
|
14
|
+
trustedMergedEntryId: trustedMergedEntry.id,
|
|
15
|
+
trustedMergedPrNumber: trustedMergedEntry.prNumber,
|
|
16
|
+
};
|
|
17
|
+
}
|
package/dist/reconciler-merge.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
3
|
import { verifyPostMergeStatus } from "./reconciler-post-merge.js";
|
|
4
|
+
import { getEffectiveMainStatus } from "./reconciler-main-status.js";
|
|
4
5
|
export async function mergeHead(ctx, entry) {
|
|
5
6
|
emit(ctx, entry, "merge_revalidating");
|
|
6
7
|
const prStatus = await ctx.github.getStatus(entry.prNumber);
|
|
@@ -31,9 +32,10 @@ export async function mergeHead(ctx, entry) {
|
|
|
31
32
|
ctx.store.transition(entry.id, "preparing_head", { ...CLEAN_CI, ...CLEAN_SPEC }, "no spec branch, re-prepare");
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
35
|
+
let currentBase = null;
|
|
34
36
|
try {
|
|
35
37
|
await ctx.git.fetch();
|
|
36
|
-
|
|
38
|
+
currentBase = await ctx.git.headSha(ref(ctx, ctx.baseBranch));
|
|
37
39
|
const isFF = await ctx.git.isAncestor(currentBase, entry.specSha);
|
|
38
40
|
if (!isFF) {
|
|
39
41
|
emit(ctx, entry, "branch_mismatch", { detail: `spec is not a fast-forward from main (${currentBase.slice(0, 8)})` });
|
|
@@ -47,8 +49,15 @@ export async function mergeHead(ctx, entry) {
|
|
|
47
49
|
// Can't verify — proceed and let push fail if needed.
|
|
48
50
|
}
|
|
49
51
|
if (ctx.ci.getMainStatus && entry.priority <= 0) {
|
|
50
|
-
const mainStatus =
|
|
51
|
-
|
|
52
|
+
const mainStatus = currentBase
|
|
53
|
+
? await getEffectiveMainStatus(ctx, currentBase)
|
|
54
|
+
: { status: await ctx.ci.getMainStatus(ctx.baseBranch) };
|
|
55
|
+
if (mainStatus.trustedMergedEntryId) {
|
|
56
|
+
emit(ctx, entry, "main_pending_bypassed", {
|
|
57
|
+
detail: `main checks pending for already-validated merge of PR #${mainStatus.trustedMergedPrNumber}`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (mainStatus.status === "pending") {
|
|
52
61
|
// Main is still verifying itself (typically post-merge CI from the
|
|
53
62
|
// previous entry). The spec was built on top of main-as-it-was
|
|
54
63
|
// (isFF check above guarantees main hasn't diverged) and already
|
|
@@ -59,7 +68,7 @@ export async function mergeHead(ctx, entry) {
|
|
|
59
68
|
ctx.store.transition(entry.id, "merging", { waitDetail: "main checks still pending, holding merge" }, "main checks still pending, holding merge");
|
|
60
69
|
return;
|
|
61
70
|
}
|
|
62
|
-
if (mainStatus === "fail") {
|
|
71
|
+
if (mainStatus.status === "fail") {
|
|
63
72
|
try {
|
|
64
73
|
const refresh = await ctx.policy.refreshOnIssue("main_unhealthy_at_merge");
|
|
65
74
|
if (refresh.attempted && refresh.changed) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CLEAN_CI, CLEAN_SPEC, emit, isBudgetExhausted, isRetryGated, ref, specBranchName } from "./reconciler-core.js";
|
|
2
2
|
import { evictEntry } from "./reconciler-evict.js";
|
|
3
|
+
import { getEffectiveMainStatus } from "./reconciler-main-status.js";
|
|
3
4
|
function summarizeCheckNames(checks, limit = 3) {
|
|
4
5
|
const names = [...new Set(checks.map((check) => check.name))];
|
|
5
6
|
if (names.length <= limit) {
|
|
@@ -46,8 +47,14 @@ export async function prepareEntry(ctx, entry, isHead, prevEntry) {
|
|
|
46
47
|
if (isHead) {
|
|
47
48
|
const bypassMainBlock = entry.priority > 0;
|
|
48
49
|
if (ctx.ci.getMainStatus && !bypassMainBlock) {
|
|
49
|
-
const mainStatus = await
|
|
50
|
-
if (mainStatus
|
|
50
|
+
const mainStatus = await getEffectiveMainStatus(ctx, baseSha);
|
|
51
|
+
if (mainStatus.trustedMergedEntryId) {
|
|
52
|
+
emit(ctx, entry, "main_pending_bypassed", {
|
|
53
|
+
baseSha,
|
|
54
|
+
detail: `main checks pending for already-validated merge of PR #${mainStatus.trustedMergedPrNumber}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (mainStatus.status !== "pass") {
|
|
51
58
|
let mainChecks = [];
|
|
52
59
|
try {
|
|
53
60
|
mainChecks = await ctx.github.listChecksForRef(ref(ctx, ctx.baseBranch));
|
package/dist/service-queue.d.ts
CHANGED
|
@@ -25,10 +25,13 @@ export declare class MergeStewardQueueCommands {
|
|
|
25
25
|
scanned: number;
|
|
26
26
|
admitted: number;
|
|
27
27
|
}>;
|
|
28
|
+
updatePriorityByPR(prNumber: number, priority: number): boolean;
|
|
28
29
|
tryAdmit(prNumber: number, branch: string, headSha: string): Promise<boolean>;
|
|
29
30
|
dequeueByPR(prNumber: number): void;
|
|
30
31
|
updateHeadByPR(prNumber: number, headSha: string): void;
|
|
31
32
|
acknowledgeExternalMerge(prNumber: number): void;
|
|
32
33
|
private invalidateDownstreamOf;
|
|
34
|
+
private findAffectedEntriesAfterPriorityChange;
|
|
35
|
+
private requeueAffectedEntries;
|
|
33
36
|
private nextPosition;
|
|
34
37
|
}
|
package/dist/service-queue.js
CHANGED
|
@@ -105,6 +105,22 @@ export class MergeStewardQueueCommands {
|
|
|
105
105
|
}
|
|
106
106
|
return { scanned: open.length, admitted };
|
|
107
107
|
}
|
|
108
|
+
updatePriorityByPR(prNumber, priority) {
|
|
109
|
+
const entry = this.store.getEntryByPR(this.config.repoId, prNumber);
|
|
110
|
+
if (!entry) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (entry.priority === priority) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const before = this.store.listActive(this.config.repoId);
|
|
117
|
+
this.store.updatePriority(entry.id, priority, `priority lane ${priority > 0 ? "enabled" : "disabled"}`);
|
|
118
|
+
const after = this.store.listActive(this.config.repoId);
|
|
119
|
+
const affected = this.findAffectedEntriesAfterPriorityChange(before, after);
|
|
120
|
+
this.requeueAffectedEntries(affected, `priority changed for entry ${entry.id.slice(0, 8)}`);
|
|
121
|
+
this.logger.info({ prNumber, entryId: entry.id, priority }, "Updated queued PR priority");
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
108
124
|
async tryAdmit(prNumber, branch, headSha) {
|
|
109
125
|
if (this.config.excludeBranches.some((pattern) => matchGlob(pattern, branch))) {
|
|
110
126
|
this.logger.debug({ prNumber, branch }, "Branch excluded from admission");
|
|
@@ -226,6 +242,31 @@ export class MergeStewardQueueCommands {
|
|
|
226
242
|
this.logger.info({ removedEntryId: removedEntry.id, invalidated: targets.length }, "Invalidated downstream entries after dequeue");
|
|
227
243
|
}
|
|
228
244
|
}
|
|
245
|
+
findAffectedEntriesAfterPriorityChange(before, after) {
|
|
246
|
+
const maxLength = Math.max(before.length, after.length);
|
|
247
|
+
let firstChangedIndex = -1;
|
|
248
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
249
|
+
if (before[index]?.id !== after[index]?.id) {
|
|
250
|
+
firstChangedIndex = index;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (firstChangedIndex < 0) {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
return after.slice(firstChangedIndex);
|
|
258
|
+
}
|
|
259
|
+
requeueAffectedEntries(entries, reason) {
|
|
260
|
+
for (const affected of entries) {
|
|
261
|
+
if (affected.specBranch) {
|
|
262
|
+
this.specBuilder.deleteSpeculative(affected.specBranch).catch(() => { });
|
|
263
|
+
}
|
|
264
|
+
this.store.transition(affected.id, "queued", INVALIDATION_PATCH, reason);
|
|
265
|
+
}
|
|
266
|
+
if (entries.length > 0) {
|
|
267
|
+
this.logger.info({ affectedEntries: entries.length, reason }, "Requeued affected entries after priority change");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
229
270
|
nextPosition() {
|
|
230
271
|
const existing = this.store.listAll(this.config.repoId);
|
|
231
272
|
let next = 1;
|
package/dist/service.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare class MergeStewardService {
|
|
|
39
39
|
updateEntryHead(entryId: string, headSha: string): boolean;
|
|
40
40
|
dequeueByPR(prNumber: number): void;
|
|
41
41
|
updateHeadByPR(prNumber: number, headSha: string): void;
|
|
42
|
+
updatePriorityByPR(prNumber: number, priority: number): boolean;
|
|
42
43
|
acknowledgeExternalMerge(prNumber: number): void;
|
|
43
44
|
getStatus(): QueueEntry[];
|
|
44
45
|
getIncident(incidentId: string): IncidentRecord | undefined;
|
package/dist/service.js
CHANGED
|
@@ -73,6 +73,9 @@ export class MergeStewardService {
|
|
|
73
73
|
updateHeadByPR(prNumber, headSha) {
|
|
74
74
|
this.queueCommands.updateHeadByPR(prNumber, headSha);
|
|
75
75
|
}
|
|
76
|
+
updatePriorityByPR(prNumber, priority) {
|
|
77
|
+
return this.queueCommands.updatePriorityByPR(prNumber, priority);
|
|
78
|
+
}
|
|
76
79
|
acknowledgeExternalMerge(prNumber) {
|
|
77
80
|
this.queueCommands.acknowledgeExternalMerge(prNumber);
|
|
78
81
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface QueueStore {
|
|
|
14
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
|
+
updatePriority(entryId: string, priority: number, detail?: string): void;
|
|
17
18
|
insertIncident(incident: IncidentRecord): void;
|
|
18
19
|
listIncidents(entryId: string): IncidentRecord[];
|
|
19
20
|
getIncident(incidentId: string): IncidentRecord | undefined;
|
package/dist/types.d.ts
CHANGED
|
@@ -196,7 +196,7 @@ export interface QueueConfig {
|
|
|
196
196
|
flakyRetries: number;
|
|
197
197
|
pollIntervalMs: number;
|
|
198
198
|
}
|
|
199
|
-
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" | "merge_waiting_main" | "sanitized_closed" | "sanitized_duplicate" | "post_merge_verification_started" | "post_merge_verification_completed" | "branch_unreachable";
|
|
199
|
+
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" | "merge_waiting_main" | "main_pending_bypassed" | "sanitized_closed" | "sanitized_duplicate" | "post_merge_verification_started" | "post_merge_verification_completed" | "branch_unreachable";
|
|
200
200
|
export interface ReconcileEvent {
|
|
201
201
|
at: string;
|
|
202
202
|
entryId: string;
|
|
@@ -3,7 +3,7 @@ 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, postMergeStatusLine, relativeTime, statusColor, summarizeQueueBlock, truncate } from "./format.js";
|
|
6
|
+
import { ciStatusIcon, formatEventSummary, humanStatus, isPendingMainVerification, nextStepLabel, postMergeStatusLine, queueBlockMatchesEntry, 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);
|
|
@@ -15,7 +15,7 @@ function QueueRow({ entry, selected, isHead, queueBlock, }) {
|
|
|
15
15
|
: "";
|
|
16
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}` })] }));
|
|
17
17
|
}
|
|
18
|
-
const blockedOnMain = isHead && queueBlock
|
|
18
|
+
const blockedOnMain = isHead && queueBlockMatchesEntry(queueBlock, entry);
|
|
19
19
|
const pendingMainVerification = blockedOnMain && isPendingMainVerification(queueBlock);
|
|
20
20
|
const status = blockedOnMain
|
|
21
21
|
? pendingMainVerification ? "verifying main" : "waiting for main"
|
|
@@ -36,13 +36,14 @@ export function QueueListView({ entries, allEntries, selectedEntryId, recentEven
|
|
|
36
36
|
// All entries are 1 row now.
|
|
37
37
|
const eventRows = Math.min(8, Math.max(4, rows - entries.length - CHROME_ROWS));
|
|
38
38
|
const displayedEvents = useMemo(() => recentEvents.slice(-eventRows), [eventRows, recentEvents]);
|
|
39
|
-
const
|
|
39
|
+
const effectiveQueueBlock = queueBlock && queueBlock.entryId === headEntryId ? queueBlock : null;
|
|
40
|
+
const queueBlockLabel = summarizeQueueBlock(effectiveQueueBlock);
|
|
40
41
|
// Chain header always shows the live queue, regardless of display filter.
|
|
41
42
|
const chainEntries = useMemo(() => buildChainEntries(allEntries), [allEntries]);
|
|
42
43
|
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) => {
|
|
43
44
|
const ci = ciStatusIcon(entry);
|
|
44
45
|
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));
|
|
45
|
-
})] })),
|
|
46
|
-
? `Head PR #${
|
|
47
|
-
: `Head PR #${
|
|
46
|
+
})] })), effectiveQueueBlock && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", queueBlockLabel ?? "waiting for main checks", effectiveQueueBlock.baseSha ? ` at ${truncate(effectiveQueueBlock.baseSha, 10)}` : "", "."] }), _jsx(Text, { dimColor: true, children: effectiveQueueBlock.missingRequiredChecks.length > 0
|
|
47
|
+
? `Head PR #${effectiveQueueBlock.headPrNumber ?? "?"} is waiting for an operator fix on ${effectiveQueueBlock.baseBranch}.`
|
|
48
|
+
: `Head PR #${effectiveQueueBlock.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: effectiveQueueBlock }, 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}`))))] })] }));
|
|
48
49
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TERMINAL_STATUSES } from "../types.js";
|
|
2
|
-
import { mergeWaitState } from "./format.js";
|
|
2
|
+
import { mergeWaitState, queueBlockMatchesEntry } from "./format.js";
|
|
3
3
|
const DECIDED_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
4
4
|
const GLYPH = {
|
|
5
5
|
running: "\u25cf",
|
|
@@ -126,7 +126,7 @@ function repoEntriesFromSnapshot(snapshot, cutoff) {
|
|
|
126
126
|
const head = latest
|
|
127
127
|
.filter((entry) => isActive(entry.status))
|
|
128
128
|
.sort((a, b) => a.position - b.position)[0] ?? null;
|
|
129
|
-
const queueBlocked =
|
|
129
|
+
const queueBlocked = queueBlockMatchesEntry(snapshot.queueBlock, head);
|
|
130
130
|
const byPr = new Map();
|
|
131
131
|
for (const entry of latest) {
|
|
132
132
|
const active = isActive(entry.status);
|
package/dist/watch/format.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
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
|
+
export declare function queueBlockMatchesEntry(block: QueueBlockState | null | undefined, entry: {
|
|
4
|
+
id: string;
|
|
5
|
+
prNumber: number;
|
|
6
|
+
} | null | undefined): block is QueueBlockState;
|
|
3
7
|
export declare function shortSha(value: string | null | undefined): string;
|
|
4
8
|
export declare function relativeTime(iso: string | number | null | undefined): string;
|
|
5
9
|
export declare function formatTokenAge(eventAt: number): string;
|
package/dist/watch/format.js
CHANGED
|
@@ -11,6 +11,15 @@ export function isPendingMainVerification(block) {
|
|
|
11
11
|
&& block.pendingChecks.length > 0
|
|
12
12
|
&& block.missingRequiredChecks.length === 0);
|
|
13
13
|
}
|
|
14
|
+
export function queueBlockMatchesEntry(block, entry) {
|
|
15
|
+
if (!block || !entry) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (block.entryId) {
|
|
19
|
+
return block.entryId === entry.id;
|
|
20
|
+
}
|
|
21
|
+
return block.headPrNumber === entry.prNumber;
|
|
22
|
+
}
|
|
14
23
|
export function shortSha(value) {
|
|
15
24
|
if (!value) {
|
|
16
25
|
return "-";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { queueBlockMatchesEntry } from "./format.js";
|
|
1
2
|
const MAIN_STATES = ["queued", "preparing_head", "validating", "merging", "merged"];
|
|
2
3
|
const EXIT_STATES = ["evicted", "dequeued"];
|
|
3
4
|
const STATE_LABELS = {
|
|
@@ -53,7 +54,7 @@ export function buildExternalRepairObservations(detail, options) {
|
|
|
53
54
|
else if (entry.status === "evicted") {
|
|
54
55
|
observations.push({ tone: "warn", text: "Removed after failed retries. Branch needs repair before re-admission." });
|
|
55
56
|
}
|
|
56
|
-
else if (options.isHead && options.queueBlock
|
|
57
|
+
else if (options.isHead && queueBlockMatchesEntry(options.queueBlock, entry)) {
|
|
57
58
|
const failingNames = options.queueBlock.failingChecks.map((check) => check.name);
|
|
58
59
|
const pendingNames = options.queueBlock.pendingChecks.map((check) => check.name);
|
|
59
60
|
const missingNames = options.queueBlock.missingRequiredChecks;
|
package/dist/webhook-handler.js
CHANGED
|
@@ -110,10 +110,17 @@ export async function processWebhookEvent(event, service, config, logger) {
|
|
|
110
110
|
if (event.label !== config.admissionLabel && event.label !== config.priorityQueueLabel)
|
|
111
111
|
return;
|
|
112
112
|
logger.info({ prNumber: event.prNumber, label: event.label }, "Queue label added, checking eligibility");
|
|
113
|
+
if (event.label === config.priorityQueueLabel && service.updatePriorityByPR(event.prNumber, 1)) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
113
116
|
await service.tryAdmit(event.prNumber, event.branch, event.headSha);
|
|
114
117
|
break;
|
|
115
118
|
}
|
|
116
119
|
case "pr_unlabeled": {
|
|
120
|
+
if (event.label === config.priorityQueueLabel) {
|
|
121
|
+
service.updatePriorityByPR(event.prNumber, 0);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
117
124
|
if (event.label !== config.admissionLabel)
|
|
118
125
|
return;
|
|
119
126
|
// Label removed — dequeue if active.
|