@ultimat3/cli 27.14.0 → 27.14.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/package.json +28 -28
- package/src/ci-runs.ts +8 -0
- package/src/cmd-ci-spec.ts +18 -1
- package/src/cmd-ci.ts +51 -6
- package/src/serve-retire.ts +4 -2
- package/src/templates/scaffold-helm-templates.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "27.14.
|
|
3
|
+
"version": "27.14.1",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -39,33 +39,33 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@babel/core": "^8.0.1",
|
|
42
|
-
"@ultimat3/action": "27.14.
|
|
43
|
-
"@ultimat3/admin": "27.14.
|
|
44
|
-
"@ultimat3/ai": "27.14.
|
|
45
|
-
"@ultimat3/auth": "27.14.
|
|
46
|
-
"@ultimat3/cache": "27.14.
|
|
47
|
-
"@ultimat3/core": "27.14.
|
|
48
|
-
"@ultimat3/db": "27.14.
|
|
49
|
-
"@ultimat3/entity": "27.14.
|
|
50
|
-
"@ultimat3/flags": "27.14.
|
|
51
|
-
"@ultimat3/http": "27.14.
|
|
52
|
-
"@ultimat3/i18n": "27.14.
|
|
53
|
-
"@ultimat3/jobs": "27.14.
|
|
54
|
-
"@ultimat3/mail": "27.14.
|
|
55
|
-
"@ultimat3/manifest": "27.14.
|
|
56
|
-
"@ultimat3/mcp": "27.14.
|
|
57
|
-
"@ultimat3/money": "27.14.
|
|
58
|
-
"@ultimat3/notify": "27.14.
|
|
59
|
-
"@ultimat3/policy": "27.14.
|
|
60
|
-
"@ultimat3/pwa": "27.14.
|
|
61
|
-
"@ultimat3/query": "27.14.
|
|
62
|
-
"@ultimat3/realtime": "27.14.
|
|
63
|
-
"@ultimat3/render": "27.14.
|
|
64
|
-
"@ultimat3/schema": "27.14.
|
|
65
|
-
"@ultimat3/seo": "27.14.
|
|
66
|
-
"@ultimat3/storage": "27.14.
|
|
67
|
-
"@ultimat3/testing": "27.14.
|
|
68
|
-
"@ultimat3/time": "27.14.
|
|
42
|
+
"@ultimat3/action": "27.14.1",
|
|
43
|
+
"@ultimat3/admin": "27.14.1",
|
|
44
|
+
"@ultimat3/ai": "27.14.1",
|
|
45
|
+
"@ultimat3/auth": "27.14.1",
|
|
46
|
+
"@ultimat3/cache": "27.14.1",
|
|
47
|
+
"@ultimat3/core": "27.14.1",
|
|
48
|
+
"@ultimat3/db": "27.14.1",
|
|
49
|
+
"@ultimat3/entity": "27.14.1",
|
|
50
|
+
"@ultimat3/flags": "27.14.1",
|
|
51
|
+
"@ultimat3/http": "27.14.1",
|
|
52
|
+
"@ultimat3/i18n": "27.14.1",
|
|
53
|
+
"@ultimat3/jobs": "27.14.1",
|
|
54
|
+
"@ultimat3/mail": "27.14.1",
|
|
55
|
+
"@ultimat3/manifest": "27.14.1",
|
|
56
|
+
"@ultimat3/mcp": "27.14.1",
|
|
57
|
+
"@ultimat3/money": "27.14.1",
|
|
58
|
+
"@ultimat3/notify": "27.14.1",
|
|
59
|
+
"@ultimat3/policy": "27.14.1",
|
|
60
|
+
"@ultimat3/pwa": "27.14.1",
|
|
61
|
+
"@ultimat3/query": "27.14.1",
|
|
62
|
+
"@ultimat3/realtime": "27.14.1",
|
|
63
|
+
"@ultimat3/render": "27.14.1",
|
|
64
|
+
"@ultimat3/schema": "27.14.1",
|
|
65
|
+
"@ultimat3/seo": "27.14.1",
|
|
66
|
+
"@ultimat3/storage": "27.14.1",
|
|
67
|
+
"@ultimat3/testing": "27.14.1",
|
|
68
|
+
"@ultimat3/time": "27.14.1",
|
|
69
69
|
"babel-preset-solid": "^1.9.15"
|
|
70
70
|
}
|
|
71
71
|
}
|
package/src/ci-runs.ts
CHANGED
|
@@ -121,11 +121,17 @@ export function latestPerWorkflow(runs: readonly CiRun[]): readonly CiRun[] {
|
|
|
121
121
|
return [...newest.values()];
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The branch's runs, newest first. `filters` narrows the ask to one workflow or one event; with
|
|
126
|
+
* neither given, the argv is byte-for-byte the one `x ci` has always sent — cmd-ci.test.ts pins
|
|
127
|
+
* it, so a filter may only ever insert its pair between `--branch` and `--limit`.
|
|
128
|
+
*/
|
|
124
129
|
export async function listRuns(
|
|
125
130
|
host: GhHost,
|
|
126
131
|
repo: GhRepo,
|
|
127
132
|
branch: string,
|
|
128
133
|
limit: number,
|
|
134
|
+
filters?: { readonly workflow?: string | undefined; readonly event?: string | undefined },
|
|
129
135
|
): Promise<readonly CiRun[]> {
|
|
130
136
|
const rows = await ghJson(
|
|
131
137
|
host,
|
|
@@ -136,6 +142,8 @@ export async function listRuns(
|
|
|
136
142
|
repo.slug,
|
|
137
143
|
'--branch',
|
|
138
144
|
branch,
|
|
145
|
+
...(filters?.workflow === undefined ? [] : ['--workflow', filters.workflow]),
|
|
146
|
+
...(filters?.event === undefined ? [] : ['--event', filters.event]),
|
|
139
147
|
'--limit',
|
|
140
148
|
String(limit),
|
|
141
149
|
'--json',
|
package/src/cmd-ci-spec.ts
CHANGED
|
@@ -6,10 +6,17 @@ import type { CommandSpec } from './parse';
|
|
|
6
6
|
/** Log lines kept per failed job. Enough to hold a findings block, short enough to read. */
|
|
7
7
|
export const TAIL_LINES = 40;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The events `--event` accepts, closed: `cmd-ci.ts` refuses every other value against this same
|
|
11
|
+
* constant, so the summary, the refusal and the `gh` filter can never name different sets.
|
|
12
|
+
*/
|
|
13
|
+
export const CI_EVENTS = ['push', 'pull_request', 'workflow_dispatch'] as const;
|
|
14
|
+
|
|
9
15
|
export const ciSpec: CommandSpec = {
|
|
10
16
|
name: 'ci',
|
|
11
17
|
summary: 'the workflow runs for this branch, and the findings inside the failed steps log',
|
|
12
|
-
usage:
|
|
18
|
+
usage:
|
|
19
|
+
'x ci [--branch <name>] [--run <id>] [--repo owner/name] [--tail <n>] [--full] [--workflow <name>] [--event <push|pull_request|workflow_dispatch>] [--json]',
|
|
13
20
|
flags: [
|
|
14
21
|
{ name: 'repo', type: 'string', summary: 'owner/name; the checkout own remote by default' },
|
|
15
22
|
{ name: 'branch', type: 'string', summary: 'branch to read runs for; this one by default' },
|
|
@@ -20,5 +27,15 @@ export const ciSpec: CommandSpec = {
|
|
|
20
27
|
summary: `log lines kept per failed job (default ${TAIL_LINES})`,
|
|
21
28
|
},
|
|
22
29
|
{ name: 'full', type: 'boolean', summary: 'the whole failed-step log, not the tail' },
|
|
30
|
+
{
|
|
31
|
+
name: 'workflow',
|
|
32
|
+
type: 'string',
|
|
33
|
+
summary: 'only this workflow name or filename (e.g. ci.yml)',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'event',
|
|
37
|
+
type: 'string',
|
|
38
|
+
summary: 'only runs of this event (push, pull_request, workflow_dispatch)',
|
|
39
|
+
},
|
|
23
40
|
],
|
|
24
41
|
};
|
package/src/cmd-ci.ts
CHANGED
|
@@ -9,8 +9,9 @@ import type { CiLogLine } from './ci-log';
|
|
|
9
9
|
import { findingKey, findingsFrom, jobsInLog, parseLogLines, tailOf } from './ci-log';
|
|
10
10
|
import type { CiJob, CiRun } from './ci-runs';
|
|
11
11
|
import { failedLog, isFailed, isRunning, latestPerWorkflow, listRuns, viewRun } from './ci-runs';
|
|
12
|
-
import { ciSpec, TAIL_LINES } from './cmd-ci-spec';
|
|
12
|
+
import { CI_EVENTS, ciSpec, TAIL_LINES } from './cmd-ci-spec';
|
|
13
13
|
import type { CliCommand, CommandContext } from './command';
|
|
14
|
+
import { BadFlagError } from './errors';
|
|
14
15
|
import { parseIntFlag } from './flag-number';
|
|
15
16
|
import { commentBlock } from './foreign-text';
|
|
16
17
|
import type { GhRepo } from './gh-target';
|
|
@@ -18,6 +19,7 @@ import { currentBranch, resolveRepo } from './gh-target';
|
|
|
18
19
|
import { msg } from './messages';
|
|
19
20
|
import type { CommandResult, Finding, JsonValue } from './output';
|
|
20
21
|
import { flagBool, flagString } from './parse';
|
|
22
|
+
import { quoteArg } from './shell-quote';
|
|
21
23
|
|
|
22
24
|
export { TAIL_LINES } from './cmd-ci-spec';
|
|
23
25
|
|
|
@@ -43,10 +45,22 @@ export const RUN_LOOKBACK = 20;
|
|
|
43
45
|
|
|
44
46
|
/** No run to triage. The remedy is a branch that has one, or the id of the run in question. */
|
|
45
47
|
export class CiRunNotFoundError extends UltimateError {
|
|
46
|
-
constructor(input: {
|
|
48
|
+
constructor(input: {
|
|
49
|
+
branch: string;
|
|
50
|
+
repo: string;
|
|
51
|
+
workflow?: string | undefined;
|
|
52
|
+
event?: string | undefined;
|
|
53
|
+
}) {
|
|
54
|
+
// A filter beside --branch makes an empty list two different situations — a branch never
|
|
55
|
+
// pushed, or a filter that matched nothing. Naming what was asked keeps the diagnosis right.
|
|
56
|
+
const given = [
|
|
57
|
+
input.workflow !== undefined ? `--workflow ${input.workflow}` : undefined,
|
|
58
|
+
input.event !== undefined ? `--event ${input.event}` : undefined,
|
|
59
|
+
].filter((clause) => clause !== undefined);
|
|
60
|
+
const matching = given.length > 0 ? ` matching ${given.join(' ')}` : '';
|
|
47
61
|
super({
|
|
48
62
|
code: 'X_CI_RUN_NOT_FOUND',
|
|
49
|
-
cause: `no workflow run on ${input.repo} for branch "${input.branch}"`,
|
|
63
|
+
cause: `no workflow run on ${input.repo} for branch "${input.branch}"${matching}`,
|
|
50
64
|
fix: `x ci --branch main --repo ${input.repo} --json`,
|
|
51
65
|
});
|
|
52
66
|
}
|
|
@@ -55,11 +69,39 @@ export class CiRunNotFoundError extends UltimateError {
|
|
|
55
69
|
const RUN_FLAG = { name: 'run', command: 'ci', min: 1, example: 'x ci --run 32484583944 --json' };
|
|
56
70
|
const TAIL_FLAG = { name: 'tail', command: 'ci', min: 1, example: 'x ci --tail 80 --json' };
|
|
57
71
|
|
|
72
|
+
/**
|
|
73
|
+
* `--event`, read against the spec's own closed set: a typo is a refusal naming the events that
|
|
74
|
+
* exist, never an empty run list a reader would take for a red branch.
|
|
75
|
+
*/
|
|
76
|
+
function readEvent(raw: string | undefined): (typeof CI_EVENTS)[number] | undefined {
|
|
77
|
+
if (raw === undefined) return undefined;
|
|
78
|
+
const known: readonly string[] = CI_EVENTS;
|
|
79
|
+
if (known.includes(raw)) return raw as (typeof CI_EVENTS)[number];
|
|
80
|
+
throw new BadFlagError({
|
|
81
|
+
flag: 'event',
|
|
82
|
+
command: 'ci',
|
|
83
|
+
reason: `${quoteArg(raw)} is not a workflow event — one of ${CI_EVENTS.join(', ')}`,
|
|
84
|
+
fix: 'x ci --event push --json',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
58
88
|
export const ciCommand: CliCommand = {
|
|
59
89
|
spec: ciSpec,
|
|
60
90
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
61
|
-
|
|
91
|
+
// Filters are validated, and crossed with --run, before any subprocess runs: a refusal must
|
|
92
|
+
// not cost a gh round trip, and a filter beside --run would otherwise be silently ignored.
|
|
62
93
|
const rawRun = flagString(ctx.args, 'run');
|
|
94
|
+
const workflow = flagString(ctx.args, 'workflow');
|
|
95
|
+
const event = readEvent(flagString(ctx.args, 'event'));
|
|
96
|
+
if (rawRun !== undefined && (workflow !== undefined || event !== undefined)) {
|
|
97
|
+
throw new BadFlagError({
|
|
98
|
+
flag: workflow !== undefined ? 'workflow' : 'event',
|
|
99
|
+
command: 'ci',
|
|
100
|
+
reason: 'filters the run list, and --run names one run already',
|
|
101
|
+
fix: 'x ci --run <id> --json',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const repo = await resolveRepo(ctx, 'ci', flagString(ctx.args, 'repo'));
|
|
63
105
|
const rawTail = flagString(ctx.args, 'tail');
|
|
64
106
|
const limit = flagBool(ctx.args, 'full')
|
|
65
107
|
? Number.POSITIVE_INFINITY
|
|
@@ -71,8 +113,11 @@ export const ciCommand: CliCommand = {
|
|
|
71
113
|
return report(repo, viewed.run.branch, [await inspect(ctx, repo, viewed, limit)]);
|
|
72
114
|
}
|
|
73
115
|
const branch = flagString(ctx.args, 'branch') ?? (await currentBranch(ctx));
|
|
74
|
-
const runs = latestPerWorkflow(
|
|
75
|
-
|
|
116
|
+
const runs = latestPerWorkflow(
|
|
117
|
+
await listRuns(ctx, repo, branch, RUN_LOOKBACK, { workflow, event }),
|
|
118
|
+
);
|
|
119
|
+
if (runs.length === 0)
|
|
120
|
+
throw new CiRunNotFoundError({ branch, repo: repo.slug, workflow, event });
|
|
76
121
|
const inspected: RunReport[] = [];
|
|
77
122
|
for (const run of runs) {
|
|
78
123
|
// Only a failed run is opened. A green run's jobs are 35 rows saying `success`, and fetching
|
package/src/serve-retire.ts
CHANGED
|
@@ -37,8 +37,10 @@ const NOTHING: WorkerRetire = Object.freeze({ adopt: () => undefined, disarm: ()
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Listen for `RETIRE_SIGNAL` NOW, before the boot: Bun's default for an unhandled SIGUSR2 ends the
|
|
40
|
-
* process
|
|
41
|
-
*
|
|
40
|
+
* process — except as PID 1 (a container's ENTRYPOINT), where the kernel DROPS a signal with no
|
|
41
|
+
* listener, so a `preStop` that lands before this line runs is lost. The chart's preStop therefore
|
|
42
|
+
* re-sends it every 5 s until PID 1 is gone; this listener waits for the worker and then retires it.
|
|
43
|
+
* One retire per process; a second signal joins it.
|
|
42
44
|
*
|
|
43
45
|
* Exits rather than holds, decided 2026-10-07: the `preStop` has no channel back but PID 1 itself,
|
|
44
46
|
* so "retired" has to be observable as "PID 1 is gone" — and a pod that has stopped claiming serves
|
|
@@ -172,7 +172,9 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
172
172
|
The worker's retire, opt-in: roles.worker.retireSeconds > 0. The preStop sends PID 1 SIGUSR2 —
|
|
173
173
|
the framework's retire: stop claiming, finish every held job however long it takes, abort
|
|
174
174
|
nothing, then drain and exit 0 — and waits until PID 1 is gone, so the SIGTERM that follows finds
|
|
175
|
-
nothing left to cut off.
|
|
175
|
+
nothing left to cut off. It re-sends the signal every 5 s while it waits: PID 1 has no default
|
|
176
|
+
signal action, so a preStop that lands before the boot arms the listener is dropped by the kernel
|
|
177
|
+
(the pod then idles to its grace); a repeat joins the retire in flight. bun -e, never kill/sleep: bun is the image's ENTRYPOINT (exec form, so it
|
|
176
178
|
IS PID 1). The kubelet counts the preStop against the grace period, so retireSeconds is added to
|
|
177
179
|
it and is the longest a retire may take.
|
|
178
180
|
*/}}
|
|
@@ -183,7 +185,7 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
183
185
|
command:
|
|
184
186
|
- bun
|
|
185
187
|
- -e
|
|
186
|
-
- "process.kill(1, 'SIGUSR2'); for (
|
|
188
|
+
- "process.kill(1, 'SIGUSR2'); for (let n = 1; ; n++) { try { process.kill(1, n % 5 === 0 ? 'SIGUSR2' : 0); } catch { break; } await Bun.sleep(1000); }"
|
|
187
189
|
{{- end }}
|
|
188
190
|
{{- end }}
|
|
189
191
|
resources: {{- toYaml $cfg.resources | nindent 4 }}
|