merge-steward 0.9.7 → 0.10.1
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/README.md +63 -27
- package/dist/cli/args.js +8 -3
- package/dist/cli/commands/dashboard.d.ts +2 -0
- package/dist/cli/commands/dashboard.js +24 -0
- package/dist/cli/commands/queue.js +5 -5
- package/dist/cli/commands/service.js +8 -2
- package/dist/cli/help.js +4 -4
- package/dist/cli/system.d.ts +8 -0
- package/dist/cli/system.js +29 -1
- package/dist/cli.js +6 -1
- package/dist/watch/App.d.ts +5 -2
- package/dist/watch/App.js +158 -88
- package/dist/watch/HelpBar.d.ts +1 -1
- package/dist/watch/HelpBar.js +3 -3
- package/dist/watch/OverviewView.d.ts +7 -0
- package/dist/watch/OverviewView.js +34 -0
- package/dist/watch/ProjectDetailView.d.ts +10 -0
- package/dist/watch/ProjectDetailView.js +55 -0
- package/dist/watch/StatusBar.d.ts +5 -4
- package/dist/watch/StatusBar.js +24 -29
- package/dist/watch/api.d.ts +4 -4
- package/dist/watch/api.js +12 -8
- package/dist/watch/dashboard-model.d.ts +38 -0
- package/dist/watch/dashboard-model.js +192 -0
- package/dist/watch/format.d.ts +1 -0
- package/dist/watch/format.js +34 -0
- package/dist/watch/index.d.ts +4 -1
- package/dist/watch/index.js +13 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# merge-steward
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
`merge-steward` is a self-hosted merge queue for bot-managed and human-managed
|
|
4
|
+
GitHub pull requests. It admits approved PRs whose required checks are green,
|
|
5
|
+
builds cumulative speculative branches, waits for CI on those exact integrated
|
|
6
|
+
SHAs, and then fast-forwards `main` to the tested result. On failure, it evicts
|
|
7
|
+
with a durable incident record and GitHub check run so an agent or human can
|
|
8
|
+
repair the branch and re-queue it.
|
|
7
9
|
|
|
8
10
|
Fully independent of PatchRelay. Communicates through GitHub — PRs, reviews,
|
|
9
11
|
checks, labels, branches.
|
|
@@ -209,34 +211,68 @@ merge-steward doctor --repo repo
|
|
|
209
211
|
merge-steward service status
|
|
210
212
|
merge-steward queue status --repo repo
|
|
211
213
|
merge-steward queue show --repo repo --pr 123
|
|
214
|
+
merge-steward dashboard
|
|
212
215
|
|
|
213
216
|
# Manual foreground start
|
|
214
217
|
merge-steward serve
|
|
215
218
|
|
|
216
|
-
#
|
|
217
|
-
merge-steward
|
|
219
|
+
# Open one project directly in the dashboard
|
|
220
|
+
merge-steward dashboard --repo app
|
|
218
221
|
```
|
|
219
222
|
|
|
220
|
-
###
|
|
223
|
+
### Dashboard
|
|
221
224
|
|
|
222
|
-
`merge-steward
|
|
225
|
+
`merge-steward dashboard` is the operator surface for day-to-day queue work.
|
|
223
226
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
-
|
|
227
|
-
-
|
|
228
|
-
-
|
|
227
|
+
The first screen shows all configured projects with:
|
|
228
|
+
|
|
229
|
+
- project-level queue health
|
|
230
|
+
- readable queue stats
|
|
231
|
+
- a compact queue chain like `#123 ● #124 ○`
|
|
232
|
+
- clear bad states such as blocked, stuck, or needs attention
|
|
233
|
+
|
|
234
|
+
Press `Enter` on a project to open the second screen. That project detail view shows:
|
|
235
|
+
|
|
236
|
+
- the same top-level queue stats for that project
|
|
237
|
+
- a readable list of PRs in the queue
|
|
238
|
+
- recent queue activity in plain language
|
|
239
|
+
- incidents for evicted PRs
|
|
240
|
+
- direct actions like reconcile and dequeue
|
|
241
|
+
|
|
242
|
+
Use `merge-steward dashboard --repo <id>` to open the project detail screen directly. Use `--pr <number>` to preselect a PR when you already know what you need to inspect.
|
|
229
243
|
|
|
230
244
|
Controls:
|
|
231
245
|
|
|
232
246
|
- `j` / `k` or arrows — move selection
|
|
233
|
-
- `Enter` — open selected
|
|
234
|
-
- `Esc` — return to
|
|
235
|
-
- `a` — toggle `active` vs `all`
|
|
236
|
-
- `r` — run a reconcile tick
|
|
237
|
-
- `d` — dequeue the selected PR
|
|
247
|
+
- `Enter` — open the selected project from overview
|
|
248
|
+
- `Esc` — return to the overview
|
|
249
|
+
- `a` — toggle `active` vs `all` in project view
|
|
250
|
+
- `r` — run a reconcile tick for the selected project
|
|
251
|
+
- `d` — dequeue the selected PR in project view
|
|
238
252
|
- `q` — quit
|
|
239
253
|
|
|
254
|
+
### Validation, Visibility, And Troubleshooting
|
|
255
|
+
|
|
256
|
+
These are the first commands to reach for after setup or when a queue looks wrong:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
merge-steward doctor --repo app
|
|
260
|
+
merge-steward service status
|
|
261
|
+
merge-steward service restart
|
|
262
|
+
merge-steward dashboard
|
|
263
|
+
merge-steward queue status --repo app
|
|
264
|
+
merge-steward queue show --repo app --pr 123
|
|
265
|
+
merge-steward service logs --lines 100
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Use them this way:
|
|
269
|
+
|
|
270
|
+
- `doctor` checks config, GitHub auth, branch rules, and required checks.
|
|
271
|
+
- `dashboard` is the best live operator view across all configured projects.
|
|
272
|
+
- `queue status` is the fastest text snapshot when you need one repo in a shell script or over SSH.
|
|
273
|
+
- `queue show --pr <number>` is the most direct way to inspect one PR's queue events and incidents.
|
|
274
|
+
- `service logs` helps when the queue is not reacting to webhooks, GitHub auth is failing, or reconcile ticks are erroring.
|
|
275
|
+
|
|
240
276
|
### systemd
|
|
241
277
|
|
|
242
278
|
```ini
|
|
@@ -264,15 +300,15 @@ WantedBy=multi-user.target
|
|
|
264
300
|
| Endpoint | Method | Description |
|
|
265
301
|
|-|-|-|
|
|
266
302
|
| `/health` | GET | Liveness check |
|
|
267
|
-
| `/queue/status` | GET | All queue entries |
|
|
268
|
-
| `/queue/watch` | GET | Queue snapshot
|
|
269
|
-
| `/queue/enqueue` | POST | Manually enqueue a PR |
|
|
270
|
-
| `/queue/reconcile` | POST | Trigger one reconcile tick immediately |
|
|
271
|
-
| `/queue/entries/:id/detail` | GET | Entry detail with recent events and incidents |
|
|
272
|
-
| `/queue/entries/:id/dequeue` | POST | Remove from queue (non-destructive) |
|
|
273
|
-
| `/queue/entries/:id/update-head` | POST | Update head SHA (force-push) |
|
|
274
|
-
| `/queue/incidents/:id` | GET | Get incident details |
|
|
275
|
-
| `/queue/entries/:id/incidents` | GET | List incidents for an entry |
|
|
303
|
+
| `/repos/:repoId/queue/status` | GET | All queue entries for one configured repo |
|
|
304
|
+
| `/repos/:repoId/queue/watch` | GET | Queue snapshot used by the dashboard |
|
|
305
|
+
| `/repos/:repoId/queue/enqueue` | POST | Manually enqueue a PR |
|
|
306
|
+
| `/repos/:repoId/queue/reconcile` | POST | Trigger one reconcile tick immediately |
|
|
307
|
+
| `/repos/:repoId/queue/entries/:id/detail` | GET | Entry detail with recent events and incidents |
|
|
308
|
+
| `/repos/:repoId/queue/entries/:id/dequeue` | POST | Remove from queue (non-destructive) |
|
|
309
|
+
| `/repos/:repoId/queue/entries/:id/update-head` | POST | Update head SHA (force-push) |
|
|
310
|
+
| `/repos/:repoId/queue/incidents/:id` | GET | Get incident details |
|
|
311
|
+
| `/repos/:repoId/queue/entries/:id/incidents` | GET | List incidents for an entry |
|
|
276
312
|
| `/webhooks/github` | POST | GitHub webhook receiver for all configured repos |
|
|
277
313
|
|
|
278
314
|
## Queue state machine
|
package/dist/cli/args.js
CHANGED
|
@@ -57,6 +57,11 @@ export function validateFlags(parsed) {
|
|
|
57
57
|
case "serve":
|
|
58
58
|
assertKnownFlags(parsed, "root", []);
|
|
59
59
|
return;
|
|
60
|
+
case "dashboard":
|
|
61
|
+
case "dash":
|
|
62
|
+
case "d":
|
|
63
|
+
assertKnownFlags(parsed, "root", ["repo", "pr"]);
|
|
64
|
+
return;
|
|
60
65
|
case "attach":
|
|
61
66
|
assertKnownFlags(parsed, "repos", ["base-branch", "required-check", "label", "merge-queue-check-name", "refresh", "json"]);
|
|
62
67
|
return;
|
|
@@ -97,15 +102,15 @@ export function validateFlags(parsed) {
|
|
|
97
102
|
}
|
|
98
103
|
case "queue":
|
|
99
104
|
switch (subcommand) {
|
|
105
|
+
case "dashboard":
|
|
106
|
+
assertKnownFlags(parsed, "queue", ["repo", "pr"]);
|
|
107
|
+
return;
|
|
100
108
|
case "status":
|
|
101
109
|
assertKnownFlags(parsed, "queue", ["repo", "events", "json"]);
|
|
102
110
|
return;
|
|
103
111
|
case "show":
|
|
104
112
|
assertKnownFlags(parsed, "queue", ["repo", "entry", "pr", "events", "json"]);
|
|
105
113
|
return;
|
|
106
|
-
case "watch":
|
|
107
|
-
assertKnownFlags(parsed, "queue", ["repo", "pr"]);
|
|
108
|
-
return;
|
|
109
114
|
case "reconcile":
|
|
110
115
|
assertKnownFlags(parsed, "queue", ["repo", "json"]);
|
|
111
116
|
return;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parseIntegerFlag } from "../args.js";
|
|
2
|
+
import { loadRepoConfigById } from "../system.js";
|
|
3
|
+
export async function handleDashboard(parsed) {
|
|
4
|
+
const initialRepoRef = typeof parsed.flags.get("repo") === "string"
|
|
5
|
+
? String(parsed.flags.get("repo"))
|
|
6
|
+
: undefined;
|
|
7
|
+
const initialPrNumber = parseIntegerFlag(parsed.flags.get("pr"), "--pr");
|
|
8
|
+
const options = {};
|
|
9
|
+
if (initialRepoRef) {
|
|
10
|
+
loadRepoConfigById(initialRepoRef);
|
|
11
|
+
options.initialRepoRef = initialRepoRef;
|
|
12
|
+
}
|
|
13
|
+
if (initialPrNumber !== undefined) {
|
|
14
|
+
options.initialPrNumber = initialPrNumber;
|
|
15
|
+
}
|
|
16
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
17
|
+
process.stderr.write("merge-steward dashboard requires an interactive TTY.\n");
|
|
18
|
+
process.stderr.write("Use `merge-steward queue status --repo <id>` or run the dashboard from a terminal.\n");
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const { startDashboard } = await import("../../watch/index.js");
|
|
22
|
+
await startDashboard(options);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
@@ -117,11 +117,11 @@ export async function handleQueue(parsed, stdout) {
|
|
|
117
117
|
throw new UsageError("merge-steward queue requires a subcommand.", "queue");
|
|
118
118
|
}
|
|
119
119
|
if (subcommand === "watch") {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
await
|
|
124
|
-
return
|
|
120
|
+
throw new UsageError("`merge-steward queue watch` was replaced by `merge-steward dashboard [--repo <id>] [--pr <number>]`.", "queue");
|
|
121
|
+
}
|
|
122
|
+
if (subcommand === "dashboard") {
|
|
123
|
+
const { handleDashboard } = await import("./dashboard.js");
|
|
124
|
+
return await handleDashboard(parsed);
|
|
125
125
|
}
|
|
126
126
|
const repoId = resolveRepoId(parsed, 2, "queue");
|
|
127
127
|
const { config } = loadRepoConfigById(repoId);
|
|
@@ -2,7 +2,7 @@ import { installServiceUnit } from "../../install.js";
|
|
|
2
2
|
import { UsageError } from "../types.js";
|
|
3
3
|
import { parseIntegerFlag } from "../args.js";
|
|
4
4
|
import { formatJson, writeOutput } from "../output.js";
|
|
5
|
-
import { formatCommandFailure, parseSystemctlShowOutput, runSystemctl } from "../system.js";
|
|
5
|
+
import { fetchServiceHealthStatus, formatCommandFailure, parseSystemctlShowOutput, runSystemctl } from "../system.js";
|
|
6
6
|
const UNIT_NAME = "merge-steward.service";
|
|
7
7
|
export async function handleService(parsed, stdout, runCommand) {
|
|
8
8
|
const subcommand = parsed.positionals[1];
|
|
@@ -31,6 +31,8 @@ export async function handleService(parsed, stdout, runCommand) {
|
|
|
31
31
|
const restart = await runSystemctl(runCommand, ["reload-or-restart", UNIT_NAME]);
|
|
32
32
|
if (parsed.flags.get("json") === true) {
|
|
33
33
|
writeOutput(stdout, formatJson({
|
|
34
|
+
service: "merge-steward",
|
|
35
|
+
unit: UNIT_NAME,
|
|
34
36
|
daemonReloaded: daemonReload.ok,
|
|
35
37
|
restarted: restart.ok,
|
|
36
38
|
errors: [
|
|
@@ -55,8 +57,9 @@ export async function handleService(parsed, stdout, runCommand) {
|
|
|
55
57
|
throw new Error(status.error);
|
|
56
58
|
}
|
|
57
59
|
const properties = parseSystemctlShowOutput(status.result.stdout);
|
|
60
|
+
const health = await fetchServiceHealthStatus();
|
|
58
61
|
if (parsed.flags.get("json") === true) {
|
|
59
|
-
writeOutput(stdout, formatJson({ unit: UNIT_NAME, systemd: properties }));
|
|
62
|
+
writeOutput(stdout, formatJson({ service: "merge-steward", unit: UNIT_NAME, systemd: properties, health }));
|
|
60
63
|
return 0;
|
|
61
64
|
}
|
|
62
65
|
writeOutput(stdout, [
|
|
@@ -65,6 +68,9 @@ export async function handleService(parsed, stdout, runCommand) {
|
|
|
65
68
|
`Enabled: ${properties.UnitFileState ?? "unknown"}`,
|
|
66
69
|
`Active: ${properties.ActiveState ?? "unknown"}${properties.SubState ? ` (${properties.SubState})` : ""}`,
|
|
67
70
|
properties.ExecMainPID ? `Main PID: ${properties.ExecMainPID}` : undefined,
|
|
71
|
+
health.reachable
|
|
72
|
+
? `Health: ${health.ok ? "ok" : "unhealthy"} (HTTP ${health.status})`
|
|
73
|
+
: `Health: not reachable (${health.error})`,
|
|
68
74
|
]
|
|
69
75
|
.filter(Boolean)
|
|
70
76
|
.join("\n") + "\n");
|
package/dist/cli/help.js
CHANGED
|
@@ -20,12 +20,12 @@ function rootHelpText() {
|
|
|
20
20
|
" repo list [--json] List attached repositories",
|
|
21
21
|
" repo show <id> [--json] Show one repo config",
|
|
22
22
|
" doctor [--repo <id>] [--json] Validate config, secrets, auth, and required binaries",
|
|
23
|
-
" service status [--json] Show systemd state",
|
|
23
|
+
" service status [--json] Show systemd state and local health",
|
|
24
24
|
" service logs [--lines <count>] [--json] Show recent journal logs",
|
|
25
25
|
" queue status --repo <id> [--json] Show queue summary and current entries",
|
|
26
26
|
" queue show --repo <id> (--entry <id> | --pr <num>) [--events <count>] [--json]",
|
|
27
27
|
" Show one queue entry with events and incidents",
|
|
28
|
-
"
|
|
28
|
+
" dashboard [--repo <id>] [--pr <number>] Open the multi-repo merge queue dashboard",
|
|
29
29
|
"",
|
|
30
30
|
"Service management:",
|
|
31
31
|
" service install [--force] [--json] Reinstall the systemd unit",
|
|
@@ -83,7 +83,7 @@ function serviceHelpText() {
|
|
|
83
83
|
"Commands:",
|
|
84
84
|
" install [--force] [--json] Reinstall the systemd unit",
|
|
85
85
|
" restart [--json] Reload-or-restart the service",
|
|
86
|
-
" status [--json] Show systemd state",
|
|
86
|
+
" status [--json] Show systemd state and local health",
|
|
87
87
|
" logs [--lines <count>] [--json]",
|
|
88
88
|
" Show recent journal logs",
|
|
89
89
|
].join("\n");
|
|
@@ -94,9 +94,9 @@ function queueHelpText() {
|
|
|
94
94
|
" merge-steward queue <command> [options]",
|
|
95
95
|
"",
|
|
96
96
|
"Commands:",
|
|
97
|
+
" dashboard [--repo <id>] [--pr <number>] Open the multi-repo merge queue dashboard",
|
|
97
98
|
" status --repo <id> Show queue summary and entries",
|
|
98
99
|
" show --repo <id> (--entry <id> | --pr <num>) Show one queue entry with events and incidents",
|
|
99
|
-
" watch --repo <id> [--pr <number>] Open the queue watch TUI",
|
|
100
100
|
" reconcile --repo <id> [--json] Ask the service to reconcile immediately",
|
|
101
101
|
].join("\n");
|
|
102
102
|
}
|
package/dist/cli/system.d.ts
CHANGED
|
@@ -40,6 +40,14 @@ export declare function fetchGatewayJson<T>(relativePath: string, options?: {
|
|
|
40
40
|
method?: string;
|
|
41
41
|
body?: unknown;
|
|
42
42
|
}): Promise<T>;
|
|
43
|
+
export declare function fetchServiceHealthStatus(): Promise<{
|
|
44
|
+
reachable: true;
|
|
45
|
+
ok: boolean;
|
|
46
|
+
status: number;
|
|
47
|
+
} | {
|
|
48
|
+
reachable: false;
|
|
49
|
+
error: string;
|
|
50
|
+
}>;
|
|
43
51
|
export declare function fetchLocalJson<T>(repoId: string, relativePath: string, options?: {
|
|
44
52
|
method?: string;
|
|
45
53
|
}): Promise<T>;
|
package/dist/cli/system.js
CHANGED
|
@@ -161,7 +161,7 @@ export function getGatewayBaseUrl() {
|
|
|
161
161
|
throw new Error("merge-steward home not initialized.");
|
|
162
162
|
}
|
|
163
163
|
const homeConfig = parseHomeConfigObject(readFileSync(homeConfigPath, "utf8"), homeConfigPath);
|
|
164
|
-
const bind = homeConfig.server.bind;
|
|
164
|
+
const bind = homeConfig.server.bind === "0.0.0.0" ? "127.0.0.1" : homeConfig.server.bind;
|
|
165
165
|
const port = homeConfig.server.gateway_port ?? (homeConfig.server.port_base - 1);
|
|
166
166
|
return `http://${bind}:${port}`;
|
|
167
167
|
}
|
|
@@ -193,6 +193,34 @@ export async function fetchGatewayJson(relativePath, options) {
|
|
|
193
193
|
const base = getGatewayBaseUrl();
|
|
194
194
|
return await requestGatewayJson(`${base}${relativePath}`, options);
|
|
195
195
|
}
|
|
196
|
+
export async function fetchServiceHealthStatus() {
|
|
197
|
+
try {
|
|
198
|
+
const response = await fetch(`${getGatewayBaseUrl()}/health`, {
|
|
199
|
+
signal: AbortSignal.timeout(2_000),
|
|
200
|
+
});
|
|
201
|
+
let ok = response.ok;
|
|
202
|
+
try {
|
|
203
|
+
const body = await response.json();
|
|
204
|
+
if (typeof body.ok === "boolean") {
|
|
205
|
+
ok = response.ok && body.ok;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
ok = response.ok;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
reachable: true,
|
|
213
|
+
ok,
|
|
214
|
+
status: response.status,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
return {
|
|
219
|
+
reachable: false,
|
|
220
|
+
error: error instanceof Error ? error.message : String(error),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
196
224
|
export async function fetchLocalJson(repoId, relativePath, options) {
|
|
197
225
|
return await fetchGatewayJson(`/repos/${repoId}${relativePath}`, options);
|
|
198
226
|
}
|
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,10 @@ export async function runCli(argv, options) {
|
|
|
21
21
|
return 0;
|
|
22
22
|
}
|
|
23
23
|
validateFlags(parsed);
|
|
24
|
-
const
|
|
24
|
+
const requestedCommand = parsed.positionals[0] ?? "help";
|
|
25
|
+
const command = requestedCommand === "dash" || requestedCommand === "d"
|
|
26
|
+
? "dashboard"
|
|
27
|
+
: requestedCommand;
|
|
25
28
|
if (hasHelpFlag(parsed) || command === "help") {
|
|
26
29
|
const topic = command === "help"
|
|
27
30
|
? (parsed.positionals[1] ?? "root")
|
|
@@ -64,6 +67,8 @@ export async function runCli(argv, options) {
|
|
|
64
67
|
}
|
|
65
68
|
case "doctor":
|
|
66
69
|
return await (await import("./cli/commands/doctor.js")).handleDoctor(parsed, stdout);
|
|
70
|
+
case "dashboard":
|
|
71
|
+
return await (await import("./cli/commands/dashboard.js")).handleDashboard(parsed);
|
|
67
72
|
case "service":
|
|
68
73
|
return await (await import("./cli/commands/service.js")).handleService(parsed, stdout, runCommand);
|
|
69
74
|
case "queue":
|
package/dist/watch/App.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { DashboardRepoConfig } from "./dashboard-model.ts";
|
|
1
2
|
interface AppProps {
|
|
2
|
-
|
|
3
|
+
gatewayBaseUrl: string;
|
|
4
|
+
repos: DashboardRepoConfig[];
|
|
5
|
+
initialRepoRef?: string | undefined;
|
|
3
6
|
initialPrNumber?: number | undefined;
|
|
4
7
|
}
|
|
5
|
-
export declare function App({
|
|
8
|
+
export declare function App({ gatewayBaseUrl, repos, initialRepoRef, initialPrNumber }: AppProps): React.JSX.Element;
|
|
6
9
|
export {};
|