@vzn/vx 0.0.15 → 0.0.18
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 +91 -130
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +19 -5
- package/plugins/local-cache/index.ts +2 -0
- package/plugins/local-executor/index.ts +2 -0
- package/plugins/schedule-history/index.ts +2 -0
- package/src/bin.ts +2 -2
- package/src/cache/archive.ts +557 -0
- package/src/cache/cache.ts +929 -250
- package/src/cache/cas-backend.ts +20 -2
- package/src/cache/chained-cache.ts +202 -0
- package/src/cache/digest.ts +0 -15
- package/src/cache/index.ts +13 -3
- package/src/cache/inputs.ts +445 -41
- package/src/cache/layered-cache.ts +129 -30
- package/src/cache/tar-stream.ts +358 -0
- package/src/cli/cache.ts +36 -10
- package/src/cli/help.ts +167 -128
- package/src/cli/index.ts +74 -43
- package/src/cli/info.ts +30 -0
- package/src/cli/last.ts +140 -0
- package/src/cli/migrate-emit.ts +21 -0
- package/src/cli/migrate-nx.ts +23 -0
- package/src/cli/migrate-scripts.ts +127 -0
- package/src/cli/migrate-turbo.ts +43 -6
- package/src/cli/migrate.ts +119 -33
- package/src/cli/plan-format.ts +58 -3
- package/src/cli/plugin-commands.ts +96 -0
- package/src/cli/prune.ts +291 -0
- package/src/cli/run.ts +272 -80
- package/src/cli/upgrade.ts +7 -6
- package/src/cli/watch.ts +259 -61
- package/src/cli/why.ts +193 -0
- package/src/config.ts +40 -23
- package/src/exec/env.ts +7 -1
- package/src/exec/executor.ts +239 -0
- package/src/exec/index.ts +19 -1
- package/src/exec/runner.ts +116 -29
- package/src/exec/sandbox-runtime.ts +194 -24
- package/src/graph/index.ts +3 -1
- package/src/graph/scheduler.ts +92 -18
- package/src/graph/task-graph.ts +220 -6
- package/src/index.ts +82 -84
- package/src/orchestrator/deferred-outputs.ts +144 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +30 -0
- package/src/orchestrator/execute-task.ts +328 -94
- package/src/orchestrator/failure-mode.ts +61 -0
- package/src/orchestrator/framed-output.ts +87 -35
- package/src/orchestrator/history.ts +78 -31
- package/src/orchestrator/index.ts +43 -90
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/logger.ts +196 -14
- package/src/orchestrator/metrics.ts +57 -1376
- package/src/orchestrator/missing-plugin.ts +9 -0
- package/src/orchestrator/options.ts +22 -4
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +199 -119
- package/src/orchestrator/plugin.ts +144 -57
- package/src/orchestrator/prepare.ts +220 -69
- package/src/orchestrator/remote-prefetch.ts +27 -8
- package/src/orchestrator/resources.ts +20 -34
- package/src/orchestrator/run-artifacts.ts +41 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-report.ts +73 -46
- package/src/orchestrator/run.ts +590 -122
- package/src/orchestrator/stable-keys.ts +74 -33
- package/src/orchestrator/status-line.ts +47 -8
- package/src/orchestrator/summary.ts +22 -1
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +105 -11
- package/src/orchestrator/task-log-buffer.ts +291 -0
- package/src/orchestrator/telemetry-host.ts +24 -6
- package/src/orchestrator/telemetry.ts +181 -21
- package/src/orchestrator/upstream.ts +72 -31
- package/src/orchestrator/verify.ts +12 -2
- package/src/plugins/local-cache/index.ts +11 -0
- package/src/plugins/local-executor/index.ts +45 -0
- package/src/plugins/schedule-history/index.ts +109 -0
- package/src/util/edit-distance.ts +21 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +7 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +43 -0
- package/src/util/settle.ts +48 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/workspace/affected.ts +142 -15
- package/src/workspace/config-cache.ts +304 -0
- package/src/workspace/config-eval.ts +210 -0
- package/src/workspace/config-imports.ts +161 -0
- package/src/workspace/filter.ts +22 -4
- package/src/workspace/fingerprint.ts +16 -1
- package/src/workspace/index.ts +15 -3
- package/src/workspace/lockfile.ts +13 -2
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +23 -20
- package/src/workspace/project-loader.ts +503 -59
- package/src/workspace/workspace.ts +213 -72
- 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/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/run-state.ts +0 -145
- 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,8 @@ 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'
|
|
20
23
|
|
|
21
24
|
export interface Logger {
|
|
22
25
|
/** Header / footer / status text. Written verbatim, one trailing \n added. */
|
|
@@ -41,6 +44,8 @@ export interface Logger {
|
|
|
41
44
|
requestedCount?: number
|
|
42
45
|
/** Run banner context — so the live region footer matches the final summary. */
|
|
43
46
|
context?: RunContext
|
|
47
|
+
/** The run's canonical start (epoch ms) — equals the final summary's `startedAt`. */
|
|
48
|
+
startedAtMs?: number
|
|
44
49
|
}): void
|
|
45
50
|
taskStart?(node: TaskNode): void
|
|
46
51
|
/** Run finished (any outcome). Idempotent. */
|
|
@@ -56,6 +61,11 @@ export interface Logger {
|
|
|
56
61
|
* Today's CI behavior; also the programmatic default.
|
|
57
62
|
* errors-only — only failed tasks print.
|
|
58
63
|
* none — no per-task output at all.
|
|
64
|
+
* hash-only — one line per task: outcome word, task id, cache key.
|
|
65
|
+
* No frames, no log replay (Turbo `--output-logs
|
|
66
|
+
* hash-only` parity); the end-of-run summary still
|
|
67
|
+
* renders. The line is the run's audit trail: which key
|
|
68
|
+
* each task resolved to, without any build output.
|
|
59
69
|
* focused — requested nodes stream raw output live (running the
|
|
60
70
|
* task should feel like running the command directly);
|
|
61
71
|
* dependency-pulled nodes are silent unless they fail.
|
|
@@ -71,7 +81,7 @@ export interface Logger {
|
|
|
71
81
|
* line even if stdout happens to be a TTY.
|
|
72
82
|
*/
|
|
73
83
|
export interface OutputView {
|
|
74
|
-
mode: 'full' | 'errors-only' | 'none' | 'focused' | 'broad'
|
|
84
|
+
mode: 'full' | 'errors-only' | 'none' | 'focused' | 'broad' | 'hash-only'
|
|
75
85
|
gha?: boolean
|
|
76
86
|
ci?: boolean
|
|
77
87
|
}
|
|
@@ -81,6 +91,39 @@ function truthyEnv(v: string | undefined): boolean {
|
|
|
81
91
|
return v !== undefined && v !== '' && v !== '0' && v !== 'false'
|
|
82
92
|
}
|
|
83
93
|
|
|
94
|
+
// ── GitHub Actions workflow-command escaping ────────────────────────
|
|
95
|
+
// The runner parses `::`-prefixed LINES out of the log, so anything
|
|
96
|
+
// interpolated into one has to be encoded or it changes the command's shape.
|
|
97
|
+
// Both halves are reachable without malice: a task name is an arbitrary TS
|
|
98
|
+
// object key (the loader accepts newlines — `run-report.ts` already escapes
|
|
99
|
+
// them for the same reason), and a task can legitimately print
|
|
100
|
+
// command-shaped text (a test asserting on annotation formatting, a nested
|
|
101
|
+
// tool emitting its own workflow commands).
|
|
102
|
+
// https://docs.github.com/actions/reference/workflow-commands-for-github-actions
|
|
103
|
+
|
|
104
|
+
/** Message text. `%` first, or we would re-escape our own escapes. */
|
|
105
|
+
function ghaData(value: string): string {
|
|
106
|
+
return value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A property VALUE additionally reserves `:` and `,` — the delimiters
|
|
111
|
+
* between a command's name, its properties and its message. Without this an
|
|
112
|
+
* id containing `::` truncates the annotation mid-title.
|
|
113
|
+
*/
|
|
114
|
+
function ghaProperty(value: string): string {
|
|
115
|
+
return ghaData(value).replace(/:/g, '%3A').replace(/,/g, '%2C')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Fence untrusted text so the runner stops interpreting workflow commands
|
|
120
|
+
* inside it. `token` must be unguessable by the fenced text itself — a fixed
|
|
121
|
+
* one could simply be printed by the task — so it is a per-run UUID.
|
|
122
|
+
*/
|
|
123
|
+
function ghaFence(body: string, token: string): string {
|
|
124
|
+
return `::stop-commands::${token}\n${body}::${token}::\n`
|
|
125
|
+
}
|
|
126
|
+
|
|
84
127
|
/** The unified outcome vocabulary word for a non-failed outcome. */
|
|
85
128
|
function outcomeWord(o: TaskOutcome): string {
|
|
86
129
|
switch (o.status) {
|
|
@@ -96,7 +139,10 @@ function outcomeWord(o: TaskOutcome): string {
|
|
|
96
139
|
}
|
|
97
140
|
|
|
98
141
|
export function resolveOutputView(
|
|
99
|
-
options: {
|
|
142
|
+
options: {
|
|
143
|
+
outputLogs?: 'full' | 'errors-only' | 'none' | 'hash-only'
|
|
144
|
+
flow?: 'focused' | 'broad'
|
|
145
|
+
},
|
|
100
146
|
env: Record<string, string | undefined> = process.env,
|
|
101
147
|
): OutputView {
|
|
102
148
|
const ci = truthyEnv(env['CI'])
|
|
@@ -153,6 +199,28 @@ export function defaultLogger(
|
|
|
153
199
|
return arr.length === 1 ? arr[0]! : arr.join('')
|
|
154
200
|
}
|
|
155
201
|
|
|
202
|
+
// `none` is the ONE mode whose contract guarantees the output is
|
|
203
|
+
// discarded — "no per-task output at all" (docs/cli.md) — so its chunks
|
|
204
|
+
// are dropped on arrival instead of accumulated and thrown away at
|
|
205
|
+
// `taskComplete`. Buffering output the mode promises never to print made
|
|
206
|
+
// `--output-logs none`, the obvious mitigation for a chatty task, cost
|
|
207
|
+
// exactly as much memory as printing it: 400 MB of task stdout drove the
|
|
208
|
+
// vx process to 852 MiB RSS, unbounded and linear, and a warm CACHE HIT
|
|
209
|
+
// paid it too (the stored stdout is replayed through this same path).
|
|
210
|
+
//
|
|
211
|
+
// Deliberately ONLY the modes that PROMISE never to print task output —
|
|
212
|
+
// `none` and `hash-only`. The boundary is the point: `full`, `broad`,
|
|
213
|
+
// `focused` and `errors-only` all PRINT this output — for `errors-only` a
|
|
214
|
+
// failed task's log is precisely what the user asked to see — and
|
|
215
|
+
// silently truncating someone's build log is a worse failure than the
|
|
216
|
+
// memory it costs. So those modes stay unbounded by decision, not by
|
|
217
|
+
// oversight.
|
|
218
|
+
const discardsOutput = view.mode === 'none' || view.mode === 'hash-only'
|
|
219
|
+
|
|
220
|
+
// One per run, and only in GHA mode: the fence token must be something the
|
|
221
|
+
// fenced task output cannot print.
|
|
222
|
+
const ghaToken = view.gha === true ? crypto.randomUUID() : ''
|
|
223
|
+
|
|
156
224
|
// All stdout flows through the writer so the status line can never
|
|
157
225
|
// interleave with content. Inert (pure passthrough) on non-TTY
|
|
158
226
|
// streams and in CI.
|
|
@@ -207,6 +275,28 @@ export function defaultLogger(
|
|
|
207
275
|
const deferredFailures: string[] = []
|
|
208
276
|
const pinnedPersistent: string[] = []
|
|
209
277
|
let flushedFailures = false
|
|
278
|
+
// A persistent task (dev server, watcher, daemon) is the one task kind
|
|
279
|
+
// nothing bounds: a one-shot command's output ends when it exits, but a
|
|
280
|
+
// server writes for as long as the run lasts. So its chunks route into a
|
|
281
|
+
// bounded tail for its WHOLE life, not the unbounded per-task buffers —
|
|
282
|
+
// registered at `taskStart` and drained at `taskComplete` for the frame,
|
|
283
|
+
// then reset to keep capturing what it writes after its outcome landed
|
|
284
|
+
// (which arrives at READY, while the child keeps running) and flushed as
|
|
285
|
+
// one trailing block at runEnd.
|
|
286
|
+
//
|
|
287
|
+
// Registering only at READY was the defect: a `readyWhen` that never
|
|
288
|
+
// matches never reaches `taskComplete`, so the 64 KiB cap never engaged
|
|
289
|
+
// and its output accumulated in the ordinary per-task buffer instead —
|
|
290
|
+
// ~100 MiB/s, with no `exec.timeout` needed to reach it.
|
|
291
|
+
//
|
|
292
|
+
// `outcome` is absent until the task completes. Registration is
|
|
293
|
+
// unconditional (the bound is a memory invariant, not a display choice);
|
|
294
|
+
// only the flush is view-gated.
|
|
295
|
+
const persistentTails = new Map<
|
|
296
|
+
string,
|
|
297
|
+
{ node: TaskNode; outcome?: TaskOutcome; out: Tail; err: Tail }
|
|
298
|
+
>()
|
|
299
|
+
let flushedPersistent = false
|
|
210
300
|
|
|
211
301
|
const refresh = (force: boolean): void => {
|
|
212
302
|
if (statusDead) return
|
|
@@ -325,6 +415,11 @@ export function defaultLogger(
|
|
|
325
415
|
emitLine(formatFrameOpen(node, colors))
|
|
326
416
|
return
|
|
327
417
|
}
|
|
418
|
+
// Bound a persistent task's output from its FIRST chunk. Waiting for
|
|
419
|
+
// its outcome would leave a never-ready one uncapped forever.
|
|
420
|
+
if (node.config.exec?.persistent !== undefined && !persistentTails.has(node.id)) {
|
|
421
|
+
persistentTails.set(node.id, { node, out: createTail(), err: createTail() })
|
|
422
|
+
}
|
|
328
423
|
const slot: WorkerSlot = { id: node.id, startedMs: Date.now() }
|
|
329
424
|
const free = slots.indexOf(null)
|
|
330
425
|
if (free >= 0) slots[free] = slot
|
|
@@ -333,6 +428,38 @@ export function defaultLogger(
|
|
|
333
428
|
},
|
|
334
429
|
runEnd() {
|
|
335
430
|
killStatus()
|
|
431
|
+
// Persistent tails first: they are context for whatever ran against
|
|
432
|
+
// the server, so they read above the failures they explain. `none`
|
|
433
|
+
// and `errors-only` state their contracts absolutely (no per-task
|
|
434
|
+
// output / only failed tasks print), so they stay silent — the tail
|
|
435
|
+
// is still captured and still bounded, just never printed. Guarded
|
|
436
|
+
// like the failures below: run() calls runEnd twice on the success
|
|
437
|
+
// path (once before the summary, once in its finally), and a
|
|
438
|
+
// kept-alive child keeps writing between the two.
|
|
439
|
+
if (!flushedPersistent) {
|
|
440
|
+
flushedPersistent = true
|
|
441
|
+
if (view.mode !== 'none' && view.mode !== 'errors-only') {
|
|
442
|
+
for (const t of persistentTails.values()) {
|
|
443
|
+
// No outcome means the task never completed — it is still
|
|
444
|
+
// pre-ready, so there is no "since ready" window to render and
|
|
445
|
+
// its buffered output belongs to the frame it never got.
|
|
446
|
+
if (t.outcome === undefined) continue
|
|
447
|
+
emitBlock(
|
|
448
|
+
formatPersistentTailBlock(
|
|
449
|
+
t.node,
|
|
450
|
+
t.outcome,
|
|
451
|
+
{ stdout: tailText(t.out), stderr: tailText(t.err) },
|
|
452
|
+
{ stdout: t.out.dropped, stderr: t.err.dropped },
|
|
453
|
+
colors,
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
for (const t of persistentTails.values()) {
|
|
459
|
+
resetTail(t.out)
|
|
460
|
+
resetTail(t.err)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
336
463
|
// Failures end the log: every deferred frame replays here, right
|
|
337
464
|
// above the summary — the ✗ one-liners marked them in the
|
|
338
465
|
// stream, the full diagnostics read last where eyes land.
|
|
@@ -343,6 +470,7 @@ export function defaultLogger(
|
|
|
343
470
|
}
|
|
344
471
|
},
|
|
345
472
|
taskStdout(node, chunk) {
|
|
473
|
+
if (discardsOutput) return
|
|
346
474
|
if (streamsLive(node)) {
|
|
347
475
|
streamed.add(node.id)
|
|
348
476
|
streamedSinceBlock = true
|
|
@@ -350,9 +478,15 @@ export function defaultLogger(
|
|
|
350
478
|
writer.write(chunk)
|
|
351
479
|
return
|
|
352
480
|
}
|
|
481
|
+
const tail = persistentTails.get(node.id)
|
|
482
|
+
if (tail !== undefined) {
|
|
483
|
+
appendTail(tail.out, chunk)
|
|
484
|
+
return
|
|
485
|
+
}
|
|
353
486
|
pushChunk(stdoutBuffers, node.id, chunk)
|
|
354
487
|
},
|
|
355
488
|
taskStderr(node, chunk) {
|
|
489
|
+
if (discardsOutput) return
|
|
356
490
|
if (streamsLive(node)) {
|
|
357
491
|
streamed.add(node.id)
|
|
358
492
|
streamedSinceBlock = true
|
|
@@ -360,11 +494,28 @@ export function defaultLogger(
|
|
|
360
494
|
writer.write(chunk)
|
|
361
495
|
return
|
|
362
496
|
}
|
|
497
|
+
const tail = persistentTails.get(node.id)
|
|
498
|
+
if (tail !== undefined) {
|
|
499
|
+
appendTail(tail.err, chunk)
|
|
500
|
+
return
|
|
501
|
+
}
|
|
363
502
|
pushChunk(stderrBuffers, node.id, chunk)
|
|
364
503
|
},
|
|
365
504
|
taskComplete(node, outcome) {
|
|
366
|
-
|
|
367
|
-
|
|
505
|
+
// A persistent task buffered into its bounded tail rather than the
|
|
506
|
+
// per-task buffers (see taskStart), so its frame is drained from
|
|
507
|
+
// there — carrying how much the cap dropped, since a truncated log
|
|
508
|
+
// that reads as complete is worse than one that says what it lost.
|
|
509
|
+
const preReady = persistentTails.get(node.id)
|
|
510
|
+
const stdout = preReady ? tailText(preReady.out) : takeChunks(stdoutBuffers, node.id)
|
|
511
|
+
const stderr = preReady ? tailText(preReady.err) : takeChunks(stderrBuffers, node.id)
|
|
512
|
+
const dropped =
|
|
513
|
+
preReady && (preReady.out.dropped > 0 || preReady.err.dropped > 0)
|
|
514
|
+
? { droppedStdout: preReady.out.dropped, droppedStderr: preReady.err.dropped }
|
|
515
|
+
: {}
|
|
516
|
+
// The pre-ready window closed either way: a failed task is over, and
|
|
517
|
+
// a ready one starts a fresh post-ready tail below.
|
|
518
|
+
persistentTails.delete(node.id)
|
|
368
519
|
// An aborted task (child killed by a shutdown signal) reverts
|
|
369
520
|
// to pending: free its worker slot, but never count or render it
|
|
370
521
|
// — the run is tearing down and it has no honest outcome.
|
|
@@ -388,6 +539,12 @@ export function defaultLogger(
|
|
|
388
539
|
// keeps running until the orchestrator SIGTERMs it at run
|
|
389
540
|
// end — pin it so its liveness stays visible.
|
|
390
541
|
pinnedPersistent.push(node.id)
|
|
542
|
+
// …and capture what it writes from here on. A live-streaming
|
|
543
|
+
// node already owns the terminal, so buffering it would emit
|
|
544
|
+
// its output twice.
|
|
545
|
+
if (!streamsLive(node)) {
|
|
546
|
+
persistentTails.set(node.id, { node, outcome, out: createTail(), err: createTail() })
|
|
547
|
+
}
|
|
391
548
|
}
|
|
392
549
|
// Free the task's slot; the longest-waiting queued task
|
|
393
550
|
// (if any) takes it over, keeping lowest-index-first reuse.
|
|
@@ -422,14 +579,24 @@ export function defaultLogger(
|
|
|
422
579
|
refresh(true)
|
|
423
580
|
}
|
|
424
581
|
if (isGroupTask(node)) return
|
|
425
|
-
const isHit = outcome.status
|
|
582
|
+
const isHit = isCacheHit(outcome.status)
|
|
426
583
|
switch (view.mode) {
|
|
427
584
|
case 'none':
|
|
428
585
|
return
|
|
586
|
+
case 'hash-only': {
|
|
587
|
+
// The word carries the outcome (skips/failures included); the
|
|
588
|
+
// key is what this mode exists to show — absent for a task
|
|
589
|
+
// that never computed one (skipped, uncacheable).
|
|
590
|
+
const key = outcome.hash === undefined ? '' : ` ${outcome.hash}`
|
|
591
|
+
emitLine(`${outcomeWord(outcome)} ${node.id}${key}`)
|
|
592
|
+
return
|
|
593
|
+
}
|
|
429
594
|
case 'errors-only':
|
|
430
595
|
if (outcome.status !== 'failed') return
|
|
431
596
|
emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
|
|
432
|
-
deferredFailures.push(
|
|
597
|
+
deferredFailures.push(
|
|
598
|
+
formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
|
|
599
|
+
)
|
|
433
600
|
return
|
|
434
601
|
case 'broad':
|
|
435
602
|
// News only: executed work gets a one-liner, failures get
|
|
@@ -439,7 +606,9 @@ export function defaultLogger(
|
|
|
439
606
|
if (outcome.status === 'failed') {
|
|
440
607
|
// ✗ marker now; the full frame replays at runEnd.
|
|
441
608
|
emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
|
|
442
|
-
deferredFailures.push(
|
|
609
|
+
deferredFailures.push(
|
|
610
|
+
formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
|
|
611
|
+
)
|
|
443
612
|
} else if (outcome.status === 'success') {
|
|
444
613
|
emitLine(formatTaskExecutedLine(node, outcome, colors))
|
|
445
614
|
}
|
|
@@ -472,19 +641,23 @@ export function defaultLogger(
|
|
|
472
641
|
// ONE atomic block from the buffered output.
|
|
473
642
|
if (outcome.status === 'failed') {
|
|
474
643
|
emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
|
|
475
|
-
deferredFailures.push(
|
|
644
|
+
deferredFailures.push(
|
|
645
|
+
formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
|
|
646
|
+
)
|
|
476
647
|
return
|
|
477
648
|
}
|
|
478
649
|
// forceCommand: a requested task's frame shows `$ cmd`
|
|
479
650
|
// whether it ran or was cached — same frame every run.
|
|
480
|
-
emitBlock(formatTaskBlock(node, outcome, { stdout, stderr }, colors, true))
|
|
651
|
+
emitBlock(formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors, true))
|
|
481
652
|
return
|
|
482
653
|
}
|
|
483
654
|
// Dependency-pulled nodes: silent on success; failures get
|
|
484
655
|
// the ✗ marker now and their frame replayed at runEnd.
|
|
485
656
|
if (outcome.status === 'failed') {
|
|
486
657
|
emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
|
|
487
|
-
deferredFailures.push(
|
|
658
|
+
deferredFailures.push(
|
|
659
|
+
formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
|
|
660
|
+
)
|
|
488
661
|
}
|
|
489
662
|
return
|
|
490
663
|
case 'full': {
|
|
@@ -502,17 +675,26 @@ export function defaultLogger(
|
|
|
502
675
|
emitLine(formatTaskSkippedLine(node, colors))
|
|
503
676
|
return
|
|
504
677
|
}
|
|
505
|
-
const block = formatTaskBlock(node, outcome, { stdout, stderr }, colors)
|
|
678
|
+
const block = formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors)
|
|
506
679
|
if (block.length === 0) return
|
|
507
680
|
if (view.gha) {
|
|
681
|
+
// The block is the task's own output — fenced, so a task that
|
|
682
|
+
// prints `::endgroup::` can't close vx's group early and an
|
|
683
|
+
// `::error` line in its output can't become a real annotation.
|
|
684
|
+
const body = ghaFence(block, ghaToken)
|
|
508
685
|
// Failed tasks stay pre-expanded in the Actions viewer:
|
|
509
686
|
// an ::error annotation instead of a collapsed group.
|
|
510
687
|
if (outcome.status === 'failed') {
|
|
511
|
-
|
|
688
|
+
// The message is ours (a literal + an integer); only the title
|
|
689
|
+
// carries user input.
|
|
690
|
+
emitBlock(
|
|
691
|
+
`::error title=${ghaProperty(node.id)}::failed (exit ${outcome.exitCode})\n${body}`,
|
|
692
|
+
)
|
|
512
693
|
} else {
|
|
513
694
|
emitBlock(
|
|
514
|
-
`::group::${
|
|
515
|
-
`${
|
|
695
|
+
`::group::${ghaData(
|
|
696
|
+
`${node.id} (${outcomeWord(outcome)} ${formatDuration(outcome.durationMs)})`,
|
|
697
|
+
)}\n${body}::endgroup::\n`,
|
|
516
698
|
)
|
|
517
699
|
}
|
|
518
700
|
return
|