@vzn/vx 0.0.16 → 0.0.21
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/LICENSE +1 -1
- package/README.md +135 -124
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +11 -7
- package/src/bin.ts +14 -4
- package/src/cache/archive.ts +659 -0
- package/src/cache/cache.ts +769 -1101
- package/src/cache/chained-cache.ts +194 -0
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +24 -5
- package/src/cache/inputs.ts +373 -456
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +96 -43
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/tar-stream.ts +358 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +75 -21
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +195 -128
- package/src/cli/index.ts +116 -46
- package/src/cli/info.ts +122 -63
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +172 -0
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +72 -6
- package/src/cli/plugin-commands.ts +103 -0
- package/src/cli/run.ts +245 -271
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +9 -7
- package/src/cli/watch.ts +858 -110
- package/src/cli/why.ts +223 -0
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +187 -150
- package/src/exec/env.ts +18 -1
- package/src/exec/executor.ts +224 -0
- package/src/exec/index.ts +24 -2
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +166 -40
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +642 -260
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +4 -10
- package/src/graph/scheduler.ts +232 -203
- package/src/graph/task-graph.ts +222 -8
- package/src/index.ts +146 -153
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +147 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +72 -8
- package/src/orchestrator/execute-task.ts +374 -549
- package/src/orchestrator/failure-mode.ts +202 -0
- package/src/orchestrator/framed-output.ts +126 -69
- package/src/orchestrator/history.ts +126 -59
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +77 -93
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +208 -35
- package/src/orchestrator/metrics.ts +102 -1388
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +55 -34
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +357 -120
- package/src/orchestrator/plugin.ts +298 -58
- package/src/orchestrator/prepare.ts +202 -106
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/run-artifacts.ts +63 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +74 -47
- package/src/orchestrator/run.ts +534 -417
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +129 -31
- package/src/orchestrator/status-line.ts +49 -9
- package/src/orchestrator/summary.ts +90 -26
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +111 -15
- package/src/orchestrator/task-log-buffer.ts +288 -0
- package/src/orchestrator/telemetry-host.ts +34 -6
- package/src/orchestrator/telemetry.ts +182 -36
- package/src/orchestrator/upstream.ts +72 -31
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +70 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +15 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +64 -0
- package/src/util/settle.ts +67 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +250 -23
- package/src/workspace/config-cache.ts +313 -0
- package/src/workspace/config-eval.ts +217 -0
- package/src/workspace/config-imports.ts +162 -0
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +65 -10
- package/src/workspace/fingerprint.ts +53 -4
- package/src/workspace/index.ts +42 -3
- package/src/workspace/lockfile.ts +25 -9
- package/src/workspace/migrate-scripts.ts +134 -0
- package/src/workspace/migration.ts +314 -0
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +114 -41
- package/src/workspace/project-loader.ts +224 -450
- package/src/workspace/workspace.ts +298 -72
- package/src/cache/cas-backend.ts +0 -97
- package/src/cache/digest.ts +0 -47
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/cli/migrate-nx.ts +0 -450
- package/src/cli/migrate-turbo.ts +0 -364
- package/src/cli/migrate.ts +0 -219
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/resources.ts +0 -59
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/verify.ts +0 -164
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
|
@@ -3,6 +3,7 @@ import { detectColors, type ColorSupport } from './colors.js'
|
|
|
3
3
|
import {
|
|
4
4
|
formatFrameClose,
|
|
5
5
|
formatFrameOpen,
|
|
6
|
+
formatPersistentTailBlock,
|
|
6
7
|
formatTaskBlock,
|
|
7
8
|
formatTaskExecutedLine,
|
|
8
9
|
formatTaskHitLine,
|
|
@@ -17,6 +18,9 @@ import {
|
|
|
17
18
|
} from './status-line.js'
|
|
18
19
|
import { formatDuration, formatSummarySection, type RunContext } from './summary.js'
|
|
19
20
|
import { isGroupTask } from '../graph/index.js'
|
|
21
|
+
import { appendTail, createTail, resetTail, tailText, type Tail } from '../util/index.js'
|
|
22
|
+
import { isCacheHit } from './telemetry.js'
|
|
23
|
+
import { outcomeWord } from './events.js'
|
|
20
24
|
|
|
21
25
|
export interface Logger {
|
|
22
26
|
/** Header / footer / status text. Written verbatim, one trailing \n added. */
|
|
@@ -41,6 +45,8 @@ export interface Logger {
|
|
|
41
45
|
requestedCount?: number
|
|
42
46
|
/** Run banner context — so the live region footer matches the final summary. */
|
|
43
47
|
context?: RunContext
|
|
48
|
+
/** The run's canonical start (epoch ms) — equals the final summary's `startedAt`. */
|
|
49
|
+
startedAtMs?: number
|
|
44
50
|
}): void
|
|
45
51
|
taskStart?(node: TaskNode): void
|
|
46
52
|
/** Run finished (any outcome). Idempotent. */
|
|
@@ -56,6 +62,11 @@ export interface Logger {
|
|
|
56
62
|
* Today's CI behavior; also the programmatic default.
|
|
57
63
|
* errors-only — only failed tasks print.
|
|
58
64
|
* none — no per-task output at all.
|
|
65
|
+
* hash-only — one line per task: outcome word, task id, cache key.
|
|
66
|
+
* No frames, no log replay (Turbo `--output-logs
|
|
67
|
+
* hash-only` parity); the end-of-run summary still
|
|
68
|
+
* renders. The line is the run's audit trail: which key
|
|
69
|
+
* each task resolved to, without any build output.
|
|
59
70
|
* focused — requested nodes stream raw output live (running the
|
|
60
71
|
* task should feel like running the command directly);
|
|
61
72
|
* dependency-pulled nodes are silent unless they fail.
|
|
@@ -71,7 +82,7 @@ export interface Logger {
|
|
|
71
82
|
* line even if stdout happens to be a TTY.
|
|
72
83
|
*/
|
|
73
84
|
export interface OutputView {
|
|
74
|
-
mode: 'full' | 'errors-only' | 'none' | 'focused' | 'broad'
|
|
85
|
+
mode: 'full' | 'errors-only' | 'none' | 'focused' | 'broad' | 'hash-only'
|
|
75
86
|
gha?: boolean
|
|
76
87
|
ci?: boolean
|
|
77
88
|
}
|
|
@@ -81,22 +92,47 @@ function truthyEnv(v: string | undefined): boolean {
|
|
|
81
92
|
return v !== undefined && v !== '' && v !== '0' && v !== 'false'
|
|
82
93
|
}
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
// ── GitHub Actions workflow-command escaping ────────────────────────
|
|
96
|
+
// The runner parses `::`-prefixed LINES out of the log, so anything
|
|
97
|
+
// interpolated into one has to be encoded or it changes the command's shape.
|
|
98
|
+
// Both halves are reachable without malice: a task name is an arbitrary TS
|
|
99
|
+
// object key (the loader accepts newlines — `run-report.ts` already escapes
|
|
100
|
+
// them for the same reason), and a task can legitimately print
|
|
101
|
+
// command-shaped text (a test asserting on annotation formatting, a nested
|
|
102
|
+
// tool emitting its own workflow commands).
|
|
103
|
+
// https://docs.github.com/actions/reference/workflow-commands-for-github-actions
|
|
104
|
+
|
|
105
|
+
/** Message text. `%` first, or we would re-escape our own escapes. */
|
|
106
|
+
function ghaData(value: string): string {
|
|
107
|
+
return value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A property VALUE additionally reserves `:` and `,` — the delimiters
|
|
112
|
+
* between a command's name, its properties and its message. Without this an
|
|
113
|
+
* id containing `::` truncates the annotation mid-title.
|
|
114
|
+
*/
|
|
115
|
+
function ghaProperty(value: string): string {
|
|
116
|
+
return ghaData(value).replace(/:/g, '%3A').replace(/,/g, '%2C')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Fence untrusted text so the runner stops interpreting workflow commands
|
|
121
|
+
* inside it. `token` must be unguessable by the fenced text itself — a fixed
|
|
122
|
+
* one could simply be printed by the task — so it is a per-run UUID.
|
|
123
|
+
*/
|
|
124
|
+
function ghaFence(body: string, token: string): string {
|
|
125
|
+
return `::stop-commands::${token}\n${body}::${token}::\n`
|
|
96
126
|
}
|
|
97
127
|
|
|
128
|
+
const cacheWordOf = (n: TaskNode): 'miss' | 'no-cache' =>
|
|
129
|
+
n.config.cache === undefined ? 'no-cache' : 'miss'
|
|
130
|
+
|
|
98
131
|
export function resolveOutputView(
|
|
99
|
-
options: {
|
|
132
|
+
options: {
|
|
133
|
+
outputLogs?: 'full' | 'errors-only' | 'none' | 'hash-only'
|
|
134
|
+
flow?: 'focused' | 'broad'
|
|
135
|
+
},
|
|
100
136
|
env: Record<string, string | undefined> = process.env,
|
|
101
137
|
): OutputView {
|
|
102
138
|
const ci = truthyEnv(env['CI'])
|
|
@@ -136,8 +172,6 @@ export function defaultLogger(
|
|
|
136
172
|
// a one-liner or streamed output since the last block does.
|
|
137
173
|
let lineEmitted = false
|
|
138
174
|
let streamedSinceBlock = false
|
|
139
|
-
// Ids whose output went straight to the terminal (focused mode).
|
|
140
|
-
const streamed = new Set<string>()
|
|
141
175
|
// True while the live stream sits mid-line (chunk without trailing
|
|
142
176
|
// newline) — the frame close must not glue onto partial output.
|
|
143
177
|
let streamMidLine = false
|
|
@@ -153,6 +187,28 @@ export function defaultLogger(
|
|
|
153
187
|
return arr.length === 1 ? arr[0]! : arr.join('')
|
|
154
188
|
}
|
|
155
189
|
|
|
190
|
+
// `none` is the ONE mode whose contract guarantees the output is
|
|
191
|
+
// discarded — "no per-task output at all" (docs/cli.md) — so its chunks
|
|
192
|
+
// are dropped on arrival instead of accumulated and thrown away at
|
|
193
|
+
// `taskComplete`. Buffering output the mode promises never to print made
|
|
194
|
+
// `--output-logs none`, the obvious mitigation for a chatty task, cost
|
|
195
|
+
// exactly as much memory as printing it: 400 MB of task stdout drove the
|
|
196
|
+
// vx process to 852 MiB RSS, unbounded and linear, and a warm CACHE HIT
|
|
197
|
+
// paid it too (the stored stdout is replayed through this same path).
|
|
198
|
+
//
|
|
199
|
+
// Deliberately ONLY the modes that PROMISE never to print task output —
|
|
200
|
+
// `none` and `hash-only`. The boundary is the point: `full`, `broad`,
|
|
201
|
+
// `focused` and `errors-only` all PRINT this output — for `errors-only` a
|
|
202
|
+
// failed task's log is precisely what the user asked to see — and
|
|
203
|
+
// silently truncating someone's build log is a worse failure than the
|
|
204
|
+
// memory it costs. So those modes stay unbounded by decision, not by
|
|
205
|
+
// oversight.
|
|
206
|
+
const discardsOutput = view.mode === 'none' || view.mode === 'hash-only'
|
|
207
|
+
|
|
208
|
+
// One per run, and only in GHA mode: the fence token must be something the
|
|
209
|
+
// fenced task output cannot print.
|
|
210
|
+
const ghaToken = view.gha === true ? crypto.randomUUID() : ''
|
|
211
|
+
|
|
156
212
|
// All stdout flows through the writer so the status line can never
|
|
157
213
|
// interleave with content. Inert (pure passthrough) on non-TTY
|
|
158
214
|
// streams and in CI.
|
|
@@ -189,6 +245,8 @@ export function defaultLogger(
|
|
|
189
245
|
let slots: (WorkerSlot | null)[] = []
|
|
190
246
|
const slotQueue: WorkerSlot[] = []
|
|
191
247
|
let succeeded = 0
|
|
248
|
+
// Executed with no `cache` block: never a miss, counted apart.
|
|
249
|
+
let noCache = 0
|
|
192
250
|
let upToDate = 0
|
|
193
251
|
let restoredLocal = 0
|
|
194
252
|
let restoredRemote = 0
|
|
@@ -207,6 +265,28 @@ export function defaultLogger(
|
|
|
207
265
|
const deferredFailures: string[] = []
|
|
208
266
|
const pinnedPersistent: string[] = []
|
|
209
267
|
let flushedFailures = false
|
|
268
|
+
// A persistent task (dev server, watcher, daemon) is the one task kind
|
|
269
|
+
// nothing bounds: a one-shot command's output ends when it exits, but a
|
|
270
|
+
// server writes for as long as the run lasts. So its chunks route into a
|
|
271
|
+
// bounded tail for its WHOLE life, not the unbounded per-task buffers —
|
|
272
|
+
// registered at `taskStart` and drained at `taskComplete` for the frame,
|
|
273
|
+
// then reset to keep capturing what it writes after its outcome landed
|
|
274
|
+
// (which arrives at READY, while the child keeps running) and flushed as
|
|
275
|
+
// one trailing block at runEnd.
|
|
276
|
+
//
|
|
277
|
+
// Registering only at READY was the defect: a `readyWhen` that never
|
|
278
|
+
// matches never reaches `taskComplete`, so the 64 KiB cap never engaged
|
|
279
|
+
// and its output accumulated in the ordinary per-task buffer instead —
|
|
280
|
+
// ~100 MiB/s, with no `exec.timeout` needed to reach it.
|
|
281
|
+
//
|
|
282
|
+
// `outcome` is absent until the task completes. Registration is
|
|
283
|
+
// unconditional (the bound is a memory invariant, not a display choice);
|
|
284
|
+
// only the flush is view-gated.
|
|
285
|
+
const persistentTails = new Map<
|
|
286
|
+
string,
|
|
287
|
+
{ node: TaskNode; outcome?: TaskOutcome; out: Tail; err: Tail }
|
|
288
|
+
>()
|
|
289
|
+
let flushedPersistent = false
|
|
210
290
|
|
|
211
291
|
const refresh = (force: boolean): void => {
|
|
212
292
|
if (statusDead) return
|
|
@@ -221,7 +301,8 @@ export function defaultLogger(
|
|
|
221
301
|
upToDate,
|
|
222
302
|
restoredLocal,
|
|
223
303
|
restoredRemote,
|
|
224
|
-
miss: succeeded + failed,
|
|
304
|
+
miss: succeeded + failed - noCache,
|
|
305
|
+
noCache,
|
|
225
306
|
left: total - done,
|
|
226
307
|
spread:
|
|
227
308
|
spreadCount > 0
|
|
@@ -325,6 +406,11 @@ export function defaultLogger(
|
|
|
325
406
|
emitLine(formatFrameOpen(node, colors))
|
|
326
407
|
return
|
|
327
408
|
}
|
|
409
|
+
// Bound a persistent task's output from its FIRST chunk. Waiting for
|
|
410
|
+
// its outcome would leave a never-ready one uncapped forever.
|
|
411
|
+
if (node.config.exec?.persistent !== undefined && !persistentTails.has(node.id)) {
|
|
412
|
+
persistentTails.set(node.id, { node, out: createTail(), err: createTail() })
|
|
413
|
+
}
|
|
328
414
|
const slot: WorkerSlot = { id: node.id, startedMs: Date.now() }
|
|
329
415
|
const free = slots.indexOf(null)
|
|
330
416
|
if (free >= 0) slots[free] = slot
|
|
@@ -333,6 +419,38 @@ export function defaultLogger(
|
|
|
333
419
|
},
|
|
334
420
|
runEnd() {
|
|
335
421
|
killStatus()
|
|
422
|
+
// Persistent tails first: they are context for whatever ran against
|
|
423
|
+
// the server, so they read above the failures they explain. `none`
|
|
424
|
+
// and `errors-only` state their contracts absolutely (no per-task
|
|
425
|
+
// output / only failed tasks print), so they stay silent — the tail
|
|
426
|
+
// is still captured and still bounded, just never printed. Guarded
|
|
427
|
+
// like the failures below: run() calls runEnd twice on the success
|
|
428
|
+
// path (once before the summary, once in its finally), and a
|
|
429
|
+
// kept-alive child keeps writing between the two.
|
|
430
|
+
if (!flushedPersistent) {
|
|
431
|
+
flushedPersistent = true
|
|
432
|
+
if (view.mode !== 'none' && view.mode !== 'errors-only') {
|
|
433
|
+
for (const t of persistentTails.values()) {
|
|
434
|
+
// No outcome means the task never completed — it is still
|
|
435
|
+
// pre-ready, so there is no "since ready" window to render and
|
|
436
|
+
// its buffered output belongs to the frame it never got.
|
|
437
|
+
if (t.outcome === undefined) continue
|
|
438
|
+
emitBlock(
|
|
439
|
+
formatPersistentTailBlock(
|
|
440
|
+
t.node,
|
|
441
|
+
t.outcome,
|
|
442
|
+
{ stdout: tailText(t.out), stderr: tailText(t.err) },
|
|
443
|
+
{ stdout: t.out.dropped, stderr: t.err.dropped },
|
|
444
|
+
colors,
|
|
445
|
+
),
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
for (const t of persistentTails.values()) {
|
|
450
|
+
resetTail(t.out)
|
|
451
|
+
resetTail(t.err)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
336
454
|
// Failures end the log: every deferred frame replays here, right
|
|
337
455
|
// above the summary — the ✗ one-liners marked them in the
|
|
338
456
|
// stream, the full diagnostics read last where eyes land.
|
|
@@ -343,28 +461,50 @@ export function defaultLogger(
|
|
|
343
461
|
}
|
|
344
462
|
},
|
|
345
463
|
taskStdout(node, chunk) {
|
|
464
|
+
if (discardsOutput) return
|
|
346
465
|
if (streamsLive(node)) {
|
|
347
|
-
streamed.add(node.id)
|
|
348
466
|
streamedSinceBlock = true
|
|
349
467
|
if (chunk.length > 0) streamMidLine = !chunk.endsWith('\n')
|
|
350
468
|
writer.write(chunk)
|
|
351
469
|
return
|
|
352
470
|
}
|
|
471
|
+
const tail = persistentTails.get(node.id)
|
|
472
|
+
if (tail !== undefined) {
|
|
473
|
+
appendTail(tail.out, chunk)
|
|
474
|
+
return
|
|
475
|
+
}
|
|
353
476
|
pushChunk(stdoutBuffers, node.id, chunk)
|
|
354
477
|
},
|
|
355
478
|
taskStderr(node, chunk) {
|
|
479
|
+
if (discardsOutput) return
|
|
356
480
|
if (streamsLive(node)) {
|
|
357
|
-
streamed.add(node.id)
|
|
358
481
|
streamedSinceBlock = true
|
|
359
482
|
if (chunk.length > 0) streamMidLine = !chunk.endsWith('\n')
|
|
360
483
|
writer.write(chunk)
|
|
361
484
|
return
|
|
362
485
|
}
|
|
486
|
+
const tail = persistentTails.get(node.id)
|
|
487
|
+
if (tail !== undefined) {
|
|
488
|
+
appendTail(tail.err, chunk)
|
|
489
|
+
return
|
|
490
|
+
}
|
|
363
491
|
pushChunk(stderrBuffers, node.id, chunk)
|
|
364
492
|
},
|
|
365
493
|
taskComplete(node, outcome) {
|
|
366
|
-
|
|
367
|
-
|
|
494
|
+
// A persistent task buffered into its bounded tail rather than the
|
|
495
|
+
// per-task buffers (see taskStart), so its frame is drained from
|
|
496
|
+
// there — carrying how much the cap dropped, since a truncated log
|
|
497
|
+
// that reads as complete is worse than one that says what it lost.
|
|
498
|
+
const preReady = persistentTails.get(node.id)
|
|
499
|
+
const stdout = preReady ? tailText(preReady.out) : takeChunks(stdoutBuffers, node.id)
|
|
500
|
+
const stderr = preReady ? tailText(preReady.err) : takeChunks(stderrBuffers, node.id)
|
|
501
|
+
const dropped =
|
|
502
|
+
preReady && (preReady.out.dropped > 0 || preReady.err.dropped > 0)
|
|
503
|
+
? { droppedStdout: preReady.out.dropped, droppedStderr: preReady.err.dropped }
|
|
504
|
+
: {}
|
|
505
|
+
// The pre-ready window closed either way: a failed task is over, and
|
|
506
|
+
// a ready one starts a fresh post-ready tail below.
|
|
507
|
+
persistentTails.delete(node.id)
|
|
368
508
|
// An aborted task (child killed by a shutdown signal) reverts
|
|
369
509
|
// to pending: free its worker slot, but never count or render it
|
|
370
510
|
// — the run is tearing down and it has no honest outcome.
|
|
@@ -383,11 +523,18 @@ export function defaultLogger(
|
|
|
383
523
|
done++
|
|
384
524
|
if (outcome.status === 'failed') {
|
|
385
525
|
failed++
|
|
526
|
+
if (node.config.cache === undefined) noCache++
|
|
386
527
|
} else if (node.config.exec?.persistent !== undefined && outcome.status === 'success') {
|
|
387
528
|
// A persistent task's outcome arrives at READY; the child
|
|
388
529
|
// keeps running until the orchestrator SIGTERMs it at run
|
|
389
530
|
// end — pin it so its liveness stays visible.
|
|
390
531
|
pinnedPersistent.push(node.id)
|
|
532
|
+
// …and capture what it writes from here on. A live-streaming
|
|
533
|
+
// node already owns the terminal, so buffering it would emit
|
|
534
|
+
// its output twice.
|
|
535
|
+
if (!streamsLive(node)) {
|
|
536
|
+
persistentTails.set(node.id, { node, outcome, out: createTail(), err: createTail() })
|
|
537
|
+
}
|
|
391
538
|
}
|
|
392
539
|
// Free the task's slot; the longest-waiting queued task
|
|
393
540
|
// (if any) takes it over, keeping lowest-index-first reuse.
|
|
@@ -400,6 +547,7 @@ export function defaultLogger(
|
|
|
400
547
|
switch (outcome.status) {
|
|
401
548
|
case 'success':
|
|
402
549
|
succeeded++
|
|
550
|
+
if (node.config.cache === undefined) noCache++
|
|
403
551
|
break
|
|
404
552
|
case 'cache-hit':
|
|
405
553
|
if (outcome.restored === false) upToDate++
|
|
@@ -422,14 +570,24 @@ export function defaultLogger(
|
|
|
422
570
|
refresh(true)
|
|
423
571
|
}
|
|
424
572
|
if (isGroupTask(node)) return
|
|
425
|
-
const isHit = outcome.status
|
|
573
|
+
const isHit = isCacheHit(outcome.status)
|
|
426
574
|
switch (view.mode) {
|
|
427
575
|
case 'none':
|
|
428
576
|
return
|
|
577
|
+
case 'hash-only': {
|
|
578
|
+
// The word carries the outcome (skips/failures included); the
|
|
579
|
+
// key is what this mode exists to show — absent for a task
|
|
580
|
+
// that never computed one (skipped, uncacheable).
|
|
581
|
+
const key = outcome.hash === undefined ? '' : ` ${outcome.hash}`
|
|
582
|
+
emitLine(`${outcomeWord(outcome)} ${node.id}${key}`)
|
|
583
|
+
return
|
|
584
|
+
}
|
|
429
585
|
case 'errors-only':
|
|
430
586
|
if (outcome.status !== 'failed') return
|
|
431
|
-
emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
|
|
432
|
-
deferredFailures.push(
|
|
587
|
+
emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
|
|
588
|
+
deferredFailures.push(
|
|
589
|
+
formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
|
|
590
|
+
)
|
|
433
591
|
return
|
|
434
592
|
case 'broad':
|
|
435
593
|
// News only: executed work gets a one-liner, failures get
|
|
@@ -438,8 +596,10 @@ export function defaultLogger(
|
|
|
438
596
|
// surface in the end-of-run summary.
|
|
439
597
|
if (outcome.status === 'failed') {
|
|
440
598
|
// ✗ marker now; the full frame replays at runEnd.
|
|
441
|
-
emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
|
|
442
|
-
deferredFailures.push(
|
|
599
|
+
emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
|
|
600
|
+
deferredFailures.push(
|
|
601
|
+
formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
|
|
602
|
+
)
|
|
443
603
|
} else if (outcome.status === 'success') {
|
|
444
604
|
emitLine(formatTaskExecutedLine(node, outcome, colors))
|
|
445
605
|
}
|
|
@@ -471,20 +631,24 @@ export function defaultLogger(
|
|
|
471
631
|
// to runEnd like everywhere else; everything else emits
|
|
472
632
|
// ONE atomic block from the buffered output.
|
|
473
633
|
if (outcome.status === 'failed') {
|
|
474
|
-
emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
|
|
475
|
-
deferredFailures.push(
|
|
634
|
+
emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
|
|
635
|
+
deferredFailures.push(
|
|
636
|
+
formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
|
|
637
|
+
)
|
|
476
638
|
return
|
|
477
639
|
}
|
|
478
640
|
// forceCommand: a requested task's frame shows `$ cmd`
|
|
479
641
|
// whether it ran or was cached — same frame every run.
|
|
480
|
-
emitBlock(formatTaskBlock(node, outcome, { stdout, stderr }, colors, true))
|
|
642
|
+
emitBlock(formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors, true))
|
|
481
643
|
return
|
|
482
644
|
}
|
|
483
645
|
// Dependency-pulled nodes: silent on success; failures get
|
|
484
646
|
// the ✗ marker now and their frame replayed at runEnd.
|
|
485
647
|
if (outcome.status === 'failed') {
|
|
486
|
-
emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
|
|
487
|
-
deferredFailures.push(
|
|
648
|
+
emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
|
|
649
|
+
deferredFailures.push(
|
|
650
|
+
formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
|
|
651
|
+
)
|
|
488
652
|
}
|
|
489
653
|
return
|
|
490
654
|
case 'full': {
|
|
@@ -502,17 +666,26 @@ export function defaultLogger(
|
|
|
502
666
|
emitLine(formatTaskSkippedLine(node, colors))
|
|
503
667
|
return
|
|
504
668
|
}
|
|
505
|
-
const block = formatTaskBlock(node, outcome, { stdout, stderr }, colors)
|
|
669
|
+
const block = formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors)
|
|
506
670
|
if (block.length === 0) return
|
|
507
671
|
if (view.gha) {
|
|
672
|
+
// The block is the task's own output — fenced, so a task that
|
|
673
|
+
// prints `::endgroup::` can't close vx's group early and an
|
|
674
|
+
// `::error` line in its output can't become a real annotation.
|
|
675
|
+
const body = ghaFence(block, ghaToken)
|
|
508
676
|
// Failed tasks stay pre-expanded in the Actions viewer:
|
|
509
677
|
// an ::error annotation instead of a collapsed group.
|
|
510
678
|
if (outcome.status === 'failed') {
|
|
511
|
-
|
|
679
|
+
// The message is ours (a literal + an integer); only the title
|
|
680
|
+
// carries user input.
|
|
681
|
+
emitBlock(
|
|
682
|
+
`::error title=${ghaProperty(node.id)}::failed (exit ${outcome.exitCode})\n${body}`,
|
|
683
|
+
)
|
|
512
684
|
} else {
|
|
513
685
|
emitBlock(
|
|
514
|
-
`::group::${
|
|
515
|
-
`${
|
|
686
|
+
`::group::${ghaData(
|
|
687
|
+
`${node.id} (${outcomeWord(outcome)} ${formatDuration(outcome.durationMs)})`,
|
|
688
|
+
)}\n${body}::endgroup::\n`,
|
|
516
689
|
)
|
|
517
690
|
}
|
|
518
691
|
return
|