@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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// What a finished run leaves behind: the `runs` rows, the `invocations`
|
|
2
|
+
// header row, and — only when a telemetry sink is active — the per-task
|
|
3
|
+
// telemetry mirror. One pass over the outcome list builds all three, so
|
|
4
|
+
// `invocations.task_count`, the terminal's "N total" and the number of
|
|
5
|
+
// telemetry tasks are the same count by construction (`tallyOutcomes`
|
|
6
|
+
// applies the same group/aborted filter).
|
|
7
|
+
|
|
8
|
+
import type { InvocationRecord, RunRecord } from '../cache/index.js'
|
|
9
|
+
import { isGroupTask, type TaskOutcome } from '../graph/index.js'
|
|
10
|
+
import { VERSION } from '../version.js'
|
|
11
|
+
import type { CiContext, GitContext, HostContext } from './run-context.js'
|
|
12
|
+
import { deriveCacheSource, isCacheHit, type TaskTelemetry } from './telemetry.js'
|
|
13
|
+
|
|
14
|
+
export interface RunRecordsInput {
|
|
15
|
+
outcomes: readonly TaskOutcome[]
|
|
16
|
+
runId: string
|
|
17
|
+
/** Wall-clock ms at run start; every per-task ns offset is anchored to it. */
|
|
18
|
+
startedAtMs: number
|
|
19
|
+
endedAtMs: number
|
|
20
|
+
totalMs: number
|
|
21
|
+
ok: boolean
|
|
22
|
+
command: string
|
|
23
|
+
requestedTasks: readonly string[]
|
|
24
|
+
/** Already compacted (`compactCachePolicy`), the form the header row stores. */
|
|
25
|
+
cachePolicy: string
|
|
26
|
+
concurrency: number
|
|
27
|
+
flow: InvocationRecord['flow']
|
|
28
|
+
forwardArgs?: readonly string[] | undefined
|
|
29
|
+
tags: Record<string, string>
|
|
30
|
+
git: GitContext
|
|
31
|
+
ci: CiContext
|
|
32
|
+
host: HostContext
|
|
33
|
+
/** Build the telemetry mirror; a run with no sink allocates none. */
|
|
34
|
+
withTelemetry: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RunRecords {
|
|
38
|
+
runs: RunRecord[]
|
|
39
|
+
invocation: InvocationRecord
|
|
40
|
+
telemetryTasks: TaskTelemetry[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every non-group, non-aborted outcome gets a row. An outcome with NO hash
|
|
45
|
+
* (a `skipped` task never probed the cache; a `persistent` one is never
|
|
46
|
+
* cacheable) used to be dropped because `runs.hash` is NOT NULL — which
|
|
47
|
+
* made a failing persistent task record `0 tasks, 0 failures` on a run the
|
|
48
|
+
* terminal called red, and a failed task with a skipped dependent record
|
|
49
|
+
* 1 of 2. `bindRun` stores `''` for those instead; the key-diff readers
|
|
50
|
+
* guard it.
|
|
51
|
+
*/
|
|
52
|
+
export function assembleRunRecords(input: RunRecordsInput): RunRecords {
|
|
53
|
+
const { outcomes, runId, startedAtMs, endedAtMs } = input
|
|
54
|
+
const runs: RunRecord[] = []
|
|
55
|
+
const telemetryTasks: TaskTelemetry[] = []
|
|
56
|
+
let failedCount = 0
|
|
57
|
+
let hitLocalCount = 0
|
|
58
|
+
let hitRemoteCount = 0
|
|
59
|
+
for (const o of outcomes) {
|
|
60
|
+
if (isGroupTask(o.node)) continue
|
|
61
|
+
// aborted (killed by a shutdown signal) isn't a real run.
|
|
62
|
+
if (o.status === 'aborted') continue
|
|
63
|
+
if (input.withTelemetry) telemetryTasks.push(telemetryOf(o))
|
|
64
|
+
runs.push({
|
|
65
|
+
...(o.hash !== undefined ? { hash: o.hash } : {}),
|
|
66
|
+
project: o.node.projectName,
|
|
67
|
+
task: o.node.taskName,
|
|
68
|
+
status: o.status,
|
|
69
|
+
exitCode: o.exitCode,
|
|
70
|
+
durationMs: o.durationMs,
|
|
71
|
+
...(input.forwardArgs !== undefined ? { forwardArgs: input.forwardArgs } : {}),
|
|
72
|
+
// Anchor to the REAL per-task wall-clock window: run-start wall time +
|
|
73
|
+
// the task's ns offset (captured for hits and executed tasks alike).
|
|
74
|
+
// The `end - duration` fallback applies to outcomes without an offset —
|
|
75
|
+
// today only `skipped`, which the scheduler finishes synchronously with
|
|
76
|
+
// no span, so it collapses to a zero-width mark at the run's end. Using
|
|
77
|
+
// run-end-minus-duration for EVERYTHING was the old bug that piled every
|
|
78
|
+
// task at the right edge of the timeline.
|
|
79
|
+
startedAt:
|
|
80
|
+
o.wallclockStartNs !== undefined
|
|
81
|
+
? startedAtMs + Math.round(Number(o.wallclockStartNs) / 1e6)
|
|
82
|
+
: endedAtMs - o.durationMs,
|
|
83
|
+
endedAt:
|
|
84
|
+
o.wallclockEndNs !== undefined
|
|
85
|
+
? startedAtMs + Math.round(Number(o.wallclockEndNs) / 1e6)
|
|
86
|
+
: endedAtMs,
|
|
87
|
+
runId,
|
|
88
|
+
...(o.cpuMs !== undefined ? { cpuMs: o.cpuMs } : {}),
|
|
89
|
+
...(o.peakRssBytes !== undefined ? { peakRssBytes: o.peakRssBytes } : {}),
|
|
90
|
+
...(o.wallclockStartNs !== undefined ? { wallclockStartNs: o.wallclockStartNs } : {}),
|
|
91
|
+
...(o.wallclockEndNs !== undefined ? { wallclockEndNs: o.wallclockEndNs } : {}),
|
|
92
|
+
cacheHit: isCacheHit(o.status),
|
|
93
|
+
...(o.attempts !== undefined ? { attempts: o.attempts } : {}),
|
|
94
|
+
cached: o.node.config.cache !== undefined,
|
|
95
|
+
})
|
|
96
|
+
if (o.status === 'failed') failedCount++
|
|
97
|
+
if (o.status === 'cache-hit') hitLocalCount++
|
|
98
|
+
if (o.status === 'cache-hit-remote') hitRemoteCount++
|
|
99
|
+
}
|
|
100
|
+
const invocation: InvocationRecord = {
|
|
101
|
+
runId,
|
|
102
|
+
command: input.command,
|
|
103
|
+
requestedTasks: JSON.stringify([...input.requestedTasks]),
|
|
104
|
+
cachePolicy: input.cachePolicy,
|
|
105
|
+
concurrency: input.concurrency,
|
|
106
|
+
flow: input.flow,
|
|
107
|
+
startedAt: startedAtMs,
|
|
108
|
+
endedAt: endedAtMs,
|
|
109
|
+
totalDurationMs: Math.round(input.totalMs),
|
|
110
|
+
taskCount: runs.length,
|
|
111
|
+
failedCount,
|
|
112
|
+
hitCount: hitLocalCount + hitRemoteCount,
|
|
113
|
+
hitLocalCount,
|
|
114
|
+
hitRemoteCount,
|
|
115
|
+
exitOk: input.ok,
|
|
116
|
+
commitSha: input.git.commitSha,
|
|
117
|
+
branch: input.git.branch,
|
|
118
|
+
dirty: input.git.dirty,
|
|
119
|
+
ci: input.ci.ci,
|
|
120
|
+
ciProvider: input.ci.provider,
|
|
121
|
+
host: input.host.host,
|
|
122
|
+
os: input.host.os,
|
|
123
|
+
arch: input.host.arch,
|
|
124
|
+
vxVersion: VERSION,
|
|
125
|
+
tags: JSON.stringify(input.tags),
|
|
126
|
+
}
|
|
127
|
+
return { runs, invocation, telemetryTasks }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function telemetryOf(o: TaskOutcome): TaskTelemetry {
|
|
131
|
+
const t: TaskTelemetry = {
|
|
132
|
+
taskId: o.node.id,
|
|
133
|
+
project: o.node.projectName,
|
|
134
|
+
task: o.node.taskName,
|
|
135
|
+
status: o.status,
|
|
136
|
+
cacheSource: deriveCacheSource(o.status),
|
|
137
|
+
exitCode: o.exitCode,
|
|
138
|
+
durationMs: o.durationMs,
|
|
139
|
+
}
|
|
140
|
+
if (o.hash !== undefined) t.hash = o.hash
|
|
141
|
+
if (o.cpuMs !== undefined) t.cpuMs = o.cpuMs
|
|
142
|
+
if (o.peakRssBytes !== undefined) t.peakRssBytes = o.peakRssBytes
|
|
143
|
+
if (o.where !== undefined) t.where = o.where
|
|
144
|
+
if (o.outputs !== undefined) t.outputs = o.outputs
|
|
145
|
+
if (o.attempts !== undefined) t.attempts = o.attempts
|
|
146
|
+
if (o.wallclockStartNs !== undefined) t.wallclockStartNs = o.wallclockStartNs.toString()
|
|
147
|
+
if (o.wallclockEndNs !== undefined) t.wallclockEndNs = o.wallclockEndNs.toString()
|
|
148
|
+
return t
|
|
149
|
+
}
|
|
@@ -1,64 +1,66 @@
|
|
|
1
|
-
// Markdown run report (`vx run --report=markdown`
|
|
2
|
-
// one row per task plus a header line of totals —
|
|
3
|
-
//
|
|
4
|
-
// (`vx run ci --report=
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// Markdown run report (`vx run --report=markdown` / `--report-file=<path>`).
|
|
2
|
+
// A moon-style table — one row per task plus a header line of totals —
|
|
3
|
+
// rendered after a run so CI can put it in a step summary
|
|
4
|
+
// (`vx run ci --report-file="$GITHUB_STEP_SUMMARY"`). Pure: the run's
|
|
5
|
+
// finished outcomes in, a markdown string out. No ANSI, no live region —
|
|
6
|
+
// keep it machine-clean and diffable.
|
|
7
|
+
//
|
|
8
|
+
// Machine-clean is a property of this STRING, not of stdout: the status
|
|
9
|
+
// logger shares that stream, so redirecting it captures the whole run log
|
|
10
|
+
// too. `--report-file` is the sink that carries the property end to end.
|
|
7
11
|
|
|
8
|
-
import type { RunResult } from './protocol.js'
|
|
9
12
|
import type { OutcomeView } from './events.js'
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
|
-
*
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* needs no graph back-ref and stays self-contained here.
|
|
15
|
+
* One finished run, reduced to what a report needs. Used to live in
|
|
16
|
+
* `protocol.ts` as the return type of the whole-run `backend` seam; that
|
|
17
|
+
* seam is gone (a run always executes in-process — see
|
|
18
|
+
* `docs/modules/executor.md`), so the shape lives with its only consumer.
|
|
17
19
|
*/
|
|
18
|
-
interface
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
export interface RunResult {
|
|
21
|
+
ok: boolean
|
|
22
|
+
outcomes: OutcomeView[]
|
|
23
|
+
}
|
|
24
|
+
import { tallyViews, type Tally } from './tally.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The report header's numbers. The outcome BUCKETS come from the shared
|
|
28
|
+
* `tallyViews` — the same rules the terminal summary and `--summarize` use,
|
|
29
|
+
* so three surfaces describing one run cannot disagree. Only the two
|
|
30
|
+
* durations are report-specific.
|
|
31
|
+
*
|
|
32
|
+
* This used to be a private copy of the partition, and it drifted: it had no
|
|
33
|
+
* group filter (it could not have one — `OutcomeView` had no `isGroup`), so
|
|
34
|
+
* every organizational node was counted as a successful task and rendered as
|
|
35
|
+
* a row claiming `success | miss | 0ms`.
|
|
36
|
+
*/
|
|
37
|
+
interface ReportTally extends Tally {
|
|
24
38
|
/** Wall-clock ms summed over tasks that actually executed (misses). */
|
|
25
39
|
executedMs: number
|
|
26
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Ms of work the cache SKIPPED — the sum of the hits' STORED exec times.
|
|
42
|
+
* Deliberately not `durationMs`, which for a hit is the restore this run
|
|
43
|
+
* paid: summing that reported a 2.01 s task as "6ms saved".
|
|
44
|
+
*/
|
|
27
45
|
savedMs: number
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
function tally(outcomes: readonly OutcomeView[]): ReportTally {
|
|
31
|
-
const t: ReportTally = {
|
|
32
|
-
total: 0,
|
|
33
|
-
successful: 0,
|
|
34
|
-
failed: 0,
|
|
35
|
-
skipped: 0,
|
|
36
|
-
cached: 0,
|
|
37
|
-
executedMs: 0,
|
|
38
|
-
savedMs: 0,
|
|
39
|
-
}
|
|
49
|
+
const t: ReportTally = { ...tallyViews(outcomes), executedMs: 0, savedMs: 0 }
|
|
40
50
|
for (const o of outcomes) {
|
|
41
|
-
|
|
42
|
-
// excluded from totals and every bucket, like the terminal tally.
|
|
43
|
-
if (o.status === 'aborted') continue
|
|
44
|
-
t.total++
|
|
51
|
+
if (o.isGroup === true) continue
|
|
45
52
|
switch (o.status) {
|
|
46
53
|
case 'success':
|
|
47
|
-
|
|
54
|
+
case 'failed':
|
|
48
55
|
t.executedMs += o.durationMs
|
|
49
56
|
break
|
|
50
57
|
case 'cache-hit':
|
|
51
58
|
case 'cache-hit-remote':
|
|
52
|
-
t
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
t.failed++
|
|
58
|
-
t.executedMs += o.durationMs
|
|
59
|
-
break
|
|
60
|
-
case 'skipped':
|
|
61
|
-
t.skipped++
|
|
59
|
+
// An outcome that doesn't know what it skipped contributes nothing:
|
|
60
|
+
// better to omit the claim than to substantiate it with the restore
|
|
61
|
+
// cost. (Only reachable across a version skew — every hit this
|
|
62
|
+
// binary produces carries the stored duration.)
|
|
63
|
+
t.savedMs += o.storedDurationMs ?? 0
|
|
62
64
|
break
|
|
63
65
|
}
|
|
64
66
|
}
|
|
@@ -87,7 +89,7 @@ function cacheWord(o: OutcomeView): string {
|
|
|
87
89
|
return o.restored === false ? 'up-to-date' : 'remote'
|
|
88
90
|
case 'success':
|
|
89
91
|
case 'failed':
|
|
90
|
-
return 'miss'
|
|
92
|
+
return o.noCache === true ? 'no-cache' : 'miss'
|
|
91
93
|
default:
|
|
92
94
|
return '—'
|
|
93
95
|
}
|
|
@@ -98,6 +100,24 @@ function fmtDuration(ms: number): string {
|
|
|
98
100
|
return `${(ms / 1000).toFixed(2)}s`
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Make a value safe inside a GFM table cell. Task names are arbitrary TS
|
|
105
|
+
* object keys and the loader accepts `|` and newlines, either of which
|
|
106
|
+
* silently breaks the table on the consumer (`>> $GITHUB_STEP_SUMMARY`):
|
|
107
|
+
* a bare pipe adds a column, a newline splits the row.
|
|
108
|
+
*
|
|
109
|
+
* Exported because this file is not the only markdown table describing a run:
|
|
110
|
+
* the cloud plugin's GitHub job summary renders the same data from the same
|
|
111
|
+
* unvalidated names and shipped WITHOUT this escape, so a `|` in a task name
|
|
112
|
+
* or an output path shifted its columns. One definition, so the two
|
|
113
|
+
* cannot disagree about what a cell may contain.
|
|
114
|
+
*/
|
|
115
|
+
export function escapeMarkdownCell(value: string): string {
|
|
116
|
+
return value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cell = escapeMarkdownCell
|
|
120
|
+
|
|
101
121
|
/**
|
|
102
122
|
* Render the finished run as a markdown report. A header line of totals
|
|
103
123
|
* followed by a per-task table. Self-contained — everything is derived
|
|
@@ -105,13 +125,15 @@ function fmtDuration(ms: number): string {
|
|
|
105
125
|
*/
|
|
106
126
|
export function formatRunReportMarkdown(result: RunResult): string {
|
|
107
127
|
const t = tally(result.outcomes)
|
|
128
|
+
const cached = t.cachedLocal + t.cachedRemote
|
|
108
129
|
const parts = [
|
|
109
130
|
`**${t.total} task${t.total === 1 ? '' : 's'}**`,
|
|
110
131
|
`${t.successful} success`,
|
|
111
132
|
`${t.failed} failed`,
|
|
112
|
-
`${
|
|
133
|
+
`${cached} cached`,
|
|
113
134
|
]
|
|
114
135
|
if (t.skipped > 0) parts.push(`${t.skipped} skipped`)
|
|
136
|
+
if (t.aborted > 0) parts.push(`${t.aborted} aborted`)
|
|
115
137
|
parts.push(`${fmtDuration(t.executedMs)} total`)
|
|
116
138
|
if (t.savedMs > 0) parts.push(`${fmtDuration(t.savedMs)} saved`)
|
|
117
139
|
|
|
@@ -123,9 +145,14 @@ export function formatRunReportMarkdown(result: RunResult): string {
|
|
|
123
145
|
lines.push('| Task | Status | Cache | Duration |')
|
|
124
146
|
lines.push('| --- | --- | --- | --- |')
|
|
125
147
|
for (const o of result.outcomes) {
|
|
126
|
-
|
|
148
|
+
// A group node did no work: no command, no cache decision, 0ms. Rendering
|
|
149
|
+
// one as `success | miss | 0ms` invents a task the user never wrote a
|
|
150
|
+
// command for. Same exclusion the header's tally makes.
|
|
151
|
+
if (o.isGroup === true) continue
|
|
127
152
|
lines.push(
|
|
128
|
-
`| ${o.taskId} | ${statusWord(o)} | ${cacheWord(o)} | ${
|
|
153
|
+
`| ${cell(o.taskId)} | ${cell(statusWord(o))} | ${cell(cacheWord(o))} | ${cell(
|
|
154
|
+
fmtDuration(o.durationMs),
|
|
155
|
+
)} |`,
|
|
129
156
|
)
|
|
130
157
|
}
|
|
131
158
|
return lines.join('\n') + '\n'
|