merge-steward 0.19.2 → 0.21.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/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/db/sqlite-store.js +4 -4
- package/dist/http-multi.js +1 -0
- package/dist/invalidation.d.ts +2 -2
- package/dist/invalidation.js +6 -3
- package/dist/reconciler-evict.js +1 -1
- package/dist/reconciler-merge.js +1 -1
- package/dist/reconciler-prepare.js +2 -1
- package/dist/service-queue.js +13 -2
- package/dist/watch/OverviewView.js +2 -1
- package/dist/watch/ProjectDetailView.js +5 -2
- package/dist/watch/dashboard-model.d.ts +1 -0
- package/dist/watch/dashboard-model.js +1 -1
- package/dist/watch/format.d.ts +7 -1
- package/dist/watch/format.js +8 -1
- package/dist/webhook-handler.d.ts +1 -0
- package/dist/webhook-handler.js +2 -2
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare const stewardConfigSchema: z.ZodObject<{
|
|
|
28
28
|
}>>;
|
|
29
29
|
}, z.core.$strip>>;
|
|
30
30
|
admissionLabel: z.ZodDefault<z.ZodString>;
|
|
31
|
+
priorityQueueLabel: z.ZodDefault<z.ZodString>;
|
|
31
32
|
mergeQueueCheckName: z.ZodDefault<z.ZodString>;
|
|
32
33
|
excludeBranches: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
33
34
|
autoResolvePatterns: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
package/dist/config.js
CHANGED
|
@@ -29,6 +29,7 @@ export const stewardConfigSchema = z.object({
|
|
|
29
29
|
level: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
30
30
|
}).default({ level: "info" }),
|
|
31
31
|
admissionLabel: z.string().default("queue"),
|
|
32
|
+
priorityQueueLabel: z.string().default("queue:priority"),
|
|
32
33
|
mergeQueueCheckName: z.string().default(DEFAULT_MERGE_QUEUE_CHECK_NAME),
|
|
33
34
|
/** Branch name patterns to exclude from admission (glob-style). */
|
|
34
35
|
excludeBranches: z.array(z.string()).default([]),
|
package/dist/db/sqlite-store.js
CHANGED
|
@@ -95,7 +95,7 @@ export class SqliteStore {
|
|
|
95
95
|
getHead(repoId) {
|
|
96
96
|
const row = this.conn.prepare(`SELECT * FROM queue_entries
|
|
97
97
|
WHERE repo_id = ? AND status NOT IN (${NOT_TERMINAL_SQL})
|
|
98
|
-
ORDER BY position ASC LIMIT 1`).get(repoId, ...TERMINAL_STATUSES);
|
|
98
|
+
ORDER BY priority DESC, position ASC LIMIT 1`).get(repoId, ...TERMINAL_STATUSES);
|
|
99
99
|
return row ? mapEntry(row) : undefined;
|
|
100
100
|
}
|
|
101
101
|
getEntry(entryId) {
|
|
@@ -105,18 +105,18 @@ export class SqliteStore {
|
|
|
105
105
|
getEntryByPR(repoId, prNumber) {
|
|
106
106
|
const row = this.conn.prepare(`SELECT * FROM queue_entries
|
|
107
107
|
WHERE repo_id = ? AND pr_number = ? AND status NOT IN (${NOT_TERMINAL_SQL})
|
|
108
|
-
ORDER BY position ASC
|
|
108
|
+
ORDER BY priority DESC, position ASC
|
|
109
109
|
LIMIT 1`).get(repoId, prNumber, ...TERMINAL_STATUSES);
|
|
110
110
|
return row ? mapEntry(row) : undefined;
|
|
111
111
|
}
|
|
112
112
|
listActive(repoId) {
|
|
113
113
|
const rows = this.conn.prepare(`SELECT * FROM queue_entries
|
|
114
114
|
WHERE repo_id = ? AND status NOT IN (${NOT_TERMINAL_SQL})
|
|
115
|
-
ORDER BY position ASC`).all(repoId, ...TERMINAL_STATUSES);
|
|
115
|
+
ORDER BY priority DESC, position ASC`).all(repoId, ...TERMINAL_STATUSES);
|
|
116
116
|
return rows.map(mapEntry);
|
|
117
117
|
}
|
|
118
118
|
listAll(repoId) {
|
|
119
|
-
const rows = this.conn.prepare("SELECT * FROM queue_entries WHERE repo_id = ? ORDER BY position ASC").all(repoId);
|
|
119
|
+
const rows = this.conn.prepare("SELECT * FROM queue_entries WHERE repo_id = ? ORDER BY priority DESC, position ASC").all(repoId);
|
|
120
120
|
return rows.map(mapEntry);
|
|
121
121
|
}
|
|
122
122
|
insert(entry) {
|
package/dist/http-multi.js
CHANGED
|
@@ -116,6 +116,7 @@ export async function buildMultiRepoHttpServer(options) {
|
|
|
116
116
|
}
|
|
117
117
|
await processWebhookEvent(event, record.instance.service, {
|
|
118
118
|
admissionLabel: record.config.admissionLabel,
|
|
119
|
+
priorityQueueLabel: record.config.priorityQueueLabel,
|
|
119
120
|
baseBranch: record.config.baseBranch,
|
|
120
121
|
repoFullName: record.config.repoFullName,
|
|
121
122
|
github: record.instance.service.githubApi,
|
package/dist/invalidation.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export declare const INVALIDATION_PATCH: {
|
|
|
9
9
|
readonly retryAttempts: 0;
|
|
10
10
|
readonly lastFailedBaseSha: null;
|
|
11
11
|
};
|
|
12
|
-
/** Select non-terminal entries after `
|
|
13
|
-
export declare function selectDownstream(allActive: QueueEntry[],
|
|
12
|
+
/** Select non-terminal entries after `entryId` from an already ordered active queue list. */
|
|
13
|
+
export declare function selectDownstream(allActive: QueueEntry[], entryId: string): QueueEntry[];
|
package/dist/invalidation.js
CHANGED
|
@@ -9,7 +9,10 @@ export const INVALIDATION_PATCH = {
|
|
|
9
9
|
retryAttempts: 0,
|
|
10
10
|
lastFailedBaseSha: null,
|
|
11
11
|
};
|
|
12
|
-
/** Select non-terminal entries after `
|
|
13
|
-
export function selectDownstream(allActive,
|
|
14
|
-
|
|
12
|
+
/** Select non-terminal entries after `entryId` from an already ordered active queue list. */
|
|
13
|
+
export function selectDownstream(allActive, entryId) {
|
|
14
|
+
const index = allActive.findIndex((entry) => entry.id === entryId);
|
|
15
|
+
if (index < 0)
|
|
16
|
+
return [];
|
|
17
|
+
return allActive.slice(index + 1).filter((entry) => !TERMINAL_STATUSES.includes(entry.status));
|
|
15
18
|
}
|
package/dist/reconciler-evict.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function cleanupSpec(ctx, entry) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
export async function invalidateDownstream(ctx, allActive, afterIndex) {
|
|
13
|
-
const targets = selectDownstream(allActive, allActive[afterIndex].
|
|
13
|
+
const targets = selectDownstream(allActive, allActive[afterIndex].id);
|
|
14
14
|
for (const downstream of targets) {
|
|
15
15
|
emit(ctx, downstream, "invalidated", { detail: `base changed after position ${afterIndex}` });
|
|
16
16
|
await cleanupSpec(ctx, downstream);
|
package/dist/reconciler-merge.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function mergeHead(ctx, entry) {
|
|
|
46
46
|
catch {
|
|
47
47
|
// Can't verify — proceed and let push fail if needed.
|
|
48
48
|
}
|
|
49
|
-
if (ctx.ci.getMainStatus) {
|
|
49
|
+
if (ctx.ci.getMainStatus && entry.priority <= 0) {
|
|
50
50
|
const mainStatus = await ctx.ci.getMainStatus(ctx.baseBranch);
|
|
51
51
|
if (mainStatus === "pending") {
|
|
52
52
|
// Main is still verifying itself (typically post-merge CI from the
|
|
@@ -44,7 +44,8 @@ export async function prepareEntry(ctx, entry, isHead, prevEntry) {
|
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
if (isHead) {
|
|
47
|
-
|
|
47
|
+
const bypassMainBlock = entry.priority > 0;
|
|
48
|
+
if (ctx.ci.getMainStatus && !bypassMainBlock) {
|
|
48
49
|
const mainStatus = await ctx.ci.getMainStatus(ctx.baseBranch);
|
|
49
50
|
if (mainStatus !== "pass") {
|
|
50
51
|
let mainChecks = [];
|
package/dist/service-queue.js
CHANGED
|
@@ -79,6 +79,9 @@ export class MergeStewardQueueCommands {
|
|
|
79
79
|
throw error;
|
|
80
80
|
}
|
|
81
81
|
this.logger.info({ prNumber: params.prNumber, entryId: entry.id }, "PR enqueued");
|
|
82
|
+
if (entry.priority > 0) {
|
|
83
|
+
this.invalidateDownstreamOf(entry);
|
|
84
|
+
}
|
|
82
85
|
return entry;
|
|
83
86
|
}
|
|
84
87
|
async scanStartupAdmissions() {
|
|
@@ -123,6 +126,8 @@ export class MergeStewardQueueCommands {
|
|
|
123
126
|
this.logger.debug({ prNumber, reviewDecision: status.reviewDecision }, "PR review gate is not satisfied, skipping admission");
|
|
124
127
|
return false;
|
|
125
128
|
}
|
|
129
|
+
const labels = await this.github.listLabels(prNumber);
|
|
130
|
+
const priority = labels.includes(this.config.priorityQueueLabel) ? 1 : 0;
|
|
126
131
|
const checks = await this.github.listChecks(prNumber);
|
|
127
132
|
const requiredChecks = this.policy.getRequiredChecks();
|
|
128
133
|
if (requiredChecks.length > 0) {
|
|
@@ -162,7 +167,13 @@ export class MergeStewardQueueCommands {
|
|
|
162
167
|
return false;
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
|
-
this.enqueue({
|
|
170
|
+
this.enqueue({
|
|
171
|
+
prNumber,
|
|
172
|
+
branch,
|
|
173
|
+
headSha,
|
|
174
|
+
priority,
|
|
175
|
+
...(status.title ? { prTitle: status.title } : {}),
|
|
176
|
+
});
|
|
166
177
|
return true;
|
|
167
178
|
}
|
|
168
179
|
catch (error) {
|
|
@@ -204,7 +215,7 @@ export class MergeStewardQueueCommands {
|
|
|
204
215
|
}
|
|
205
216
|
invalidateDownstreamOf(removedEntry) {
|
|
206
217
|
const allActive = this.store.listActive(this.config.repoId);
|
|
207
|
-
const targets = selectDownstream(allActive, removedEntry.
|
|
218
|
+
const targets = selectDownstream(allActive, removedEntry.id);
|
|
208
219
|
for (const downstream of targets) {
|
|
209
220
|
if (downstream.specBranch) {
|
|
210
221
|
this.specBuilder.deleteSpeculative(downstream.specBranch).catch(() => { });
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useStdout } from "ink";
|
|
3
|
+
import { formatRepoTokenText } from "./format.js";
|
|
3
4
|
function RepoTokens({ tokens, width }) {
|
|
4
5
|
if (tokens.length === 0 || width < 6)
|
|
5
6
|
return null;
|
|
6
7
|
const parts = [];
|
|
7
8
|
let used = 0;
|
|
8
9
|
for (const token of tokens) {
|
|
9
|
-
const text =
|
|
10
|
+
const text = formatRepoTokenText(token);
|
|
10
11
|
const separatorWidth = parts.length === 0 ? 0 : 2;
|
|
11
12
|
if (used + separatorWidth + text.length > width)
|
|
12
13
|
break;
|
|
@@ -2,9 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text, useStdout } from "ink";
|
|
3
3
|
import { RepoRow } from "./OverviewView.js";
|
|
4
4
|
import { clipSummary } from "./dashboard-model.js";
|
|
5
|
+
import { formatTokenAge } from "./format.js";
|
|
5
6
|
const PR_ID_WIDTH = 7;
|
|
6
7
|
const PR_PHRASE_WIDTH = 20;
|
|
7
8
|
const SUMMARY_INDENT = 9;
|
|
9
|
+
const AGE_WIDTH = 4;
|
|
8
10
|
function truncate(value, maxWidth) {
|
|
9
11
|
if (value.length <= maxWidth)
|
|
10
12
|
return value;
|
|
@@ -33,11 +35,12 @@ export function buildContentLines(repo, width) {
|
|
|
33
35
|
function EntryHeaderRow({ entry, width }) {
|
|
34
36
|
const idText = `#${entry.prNumber}`.padEnd(PR_ID_WIDTH, " ");
|
|
35
37
|
const paddedPhrase = entry.phrase.padEnd(PR_PHRASE_WIDTH, " ");
|
|
36
|
-
const
|
|
38
|
+
const age = formatTokenAge(entry.eventAt);
|
|
39
|
+
const titleSpace = Math.max(0, width - (PR_ID_WIDTH + 3 + PR_PHRASE_WIDTH + 3 + AGE_WIDTH));
|
|
37
40
|
const title = entry.title && entry.title !== entry.phrase && titleSpace >= 8
|
|
38
41
|
? truncate(entry.title, titleSpace)
|
|
39
42
|
: "";
|
|
40
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: entry.color, children: idText }), _jsx(Text, { color: entry.color, children: entry.glyph }), _jsx(Text, { children: ` ${paddedPhrase}` }), title ? _jsx(Text, { dimColor: true, children: ` ${title}` }) : null] }));
|
|
43
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: entry.color, children: idText }), _jsx(Text, { color: entry.color, children: entry.glyph }), _jsx(Text, { children: ` ${paddedPhrase}` }), _jsx(Text, { dimColor: true, children: ` ${age}` }), title ? _jsx(Text, { dimColor: true, children: ` ${title}` }) : null] }));
|
|
41
44
|
}
|
|
42
45
|
function renderLine(line, key, width) {
|
|
43
46
|
if (line.kind === "blank")
|
|
@@ -193,7 +193,7 @@ export function buildDashboard(repos, opts = {}) {
|
|
|
193
193
|
return {
|
|
194
194
|
repoId: repo.repoId,
|
|
195
195
|
repoFullName: repo.repoFullName,
|
|
196
|
-
tokens: entries.map(({ prNumber, glyph, color, kind }) => ({ prNumber, glyph, color, kind })),
|
|
196
|
+
tokens: entries.map(({ prNumber, glyph, color, kind, eventAt }) => ({ prNumber, glyph, color, kind, eventAt })),
|
|
197
197
|
entries,
|
|
198
198
|
latestActivityAt,
|
|
199
199
|
hasActivity: entries.length > 0,
|
package/dist/watch/format.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
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
3
|
export declare function shortSha(value: string | null | undefined): string;
|
|
4
|
-
export declare function relativeTime(iso: string | null | undefined): string;
|
|
4
|
+
export declare function relativeTime(iso: string | number | null | undefined): string;
|
|
5
|
+
export declare function formatTokenAge(eventAt: number): string;
|
|
6
|
+
export declare function formatRepoTokenText(token: {
|
|
7
|
+
prNumber: number;
|
|
8
|
+
glyph: string;
|
|
9
|
+
eventAt: number;
|
|
10
|
+
}): string;
|
|
5
11
|
export declare function mergeWaitState(entry: {
|
|
6
12
|
status: QueueEntryStatus;
|
|
7
13
|
waitDetail?: string | null | undefined;
|
package/dist/watch/format.js
CHANGED
|
@@ -21,7 +21,8 @@ export function relativeTime(iso) {
|
|
|
21
21
|
if (!iso) {
|
|
22
22
|
return "-";
|
|
23
23
|
}
|
|
24
|
-
const
|
|
24
|
+
const timestamp = typeof iso === "number" ? iso : new Date(iso).getTime();
|
|
25
|
+
const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000));
|
|
25
26
|
if (seconds < 5)
|
|
26
27
|
return "now";
|
|
27
28
|
if (seconds < 60)
|
|
@@ -35,6 +36,12 @@ export function relativeTime(iso) {
|
|
|
35
36
|
const days = Math.floor(hours / 24);
|
|
36
37
|
return `${days}d`;
|
|
37
38
|
}
|
|
39
|
+
export function formatTokenAge(eventAt) {
|
|
40
|
+
return relativeTime(eventAt).padStart(4, " ");
|
|
41
|
+
}
|
|
42
|
+
export function formatRepoTokenText(token) {
|
|
43
|
+
return `#${token.prNumber} ${token.glyph} ${relativeTime(token.eventAt)}`;
|
|
44
|
+
}
|
|
38
45
|
export function mergeWaitState(entry) {
|
|
39
46
|
if (!entry || entry.status !== "merging" || !entry.waitDetail) {
|
|
40
47
|
return null;
|
|
@@ -60,6 +60,7 @@ export declare function normalizeWebhook(eventType: string, payload: Record<stri
|
|
|
60
60
|
*/
|
|
61
61
|
export declare function processWebhookEvent(event: StewardWebhookEvent, service: MergeStewardService, config: {
|
|
62
62
|
admissionLabel: string;
|
|
63
|
+
priorityQueueLabel: string;
|
|
63
64
|
baseBranch: string;
|
|
64
65
|
repoFullName: string;
|
|
65
66
|
github?: GitHubPRApi;
|
package/dist/webhook-handler.js
CHANGED
|
@@ -107,9 +107,9 @@ export function normalizeWebhook(eventType, payload) {
|
|
|
107
107
|
export async function processWebhookEvent(event, service, config, logger) {
|
|
108
108
|
switch (event.type) {
|
|
109
109
|
case "pr_labeled": {
|
|
110
|
-
if (event.label !== config.admissionLabel)
|
|
110
|
+
if (event.label !== config.admissionLabel && event.label !== config.priorityQueueLabel)
|
|
111
111
|
return;
|
|
112
|
-
logger.info({ prNumber: event.prNumber }, "
|
|
112
|
+
logger.info({ prNumber: event.prNumber, label: event.label }, "Queue label added, checking eligibility");
|
|
113
113
|
await service.tryAdmit(event.prNumber, event.branch, event.headSha);
|
|
114
114
|
break;
|
|
115
115
|
}
|