merge-steward 0.33.2 → 0.34.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/cli/args.js +0 -8
- package/dist/cli/commands/attach.js +4 -4
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/repos.js +1 -1
- package/dist/cli/help.js +0 -5
- package/dist/cli/system.js +1 -1
- package/dist/cli/types.d.ts +1 -1
- package/dist/cli.js +7 -14
- package/dist/db/schema.d.ts +1 -5
- package/dist/db/schema.js +54 -92
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/dist/watch/display-filter.d.ts +0 -14
- package/dist/watch/display-filter.js +0 -69
package/dist/cli/args.js
CHANGED
|
@@ -58,16 +58,8 @@ export function validateFlags(parsed) {
|
|
|
58
58
|
assertKnownFlags(parsed, "root", []);
|
|
59
59
|
return;
|
|
60
60
|
case "dashboard":
|
|
61
|
-
case "dash":
|
|
62
|
-
case "d":
|
|
63
61
|
assertKnownFlags(parsed, "root", ["repo", "pr"]);
|
|
64
62
|
return;
|
|
65
|
-
case "attach":
|
|
66
|
-
assertKnownFlags(parsed, "repos", ["base-branch", "label", "merge-queue-check-name", "refresh", "json"]);
|
|
67
|
-
return;
|
|
68
|
-
case "repos":
|
|
69
|
-
assertKnownFlags(parsed, "repos", ["json"]);
|
|
70
|
-
return;
|
|
71
63
|
case "repo":
|
|
72
64
|
switch (subcommand) {
|
|
73
65
|
case undefined:
|
|
@@ -10,7 +10,7 @@ function deriveRepoId(repoFullName) {
|
|
|
10
10
|
.replace(/^[^a-z0-9]+/, "")
|
|
11
11
|
.replace(/[^a-z0-9]+$/, "");
|
|
12
12
|
if (!normalized) {
|
|
13
|
-
throw new UsageError(`Could not derive a repo id from ${repoFullName}. Pass an explicit <id>.`, "
|
|
13
|
+
throw new UsageError(`Could not derive a repo id from ${repoFullName}. Pass an explicit <id>.`, "repo");
|
|
14
14
|
}
|
|
15
15
|
return normalized;
|
|
16
16
|
}
|
|
@@ -18,13 +18,13 @@ function parseAttachTarget(parsed) {
|
|
|
18
18
|
const first = parsed.positionals[1];
|
|
19
19
|
const second = parsed.positionals[2];
|
|
20
20
|
if (!first) {
|
|
21
|
-
throw new UsageError("merge-steward attach requires <owner/repo> or <id> <owner/repo>.", "
|
|
21
|
+
throw new UsageError("merge-steward repo attach requires <owner/repo> or <id> <owner/repo>.", "repo");
|
|
22
22
|
}
|
|
23
23
|
if (second) {
|
|
24
24
|
return { repoId: first, repoFullName: second };
|
|
25
25
|
}
|
|
26
26
|
if (!first.includes("/")) {
|
|
27
|
-
throw new UsageError("merge-steward attach requires <owner/repo> or <id> <owner/repo>.", "
|
|
27
|
+
throw new UsageError("merge-steward repo attach requires <owner/repo> or <id> <owner/repo>.", "repo");
|
|
28
28
|
}
|
|
29
29
|
const existing = listRepoConfigs().find((repo) => repo.repoFullName === first);
|
|
30
30
|
if (existing) {
|
|
@@ -33,7 +33,7 @@ function parseAttachTarget(parsed) {
|
|
|
33
33
|
const repoId = deriveRepoId(first);
|
|
34
34
|
const conflict = listRepoConfigs().find((repo) => repo.repoId === repoId && repo.repoFullName !== first);
|
|
35
35
|
if (conflict) {
|
|
36
|
-
throw new UsageError(`Derived repo id '${repoId}' is already used by ${conflict.repoFullName}. Pass an explicit <id>.`, "
|
|
36
|
+
throw new UsageError(`Derived repo id '${repoId}' is already used by ${conflict.repoFullName}. Pass an explicit <id>.`, "repo");
|
|
37
37
|
}
|
|
38
38
|
return { repoId, repoFullName: first };
|
|
39
39
|
}
|
|
@@ -47,7 +47,7 @@ export async function handleInit(parsed, stdout, runCommand) {
|
|
|
47
47
|
"",
|
|
48
48
|
"Next steps:",
|
|
49
49
|
`1. Put the webhook secret and GitHub App credentials into ${home.serviceEnvPath} for dev or systemd-creds for prod`,
|
|
50
|
-
"2. Run `merge-steward attach <owner/repo>`",
|
|
50
|
+
"2. Run `merge-steward repo attach <owner/repo>`",
|
|
51
51
|
"3. Configure your GitHub App webhook URL to the webhook URL above",
|
|
52
52
|
"4. Run `merge-steward doctor --repo <id>`",
|
|
53
53
|
].join("\n") + "\n");
|
|
@@ -9,7 +9,7 @@ export async function handleRepos(parsed, stdout) {
|
|
|
9
9
|
return 0;
|
|
10
10
|
}
|
|
11
11
|
if (repos.length === 0) {
|
|
12
|
-
writeOutput(stdout, "No attached repositories yet. Run `merge-steward attach <owner/repo>`.\n");
|
|
12
|
+
writeOutput(stdout, "No attached repositories yet. Run `merge-steward repo attach <owner/repo>`.\n");
|
|
13
13
|
return 0;
|
|
14
14
|
}
|
|
15
15
|
writeOutput(stdout, repos
|
package/dist/cli/help.js
CHANGED
|
@@ -78,10 +78,6 @@ function repoHelpText() {
|
|
|
78
78
|
" --refresh Re-discover the base branch from GitHub",
|
|
79
79
|
" --json Emit structured JSON",
|
|
80
80
|
"",
|
|
81
|
-
"Compatibility aliases:",
|
|
82
|
-
" merge-steward attach ... Alias for `merge-steward repo attach ...`",
|
|
83
|
-
" merge-steward repos ... Alias for `merge-steward repo list/show ...`",
|
|
84
|
-
"",
|
|
85
81
|
"Examples:",
|
|
86
82
|
" merge-steward repo attach owner/repo",
|
|
87
83
|
" merge-steward repo attach owner/repo --refresh",
|
|
@@ -120,7 +116,6 @@ function queueHelpText() {
|
|
|
120
116
|
export function helpTextFor(topic) {
|
|
121
117
|
switch (topic) {
|
|
122
118
|
case "repo":
|
|
123
|
-
case "repos":
|
|
124
119
|
return repoHelpText();
|
|
125
120
|
case "service":
|
|
126
121
|
return serviceHelpText();
|
package/dist/cli/system.js
CHANGED
|
@@ -87,7 +87,7 @@ export function loadRepoConfigById(repoId) {
|
|
|
87
87
|
const configuredHint = configured.length > 0
|
|
88
88
|
? ` Configured repos: ${configured.map((repo) => repo.repoId).join(", ")}.`
|
|
89
89
|
: "";
|
|
90
|
-
throw new UsageError(`Repo config not found for ${repoId}: ${configPath}. Run \`merge-steward attach ${repoId} <owner/repo>\` first.${configuredHint}`, "
|
|
90
|
+
throw new UsageError(`Repo config not found for ${repoId}: ${configPath}. Run \`merge-steward repo attach ${repoId} <owner/repo>\` first.${configuredHint}`, "repo");
|
|
91
91
|
}
|
|
92
92
|
return {
|
|
93
93
|
configPath,
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export interface ParsedArgs {
|
|
|
2
2
|
positionals: string[];
|
|
3
3
|
flags: Map<string, string | boolean>;
|
|
4
4
|
}
|
|
5
|
-
export type HelpTopic = "root" | "repo" | "
|
|
5
|
+
export type HelpTopic = "root" | "repo" | "service" | "queue";
|
|
6
6
|
export interface Output {
|
|
7
7
|
write(chunk: string): boolean;
|
|
8
8
|
}
|
package/dist/cli.js
CHANGED
|
@@ -21,22 +21,19 @@ export async function runCli(argv, options) {
|
|
|
21
21
|
return 0;
|
|
22
22
|
}
|
|
23
23
|
validateFlags(parsed);
|
|
24
|
-
const
|
|
25
|
-
const command = requestedCommand === "dash" || requestedCommand === "d"
|
|
26
|
-
? "dashboard"
|
|
27
|
-
: requestedCommand;
|
|
24
|
+
const command = parsed.positionals[0] ?? "help";
|
|
28
25
|
if (hasHelpFlag(parsed) || command === "help") {
|
|
29
26
|
const topic = command === "help"
|
|
30
27
|
? (parsed.positionals[1] ?? "root")
|
|
31
|
-
: (command === "
|
|
28
|
+
: (command === "repo"
|
|
32
29
|
? "repo"
|
|
33
30
|
: command === "service" || command === "queue"
|
|
34
31
|
? command
|
|
35
32
|
: "root");
|
|
36
|
-
if (!["root", "
|
|
33
|
+
if (!["root", "repo", "service", "queue"].includes(topic)) {
|
|
37
34
|
throw new UsageError(`Unknown help topic: ${String(topic)}`);
|
|
38
35
|
}
|
|
39
|
-
writeOutput(stdout, `${helpTextFor(topic
|
|
36
|
+
writeOutput(stdout, `${helpTextFor(topic)}\n`);
|
|
40
37
|
return 0;
|
|
41
38
|
}
|
|
42
39
|
switch (command) {
|
|
@@ -45,23 +42,19 @@ export async function runCli(argv, options) {
|
|
|
45
42
|
return 0;
|
|
46
43
|
case "init":
|
|
47
44
|
return await (await import("./cli/commands/init.js")).handleInit(parsed, stdout, runCommand);
|
|
48
|
-
case "attach":
|
|
49
|
-
return await (await import("./cli/commands/attach.js")).handleAttach(parsed, stdout, runCommand);
|
|
50
|
-
case "repos":
|
|
51
|
-
return await (await import("./cli/commands/repos.js")).handleRepos(parsed, stdout);
|
|
52
45
|
case "repo": {
|
|
53
46
|
const subcommand = parsed.positionals[1] ?? "list";
|
|
54
47
|
if (subcommand === "attach") {
|
|
55
|
-
return await (await import("./cli/commands/attach.js")).handleAttach(rewriteParsedArgs(parsed, ["
|
|
48
|
+
return await (await import("./cli/commands/attach.js")).handleAttach(rewriteParsedArgs(parsed, ["repo", ...parsed.positionals.slice(2)]), stdout, runCommand);
|
|
56
49
|
}
|
|
57
50
|
if (subcommand === "list") {
|
|
58
|
-
return await (await import("./cli/commands/repos.js")).handleRepos(rewriteParsedArgs(parsed, ["
|
|
51
|
+
return await (await import("./cli/commands/repos.js")).handleRepos(rewriteParsedArgs(parsed, ["repo", ...parsed.positionals.slice(2)]), stdout);
|
|
59
52
|
}
|
|
60
53
|
if (subcommand === "show") {
|
|
61
54
|
if (!parsed.positionals[2]) {
|
|
62
55
|
throw new UsageError("merge-steward repo show requires <id>.", "repo");
|
|
63
56
|
}
|
|
64
|
-
return await (await import("./cli/commands/repos.js")).handleRepos(rewriteParsedArgs(parsed, ["
|
|
57
|
+
return await (await import("./cli/commands/repos.js")).handleRepos(rewriteParsedArgs(parsed, ["repo", ...parsed.positionals.slice(2)]), stdout);
|
|
65
58
|
}
|
|
66
59
|
throw new UsageError(`Unknown repo command: ${subcommand}`, "repo");
|
|
67
60
|
}
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
1
|
import type { DatabaseConnection } from "./shared.ts";
|
|
2
|
-
/**
|
|
3
|
-
* Create and upgrade the steward's SQLite schema. Idempotent — safe to call
|
|
4
|
-
* on every startup. Migrations preserve active queue state, then remove
|
|
5
|
-
* obsolete columns so the runtime has one candidate model.
|
|
6
|
-
*/
|
|
2
|
+
/** Create the current steward schema and reject any other schema version. */
|
|
7
3
|
export declare function ensureSchema(connection: DatabaseConnection): void;
|
package/dist/db/schema.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create and upgrade the steward's SQLite schema. Idempotent — safe to call
|
|
3
|
-
* on every startup. Migrations preserve active queue state, then remove
|
|
4
|
-
* obsolete columns so the runtime has one candidate model.
|
|
5
|
-
*/
|
|
1
|
+
/** Create the current steward schema and reject any other schema version. */
|
|
6
2
|
export function ensureSchema(connection) {
|
|
3
|
+
const existingTables = connection.prepare(`
|
|
4
|
+
SELECT name FROM sqlite_master
|
|
5
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
6
|
+
`).all();
|
|
7
|
+
if (existingTables.length > 0) {
|
|
8
|
+
assertCurrentSchema(connection);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
7
11
|
connection.exec(`
|
|
8
12
|
CREATE TABLE IF NOT EXISTS queue_entries (
|
|
9
13
|
id TEXT PRIMARY KEY,
|
|
@@ -22,11 +26,13 @@ export function ensureSchema(connection) {
|
|
|
22
26
|
max_retries INTEGER NOT NULL DEFAULT 3,
|
|
23
27
|
last_failed_base_sha TEXT,
|
|
24
28
|
issue_key TEXT,
|
|
29
|
+
pr_title TEXT,
|
|
25
30
|
candidate_kind TEXT,
|
|
26
31
|
candidate_policy_fingerprint TEXT,
|
|
27
32
|
candidate_ref TEXT,
|
|
28
33
|
candidate_sha TEXT,
|
|
29
34
|
candidate_based_on TEXT,
|
|
35
|
+
base_ref_name TEXT,
|
|
30
36
|
wait_detail TEXT,
|
|
31
37
|
post_merge_status TEXT,
|
|
32
38
|
post_merge_sha TEXT,
|
|
@@ -73,45 +79,6 @@ export function ensureSchema(connection) {
|
|
|
73
79
|
connection.exec(`
|
|
74
80
|
CREATE INDEX IF NOT EXISTS idx_queue_events_entry
|
|
75
81
|
ON queue_events(entry_id, id)
|
|
76
|
-
`);
|
|
77
|
-
ensureColumn(connection, "queue_entries", "post_merge_status", "TEXT");
|
|
78
|
-
ensureColumn(connection, "queue_entries", "post_merge_sha", "TEXT");
|
|
79
|
-
ensureColumn(connection, "queue_entries", "post_merge_summary", "TEXT");
|
|
80
|
-
ensureColumn(connection, "queue_entries", "post_merge_checked_at", "TEXT");
|
|
81
|
-
ensureColumn(connection, "queue_entries", "wait_detail", "TEXT");
|
|
82
|
-
ensureColumn(connection, "queue_entries", "pr_title", "TEXT");
|
|
83
|
-
ensureColumn(connection, "queue_entries", "candidate_kind", "TEXT");
|
|
84
|
-
ensureColumn(connection, "queue_entries", "candidate_policy_fingerprint", "TEXT");
|
|
85
|
-
ensureColumn(connection, "queue_entries", "candidate_ref", "TEXT");
|
|
86
|
-
ensureColumn(connection, "queue_entries", "candidate_sha", "TEXT");
|
|
87
|
-
ensureColumn(connection, "queue_entries", "candidate_based_on", "TEXT");
|
|
88
|
-
ensureColumn(connection, "queue_entries", "base_ref_name", "TEXT");
|
|
89
|
-
ensureColumn(connection, "queue_entries", "decided_at", "TEXT");
|
|
90
|
-
migrateLegacyCandidateColumns(connection);
|
|
91
|
-
// Retire patch-equivalence caches from older installations. Candidate
|
|
92
|
-
// validity is SHA-bound; preserving these columns would imply otherwise.
|
|
93
|
-
dropColumnIfPresent(connection, "queue_entries", "head_patch_id");
|
|
94
|
-
dropColumnIfPresent(connection, "queue_entries", "spec_tree_id");
|
|
95
|
-
connection.exec(`
|
|
96
|
-
UPDATE queue_entries
|
|
97
|
-
SET candidate_kind = 'integration'
|
|
98
|
-
WHERE candidate_kind IS NULL
|
|
99
|
-
AND candidate_sha IS NOT NULL
|
|
100
|
-
AND candidate_ref IS NOT NULL
|
|
101
|
-
`);
|
|
102
|
-
connection.exec(`
|
|
103
|
-
UPDATE queue_entries
|
|
104
|
-
SET post_merge_status = 'pending'
|
|
105
|
-
WHERE status = 'merged'
|
|
106
|
-
AND post_merge_status IS NULL
|
|
107
|
-
`);
|
|
108
|
-
// Backfill decided_at for already-terminal rows so historic entries show a
|
|
109
|
-
// sensible duration/age; updated_at is the best estimate we have for them.
|
|
110
|
-
connection.exec(`
|
|
111
|
-
UPDATE queue_entries
|
|
112
|
-
SET decided_at = updated_at
|
|
113
|
-
WHERE decided_at IS NULL
|
|
114
|
-
AND status IN ('merged', 'evicted', 'dequeued')
|
|
115
82
|
`);
|
|
116
83
|
// Must match TERMINAL_STATUSES in types.ts: merged, evicted, dequeued
|
|
117
84
|
connection.exec(`
|
|
@@ -119,56 +86,51 @@ export function ensureSchema(connection) {
|
|
|
119
86
|
ON queue_entries(repo_id, pr_number)
|
|
120
87
|
WHERE status NOT IN ('merged', 'evicted', 'dequeued')
|
|
121
88
|
`);
|
|
89
|
+
assertCurrentSchema(connection);
|
|
122
90
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
91
|
+
const QUEUE_ENTRY_COLUMNS = [
|
|
92
|
+
"id", "repo_id", "pr_number", "branch", "head_sha", "base_sha", "status",
|
|
93
|
+
"position", "priority", "generation", "ci_run_id", "ci_retries",
|
|
94
|
+
"retry_attempts", "max_retries", "last_failed_base_sha", "issue_key",
|
|
95
|
+
"pr_title", "candidate_kind", "candidate_policy_fingerprint", "candidate_ref",
|
|
96
|
+
"candidate_sha", "candidate_based_on", "base_ref_name", "wait_detail",
|
|
97
|
+
"post_merge_status", "post_merge_sha", "post_merge_summary",
|
|
98
|
+
"post_merge_checked_at", "decided_at", "enqueued_at", "updated_at",
|
|
99
|
+
];
|
|
100
|
+
const QUEUE_INCIDENT_COLUMNS = [
|
|
101
|
+
"id", "entry_id", "at", "failure_class", "context_json", "outcome",
|
|
102
|
+
];
|
|
103
|
+
const QUEUE_EVENT_COLUMNS = [
|
|
104
|
+
"id", "entry_id", "at", "from_status", "to_status", "detail", "base_sha",
|
|
105
|
+
];
|
|
106
|
+
const REQUIRED_INDEXES = [
|
|
107
|
+
"idx_queue_entries_repo_status",
|
|
108
|
+
"idx_queue_entries_repo_position",
|
|
109
|
+
"idx_queue_incidents_entry",
|
|
110
|
+
"idx_queue_events_entry",
|
|
111
|
+
"idx_one_active_per_pr",
|
|
112
|
+
];
|
|
113
|
+
function assertCurrentSchema(connection) {
|
|
114
|
+
assertExactColumns(connection, "queue_entries", QUEUE_ENTRY_COLUMNS);
|
|
115
|
+
assertExactColumns(connection, "queue_incidents", QUEUE_INCIDENT_COLUMNS);
|
|
116
|
+
assertExactColumns(connection, "queue_events", QUEUE_EVENT_COLUMNS);
|
|
117
|
+
const indexes = new Set(connection.prepare("SELECT name FROM sqlite_master WHERE type = 'index'").all()
|
|
118
|
+
.map((row) => String(row.name)));
|
|
119
|
+
const missing = REQUIRED_INDEXES.filter((index) => !indexes.has(index));
|
|
120
|
+
if (missing.length > 0) {
|
|
121
|
+
throw new Error(`Merge Steward database schema is incompatible (missing indexes: ${missing.join(", ")})`);
|
|
138
122
|
}
|
|
139
|
-
// Derive kind while the legacy ref is still available, then remove all
|
|
140
|
-
// old candidate representations in this same startup migration.
|
|
141
|
-
connection.exec(`
|
|
142
|
-
UPDATE queue_entries
|
|
143
|
-
SET candidate_kind = CASE
|
|
144
|
-
WHEN candidate_sha = head_sha AND candidate_ref IS NULL THEN 'head'
|
|
145
|
-
WHEN candidate_sha IS NOT NULL THEN 'integration'
|
|
146
|
-
ELSE NULL
|
|
147
|
-
END
|
|
148
|
-
WHERE candidate_kind IS NULL
|
|
149
|
-
`);
|
|
150
|
-
for (const [legacy] of legacyColumns) {
|
|
151
|
-
dropColumnIfPresent(connection, "queue_entries", legacy);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function hasColumn(connection, table, column) {
|
|
155
|
-
const rows = connection.prepare(`PRAGMA table_info(${table})`).all();
|
|
156
|
-
for (const row of rows) {
|
|
157
|
-
if (String(row.name) === column) {
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return false;
|
|
162
123
|
}
|
|
163
|
-
function
|
|
164
|
-
|
|
124
|
+
function assertExactColumns(connection, table, expected) {
|
|
125
|
+
const actual = connection.prepare(`PRAGMA table_info(${table})`).all()
|
|
126
|
+
.map((row) => String(row.name));
|
|
127
|
+
const missing = expected.filter((column) => !actual.includes(column));
|
|
128
|
+
const unexpected = actual.filter((column) => !expected.includes(column));
|
|
129
|
+
if (missing.length === 0 && unexpected.length === 0)
|
|
165
130
|
return;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
connection.exec(`ALTER TABLE ${table} DROP COLUMN ${column}`);
|
|
131
|
+
const details = [
|
|
132
|
+
...(missing.length > 0 ? [`missing: ${missing.join(", ")}`] : []),
|
|
133
|
+
...(unexpected.length > 0 ? [`unexpected: ${unexpected.join(", ")}`] : []),
|
|
134
|
+
].join("; ");
|
|
135
|
+
throw new Error(`Merge Steward database schema is incompatible (${table}: ${details})`);
|
|
174
136
|
}
|
package/dist/server.js
CHANGED
|
@@ -85,7 +85,7 @@ export async function startMultiServer() {
|
|
|
85
85
|
};
|
|
86
86
|
setRuntimeGitHubAuthProvider(undefined);
|
|
87
87
|
if (configs.length === 0) {
|
|
88
|
-
logger.warn("No repo configs found. Run `merge-steward attach <owner/repo>` to add repos.");
|
|
88
|
+
logger.warn("No repo configs found. Run `merge-steward repo attach <owner/repo>` to add repos.");
|
|
89
89
|
}
|
|
90
90
|
if (githubAuth.mode === "app") {
|
|
91
91
|
githubRuntimeStatus = {
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { QueueEntry } from "../types.ts";
|
|
2
|
-
/**
|
|
3
|
-
* Build the display row list for the queue watch. In "active" mode,
|
|
4
|
-
* includes active entries plus recently-terminal entries (within 60s),
|
|
5
|
-
* deduplicated by prNumber (active wins over terminal for re-admissions).
|
|
6
|
-
* In "all" mode, returns all entries as-is.
|
|
7
|
-
*/
|
|
8
|
-
export declare function buildDisplayEntries(entries: QueueEntry[], filter: "active" | "all"): QueueEntry[];
|
|
9
|
-
/**
|
|
10
|
-
* Build the spec chain header entries. Always shows the current live
|
|
11
|
-
* chain: active entries plus recently-merged entries (for cascade
|
|
12
|
-
* visibility), regardless of the display filter. One entry per PR.
|
|
13
|
-
*/
|
|
14
|
-
export declare function buildChainEntries(entries: QueueEntry[]): QueueEntry[];
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { TERMINAL_STATUSES } from "../types.js";
|
|
2
|
-
const RECENTLY_COMPLETED_MS = 60_000;
|
|
3
|
-
function isPostMergeUnresolved(entry) {
|
|
4
|
-
if (entry.status !== "merged")
|
|
5
|
-
return false;
|
|
6
|
-
return entry.postMergeStatus === null
|
|
7
|
-
|| entry.postMergeStatus === "pending"
|
|
8
|
-
|| entry.postMergeStatus === "unknown";
|
|
9
|
-
}
|
|
10
|
-
function isActiveFilterVisible(entry, cutoff) {
|
|
11
|
-
if (!TERMINAL_STATUSES.includes(entry.status)) {
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
// Keep a merged entry visible until main's own post-merge CI resolves so
|
|
15
|
-
// the operator can watch the verification land. Once it's pass/fail we
|
|
16
|
-
// fall back to the recently-completed window.
|
|
17
|
-
if (isPostMergeUnresolved(entry)) {
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
return new Date(entry.updatedAt).getTime() > cutoff;
|
|
21
|
-
}
|
|
22
|
-
function isLaterEntry(left, right) {
|
|
23
|
-
if (left.position !== right.position) {
|
|
24
|
-
return left.position > right.position;
|
|
25
|
-
}
|
|
26
|
-
return new Date(left.updatedAt).getTime() >= new Date(right.updatedAt).getTime();
|
|
27
|
-
}
|
|
28
|
-
function pickLatestPerPR(entries) {
|
|
29
|
-
const byPR = new Map();
|
|
30
|
-
for (const entry of entries) {
|
|
31
|
-
const existing = byPR.get(entry.prNumber);
|
|
32
|
-
if (!existing || isLaterEntry(entry, existing)) {
|
|
33
|
-
byPR.set(entry.prNumber, entry);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return [...byPR.values()];
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Build the display row list for the queue watch. In "active" mode,
|
|
40
|
-
* includes active entries plus recently-terminal entries (within 60s),
|
|
41
|
-
* deduplicated by prNumber (active wins over terminal for re-admissions).
|
|
42
|
-
* In "all" mode, returns all entries as-is.
|
|
43
|
-
*/
|
|
44
|
-
export function buildDisplayEntries(entries, filter) {
|
|
45
|
-
if (filter !== "active")
|
|
46
|
-
return entries;
|
|
47
|
-
const cutoff = Date.now() - RECENTLY_COMPLETED_MS;
|
|
48
|
-
return pickLatestPerPR(entries)
|
|
49
|
-
.filter((entry) => {
|
|
50
|
-
const isActive = !TERMINAL_STATUSES.includes(entry.status);
|
|
51
|
-
return isActive || isActiveFilterVisible(entry, cutoff);
|
|
52
|
-
})
|
|
53
|
-
.sort((a, b) => a.position - b.position);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Build the spec chain header entries. Always shows the current live
|
|
57
|
-
* chain: active entries plus recently-merged entries (for cascade
|
|
58
|
-
* visibility), regardless of the display filter. One entry per PR.
|
|
59
|
-
*/
|
|
60
|
-
export function buildChainEntries(entries) {
|
|
61
|
-
const cutoff = Date.now() - RECENTLY_COMPLETED_MS;
|
|
62
|
-
return pickLatestPerPR(entries)
|
|
63
|
-
.filter((entry) => {
|
|
64
|
-
const isActive = !TERMINAL_STATUSES.includes(entry.status);
|
|
65
|
-
const isRecent = !isActive && new Date(entry.updatedAt).getTime() > cutoff;
|
|
66
|
-
return isActive || isRecent || isPostMergeUnresolved(entry);
|
|
67
|
-
})
|
|
68
|
-
.sort((a, b) => a.position - b.position);
|
|
69
|
-
}
|