@vzn/vx 0.0.646 → 0.0.648
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 +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
package/src/orchestrator/run.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// run with caching. Each step delegates to a single-purpose sibling file
|
|
3
3
|
// so the layers can be swapped without touching the others.
|
|
4
4
|
|
|
5
|
+
import type { InputStability } from '../config.js'
|
|
5
6
|
import type { ProjectEntry } from '../workspace/index.js'
|
|
6
|
-
import { inputStabilityEnv, LOCK_INVALID, lockEntryBroken } from '../workspace/index.js'
|
|
7
|
+
import { EMPTY_LOCK, inputStabilityEnv, LOCK_INVALID, lockEntryBroken } from '../workspace/index.js'
|
|
7
8
|
import { realpathSync } from 'node:fs'
|
|
8
9
|
import path from 'node:path'
|
|
9
10
|
import {
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
} from '../cache/index.js'
|
|
18
19
|
import { VERSION } from '../version.js'
|
|
19
20
|
import {
|
|
21
|
+
hostPlatform,
|
|
20
22
|
resetSandbox,
|
|
21
23
|
signalExitCode,
|
|
22
24
|
VX_RUN_TASK_ENV,
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
isGroupTask,
|
|
32
34
|
markSurfacedDeps,
|
|
33
35
|
runGraph,
|
|
36
|
+
taskDefinitions,
|
|
34
37
|
type TaskNode,
|
|
35
38
|
type TaskOutcome,
|
|
36
39
|
} from '../graph/index.js'
|
|
@@ -51,17 +54,18 @@ import {
|
|
|
51
54
|
detectColors,
|
|
52
55
|
type ColorSupport,
|
|
53
56
|
SecretBroker,
|
|
57
|
+
thrownMessage,
|
|
54
58
|
type ui,
|
|
55
59
|
} from '../util/index.js'
|
|
56
60
|
import { keyedProjects } from './keyed-projects.js'
|
|
57
|
-
import { isDefaultBuild } from '
|
|
61
|
+
import { isDefaultBuild } from '../graph/index.js'
|
|
58
62
|
import { commandWriteReach, prepareSandbox } from './sandbox-request.js'
|
|
59
63
|
import { recordOutputDirSnapshots, type OutputDirSnapshot, type SaveFacts } from './miss-save.js'
|
|
60
64
|
import { admitTasks, taintTracker } from './admission.js'
|
|
61
|
-
import { unkeyedGroupOutcome, type ExecuteArgs } from './execute-task.js'
|
|
65
|
+
import { settleProbedHit, unkeyedGroupOutcome, type ExecuteArgs } from './execute-task.js'
|
|
62
66
|
import { executeArgsOf, type RunScope } from './execute-args.js'
|
|
63
67
|
import { excludedTaint } from './excluded-keys.js'
|
|
64
|
-
import { keyUpstream } from './upstream.js'
|
|
68
|
+
import { dependedOn, keyUpstream } from './upstream.js'
|
|
65
69
|
import {
|
|
66
70
|
busLogger,
|
|
67
71
|
createEventBus,
|
|
@@ -80,9 +84,16 @@ import {
|
|
|
80
84
|
} from '../report/index.js'
|
|
81
85
|
import { installPlugins, PluginSetupError, type RunReport } from './plugin.js'
|
|
82
86
|
import { keepsFailures, reportOf, RunKeeper } from './run-report.js'
|
|
83
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
buildAdmission,
|
|
89
|
+
executorDemand,
|
|
90
|
+
executorLabel,
|
|
91
|
+
resolveExecutors,
|
|
92
|
+
teardownPlugins,
|
|
93
|
+
} from './plugin-host.js'
|
|
84
94
|
import { createSteps } from './steps.js'
|
|
85
|
-
import {
|
|
95
|
+
import { liveSocketOf, serveLive } from './run-live.js'
|
|
96
|
+
import { writeRunSteps, writeRunWarnings } from './run-steps.js'
|
|
86
97
|
import { subscribeTelemetry, type TelemetryHandle } from './telemetry-host.js'
|
|
87
98
|
import { claimOutput, guardedOutput } from './output-host.js'
|
|
88
99
|
import { assembleRunSummary } from './telemetry.js'
|
|
@@ -90,14 +101,15 @@ import { isPassStatus } from '../graph/index.js'
|
|
|
90
101
|
import type { RunContextRecord } from './telemetry.js'
|
|
91
102
|
import { plan, type RunPlan } from './plan.js'
|
|
92
103
|
import type { AffectedReason } from './affected-tasks.js'
|
|
93
|
-
import { openRemotes, prepareRun, type PreparedRun } from './prepare.js'
|
|
104
|
+
import { namesRemote, openRemotes, prepareRun, type PreparedRun } from './prepare.js'
|
|
94
105
|
import { acquireRunLock } from './run-lock.js'
|
|
95
106
|
import { terminateChildren } from './signals.js'
|
|
96
|
-
import { runStop } from './run-stop.js'
|
|
107
|
+
import { CYCLE_INTERRUPT, runStop } from './run-stop.js'
|
|
97
108
|
import {
|
|
98
109
|
hasPooledExecutor,
|
|
99
110
|
placeTasks,
|
|
100
111
|
planExecutorOf,
|
|
112
|
+
platformVerdict,
|
|
101
113
|
poolOfPlacement,
|
|
102
114
|
terminalHolders,
|
|
103
115
|
locallyPlaced,
|
|
@@ -127,6 +139,7 @@ import {
|
|
|
127
139
|
sessionEnd,
|
|
128
140
|
takeHeldServers,
|
|
129
141
|
} from './persistent.js'
|
|
142
|
+
import { serverKey } from './persistent-task.js'
|
|
130
143
|
import { writeRunProfile } from './run-artifacts.js'
|
|
131
144
|
import { createSaveLane } from './save-lane.js'
|
|
132
145
|
import { commandWindows } from './command-windows.js'
|
|
@@ -188,18 +201,37 @@ export function shouldShortCircuit(nodes: Map<string, TaskNode>, policy: CachePo
|
|
|
188
201
|
return nodes.size > 0
|
|
189
202
|
}
|
|
190
203
|
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
/**
|
|
205
|
+
* The run's own secrets: vx's environment and every selected task's
|
|
206
|
+
* `define` and `exec.env.secret`, as each task's output masks them. With
|
|
207
|
+
* the environment's alone, a secret only a config names reached the stored
|
|
208
|
+
* line `vx last` prints in full.
|
|
209
|
+
*/
|
|
210
|
+
function invocationMask(nodes: Iterable<TaskNode>): (command: string) => string {
|
|
211
|
+
const sources: Array<Readonly<Record<string, string>>> = [process.env as Record<string, string>]
|
|
212
|
+
const named = new Set<string>()
|
|
213
|
+
for (const n of nodes) {
|
|
214
|
+
const env = n.config.exec?.env
|
|
215
|
+
if (env?.define !== undefined) sources.push(env.define)
|
|
216
|
+
for (const name of env?.secret ?? []) named.add(name)
|
|
217
|
+
}
|
|
218
|
+
const mask = secretMask(sources, [...named])
|
|
219
|
+
return (command) => mask?.mask(command) ?? command
|
|
193
220
|
}
|
|
194
221
|
|
|
195
222
|
/**
|
|
196
223
|
* The command line a run hands to its telemetry sinks, with what follows
|
|
197
224
|
* `--` counted, not quoted. Those are the task's arguments,
|
|
198
|
-
* often a token (`-- --token=…`), and the line reached an OTLP span
|
|
199
|
-
* GitHub job summary
|
|
200
|
-
* 1057). Local history (`vx last`) keeps the whole line, on this machine.
|
|
225
|
+
* often a token (`-- --token=…`), and the line reached an OTLP span and
|
|
226
|
+
* the GitHub job summary verbatim (item 1057). Local history (`vx last`) keeps the whole line, on this machine.
|
|
201
227
|
*/
|
|
202
|
-
export function invocationCommand(
|
|
228
|
+
export function invocationCommand(line: readonly string[] | string): string {
|
|
229
|
+
// A line is shell-quoted (the CLI's): its words as written, so `'a b'`
|
|
230
|
+
// counts one argument, not the two a split at blanks made.
|
|
231
|
+
const argv =
|
|
232
|
+
typeof line === 'string'
|
|
233
|
+
? (line.match(/(?:[^\s'"\\]|\\.|'[^']*'|"(?:[^"\\]|\\.)*")+/g) ?? [])
|
|
234
|
+
: line
|
|
203
235
|
const sep = argv.indexOf('--')
|
|
204
236
|
if (sep === -1) return argv.join(' ')
|
|
205
237
|
const rest = argv.length - sep - 1
|
|
@@ -313,8 +345,14 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
313
345
|
// bytes as written, as the cache holds them: it formats its own output.
|
|
314
346
|
const render = terminalSubscriber(terminal === null || colors.enabled ? sink : plainOutput(sink))
|
|
315
347
|
let unsubscribeTerminal = bus.subscribe(render)
|
|
348
|
+
// What the run warned of, no task's: the footer repeats it (`warn`).
|
|
349
|
+
const warned: string[] = []
|
|
350
|
+
const unsubscribeWarned = bus.subscribe((e) => {
|
|
351
|
+
if (e.kind === 'run:status' && 'warn' in e) warned.push(e.line)
|
|
352
|
+
})
|
|
316
353
|
const detach = (): void => {
|
|
317
354
|
unsubscribeTerminal()
|
|
355
|
+
unsubscribeWarned()
|
|
318
356
|
terminal?.settle()
|
|
319
357
|
}
|
|
320
358
|
// A plugin's `output` renders the run from its start; core's renderer
|
|
@@ -333,7 +371,7 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
333
371
|
// vx's own steps are timed only where something renders them.
|
|
334
372
|
const showsSteps =
|
|
335
373
|
terminal !== null ? terminal.showsSteps : options.log?.stepStart !== undefined
|
|
336
|
-
const summary = await runOnBus(options, bus, colors, terminal, showsSteps, takeOutput)
|
|
374
|
+
const summary = await runOnBus(options, bus, warned, colors, terminal, showsSteps, takeOutput)
|
|
337
375
|
if (summary.persistent === undefined) return summary
|
|
338
376
|
// Servers handed back still running still write: `vx watch dev` printed
|
|
339
377
|
// none of its server's log while it idled, the renderer gone with this
|
|
@@ -355,6 +393,7 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
355
393
|
async function runOnBus(
|
|
356
394
|
options: RunOptions,
|
|
357
395
|
bus: EventBus,
|
|
396
|
+
warned: readonly string[],
|
|
358
397
|
colors: ColorSupport,
|
|
359
398
|
terminal: DefaultLogger | null,
|
|
360
399
|
showsSteps: boolean,
|
|
@@ -377,7 +416,6 @@ async function runOnBus(
|
|
|
377
416
|
}
|
|
378
417
|
|
|
379
418
|
const prepared = await step('vx:load', () => prepareRun(options, log))
|
|
380
|
-
options = withRunSettings(options, prepared.workspaceConfig)
|
|
381
419
|
mark('plugin stages')
|
|
382
420
|
// Every exit from here tears the plugins down: their capability
|
|
383
421
|
// factories ran in prepareRun, and teardown is what releases what those
|
|
@@ -392,6 +430,27 @@ async function runOnBus(
|
|
|
392
430
|
const plugins = prepared.plugins.filter((p) => p !== except)
|
|
393
431
|
await teardownPlugins(plugins, warnOf(log), report, step)
|
|
394
432
|
}
|
|
433
|
+
// The setting variables are read before any task, so a bad value is
|
|
434
|
+
// refused here, not mid-run; an embedder's (the CLI refuses it first)
|
|
435
|
+
// threw past the teardown and the cache close.
|
|
436
|
+
let inputStability: { run: InputStability; workspace?: InputStability }
|
|
437
|
+
let cleanOutputs: boolean | undefined
|
|
438
|
+
try {
|
|
439
|
+
options = withRunSettings(options, prepared.workspaceConfig)
|
|
440
|
+
inputStability = {
|
|
441
|
+
run: inputStabilityEnv(),
|
|
442
|
+
...(prepared.workspaceConfig?.cache?.inputStability !== undefined
|
|
443
|
+
? { workspace: prepared.workspaceConfig.cache.inputStability }
|
|
444
|
+
: {}),
|
|
445
|
+
}
|
|
446
|
+
cleanOutputs = resolveSetting('cache.outputs.clean', prepared.workspaceConfig).value as
|
|
447
|
+
| boolean
|
|
448
|
+
| undefined
|
|
449
|
+
} catch (err) {
|
|
450
|
+
await teardown()
|
|
451
|
+
prepared.cache.close()
|
|
452
|
+
throw err
|
|
453
|
+
}
|
|
395
454
|
// A task whose command re-enters `vx run` in the workspace running it
|
|
396
455
|
// is refused. When the inner run reaches this task again it forks a run
|
|
397
456
|
// per run until the machine gives out; when it does not (`ci` shelling
|
|
@@ -401,7 +460,8 @@ async function runOnBus(
|
|
|
401
460
|
// name the task; the check is on the root so both shapes are caught.
|
|
402
461
|
// Compared canonical: a root reached through a symlink (`run({ cwd })`,
|
|
403
462
|
// macOS's /var/folders) and the inner vx's realpath'd cwd are one tree.
|
|
404
|
-
|
|
463
|
+
// Empty is unset: resolved, '' named the cwd.
|
|
464
|
+
const outerRoot = process.env[VX_RUN_WORKSPACE_ENV] || undefined
|
|
405
465
|
if (outerRoot !== undefined && canonical(outerRoot) === canonical(prepared.workspaceRoot)) {
|
|
406
466
|
await teardown()
|
|
407
467
|
prepared.cache.close()
|
|
@@ -517,7 +577,6 @@ async function runOnBus(
|
|
|
517
577
|
}
|
|
518
578
|
const {
|
|
519
579
|
workspaceRoot,
|
|
520
|
-
workspaceConfig,
|
|
521
580
|
cacheDir,
|
|
522
581
|
cache,
|
|
523
582
|
nodes,
|
|
@@ -527,12 +586,14 @@ async function runOnBus(
|
|
|
527
586
|
hashCache,
|
|
528
587
|
workspaceProjectCount,
|
|
529
588
|
} = prepared
|
|
589
|
+
const maskInvocation = invocationMask(nodes.values())
|
|
530
590
|
const concurrency = options.concurrency ?? machineParallelism()
|
|
531
591
|
// An earlier cycle's servers: the ones this graph starts again unchanged
|
|
532
592
|
// stay up; the others go now, before anything binds their ports. A stop
|
|
533
593
|
// that lands meanwhile ends the run here, having run nothing: an aborted
|
|
534
594
|
// graph printed a `not run` row and a footer above `stopped` (WD-22).
|
|
535
595
|
const heldBefore = takeHeldServers(options.keep?.servers, nodes, options.forwardArgs)
|
|
596
|
+
for (const server of heldBefore.kept.values()) server.out.bus = bus
|
|
536
597
|
if (heldBefore.stale.length > 0) {
|
|
537
598
|
await terminateChildren(() => heldBefore.stale)
|
|
538
599
|
if (options.signal?.aborted === true) {
|
|
@@ -578,8 +639,29 @@ async function runOnBus(
|
|
|
578
639
|
warnOf(log)(`[vx] ${id}: exec.remote is 'only' and ${why} — nothing ran`)
|
|
579
640
|
}
|
|
580
641
|
}
|
|
581
|
-
|
|
582
|
-
|
|
642
|
+
const platforms = platformVerdict(placements, ciContext.ci)
|
|
643
|
+
if (platforms !== undefined && 'refuse' in platforms) {
|
|
644
|
+
await abandoning(() => {
|
|
645
|
+
throw new UserError(platforms.refuse)
|
|
646
|
+
})
|
|
647
|
+
}
|
|
648
|
+
if (platforms !== undefined && 'warn' in platforms) warnOf(log)(platforms.warn)
|
|
649
|
+
/** A task its `exec.platforms` skip here: it never starts, and nor does what depends on it. */
|
|
650
|
+
const platformSkip = (node: TaskNode): TaskOutcome | undefined => {
|
|
651
|
+
const needs = placements.otherPlatform.get(node.id)
|
|
652
|
+
return needs === undefined
|
|
653
|
+
? undefined
|
|
654
|
+
: {
|
|
655
|
+
node,
|
|
656
|
+
status: 'skipped',
|
|
657
|
+
exitCode: 0,
|
|
658
|
+
durationMs: 0,
|
|
659
|
+
otherPlatform: { needs, host: hostPlatform() },
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Demand: what each executor still has left to run (`executorDemand`),
|
|
663
|
+
// told once the cache has said what it answers.
|
|
664
|
+
const demand = executorDemand(placements.executors, warnOf(log))
|
|
583
665
|
|
|
584
666
|
// `cache.download` (default `all`) decides ONCE per task, here, whether a
|
|
585
667
|
// remote execution's outputs come home.
|
|
@@ -594,19 +676,11 @@ async function runOnBus(
|
|
|
594
676
|
|
|
595
677
|
// `run.timeout` (ms), for any task WITHOUT its own `exec.timeout`.
|
|
596
678
|
const taskTimeoutDefault = options.timeout
|
|
597
|
-
// Read once, before any task: a bad value is refused here, not mid-run.
|
|
598
|
-
const inputStability = {
|
|
599
|
-
run: inputStabilityEnv(),
|
|
600
|
-
...(workspaceConfig?.cache?.inputStability !== undefined
|
|
601
|
-
? { workspace: workspaceConfig.cache.inputStability }
|
|
602
|
-
: {}),
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const cleanOutputs = resolveSetting('cache.outputs.clean', workspaceConfig).value as
|
|
606
|
-
| boolean
|
|
607
|
-
| undefined
|
|
608
679
|
|
|
680
|
+
// The held servers this run took over, spared by a `CYCLE_INTERRUPT`.
|
|
681
|
+
const handed = new Set<ReturnType<typeof Bun.spawn>>()
|
|
609
682
|
const stop = runStop({
|
|
683
|
+
handed: (child) => handed.has(child),
|
|
610
684
|
log,
|
|
611
685
|
cache,
|
|
612
686
|
external: options.signal,
|
|
@@ -630,11 +704,13 @@ async function runOnBus(
|
|
|
630
704
|
// because the normal path closes before the persistent-task wait —
|
|
631
705
|
// holding the handle open for a dev server's whole lifetime would be
|
|
632
706
|
// worse.
|
|
707
|
+
let live: ReturnType<typeof serveLive> | undefined
|
|
633
708
|
let cacheClosed = false
|
|
634
709
|
const closeCache = async (): Promise<void> => {
|
|
635
710
|
if (cacheClosed) return
|
|
636
711
|
cacheClosed = true
|
|
637
712
|
cache.close()
|
|
713
|
+
live?.unlisten()
|
|
638
714
|
// Released with the handle: before a persistent task's wait, on every
|
|
639
715
|
// exit path (see run-lock.ts for why a run holds one at all). AWAITED:
|
|
640
716
|
// fired and forgotten, the release lost the race with the CLI's exit —
|
|
@@ -654,6 +730,16 @@ async function runOnBus(
|
|
|
654
730
|
}),
|
|
655
731
|
),
|
|
656
732
|
)
|
|
733
|
+
// Answers `vx last --live` while the lock is held (run-live.ts).
|
|
734
|
+
live = serveLive({
|
|
735
|
+
socketPath: liveSocketOf(releaseRunLock.path),
|
|
736
|
+
bus,
|
|
737
|
+
runId,
|
|
738
|
+
command: maskInvocation(invocationCommand(options.command ?? process.argv.slice(1))),
|
|
739
|
+
startedAt: Date.now(),
|
|
740
|
+
nodes: () => nodes.values(),
|
|
741
|
+
warnings: () => warned,
|
|
742
|
+
})
|
|
657
743
|
try {
|
|
658
744
|
const runStartHrTimeNs = process.hrtime.bigint()
|
|
659
745
|
const endedAtMsAtStart = Date.now()
|
|
@@ -666,9 +752,17 @@ async function runOnBus(
|
|
|
666
752
|
// clean their outputs before every exec for a save that never
|
|
667
753
|
// happened.
|
|
668
754
|
const policy: CachePolicy = effectiveCachePolicy(prepared.cachePolicy, remoteCacheEnabled)
|
|
669
|
-
|
|
755
|
+
// The workspace's `cache.policy` asks as a flag does: it was the one source never told.
|
|
756
|
+
const remoteNamed =
|
|
757
|
+
options.remoteRequested ??
|
|
758
|
+
(options.cache === undefined &&
|
|
759
|
+
options.cacheFlags?.noCache !== true &&
|
|
760
|
+
namesRemote(prepared.workspaceConfig?.cache?.policy)
|
|
761
|
+
? 'cache.policy'
|
|
762
|
+
: undefined)
|
|
763
|
+
if (remoteNamed !== undefined && !prepared.hasRemoteLayer) {
|
|
670
764
|
warnOf(log)(
|
|
671
|
-
|
|
765
|
+
`vx: ${remoteNamed} named the remote cache, but no cache plugin in .vx/workspace.* supplies one — the remote axes are off for this run`,
|
|
672
766
|
)
|
|
673
767
|
}
|
|
674
768
|
const deferredOutputs = new DeferredOutputs({
|
|
@@ -716,9 +810,7 @@ async function runOnBus(
|
|
|
716
810
|
// token after `--` to every sink verbatim (C-67).
|
|
717
811
|
// Masked as the stored line is: a flag's secret value sits before
|
|
718
812
|
// the `--` the count covers (L-44).
|
|
719
|
-
command: maskInvocation(
|
|
720
|
-
invocationCommand(options.command?.split(' ') ?? process.argv.slice(1)),
|
|
721
|
-
),
|
|
813
|
+
command: maskInvocation(invocationCommand(options.command ?? process.argv.slice(1))),
|
|
722
814
|
requestedTasks: options.tasks,
|
|
723
815
|
cachePolicy: formatCachePolicy(policy),
|
|
724
816
|
concurrency,
|
|
@@ -752,7 +844,7 @@ async function runOnBus(
|
|
|
752
844
|
!(policy.localRead || policy.remoteRead)
|
|
753
845
|
// Only what runs here arms this machine's sandbox: a `sandbox` executor
|
|
754
846
|
// enforces it where it runs the task, so a distributed run's
|
|
755
|
-
// orchestrator needs none (
|
|
847
|
+
// orchestrator needs none (2026-10-22).
|
|
756
848
|
const sandboxed =
|
|
757
849
|
sandboxArmer === null
|
|
758
850
|
? []
|
|
@@ -815,14 +907,17 @@ async function runOnBus(
|
|
|
815
907
|
|
|
816
908
|
// Run context for the footer. The top-of-run header is gone — the
|
|
817
909
|
// banner now lives in the summary, where the eye lands at the end.
|
|
910
|
+
const fault = executorFaults(warnOf(log))
|
|
818
911
|
const footers = executors.filter((e) => e.footer !== undefined)
|
|
819
912
|
const placers = executors.filter((e) => e.running !== undefined)
|
|
820
913
|
const runContext = {
|
|
821
|
-
...(footers.length > 0
|
|
914
|
+
...(footers.length > 0
|
|
915
|
+
? { executorRows: () => footers.flatMap((e) => executorFooter(e, fault)) }
|
|
916
|
+
: {}),
|
|
822
917
|
...(placers.length > 0
|
|
823
918
|
? {
|
|
824
919
|
whereRunning: (id: string) =>
|
|
825
|
-
placers.map((e) => executorWhere(e, id)).find((w) => w !== undefined),
|
|
920
|
+
placers.map((e) => executorWhere(e, id, fault)).find((w) => w !== undefined),
|
|
826
921
|
}
|
|
827
922
|
: {}),
|
|
828
923
|
version: VERSION,
|
|
@@ -840,8 +935,9 @@ async function runOnBus(
|
|
|
840
935
|
},
|
|
841
936
|
concurrency,
|
|
842
937
|
workspaceProjectCount,
|
|
843
|
-
// Live too: a long run is read while it runs (
|
|
938
|
+
// Live too: a long run is read while it runs (2026-10-21).
|
|
844
939
|
runId,
|
|
940
|
+
warnings: () => warned,
|
|
845
941
|
}
|
|
846
942
|
|
|
847
943
|
// Asked only when a plugin declares `output`: a plain run pays nothing.
|
|
@@ -879,6 +975,7 @@ async function runOnBus(
|
|
|
879
975
|
prepared.localCache.dbHandle(),
|
|
880
976
|
undefined,
|
|
881
977
|
prepared.branch(),
|
|
978
|
+
taskDefinitions(nodes.values()),
|
|
882
979
|
).p50sFor(ids)
|
|
883
980
|
return createForecast(nodes, p50s, concurrency)
|
|
884
981
|
},
|
|
@@ -892,9 +989,20 @@ async function runOnBus(
|
|
|
892
989
|
// local-only runs never reach here, so their behavior + perf is
|
|
893
990
|
// unchanged (no upfront key pass, no local probing).
|
|
894
991
|
let prefetchDone: Promise<void> = Promise.resolve()
|
|
992
|
+
/** `--profile`'s run events beside the tasks: the cache probe's. */
|
|
993
|
+
const runSpans: Record<string, unknown>[] = []
|
|
994
|
+
// A remote that is read answers which tasks it holds: demand settles
|
|
995
|
+
// on that answer, a failed prefetch settling it on none.
|
|
996
|
+
const prefetchSettles = prepared.hasRemoteLayer && policy.remoteRead
|
|
895
997
|
if (prepared.hasRemoteLayer) {
|
|
896
998
|
prefetchDone = steps.background('vx:prefetch', () =>
|
|
897
999
|
startRemotePrefetch({
|
|
1000
|
+
...(demand.any
|
|
1001
|
+
? {
|
|
1002
|
+
held: (ids: readonly string[], missed: readonly string[]) =>
|
|
1003
|
+
demand.settle(ids, missed),
|
|
1004
|
+
}
|
|
1005
|
+
: {}),
|
|
898
1006
|
nodes,
|
|
899
1007
|
cache,
|
|
900
1008
|
workspaceRoot,
|
|
@@ -906,6 +1014,7 @@ async function runOnBus(
|
|
|
906
1014
|
concurrency: options.downloadConcurrency ?? networkConcurrency(),
|
|
907
1015
|
remoteRead: policy.remoteRead,
|
|
908
1016
|
remoteWrite: policy.remoteWrite,
|
|
1017
|
+
spans: options.profile === undefined ? undefined : runSpans,
|
|
909
1018
|
}),
|
|
910
1019
|
)
|
|
911
1020
|
}
|
|
@@ -960,6 +1069,19 @@ async function runOnBus(
|
|
|
960
1069
|
if (!remote && sandboxed.some((n) => shortCircuit.preProbed.get(n.id)?.hit === null))
|
|
961
1070
|
prewarm()
|
|
962
1071
|
}
|
|
1072
|
+
if (demand.any) {
|
|
1073
|
+
if (prefetchSettles) void prefetchDone.finally(() => demand.settle([]))
|
|
1074
|
+
else {
|
|
1075
|
+
const hits: string[] = []
|
|
1076
|
+
const missed: string[] = []
|
|
1077
|
+
for (const [id, probe] of shortCircuit.preProbed)
|
|
1078
|
+
(probe.hit !== null ? hits : missed).push(id)
|
|
1079
|
+
// An empty store was not asked: it holds none of them.
|
|
1080
|
+
if (coldLocal && !remote)
|
|
1081
|
+
for (const [id, n] of nodes) if (n.config.cache !== undefined) missed.push(id)
|
|
1082
|
+
demand.settle(hits, missed)
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
963
1085
|
|
|
964
1086
|
// Whether this task runs behind a failure (`continueMode: 'always'`
|
|
965
1087
|
// only) or on the key of a dependency `--exclude-dependencies` kept
|
|
@@ -985,8 +1107,7 @@ async function runOnBus(
|
|
|
985
1107
|
const taint = taintTracker(options.continueMode === 'always', taintSeeds, nodes)
|
|
986
1108
|
// Each task that ran tainted, and the task at the root of it.
|
|
987
1109
|
const taintedRan = new Map<string, string>()
|
|
988
|
-
const
|
|
989
|
-
for (const n of nodes.values()) for (const d of n.deps) dependedOn.add(d)
|
|
1110
|
+
const depended = dependedOn(nodes.values())
|
|
990
1111
|
|
|
991
1112
|
// Why a task missed, for a sink: a read of the history per miss, so only
|
|
992
1113
|
// when a sink exists.
|
|
@@ -1026,13 +1147,14 @@ async function runOnBus(
|
|
|
1026
1147
|
if (explainMiss !== undefined) scope.explainMiss = explainMiss
|
|
1027
1148
|
if (keepRun !== undefined) scope.keepRun = keepRun
|
|
1028
1149
|
if (sandboxArmer !== null) scope.armSandbox = () => sandboxArmer.arm()
|
|
1150
|
+
if (demand.any) scope.executing = demand.executing
|
|
1029
1151
|
const argsOf = executeArgsOf(scope, {
|
|
1030
1152
|
log: (node) => (node.config.exec?.persistent !== undefined ? serverLog(node.id) : log),
|
|
1031
1153
|
placements,
|
|
1032
1154
|
nestedDirsByProject,
|
|
1033
1155
|
downloadModeOf: download.modeOf,
|
|
1034
1156
|
shortCircuit,
|
|
1035
|
-
dependedOn,
|
|
1157
|
+
dependedOn: depended,
|
|
1036
1158
|
holders,
|
|
1037
1159
|
})
|
|
1038
1160
|
const buildExecuteArgs = (
|
|
@@ -1112,6 +1234,7 @@ async function runOnBus(
|
|
|
1112
1234
|
heldBefore.kept.delete(node.id)
|
|
1113
1235
|
if (hasEnded(server.child)) return undefined
|
|
1114
1236
|
server.handOver()
|
|
1237
|
+
handed.add(server.child)
|
|
1115
1238
|
server.out.bus = bus
|
|
1116
1239
|
serverOuts.set(node.id, server.out)
|
|
1117
1240
|
persistentRegistry.set(node.id, server.child)
|
|
@@ -1147,7 +1270,14 @@ async function runOnBus(
|
|
|
1147
1270
|
judgeFlaky(o)
|
|
1148
1271
|
keeper?.taskDone(o)
|
|
1149
1272
|
log.taskComplete(o.node, o)
|
|
1150
|
-
|
|
1273
|
+
demand.finished(o.node.id)
|
|
1274
|
+
// A server that dies while the graph runs is no longer running:
|
|
1275
|
+
// its pin stayed `running` under the line naming its exit.
|
|
1276
|
+
const server = o.status === 'success' ? persistentRegistry.get(o.node.id) : undefined
|
|
1277
|
+
if (server !== undefined && o.serverExit === undefined)
|
|
1278
|
+
void server.exited.then((code) => {
|
|
1279
|
+
if (code !== 0 && !graphOver && !stop.signal.aborted) o.serverExit = code
|
|
1280
|
+
})
|
|
1151
1281
|
},
|
|
1152
1282
|
// A crash a hand-broken lock entry caused is named as that; asked
|
|
1153
1283
|
// only here, so a run that does not crash pays nothing.
|
|
@@ -1159,24 +1289,36 @@ async function runOnBus(
|
|
|
1159
1289
|
: line,
|
|
1160
1290
|
),
|
|
1161
1291
|
execute: (node, upstream) => {
|
|
1292
|
+
const skip = platformSkip(node)
|
|
1293
|
+
if (skip !== undefined) return Promise.resolve(skip)
|
|
1162
1294
|
const kept = keepServer(node)
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1295
|
+
if (kept === undefined) return executeWithDedup(node, keyUpstream(node, upstream))
|
|
1296
|
+
// Keyed as a started server is: a cached dependant folds its key,
|
|
1297
|
+
// and without it an edit to the server's sources hit stale.
|
|
1298
|
+
return Promise.resolve(
|
|
1299
|
+
serverKey(argsOf(node, keyUpstream(node, upstream), true, false)),
|
|
1300
|
+
).then((hash) => (hash === undefined ? kept : { ...kept, hash }))
|
|
1166
1301
|
},
|
|
1167
|
-
// An unkeyed group runs nothing
|
|
1168
|
-
//
|
|
1302
|
+
// An unkeyed group runs nothing, and a restore-tier hit whose tree is
|
|
1303
|
+
// current restores nothing: settled in place, unless a taint may ride
|
|
1304
|
+
// it, which `buildExecuteArgs` records on dispatch.
|
|
1169
1305
|
...(options.continueMode !== 'always' && taintSeeds.size === 0
|
|
1170
1306
|
? {
|
|
1171
|
-
settleNow: (node: TaskNode, upstream: TaskOutcome[]) =>
|
|
1172
|
-
|
|
1173
|
-
|
|
1307
|
+
settleNow: (node: TaskNode, upstream: TaskOutcome[]) => {
|
|
1308
|
+
const skip = platformSkip(node)
|
|
1309
|
+
if (skip !== undefined) return () => skip
|
|
1310
|
+
if (isGroupTask(node) && node.config.inputs === undefined)
|
|
1311
|
+
return () =>
|
|
1312
|
+
unkeyedGroupOutcome(
|
|
1174
1313
|
node,
|
|
1175
1314
|
keyUpstream(node, upstream),
|
|
1176
1315
|
shortCircuit.groupKeys.get(node.id),
|
|
1177
1316
|
runStartHrTimeNs,
|
|
1178
1317
|
)
|
|
1179
|
-
|
|
1318
|
+
if (!shortCircuit.restoreTier.has(node.id) || heldBefore.kept.has(node.id))
|
|
1319
|
+
return undefined
|
|
1320
|
+
return settleProbedHit(buildExecuteArgs(node, keyUpstream(node, upstream)))
|
|
1321
|
+
},
|
|
1180
1322
|
}
|
|
1181
1323
|
: {}),
|
|
1182
1324
|
// A `schedule` plugin's weights; the scheduler keeps its structural
|
|
@@ -1214,8 +1356,11 @@ async function runOnBus(
|
|
|
1214
1356
|
// signal loop) and embedders that manage lifecycle themselves — both
|
|
1215
1357
|
// expect run() to return, not block on a server.
|
|
1216
1358
|
const foreground = options.log === undefined && (options.handleSignals ?? true)
|
|
1217
|
-
// An aborted run's children are already being torn down: nothing to
|
|
1218
|
-
|
|
1359
|
+
// An aborted run's children are already being torn down: nothing to
|
|
1360
|
+
// hold, but the servers a `CYCLE_INTERRUPT` spared.
|
|
1361
|
+
const hold =
|
|
1362
|
+
options.holdPersistent === true &&
|
|
1363
|
+
(!stop.signal.aborted || stop.signal.reason === CYCLE_INTERRUPT)
|
|
1219
1364
|
const { keepAlive, crashed: crashedPersistent } = await disposeServers({
|
|
1220
1365
|
registry: persistentRegistry,
|
|
1221
1366
|
nodes,
|
|
@@ -1231,7 +1376,14 @@ async function runOnBus(
|
|
|
1231
1376
|
// Clear the status line for good before the summary prints.
|
|
1232
1377
|
log.runEnd?.()
|
|
1233
1378
|
const list = [...outcomes.values()]
|
|
1234
|
-
|
|
1379
|
+
// A platform skip, and a skip it blocked, is no failure: the task is not this machine's.
|
|
1380
|
+
const ok = list.every(
|
|
1381
|
+
(o) =>
|
|
1382
|
+
isPassStatus(o.status) ||
|
|
1383
|
+
(o.status === 'skipped' &&
|
|
1384
|
+
(o.otherPlatform !== undefined ||
|
|
1385
|
+
(o.blockedBy !== undefined && placements.otherPlatform.has(o.blockedBy)))),
|
|
1386
|
+
)
|
|
1235
1387
|
|
|
1236
1388
|
// The summary + artifact writers + recordRun pass all exclude group
|
|
1237
1389
|
// tasks via the shared tallyOutcomes helper. We pass the full
|
|
@@ -1261,7 +1413,7 @@ async function runOnBus(
|
|
|
1261
1413
|
const stillDeferred = deferredOutputs.pending()
|
|
1262
1414
|
if (stillDeferred.length > 0) {
|
|
1263
1415
|
log.status(
|
|
1264
|
-
` Deferred: left outputs remote (--cache-download
|
|
1416
|
+
` Deferred: left outputs remote${downloadPolicy === 'all' ? '' : ` (--cache-download=${downloadPolicy})`}: ${stillDeferred.join(', ')}`,
|
|
1265
1417
|
)
|
|
1266
1418
|
}
|
|
1267
1419
|
|
|
@@ -1277,6 +1429,9 @@ async function runOnBus(
|
|
|
1277
1429
|
target: profile,
|
|
1278
1430
|
cwd: options.cwd,
|
|
1279
1431
|
outcomes: list,
|
|
1432
|
+
runStartNs: runStartHrTimeNs,
|
|
1433
|
+
executors: executors.map((e) => executorProfile(e, fault)),
|
|
1434
|
+
spans: runSpans,
|
|
1280
1435
|
}),
|
|
1281
1436
|
)
|
|
1282
1437
|
log.status(`vx: profile written to ${wrote}`)
|
|
@@ -1334,8 +1489,12 @@ async function runOnBus(
|
|
|
1334
1489
|
// read `ok` over the exit 1 and fed the flaky list a pass (X-23).
|
|
1335
1490
|
const historyAfterWait = keepAlive.children.length > 0 && !hold && stop.stoppedBy === undefined
|
|
1336
1491
|
// A failed task's output, for `vx last` and an agent (run-failures.ts).
|
|
1337
|
-
|
|
1338
|
-
|
|
1492
|
+
// A cache directory this user cannot write keeps none of the run's
|
|
1493
|
+
// records: the run's one notice said so.
|
|
1494
|
+
const keeps = prepared.localCache.readOnlyNotice() === null
|
|
1495
|
+
const recordFailures = (final: readonly TaskOutcome[]): void => {
|
|
1496
|
+
if (keeps) writeRunFailures(prepared.cacheDir, runId, final, warnOf(log))
|
|
1497
|
+
}
|
|
1339
1498
|
if (stop.stoppedBy === undefined && !historyAfterWait) {
|
|
1340
1499
|
stepSync('vx:history', () => {
|
|
1341
1500
|
recordHistory(() => cache.recordRunBundle(records))
|
|
@@ -1343,11 +1502,12 @@ async function runOnBus(
|
|
|
1343
1502
|
})
|
|
1344
1503
|
} else if (stop.stoppedBy !== undefined) {
|
|
1345
1504
|
// Stopped: not a run `vx last` lists (item 854), but what finished is
|
|
1346
|
-
// measured, or the next run knows none of it (
|
|
1505
|
+
// measured, or the next run knows none of it (2026-10-21).
|
|
1347
1506
|
const ended = list.filter((o) => o.status !== 'aborted' && o.status !== 'skipped')
|
|
1348
|
-
stepSync('vx:history', () =>
|
|
1349
|
-
recordHistory(() => prepared.localCache.recordRuns(recordsOf(ended, false).runs))
|
|
1350
|
-
|
|
1507
|
+
stepSync('vx:history', () => {
|
|
1508
|
+
recordHistory(() => prepared.localCache.recordRuns(recordsOf(ended, false).runs))
|
|
1509
|
+
recordFailures(ended)
|
|
1510
|
+
})
|
|
1351
1511
|
}
|
|
1352
1512
|
mark('record history')
|
|
1353
1513
|
// Drain any still-in-flight background prefetches before closing the
|
|
@@ -1374,7 +1534,10 @@ async function runOnBus(
|
|
|
1374
1534
|
await prefetchDone
|
|
1375
1535
|
await Promise.all([cache.drainUploads?.(), keeper?.settled()])
|
|
1376
1536
|
})
|
|
1377
|
-
|
|
1537
|
+
if (keeps) {
|
|
1538
|
+
writeRunSteps(prepared.cacheDir, runId, steps.ended, warnOf(log))
|
|
1539
|
+
writeRunWarnings(prepared.cacheDir, runId, warned, warnOf(log))
|
|
1540
|
+
}
|
|
1378
1541
|
// An upload that failed after the rows went in: its row says so too.
|
|
1379
1542
|
if (prepared.localCache.uploads.failed > 0 && stop.stoppedBy === undefined && !historyAfterWait)
|
|
1380
1543
|
recordHistory(() => recordLateRemoteErrors(prepared.localCache, records.runs, runId))
|
|
@@ -1395,7 +1558,8 @@ async function runOnBus(
|
|
|
1395
1558
|
abortedCount: list.filter((o) => o.status === 'aborted' && !isGroupTask(o.node)).length,
|
|
1396
1559
|
stages: stageTimes(),
|
|
1397
1560
|
uploads: prepared.localCache.uploads,
|
|
1398
|
-
machines: executors.flatMap(executorMachines),
|
|
1561
|
+
machines: executors.flatMap((e) => executorMachines(e, fault)),
|
|
1562
|
+
idealMs: executors.map((e) => executorIdeal(e, fault)).find((ms) => ms !== undefined),
|
|
1399
1563
|
})
|
|
1400
1564
|
telemetry.emitSummary(summary)
|
|
1401
1565
|
const sinks = telemetry
|
|
@@ -1433,16 +1597,20 @@ async function runOnBus(
|
|
|
1433
1597
|
}
|
|
1434
1598
|
})
|
|
1435
1599
|
|
|
1436
|
-
// The footer is the run's last word (
|
|
1600
|
+
// The footer is the run's last word (2026-10-06): it prints once
|
|
1437
1601
|
// the run's own work is done, history, uploads, plugin teardown and the
|
|
1438
1602
|
// sandbox reset included, so any warning they raise lands above it. A
|
|
1439
1603
|
// task's own facts (flaky, blocked) ride its row. Only a kept server's
|
|
1440
1604
|
// own output follows.
|
|
1441
1605
|
// The footer replaces the live region in place.
|
|
1442
1606
|
log.runDone?.()
|
|
1443
|
-
if (options.summaryTable === true)
|
|
1607
|
+
if (options.summaryTable === true)
|
|
1608
|
+
for (const line of formatOutcomeTable(list, colors)) log.status(line)
|
|
1609
|
+
// A run a signal stopped records nothing `vx last` opens: no id to copy.
|
|
1610
|
+
const footerContext =
|
|
1611
|
+
stop.stoppedBy === undefined ? runContext : { ...runContext, runId: undefined }
|
|
1444
1612
|
if (options.footer !== false)
|
|
1445
|
-
for (const line of formatRunSummary(list, totalMs, colors,
|
|
1613
|
+
for (const line of formatRunSummary(list, totalMs, colors, footerContext)) log.status(line)
|
|
1446
1614
|
|
|
1447
1615
|
// Edge case the summary already reported: the user requested a
|
|
1448
1616
|
// persistent task (dev server / watcher). The run is "done" in every
|
|
@@ -1465,7 +1633,6 @@ async function runOnBus(
|
|
|
1465
1633
|
persistent: holdServers(keepAlive, {
|
|
1466
1634
|
forwardArgs: options.forwardArgs,
|
|
1467
1635
|
outOf: (id) => serverOuts.get(id) ?? { bus },
|
|
1468
|
-
warn: warnOf(log),
|
|
1469
1636
|
}),
|
|
1470
1637
|
}
|
|
1471
1638
|
}
|
|
@@ -1492,7 +1659,9 @@ async function runOnBus(
|
|
|
1492
1659
|
if (stop.signal.aborted) await stop.aborting?.catch(() => {})
|
|
1493
1660
|
else await terminateChildren(() => keepAlive.children)
|
|
1494
1661
|
// The server that ended the session on its own, not cleanly, failed.
|
|
1495
|
-
|
|
1662
|
+
// A stop's own kill is no crash: the user ended the session.
|
|
1663
|
+
const crashed = !stop.signal.aborted && first.code !== 0
|
|
1664
|
+
if (crashed) failServer(outcomes, node.id, first.code)
|
|
1496
1665
|
const final = [...outcomes.values()]
|
|
1497
1666
|
if (historyAfterWait) {
|
|
1498
1667
|
recordHistory(() => {
|
|
@@ -1500,14 +1669,14 @@ async function runOnBus(
|
|
|
1500
1669
|
// to its schema during the session, and a mismatch resets silently.
|
|
1501
1670
|
const local = new Cache(prepared.cacheDir, undefined, prepared.workspaceRoot)
|
|
1502
1671
|
try {
|
|
1503
|
-
local.recordRunBundle(recordsOf(final, ok &&
|
|
1672
|
+
local.recordRunBundle(recordsOf(final, ok && !crashed))
|
|
1504
1673
|
} finally {
|
|
1505
1674
|
local.close()
|
|
1506
1675
|
}
|
|
1507
1676
|
})
|
|
1508
1677
|
recordFailures(final)
|
|
1509
1678
|
}
|
|
1510
|
-
return { ok: ok &&
|
|
1679
|
+
return { ok: ok && !crashed, outcomes: final }
|
|
1511
1680
|
}
|
|
1512
1681
|
|
|
1513
1682
|
return { ok, outcomes: list }
|
|
@@ -1521,6 +1690,7 @@ async function runOnBus(
|
|
|
1521
1690
|
// floor for a throw a later change adds, not a path a row can drive.
|
|
1522
1691
|
log.runEnd?.()
|
|
1523
1692
|
log.runDone?.()
|
|
1693
|
+
live?.dispose()
|
|
1524
1694
|
stop.detach()
|
|
1525
1695
|
// Plugins installed at the top of run() get their bus subscriptions
|
|
1526
1696
|
// released here. Idempotent; safe even if installPlugins threw.
|
|
@@ -1578,8 +1748,8 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1578
1748
|
const log = options.log ?? defaultLogger(undefined, undefined, process.stderr)
|
|
1579
1749
|
const reasons = new Map<string, AffectedReason>()
|
|
1580
1750
|
const prepared = await prepareRun({ ...options, affectedReasons: reasons }, log)
|
|
1581
|
-
options = withRunSettings(options, prepared.workspaceConfig)
|
|
1582
1751
|
try {
|
|
1752
|
+
options = withRunSettings(options, prepared.workspaceConfig)
|
|
1583
1753
|
if (prepared.unresolvedTasks.length > 0) {
|
|
1584
1754
|
return {
|
|
1585
1755
|
tasks: [],
|
|
@@ -1599,6 +1769,7 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1599
1769
|
if (prepared.empty !== null) return { tasks: [] }
|
|
1600
1770
|
// The plan orders nothing, but it refuses what the run would refuse.
|
|
1601
1771
|
await prepared.priorities()
|
|
1772
|
+
terminalHolders(prepared.nodes, options.tty === true)
|
|
1602
1773
|
// Its own mark: a dry run's plan (every task's hash, the cache lookups,
|
|
1603
1774
|
// the history p50s) was booked under `close`, the next mark, and read
|
|
1604
1775
|
// as 105 ms of closing a cache at 1,000 projects (item 601).
|
|
@@ -1621,13 +1792,14 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1621
1792
|
prepared.localCache.dbHandle(),
|
|
1622
1793
|
undefined,
|
|
1623
1794
|
prepared.branch(),
|
|
1795
|
+
taskDefinitions(prepared.nodes.values()),
|
|
1624
1796
|
),
|
|
1625
1797
|
// Placement, by the same rules the run applies. Only worth showing
|
|
1626
1798
|
// when there is a choice to show — with one declared executor every
|
|
1627
1799
|
// line would carry the same label. Resolving the executors here is
|
|
1628
1800
|
// the same plugin-factory call `prepareRun` already makes for the
|
|
1629
1801
|
// cache capability, so plan mode gains no new class of side effect.
|
|
1630
|
-
...(await planExecutorOf(prepared, log, options.download ?? 'all')),
|
|
1802
|
+
...(await planExecutorOf(prepared, log, options.download ?? 'all', detectCi(process.env).ci)),
|
|
1631
1803
|
...(await planRestorable(prepared, policy, options.forwardArgs)),
|
|
1632
1804
|
})
|
|
1633
1805
|
mark('plan')
|
|
@@ -1698,12 +1870,15 @@ async function unresolvedHint(prepared: {
|
|
|
1698
1870
|
hintProjects: ReadonlyMap<string, ProjectEntry>
|
|
1699
1871
|
declaredElsewhere: readonly string[]
|
|
1700
1872
|
anyProjectConfig: boolean
|
|
1873
|
+
taskless: (name: string) => boolean
|
|
1874
|
+
unlockedConfig: () => string | undefined
|
|
1701
1875
|
}): Promise<string> {
|
|
1702
1876
|
const elsewhere = elsewhereHint(prepared.declaredElsewhere)
|
|
1703
1877
|
const near = didYouMean(
|
|
1704
1878
|
prepared.unresolvedTasks,
|
|
1705
1879
|
prepared.hintProjects,
|
|
1706
1880
|
prepared.declaredElsewhere,
|
|
1881
|
+
prepared.taskless,
|
|
1707
1882
|
)
|
|
1708
1883
|
const init = await initHint(prepared)
|
|
1709
1884
|
const lock =
|
|
@@ -1714,13 +1889,20 @@ async function unresolvedHint(prepared: {
|
|
|
1714
1889
|
}
|
|
1715
1890
|
|
|
1716
1891
|
/**
|
|
1717
|
-
* The first-run case, told apart from a typo:
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
1892
|
+
* The first-run case, told apart from a typo: the lock holds no project,
|
|
1893
|
+
* so no name could have resolved. Said of the lock, the only thing a run
|
|
1894
|
+
* reads: "no directory holds a vx.config" was false beside configs never
|
|
1895
|
+
* locked. A config written after the lock is no first run: its fix is
|
|
1896
|
+
* `vx lock`. Reached only on the error path.
|
|
1720
1897
|
*/
|
|
1721
|
-
async function initHint(prepared: {
|
|
1898
|
+
async function initHint(prepared: {
|
|
1899
|
+
anyProjectConfig: boolean
|
|
1900
|
+
unlockedConfig: () => string | undefined
|
|
1901
|
+
}): Promise<string> {
|
|
1722
1902
|
if (prepared.anyProjectConfig) return ''
|
|
1723
|
-
|
|
1903
|
+
const unlocked = prepared.unlockedConfig()
|
|
1904
|
+
if (unlocked !== undefined) return ` ${unlocked} is not in the lock yet — run \`vx lock\`.`
|
|
1905
|
+
return ` ${EMPTY_LOCK}.`
|
|
1724
1906
|
}
|
|
1725
1907
|
|
|
1726
1908
|
/**
|
|
@@ -1733,7 +1915,8 @@ async function initHint(prepared: { anyProjectConfig: boolean }): Promise<string
|
|
|
1733
1915
|
function didYouMean(
|
|
1734
1916
|
unresolved: readonly string[],
|
|
1735
1917
|
projects: ReadonlyMap<string, ProjectEntry>,
|
|
1736
|
-
elsewhere: readonly string[]
|
|
1918
|
+
elsewhere: readonly string[],
|
|
1919
|
+
taskless: (name: string) => boolean,
|
|
1737
1920
|
): string {
|
|
1738
1921
|
const tasksOf = (p: ProjectEntry | undefined): string[] => Object.keys(p?.config.tasks ?? {})
|
|
1739
1922
|
// What a bare name can select: a default `build` is no match (X-102).
|
|
@@ -1778,10 +1961,20 @@ function didYouMean(
|
|
|
1778
1961
|
)
|
|
1779
1962
|
continue
|
|
1780
1963
|
}
|
|
1964
|
+
if (taskless(proj)) {
|
|
1965
|
+
lists.add(
|
|
1966
|
+
`${proj} has no vx config, and no configured project depends on it: it has no tasks.`,
|
|
1967
|
+
)
|
|
1968
|
+
continue
|
|
1969
|
+
}
|
|
1781
1970
|
if (!projects.has(proj)) {
|
|
1782
1971
|
const p = projectNamed(proj, projects) ?? nearest(proj, projects.keys())
|
|
1783
|
-
if (p !== undefined
|
|
1784
|
-
|
|
1972
|
+
if (p !== undefined) {
|
|
1973
|
+
const tasks = tasksOf(projects.get(p))
|
|
1974
|
+
const t = tasks.includes(task) ? task : nearest(task, tasks)
|
|
1975
|
+
if (t !== undefined) hints.add(`${p}#${t}`)
|
|
1976
|
+
else lists.add(`No project is named ${proj}; ${p}'s tasks: ${listed(tasks)}.`)
|
|
1977
|
+
} else if (projects.size > 0) {
|
|
1785
1978
|
lists.add(`No project is named ${proj}; projects: ${listed(projects.keys())}.`)
|
|
1786
1979
|
}
|
|
1787
1980
|
continue
|
|
@@ -1868,8 +2061,25 @@ function canonical(dir: string): string {
|
|
|
1868
2061
|
}
|
|
1869
2062
|
}
|
|
1870
2063
|
|
|
2064
|
+
/** What an executor's report threw: said once by `executorFaults`, the report left out. */
|
|
2065
|
+
type ExecutorFault = (e: TaskExecutor, what: string, err: unknown) => void
|
|
2066
|
+
|
|
2067
|
+
/**
|
|
2068
|
+
* Each executor report that throws is said once and left out: isolated,
|
|
2069
|
+
* it was dropped with nothing said, a summary short its machines.
|
|
2070
|
+
*/
|
|
2071
|
+
function executorFaults(warn: (line: string) => void): ExecutorFault {
|
|
2072
|
+
const said = new Set<string>()
|
|
2073
|
+
return (e, what, err) => {
|
|
2074
|
+
const k = `${e.name}\0${what}`
|
|
2075
|
+
if (said.has(k)) return
|
|
2076
|
+
said.add(k)
|
|
2077
|
+
warn(`[vx] ${executorLabel(e)}: ${what}() threw: ${thrownMessage(err)} — left out`)
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1871
2081
|
/** An executor's footer rows; a plugin's throw or bad shape shows none. */
|
|
1872
|
-
function executorFooter(e: TaskExecutor): (readonly [string, ui.Inline])[] {
|
|
2082
|
+
function executorFooter(e: TaskExecutor, fault: ExecutorFault): (readonly [string, ui.Inline])[] {
|
|
1873
2083
|
try {
|
|
1874
2084
|
const rows = e.footer?.()
|
|
1875
2085
|
if (!Array.isArray(rows)) return []
|
|
@@ -1879,27 +2089,60 @@ function executorFooter(e: TaskExecutor): (readonly [string, ui.Inline])[] {
|
|
|
1879
2089
|
typeof r[0] === 'string' &&
|
|
1880
2090
|
(typeof r[1] === 'string' || (typeof r[1] === 'object' && r[1] !== null)),
|
|
1881
2091
|
)
|
|
1882
|
-
} catch {
|
|
2092
|
+
} catch (err) {
|
|
2093
|
+
fault(e, 'footer', err)
|
|
1883
2094
|
return []
|
|
1884
2095
|
}
|
|
1885
2096
|
}
|
|
1886
2097
|
|
|
1887
|
-
|
|
2098
|
+
/** An executor's `--profile` events; a throw or a bad shape adds none. */
|
|
2099
|
+
function executorProfile(
|
|
2100
|
+
e: TaskExecutor,
|
|
2101
|
+
fault: ExecutorFault,
|
|
2102
|
+
): readonly Record<string, unknown>[] {
|
|
2103
|
+
try {
|
|
2104
|
+
const events = e.profile?.()
|
|
2105
|
+
return Array.isArray(events)
|
|
2106
|
+
? events.filter((x): x is Record<string, unknown> => typeof x === 'object' && x !== null)
|
|
2107
|
+
: []
|
|
2108
|
+
} catch (err) {
|
|
2109
|
+
fault(e, 'profile', err)
|
|
2110
|
+
return []
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function executorMachines(e: TaskExecutor, fault: ExecutorFault): readonly MachineReport[] {
|
|
1888
2115
|
try {
|
|
1889
2116
|
const m = e.machines?.()
|
|
1890
2117
|
return Array.isArray(m) ? m : []
|
|
1891
|
-
} catch {
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
fault(e, 'machines', err)
|
|
1892
2120
|
return []
|
|
1893
2121
|
}
|
|
1894
2122
|
}
|
|
1895
2123
|
|
|
2124
|
+
function executorIdeal(e: TaskExecutor, fault: ExecutorFault): number | undefined {
|
|
2125
|
+
try {
|
|
2126
|
+
const ms = e.idealMs?.()
|
|
2127
|
+
return typeof ms === 'number' && Number.isFinite(ms) && ms >= 0 ? Math.round(ms) : undefined
|
|
2128
|
+
} catch (err) {
|
|
2129
|
+
fault(e, 'idealMs', err)
|
|
2130
|
+
return undefined
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
1896
2134
|
/** Where `e` says a task runs, or `null` while it waits; a throw or anything else says nothing. */
|
|
1897
|
-
function executorWhere(
|
|
2135
|
+
function executorWhere(
|
|
2136
|
+
e: TaskExecutor,
|
|
2137
|
+
id: string,
|
|
2138
|
+
fault: ExecutorFault,
|
|
2139
|
+
): string | null | undefined {
|
|
1898
2140
|
try {
|
|
1899
2141
|
const w = e.running?.(id)
|
|
1900
2142
|
if (w === null) return null
|
|
1901
2143
|
return typeof w === 'string' && w !== '' ? w : undefined
|
|
1902
|
-
} catch {
|
|
2144
|
+
} catch (err) {
|
|
2145
|
+
fault(e, 'running', err)
|
|
1903
2146
|
return undefined
|
|
1904
2147
|
}
|
|
1905
2148
|
}
|