@vzn/vx-ci 0.0.651 → 0.0.653
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 +11 -6
- package/package.json +2 -2
- package/src/setup.ts +15 -7
- package/src/summary.ts +67 -14
package/README.md
CHANGED
|
@@ -24,22 +24,27 @@ export default defineWorkspace({
|
|
|
24
24
|
That's the whole setup. On a GitHub Actions runner (`GITHUB_STEP_SUMMARY`
|
|
25
25
|
set) every `vx run` appends a summary block, on its own line after
|
|
26
26
|
anything already written in the step: verdict headline, stats
|
|
27
|
-
(tasks / executed / cache hits / duration, and the ideal time an executor says
|
|
28
|
-
the run could have taken, as `@vzn/vx-dte` replays it),
|
|
27
|
+
(tasks / executed / cache hits / aborted and not run when a stop cut the run short / duration, and the ideal time an executor says
|
|
28
|
+
the run could have taken, as `@vzn/vx-dte` replays it), the workers
|
|
29
|
+
that would have run it best (`@vzn/vx-dte`'s "Workers to declare"), failures called out above the
|
|
29
30
|
per-task table (rows by status, failures first, then the longest first) with their exit code, the signal an exit above 128
|
|
30
31
|
stands for (`exit 137 (128 + SIGKILL)`, as the run's own frame and
|
|
31
32
|
`vx last` say it), a timeout as `timed out, exit 143`, a persistent task
|
|
32
|
-
that never became ready as `never ready (timed out)`, a
|
|
33
|
-
|
|
33
|
+
that never became ready as `never ready (timed out)`, a hit whose restore
|
|
34
|
+
failed as `cache restore failed` (a failure, not executed and not a hit), a task whose
|
|
35
|
+
command exited 0 but whose save vx refused as `cache save failed`, a sandboxed task's
|
|
36
|
+
violation count, and the tasks each failure blocked. A task that retried
|
|
37
|
+
or proved flaky carries the run's own row mark after its status
|
|
38
|
+
(`✅ ran · flaky - failed 1× before · 2 attempts`). A server a
|
|
34
39
|
DTE run carried to its dependants' workers reads `served`,
|
|
35
|
-
with no duration of its own. When an executor
|
|
40
|
+
with no duration of its own, as a skipped task has none. When an executor
|
|
36
41
|
ran the run on other machines (`@vzn/vx-dte`'s workers), a Workers table
|
|
37
42
|
shows each one's status, when it was asked for, its total time (warm-up
|
|
38
43
|
plus online), its warm-up, its time online after the warm-up and the part
|
|
39
44
|
of that with no task on it (a task counts from its send to its result,
|
|
40
45
|
restore and save included), its tasks, the cores
|
|
41
46
|
its tasks were expected to use (their last run's) beside the cores it used, its peak RAM of its memory, and
|
|
42
|
-
its utilization, under a line
|
|
47
|
+
its machine's cpu utilization from its first task's send to its last one's end, under a line
|
|
43
48
|
of the expected and used share of all their core-time (the expected share over the workers whose executor reports one). Tasks the executor left to the run's own machine get a `this machine` row, and a line splits the executed count between workers and this machine. A footer line
|
|
44
49
|
names the vx version, the command (what follows `--` counted, not
|
|
45
50
|
quoted), tasks passed and outputs restored.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-ci",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.653",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "GitHub Actions and GitLab CI integration for vx — a job summary on Actions and the branch cache scope on both.",
|
|
6
6
|
"keywords": [
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@vzn/vx": "^0.0.
|
|
36
|
+
"@vzn/vx": "^0.0.653"
|
|
37
37
|
},
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
package/src/setup.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// verb is the plugin's, so a repo on neither CI has none.
|
|
6
6
|
|
|
7
7
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
8
|
-
import { mkdir, writeFile } from 'node:fs/promises'
|
|
8
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
9
9
|
import os from 'node:os'
|
|
10
10
|
import path from 'node:path'
|
|
11
11
|
import { ui, UserError } from '@vzn/vx'
|
|
@@ -124,12 +124,20 @@ export async function setup(o: SetupOptions): Promise<number> {
|
|
|
124
124
|
await run(['apparmor_parser', '-R', '/etc/apparmor.d/bwrap-userns-restrict'])
|
|
125
125
|
await run(['rm', '/etc/apparmor.d/bwrap-userns-restrict'])
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
// A fresh directory, only this user's: at tmpdir/vx-bwrap-<pid>.apparmor,
|
|
128
|
+
// a guessable name, a planted symlink took the write and root copied
|
|
129
|
+
// whatever that file held into /etc/apparmor.d.
|
|
130
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), 'vx-bwrap-'))
|
|
131
|
+
try {
|
|
132
|
+
const profile = path.join(dir, 'bwrap.apparmor')
|
|
133
|
+
await writeFile(
|
|
134
|
+
profile,
|
|
135
|
+
'abi <abi/4.0>,\ninclude <tunables/global>\nprofile bwrap /usr/bin/bwrap flags=(unconfined) {\n userns,\n}\n',
|
|
136
|
+
)
|
|
137
|
+
await run(['cp', profile, '/etc/apparmor.d/bwrap'])
|
|
138
|
+
} finally {
|
|
139
|
+
await rm(dir, { recursive: true, force: true })
|
|
140
|
+
}
|
|
133
141
|
await run(['apparmor_parser', '-r', '/etc/apparmor.d/bwrap'])
|
|
134
142
|
}
|
|
135
143
|
// GitLab keeps it in the checkout, where a root task's `**` keyed the
|
package/src/summary.ts
CHANGED
|
@@ -43,6 +43,30 @@ function statusLabel(t: TaskTelemetry): string {
|
|
|
43
43
|
return STATUS_LABEL[t.status] ?? t.status
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* A retried or flaky task's mark, in the words of the run's own row
|
|
48
|
+
* (`flaky - failed 1× before · 2 attempts`): a pass that needed a retry
|
|
49
|
+
* read as a plain `ran`. Numbers only, so no escape.
|
|
50
|
+
*/
|
|
51
|
+
function flakyMark(t: TaskTelemetry): string {
|
|
52
|
+
const attempts = (t.attempts ?? 1) > 1 ? `${t.attempts} attempts` : undefined
|
|
53
|
+
if (t.flaky === undefined) return attempts === undefined ? '' : ` · ${attempts}`
|
|
54
|
+
const said: string[] = []
|
|
55
|
+
if (t.status === 'failed' && t.flaky.passes > 0) said.push(`passed ${t.flaky.passes}× before`)
|
|
56
|
+
if (t.status !== 'failed' && t.flaky.failures > 0) said.push(`failed ${t.flaky.failures}× before`)
|
|
57
|
+
if (attempts !== undefined) said.push(attempts)
|
|
58
|
+
return said.length === 0 ? ' · flaky' : ` · flaky - ${said.join(' · ')}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether the task ran its command here or on a worker. A carried server ran
|
|
63
|
+
* inside its dependants, on their worker; a failure in no cache column
|
|
64
|
+
* (`cacheSource: 'none'`) is a hit whose restore failed, which ran nothing.
|
|
65
|
+
*/
|
|
66
|
+
const executed = (t: TaskTelemetry): boolean =>
|
|
67
|
+
(t.status === 'success' || (t.status === 'failed' && t.cacheSource !== 'none')) &&
|
|
68
|
+
t.carried !== true
|
|
69
|
+
|
|
46
70
|
/** Rows by status label, in this order; any other after them. */
|
|
47
71
|
const ORDER = [
|
|
48
72
|
'❌ failed',
|
|
@@ -143,22 +167,21 @@ function codeSpan(raw: string): string {
|
|
|
143
167
|
/** Render the whole job summary. Deterministic for a given record. */
|
|
144
168
|
export function renderJobSummary(summary: RunSummaryRecord, title = 'vx run'): string {
|
|
145
169
|
const failed = summary.tasks.filter((t) => t.status === 'failed')
|
|
146
|
-
const
|
|
170
|
+
const notRun = summary.notRunCount ?? 0
|
|
171
|
+
const cancelled =
|
|
172
|
+
!summary.exitOk && summary.failedCount === 0 && summary.abortedCount + notRun > 0
|
|
147
173
|
const verdict = summary.exitOk ? '✅' : cancelled ? '⏹️' : '❌'
|
|
148
174
|
const lines: string[] = []
|
|
149
175
|
lines.push(`## ${verdict} ${title}`)
|
|
150
176
|
lines.push('')
|
|
151
|
-
// A carried server ran inside its dependants, on their worker.
|
|
152
|
-
const executed = summary.tasks.filter(
|
|
153
|
-
(t) => (t.status === 'success' || t.status === 'failed') && t.carried !== true,
|
|
154
|
-
).length
|
|
155
177
|
const stats = [
|
|
156
178
|
`**${summary.taskCount}** task${summary.taskCount === 1 ? '' : 's'}`,
|
|
157
|
-
`**${executed}** executed`,
|
|
179
|
+
`**${summary.tasks.filter(executed).length}** executed`,
|
|
158
180
|
`**${summary.hitCount}** cache hit${summary.hitCount === 1 ? '' : 's'}` +
|
|
159
181
|
(summary.hitCount > 0 ? ` ${hitSplit(summary)}` : ''),
|
|
160
182
|
...(summary.failedCount > 0 ? [`**${summary.failedCount}** failed`] : []),
|
|
161
183
|
...(summary.abortedCount > 0 ? [`**${summary.abortedCount}** aborted`] : []),
|
|
184
|
+
...(notRun > 0 ? [`**${notRun}** not run`] : []),
|
|
162
185
|
// The run played perfectly on what each task took, beside the real time (2026-10-23).
|
|
163
186
|
summary.idealMs === undefined
|
|
164
187
|
? fmtMs(summary.totalDurationMs)
|
|
@@ -189,7 +212,7 @@ export function renderJobSummary(summary: RunSummaryRecord, title = 'vx run'): s
|
|
|
189
212
|
const signal = exitSignal(t.exitCode)
|
|
190
213
|
const blocked = blockedBy.get(t.taskId) ?? []
|
|
191
214
|
lines.push(
|
|
192
|
-
`- **${escapeMarkdownCell(escapeInline(t.taskId))}** — ${t.notReady !== undefined ? `never ready (${t.notReady === 'timeout' ? 'timed out' : t.notReady === 'exited' ? 'exited' : 'spawn failed'}), exit ${t.exitCode}` : t.timedOut === true ? `timed out, exit ${t.exitCode}` : `exit ${t.exitCode}${signal === undefined ? '' : ` (128 + ${signal})`}`}${t.sandboxViolations !== undefined && t.sandboxViolations > 0 ? ` · ${t.sandboxViolations} sandbox violation${t.sandboxViolations === 1 ? '' : 's'}` : ''}${blocked.length > 0 ? ` · blocked ${blocked.join(', ')}` : ''}`,
|
|
215
|
+
`- **${escapeMarkdownCell(escapeInline(t.taskId))}** — ${t.cacheSource === 'none' ? 'cache restore failed' : t.notReady !== undefined ? `never ready (${t.notReady === 'timeout' ? 'timed out' : t.notReady === 'exited' ? 'exited' : 'spawn failed'}), exit ${t.exitCode}` : t.timedOut === true ? `timed out, exit ${t.exitCode}` : t.exitCode === 0 ? 'cache save failed' : `exit ${t.exitCode}${signal === undefined ? '' : ` (128 + ${signal})`}`}${t.sandboxViolations !== undefined && t.sandboxViolations > 0 ? ` · ${t.sandboxViolations} sandbox violation${t.sandboxViolations === 1 ? '' : 's'}` : ''}${blocked.length > 0 ? ` · blocked ${blocked.join(', ')}` : ''}`,
|
|
193
216
|
)
|
|
194
217
|
}
|
|
195
218
|
lines.push('')
|
|
@@ -198,6 +221,9 @@ export function renderJobSummary(summary: RunSummaryRecord, title = 'vx run'): s
|
|
|
198
221
|
if (summary.machines !== undefined && summary.machines.length > 0) {
|
|
199
222
|
lines.push(...renderWorkers(summary.machines, summary.tasks), '')
|
|
200
223
|
}
|
|
224
|
+
if (summary.advice !== undefined && summary.advice.length > 0) {
|
|
225
|
+
lines.push(...renderAdvice(summary.advice), '')
|
|
226
|
+
}
|
|
201
227
|
|
|
202
228
|
const header = ['Task', 'Status', 'Duration']
|
|
203
229
|
lines.push(`| ${header.join(' | ')} |`)
|
|
@@ -212,8 +238,8 @@ export function renderJobSummary(summary: RunSummaryRecord, title = 'vx run'): s
|
|
|
212
238
|
for (const t of ordered) {
|
|
213
239
|
const cells = [
|
|
214
240
|
escapeMarkdownCell(escapeInline(t.taskId)),
|
|
215
|
-
statusLabel(t),
|
|
216
|
-
t.carried === true ? '' : fmtMs(t.durationMs),
|
|
241
|
+
statusLabel(t) + flakyMark(t),
|
|
242
|
+
t.carried === true || t.status === 'skipped' ? '' : fmtMs(t.durationMs),
|
|
217
243
|
]
|
|
218
244
|
lines.push(`| ${cells.join(' | ')} |`)
|
|
219
245
|
}
|
|
@@ -237,6 +263,36 @@ const tasksCell = (ok: number, failed: number): string =>
|
|
|
237
263
|
|
|
238
264
|
const pct = (x: number): string => `${Math.round(x * 100)}%`
|
|
239
265
|
|
|
266
|
+
/** What each `WorkerAdvice` played. */
|
|
267
|
+
const ADVICE = {
|
|
268
|
+
declared: 'The fewest of the declared sizes',
|
|
269
|
+
more: "Past the sizes' max",
|
|
270
|
+
any: 'Any cores, least core time',
|
|
271
|
+
} as const
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The workers that would have run it best (2026-11-05: "So I can add more
|
|
275
|
+
* or change their specs"): played again from what each task took.
|
|
276
|
+
*/
|
|
277
|
+
function renderAdvice(advice: NonNullable<RunSummaryRecord['advice']>): string[] {
|
|
278
|
+
const lines = [
|
|
279
|
+
'### Workers to declare',
|
|
280
|
+
'',
|
|
281
|
+
'| Played on | Workers | Ends at |',
|
|
282
|
+
'| --- | --- | --- |',
|
|
283
|
+
]
|
|
284
|
+
for (const a of advice)
|
|
285
|
+
lines.push(
|
|
286
|
+
`| ${ADVICE[a.of]} | ${a.workers
|
|
287
|
+
.map(
|
|
288
|
+
(w) =>
|
|
289
|
+
`${w.count} × ${w.size === undefined ? '' : `${escapeMarkdownCell(escapeInline(w.size))} `}(${w.cores} cores, ${gb(w.memoryBytes)} GB${w.platform === undefined ? '' : `, ${escapeMarkdownCell(escapeInline(w.platform))}`})`,
|
|
290
|
+
)
|
|
291
|
+
.join(' + ')} | ${fmtMs(a.endMs)} |`,
|
|
292
|
+
)
|
|
293
|
+
return lines
|
|
294
|
+
}
|
|
295
|
+
|
|
240
296
|
/**
|
|
241
297
|
* The workers an executor ran the run on (vx-dte's): the cores its tasks
|
|
242
298
|
* booked beside the cores they used, each averaged over the time the
|
|
@@ -285,11 +341,8 @@ function renderWorkers(
|
|
|
285
341
|
)
|
|
286
342
|
}
|
|
287
343
|
// Nothing runs on a distributed run's own machine (2026-10-23):
|
|
288
|
-
// a task that did is named, a bug to see.
|
|
289
|
-
|
|
290
|
-
const ran = tasks.filter(
|
|
291
|
-
(t) => (t.status === 'success' || t.status === 'failed') && t.carried !== true,
|
|
292
|
-
)
|
|
344
|
+
// a task that did is named, a bug to see.
|
|
345
|
+
const ran = tasks.filter(executed)
|
|
293
346
|
const here = ran.filter((t) => t.where === undefined)
|
|
294
347
|
if (here.length > 0) {
|
|
295
348
|
const failed = here.filter((t) => t.status === 'failed').length
|