@vzn/vx-ci 0.0.652 → 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 CHANGED
@@ -24,23 +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
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
28
  the run could have taken, as `@vzn/vx-dte` replays it), the workers
29
29
  that would have run it best (`@vzn/vx-dte`'s "Workers to declare"), failures called out above the
30
30
  per-task table (rows by status, failures first, then the longest first) with their exit code, the signal an exit above 128
31
31
  stands for (`exit 137 (128 + SIGKILL)`, as the run's own frame and
32
32
  `vx last` say it), a timeout as `timed out, exit 143`, a persistent task
33
- that never became ready as `never ready (timed out)`, a sandboxed task's
34
- violation count, and the tasks each failure blocked. A server a
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
35
39
  DTE run carried to its dependants' workers reads `served`,
36
- 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
37
41
  ran the run on other machines (`@vzn/vx-dte`'s workers), a Workers table
38
42
  shows each one's status, when it was asked for, its total time (warm-up
39
43
  plus online), its warm-up, its time online after the warm-up and the part
40
44
  of that with no task on it (a task counts from its send to its result,
41
45
  restore and save included), its tasks, the cores
42
46
  its tasks were expected to use (their last run's) beside the cores it used, its peak RAM of its memory, and
43
- 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
44
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
45
49
  names the vx version, the command (what follows `--` counted, not
46
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.652",
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.652"
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
- const profile = path.join(os.tmpdir(), `vx-bwrap-${process.pid}.apparmor`)
128
- await writeFile(
129
- profile,
130
- 'abi <abi/4.0>,\ninclude <tunables/global>\nprofile bwrap /usr/bin/bwrap flags=(unconfined) {\n userns,\n}\n',
131
- )
132
- await run(['cp', profile, '/etc/apparmor.d/bwrap'])
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 cancelled = !summary.exitOk && summary.failedCount === 0 && summary.abortedCount > 0
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('')
@@ -215,8 +238,8 @@ export function renderJobSummary(summary: RunSummaryRecord, title = 'vx run'): s
215
238
  for (const t of ordered) {
216
239
  const cells = [
217
240
  escapeMarkdownCell(escapeInline(t.taskId)),
218
- statusLabel(t),
219
- t.carried === true ? '' : fmtMs(t.durationMs),
241
+ statusLabel(t) + flakyMark(t),
242
+ t.carried === true || t.status === 'skipped' ? '' : fmtMs(t.durationMs),
220
243
  ]
221
244
  lines.push(`| ${cells.join(' | ')} |`)
222
245
  }
@@ -318,11 +341,8 @@ function renderWorkers(
318
341
  )
319
342
  }
320
343
  // Nothing runs on a distributed run's own machine (2026-10-23):
321
- // a task that did is named, a bug to see. A carried server ran inside its
322
- // dependants, on their worker.
323
- const ran = tasks.filter(
324
- (t) => (t.status === 'success' || t.status === 'failed') && t.carried !== true,
325
- )
344
+ // a task that did is named, a bug to see.
345
+ const ran = tasks.filter(executed)
326
346
  const here = ran.filter((t) => t.where === undefined)
327
347
  if (here.length > 0) {
328
348
  const failed = here.filter((t) => t.status === 'failed').length