@vzn/vx 0.0.652 → 0.0.653
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +1 -0
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +25 -3
- package/src/cache/archive.ts +196 -31
- package/src/cache/cache.ts +863 -225
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +22 -5
- package/src/cache/index.ts +10 -1
- package/src/cache/inputs.ts +573 -152
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +23 -10
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +50 -10
- package/src/cache/schema.ts +42 -9
- package/src/cli/cache.ts +147 -34
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +25 -2
- package/src/cli/index.ts +38 -7
- package/src/cli/info.ts +4 -3
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +184 -41
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +63 -18
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +72 -8
- package/src/cli/watch-set.ts +6 -4
- package/src/cli/watch.ts +179 -25
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +9 -0
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +76 -15
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-runtime.ts +134 -37
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +173 -38
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +327 -141
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +15 -5
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +189 -69
- package/src/index.ts +2 -1
- package/src/orchestrator/admission.ts +13 -9
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +41 -12
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/excluded-keys.ts +14 -2
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +564 -123
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +136 -87
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +25 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +33 -53
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +31 -17
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +81 -15
- package/src/orchestrator/plan.ts +37 -15
- package/src/orchestrator/plugin-host.ts +232 -108
- package/src/orchestrator/plugin.ts +53 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +43 -14
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +12 -10
- package/src/orchestrator/run-report.ts +2 -1
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +443 -107
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +286 -59
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +20 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +17 -9
- package/src/orchestrator/task-artifacts.ts +15 -2
- package/src/orchestrator/task-hash.ts +187 -30
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +80 -12
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +20 -3
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +165 -157
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +107 -30
- package/src/report/plain-output.ts +13 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +57 -14
- package/src/settings.ts +42 -6
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +17 -3
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +50 -6
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +16 -0
- package/src/util/size.ts +12 -2
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +126 -25
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +10 -1
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +46 -25
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
package/src/orchestrator/run.ts
CHANGED
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import type { InputStability } from '../config.js'
|
|
6
6
|
import type { ProjectEntry } from '../workspace/index.js'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
EMPTY_LOCK,
|
|
9
|
+
findProjectConfigs,
|
|
10
|
+
inputStabilityEnv,
|
|
11
|
+
LOCK_INVALID,
|
|
12
|
+
lockEntryBroken,
|
|
13
|
+
lockKey,
|
|
14
|
+
readLockfile,
|
|
15
|
+
} from '../workspace/index.js'
|
|
8
16
|
import { realpathSync } from 'node:fs'
|
|
9
17
|
import path from 'node:path'
|
|
10
18
|
import {
|
|
@@ -14,6 +22,7 @@ import {
|
|
|
14
22
|
type CachePolicy,
|
|
15
23
|
cachesNothing,
|
|
16
24
|
formatCachePolicy,
|
|
25
|
+
holdCacheDir,
|
|
17
26
|
HistoryStream,
|
|
18
27
|
networkConcurrency,
|
|
19
28
|
keepRunNotes,
|
|
@@ -29,6 +38,7 @@ import {
|
|
|
29
38
|
VX_RUN_WORKSPACE_ENV,
|
|
30
39
|
} from '../exec/index.js'
|
|
31
40
|
import { DeferredOutputs } from './deferred-outputs.js'
|
|
41
|
+
import { workspaceCleanOf } from './task-hash.js'
|
|
32
42
|
import { resolveDownloadModes } from './download-policy.js'
|
|
33
43
|
import type { MachineReport, TaskExecutor, WorkerAdvice } from '../exec/index.js'
|
|
34
44
|
import {
|
|
@@ -59,6 +69,7 @@ import {
|
|
|
59
69
|
isUserError,
|
|
60
70
|
SecretBroker,
|
|
61
71
|
thrownMessage,
|
|
72
|
+
relPosix,
|
|
62
73
|
} from '../util/index.js'
|
|
63
74
|
import * as ui from '../ui/index.js'
|
|
64
75
|
|
|
@@ -68,7 +79,7 @@ function printTimings(): void {
|
|
|
68
79
|
if (lines !== undefined) ui.print('err', ui.trace(lines))
|
|
69
80
|
}
|
|
70
81
|
import { detectColors, type ColorSupport } from '../ui/index.js'
|
|
71
|
-
import { keyedProjects, readableDeps } from './keyed-projects.js'
|
|
82
|
+
import { keyedProjects, nestedDirsOf, readableDeps } from './keyed-projects.js'
|
|
72
83
|
import { isDefaultBuild } from '../graph/index.js'
|
|
73
84
|
import { commandWriteReach, prepareSandbox } from './sandbox-request.js'
|
|
74
85
|
import { recordOutputDirSnapshots, type OutputDirSnapshot, type SaveFacts } from './miss-save.js'
|
|
@@ -89,13 +100,16 @@ import {
|
|
|
89
100
|
formatPersistentList,
|
|
90
101
|
formatRunSummary,
|
|
91
102
|
type Logger,
|
|
103
|
+
neverStarted,
|
|
92
104
|
plainOutput,
|
|
93
105
|
resolveOutputView,
|
|
106
|
+
type RunEvent,
|
|
94
107
|
terminalSubscriber,
|
|
95
108
|
warnOf,
|
|
96
109
|
} from '../report/index.js'
|
|
97
110
|
import { installPlugins, PluginSetupError, type RunReport } from './plugin.js'
|
|
98
111
|
import { keepsFailures, reportOf, RunKeeper } from './run-report.js'
|
|
112
|
+
import { freezeDeep } from './read-only.js'
|
|
99
113
|
import {
|
|
100
114
|
buildAdmission,
|
|
101
115
|
executorDemand,
|
|
@@ -112,9 +126,21 @@ import { isPassStatus } from '../graph/index.js'
|
|
|
112
126
|
import type { RunContextRecord } from './telemetry.js'
|
|
113
127
|
import { plan, type RunPlan } from './plan.js'
|
|
114
128
|
import type { AffectedReason } from './affected-tasks.js'
|
|
115
|
-
import {
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
import {
|
|
130
|
+
namesRemote,
|
|
131
|
+
openRemotes,
|
|
132
|
+
outputWallsOf,
|
|
133
|
+
prepareRunLocked,
|
|
134
|
+
type PreparedRun,
|
|
135
|
+
} from './prepare.js'
|
|
136
|
+
import { acquireRunLock, runLockPath, type RunLockRelease } from './run-lock.js'
|
|
137
|
+
import {
|
|
138
|
+
exitOfSignal,
|
|
139
|
+
type HeldSignals,
|
|
140
|
+
holdSignals,
|
|
141
|
+
terminateChildren,
|
|
142
|
+
unlessStopped,
|
|
143
|
+
} from './signals.js'
|
|
118
144
|
import { CYCLE_INTERRUPT, runStop } from './run-stop.js'
|
|
119
145
|
import {
|
|
120
146
|
hasPooledExecutor,
|
|
@@ -153,7 +179,10 @@ import {
|
|
|
153
179
|
failServer,
|
|
154
180
|
holdServers,
|
|
155
181
|
sessionEnd,
|
|
182
|
+
serverUsers,
|
|
183
|
+
stopServer,
|
|
156
184
|
takeHeldServers,
|
|
185
|
+
unneededServers,
|
|
157
186
|
} from './persistent.js'
|
|
158
187
|
import { serverKey } from './persistent-task.js'
|
|
159
188
|
import { writeRunProfile } from './run-artifacts.js'
|
|
@@ -389,11 +418,21 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
389
418
|
unsubscribeTerminal = bus.subscribe(guardedOutput(claimed, render))
|
|
390
419
|
}
|
|
391
420
|
let held = false
|
|
421
|
+
const signals: { held?: HeldSignals } = {}
|
|
392
422
|
try {
|
|
393
423
|
// vx's own steps are timed only where something renders them.
|
|
394
424
|
const showsSteps =
|
|
395
425
|
terminal !== null ? terminal.showsSteps : options.log?.stepStart !== undefined
|
|
396
|
-
const summary = await runOnBus(
|
|
426
|
+
const summary = await runOnBus(
|
|
427
|
+
options,
|
|
428
|
+
bus,
|
|
429
|
+
warned,
|
|
430
|
+
colors,
|
|
431
|
+
terminal,
|
|
432
|
+
showsSteps,
|
|
433
|
+
signals,
|
|
434
|
+
takeOutput,
|
|
435
|
+
)
|
|
397
436
|
if (summary.persistent === undefined) return summary
|
|
398
437
|
// Servers handed back still running still write: `vx watch dev` printed
|
|
399
438
|
// none of its server's log while it idled, the renderer gone with this
|
|
@@ -409,6 +448,11 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
409
448
|
}
|
|
410
449
|
} finally {
|
|
411
450
|
if (!held) detach()
|
|
451
|
+
// A signal heard before the run's stop took over: the run left before
|
|
452
|
+
// its tasks (a refusal, a setup that threw), its plugins torn down and
|
|
453
|
+
// its lock released, and vx leaves as that signal says.
|
|
454
|
+
const heard = signals.held?.takeOver()
|
|
455
|
+
if (heard !== undefined) await exitOfSignal(heard)
|
|
412
456
|
}
|
|
413
457
|
}
|
|
414
458
|
|
|
@@ -419,6 +463,7 @@ async function runOnBus(
|
|
|
419
463
|
colors: ColorSupport,
|
|
420
464
|
terminal: DefaultLogger | null,
|
|
421
465
|
showsSteps: boolean,
|
|
466
|
+
signals: { held?: HeldSignals },
|
|
422
467
|
takeOutput?: (claimed: { logger: Logger; owner: string }) => void,
|
|
423
468
|
): Promise<RunSummary> {
|
|
424
469
|
const log = busLogger(bus)
|
|
@@ -427,17 +472,82 @@ async function runOnBus(
|
|
|
427
472
|
// A server's output goes through a cell of its own: a later watch cycle
|
|
428
473
|
// that keeps the server points it at that cycle's bus (`RunOptions.keep`).
|
|
429
474
|
const serverOuts = new Map<string, { bus: EventBus }>()
|
|
475
|
+
// What a server writes from ready until its outcome is logged: renderers
|
|
476
|
+
// keep output before the outcome for its frame, which a ready server's
|
|
477
|
+
// row never prints, so its first lines since ready were lost.
|
|
478
|
+
const sinceReady = new Map<string, RunEvent[]>()
|
|
430
479
|
const serverLog = (id: string): Logger => {
|
|
431
480
|
const out = { bus }
|
|
432
481
|
serverOuts.set(id, out)
|
|
482
|
+
const emit = (event: RunEvent): void => {
|
|
483
|
+
const held = sinceReady.get(id)
|
|
484
|
+
if (held === undefined) out.bus.emit(event)
|
|
485
|
+
else held.push(event)
|
|
486
|
+
}
|
|
433
487
|
return {
|
|
434
488
|
...log,
|
|
435
|
-
taskStdout: (node, chunk) =>
|
|
436
|
-
taskStderr: (node, chunk) =>
|
|
489
|
+
taskStdout: (node, chunk) => emit({ kind: 'task:stdout', node, chunk }),
|
|
490
|
+
taskStderr: (node, chunk) => emit({ kind: 'task:stderr', node, chunk }),
|
|
437
491
|
}
|
|
438
492
|
}
|
|
493
|
+
const releaseSinceReady = (id: string): void => {
|
|
494
|
+
const held = sinceReady.get(id)
|
|
495
|
+
if (held === undefined) return
|
|
496
|
+
sinceReady.delete(id)
|
|
497
|
+
const out = serverOuts.get(id) ?? { bus }
|
|
498
|
+
for (const event of held) out.bus.emit(event)
|
|
499
|
+
}
|
|
439
500
|
|
|
440
|
-
|
|
501
|
+
// Taken before the cache opens, never after: a run that held the store
|
|
502
|
+
// while it waited here, and a `vx cache clean` that holds this lock while
|
|
503
|
+
// it waits for every process with the store open, waited on each other
|
|
504
|
+
// for ever; and a clear deleted the store under a run opening it.
|
|
505
|
+
let heldLock: RunLockRelease | undefined
|
|
506
|
+
const takeRunLock = async (root: string, sharedCacheDir: string | undefined): Promise<void> => {
|
|
507
|
+
// A nested run is refused below with its own reason, holding nothing.
|
|
508
|
+
const outer = process.env[VX_RUN_WORKSPACE_ENV] || undefined
|
|
509
|
+
if (outer !== undefined && canonical(outer) === canonical(root)) return
|
|
510
|
+
// Heard from here, not only from the stop below: a signal between the
|
|
511
|
+
// lock and that stop killed vx with the lock held (`holdSignals`).
|
|
512
|
+
if (options.handleSignals ?? true) signals.held = holdSignals()
|
|
513
|
+
const held = signals.held?.signal
|
|
514
|
+
const signal =
|
|
515
|
+
held !== undefined && options.signal !== undefined
|
|
516
|
+
? AbortSignal.any([options.signal, held])
|
|
517
|
+
: (held ?? options.signal)
|
|
518
|
+
const lock = await step('vx:run-lock', () => acquireRunLock(root, { log: warnOf(log), signal }))
|
|
519
|
+
heldLock = lock
|
|
520
|
+
// Another workspace's clean of a shared cache directory takes no lock of
|
|
521
|
+
// this one's: it waits for this hold, and this for it. Released with the
|
|
522
|
+
// run lock.
|
|
523
|
+
if (sharedCacheDir !== undefined) {
|
|
524
|
+
const dir = await holdCacheDir(sharedCacheDir, 'shared', warnOf(log), signal)
|
|
525
|
+
heldLock = Object.assign(
|
|
526
|
+
async (): Promise<void> => {
|
|
527
|
+
dir()
|
|
528
|
+
await lock()
|
|
529
|
+
},
|
|
530
|
+
{ path: lock.path },
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
// A wait the signal ended holds no lock: opening the store now could
|
|
534
|
+
// meet a `vx cache clean` deleting it. run() exits of the signal.
|
|
535
|
+
if (held?.aborted === true) throw new Error(`stopped by ${String(held.reason)}`)
|
|
536
|
+
}
|
|
537
|
+
let prepared: PreparedRun
|
|
538
|
+
try {
|
|
539
|
+
prepared = await step('vx:load', () => prepareRunLocked(options, log, takeRunLock, true))
|
|
540
|
+
} catch (err) {
|
|
541
|
+
await heldLock?.()
|
|
542
|
+
throw err
|
|
543
|
+
}
|
|
544
|
+
const releaseRunLock: RunLockRelease =
|
|
545
|
+
heldLock ?? Object.assign(async () => {}, { path: runLockPath(prepared.workspaceRoot) })
|
|
546
|
+
// The cache and the lock go together on every exit before the run's own.
|
|
547
|
+
const closeUnrun = async (): Promise<void> => {
|
|
548
|
+
prepared.cache.close()
|
|
549
|
+
await releaseRunLock()
|
|
550
|
+
}
|
|
441
551
|
mark('plugin stages')
|
|
442
552
|
// Every exit from here tears the plugins down: their capability
|
|
443
553
|
// factories ran in prepareRun, and teardown is what releases what those
|
|
@@ -465,12 +575,11 @@ async function runOnBus(
|
|
|
465
575
|
? { workspace: prepared.workspaceConfig.inputs.stability }
|
|
466
576
|
: {}),
|
|
467
577
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
| undefined
|
|
578
|
+
// Resolved once, beside the keys that fold it (prepare.ts).
|
|
579
|
+
cleanOutputs = workspaceCleanOf(prepared.hashCache)
|
|
471
580
|
} catch (err) {
|
|
472
581
|
await teardown()
|
|
473
|
-
|
|
582
|
+
await closeUnrun()
|
|
474
583
|
throw err
|
|
475
584
|
}
|
|
476
585
|
// A task whose command re-enters `vx run` in the workspace running it
|
|
@@ -486,7 +595,7 @@ async function runOnBus(
|
|
|
486
595
|
const outerRoot = process.env[VX_RUN_WORKSPACE_ENV] || undefined
|
|
487
596
|
if (outerRoot !== undefined && canonical(outerRoot) === canonical(prepared.workspaceRoot)) {
|
|
488
597
|
await teardown()
|
|
489
|
-
|
|
598
|
+
await closeUnrun()
|
|
490
599
|
throw new UserError(
|
|
491
600
|
`task ${process.env[VX_RUN_TASK_ENV] ?? '<unknown>'} runs \`vx run\` inside its own workspace: a nested run is invisible to the outer graph (its tasks escape the schedule, the concurrency budget and the cache key) and a loop back to this task forks without bound. Declare what it needs with dependsOn instead.`,
|
|
492
601
|
)
|
|
@@ -502,13 +611,13 @@ async function runOnBus(
|
|
|
502
611
|
if (prepared.unresolvedTasks.length > 0) {
|
|
503
612
|
const refused = `no projects declare task(s): ${prepared.unresolvedTasks.join(', ')}.${await unresolvedHint(prepared)}`
|
|
504
613
|
await teardown()
|
|
505
|
-
|
|
614
|
+
await closeUnrun()
|
|
506
615
|
return { ok: false, outcomes: [], refused }
|
|
507
616
|
}
|
|
508
617
|
if (prepared.empty === 'none-affected') {
|
|
509
618
|
log.status(noneAffected(prepared, options))
|
|
510
619
|
await teardown()
|
|
511
|
-
|
|
620
|
+
await closeUnrun()
|
|
512
621
|
return { ok: true, outcomes: [] }
|
|
513
622
|
}
|
|
514
623
|
if (prepared.empty !== null) {
|
|
@@ -521,7 +630,7 @@ async function runOnBus(
|
|
|
521
630
|
? `no projects declare task(s): ${options.tasks.join(', ')}.${elsewhereHint(prepared.declaredElsewhere)}${await initHint(prepared)}`
|
|
522
631
|
: 'no tasks to run.'
|
|
523
632
|
await teardown()
|
|
524
|
-
|
|
633
|
+
await closeUnrun()
|
|
525
634
|
return { ok: false, outcomes: [], refused }
|
|
526
635
|
}
|
|
527
636
|
// Install user plugins as additional bus subscribers BEFORE the run
|
|
@@ -534,6 +643,7 @@ async function runOnBus(
|
|
|
534
643
|
// carries it so analytics queries can group by invocation; plugins get it
|
|
535
644
|
// in their teardown's `RunReport`.
|
|
536
645
|
const runId = ulid()
|
|
646
|
+
prepared.localCache.runId = runId
|
|
537
647
|
// A killed run's restore staging (`.vx/tmp/<pid>-0`), once per run.
|
|
538
648
|
sweepStaging(prepared.workspaceRoot)
|
|
539
649
|
const head = captureGitContext(prepared.workspaceRoot)
|
|
@@ -553,7 +663,7 @@ async function runOnBus(
|
|
|
553
663
|
: new RunKeeper({
|
|
554
664
|
runId,
|
|
555
665
|
workspaceRoot: prepared.workspaceRoot,
|
|
556
|
-
nestedDirsByProject: prepared.nestedDirsByProject,
|
|
666
|
+
nestedDirsByProject: outputWallsOf(prepared.nestedDirsByProject),
|
|
557
667
|
keep: prepared.keepRunEntry,
|
|
558
668
|
failures: keepRun.failures,
|
|
559
669
|
traffic: prepared.localCache.traffic,
|
|
@@ -567,6 +677,7 @@ async function runOnBus(
|
|
|
567
677
|
terminal === null ? undefined : () => terminal.holdStatus(),
|
|
568
678
|
)
|
|
569
679
|
const remotesClosed = await openRemotes(prepared.remotes, ciContext.ci, secrets)
|
|
680
|
+
signals.held?.arm()
|
|
570
681
|
try {
|
|
571
682
|
disposePlugins = await installPlugins({
|
|
572
683
|
plugins: prepared.plugins,
|
|
@@ -579,11 +690,12 @@ async function runOnBus(
|
|
|
579
690
|
branch: prepared.branch(),
|
|
580
691
|
secrets,
|
|
581
692
|
step,
|
|
693
|
+
signal: signals.held?.signal,
|
|
582
694
|
})
|
|
583
695
|
} catch (err) {
|
|
584
696
|
disposePlugins?.()
|
|
585
697
|
await teardown(err instanceof PluginSetupError ? err.plugin : undefined)
|
|
586
|
-
|
|
698
|
+
await closeUnrun()
|
|
587
699
|
throw err
|
|
588
700
|
}
|
|
589
701
|
// Until the run's own try below, a throw leaves through here.
|
|
@@ -593,7 +705,7 @@ async function runOnBus(
|
|
|
593
705
|
} catch (err) {
|
|
594
706
|
disposePlugins?.()
|
|
595
707
|
await teardown()
|
|
596
|
-
|
|
708
|
+
await closeUnrun()
|
|
597
709
|
throw err
|
|
598
710
|
}
|
|
599
711
|
}
|
|
@@ -621,22 +733,26 @@ async function runOnBus(
|
|
|
621
733
|
if (options.signal?.aborted === true) {
|
|
622
734
|
disposePlugins?.()
|
|
623
735
|
await teardown()
|
|
624
|
-
|
|
736
|
+
await closeUnrun()
|
|
625
737
|
return { ok: true, outcomes: [] }
|
|
626
738
|
}
|
|
627
739
|
}
|
|
628
740
|
|
|
629
741
|
// Resolved ONCE per run, in declaration order, the local executor last.
|
|
630
|
-
// A broken factory aborts here, before any task starts
|
|
742
|
+
// A broken factory aborts here, before any task starts, and a held
|
|
743
|
+
// signal ends the wait on one that never settles.
|
|
631
744
|
const executors: readonly TaskExecutor[] = await abandoning(() =>
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
745
|
+
unlessStopped(
|
|
746
|
+
resolveExecutors(prepared.plugins, {
|
|
747
|
+
workspaceRoot: prepared.workspaceRoot,
|
|
748
|
+
cacheDir: prepared.cacheDir,
|
|
749
|
+
warn: warnOf(log),
|
|
750
|
+
concurrency,
|
|
751
|
+
artifacts: taskArtifacts(prepared.cache, prepared.workspaceRoot),
|
|
752
|
+
cacheFacts: () => cacheFacts(prepared.remotes, prepared.cacheRefs),
|
|
753
|
+
}),
|
|
754
|
+
signals.held?.signal,
|
|
755
|
+
),
|
|
640
756
|
)
|
|
641
757
|
// Placement: decided ONCE per task, before scheduling, so the scheduler
|
|
642
758
|
// can admit a remote-pooled task against its pool instead of a local
|
|
@@ -688,26 +804,28 @@ async function runOnBus(
|
|
|
688
804
|
// `cache.download` (default `all`) decides ONCE per task, here, whether a
|
|
689
805
|
// remote execution's outputs come home.
|
|
690
806
|
const downloadPolicy = options.download ?? 'all'
|
|
691
|
-
const localPlaced = locallyPlaced(placements
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
807
|
+
const localPlaced = locallyPlaced(placements)
|
|
808
|
+
// `all` keeps every task eager: no per-task map to say so, as `--dry`'s.
|
|
809
|
+
const download =
|
|
810
|
+
downloadPolicy === 'all'
|
|
811
|
+
? undefined
|
|
812
|
+
: resolveDownloadModes({
|
|
813
|
+
nodes,
|
|
814
|
+
policy: downloadPolicy,
|
|
815
|
+
localPlaced,
|
|
816
|
+
remoteOnly: placements.remoteOnly,
|
|
817
|
+
})
|
|
698
818
|
|
|
699
819
|
// `run.timeout` (ms), for any task WITHOUT its own `exec.timeout`.
|
|
700
820
|
const taskTimeoutDefault = options.timeout
|
|
701
821
|
|
|
702
|
-
// The held servers this run took over, spared by a `CYCLE_INTERRUPT`.
|
|
703
|
-
const handed = new Set<ReturnType<typeof Bun.spawn>>()
|
|
704
822
|
const stop = runStop({
|
|
705
|
-
handed: (child) => handed.has(child),
|
|
706
823
|
log,
|
|
707
824
|
cache,
|
|
708
825
|
external: options.signal,
|
|
709
826
|
handleSignals: options.handleSignals ?? true,
|
|
710
827
|
handsTerminal: holders.size > 0,
|
|
828
|
+
held: signals.held,
|
|
711
829
|
teardowns: prepared.plugins.filter((p) => p.teardown).length,
|
|
712
830
|
onStop: () => {
|
|
713
831
|
log.runStop?.()
|
|
@@ -731,6 +849,7 @@ async function runOnBus(
|
|
|
731
849
|
const closeCache = async (): Promise<void> => {
|
|
732
850
|
if (cacheClosed) return
|
|
733
851
|
cacheClosed = true
|
|
852
|
+
await prepared.localCache.stopCheckpoints()
|
|
734
853
|
cache.close()
|
|
735
854
|
live?.unlisten()
|
|
736
855
|
// Released with the handle: before a persistent task's wait, on every
|
|
@@ -742,16 +861,6 @@ async function runOnBus(
|
|
|
742
861
|
// waited, but a directory per run is litter the box never sheds.
|
|
743
862
|
await releaseRunLock()
|
|
744
863
|
}
|
|
745
|
-
// Taken after the early exits above (nothing they do touches a tree) and
|
|
746
|
-
// before the schedule: from here on tasks clean, restore and write.
|
|
747
|
-
const releaseRunLock = await abandoning(() =>
|
|
748
|
-
step('vx:run-lock', () =>
|
|
749
|
-
acquireRunLock(prepared.workspaceRoot, {
|
|
750
|
-
log: warnOf(log),
|
|
751
|
-
signal: stop.signal,
|
|
752
|
-
}),
|
|
753
|
-
),
|
|
754
|
-
)
|
|
755
864
|
// Answers `vx last --live` while the lock is held (run-live.ts).
|
|
756
865
|
live = serveLive({
|
|
757
866
|
socketPath: liveSocketOf(releaseRunLock.path),
|
|
@@ -774,26 +883,17 @@ async function runOnBus(
|
|
|
774
883
|
// clean their outputs before every exec for a save that never
|
|
775
884
|
// happened.
|
|
776
885
|
const policy: CachePolicy = effectiveCachePolicy(prepared.cachePolicy, remoteCacheEnabled)
|
|
777
|
-
|
|
778
|
-
const
|
|
779
|
-
options.remoteRequested ??
|
|
780
|
-
(options.cache === undefined &&
|
|
781
|
-
options.cacheFlags?.noCache !== true &&
|
|
782
|
-
namesRemote(prepared.workspaceConfig?.cache?.policy)
|
|
783
|
-
? 'cache.policy'
|
|
784
|
-
: undefined)
|
|
785
|
-
if (remoteNamed !== undefined && !prepared.hasRemoteLayer) {
|
|
786
|
-
warnOf(log)(
|
|
787
|
-
`vx: ${remoteNamed} named the remote cache, but no cache plugin in .vx/workspace.* supplies one — the remote axes are off for this run`,
|
|
788
|
-
)
|
|
789
|
-
}
|
|
886
|
+
warnNoRemote(prepared, options, log)
|
|
887
|
+
const windows = options.commandWindows ?? commandWindows()
|
|
790
888
|
const deferredOutputs = new DeferredOutputs({
|
|
791
889
|
nodes,
|
|
792
890
|
cache,
|
|
793
891
|
workspaceRoot,
|
|
794
|
-
nestedDirsByProject,
|
|
892
|
+
nestedDirsByProject: outputWallsOf(nestedDirsByProject),
|
|
795
893
|
...(gitFilesCache !== undefined ? { gitFilesCache } : {}),
|
|
796
894
|
localWrite: policy.localWrite,
|
|
895
|
+
log,
|
|
896
|
+
commandWindows: windows,
|
|
797
897
|
})
|
|
798
898
|
|
|
799
899
|
// Per-run context for the Tier-3 `invocations` header row. Captured
|
|
@@ -874,7 +974,7 @@ async function runOnBus(
|
|
|
874
974
|
(n) =>
|
|
875
975
|
n.config.exec?.sandbox !== undefined &&
|
|
876
976
|
(n.config.exec.persistent === undefined
|
|
877
|
-
? localPlaced
|
|
977
|
+
? localPlaced(n.id)
|
|
878
978
|
: !placements.carried.has(n.id)),
|
|
879
979
|
)
|
|
880
980
|
let prewarming: Promise<void> | undefined
|
|
@@ -887,12 +987,19 @@ async function runOnBus(
|
|
|
887
987
|
const outputDirSnapshots: OutputDirSnapshot[] = []
|
|
888
988
|
// Saves run off the execution slot, twice the cap at once (memory:
|
|
889
989
|
// each pack holds an artifact's bytes); a failed save is a miss next
|
|
890
|
-
// time, said once — the task's work ran.
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
990
|
+
// time, said once — the task's work ran. A full disk fails every save
|
|
991
|
+
// after the first the same way: the first is said with its task, the
|
|
992
|
+
// rest named once the graph ends.
|
|
993
|
+
const unsaved: string[] = []
|
|
994
|
+
const saveLane = createSaveLane(2 * concurrency, (err, taskId) => {
|
|
995
|
+
if (unsaved.push(taskId) > 1) return
|
|
996
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
997
|
+
// The artifact ceiling's refusal names its task itself.
|
|
998
|
+
warnOf(log)(
|
|
999
|
+
`[vx] cache save failed: ${msg.startsWith(`${taskId} `) ? msg : `${taskId}: ${msg}`}`,
|
|
1000
|
+
)
|
|
1001
|
+
})
|
|
894
1002
|
const deferredSaves = new Map<string, Promise<SaveFacts>>()
|
|
895
|
-
const windows = commandWindows()
|
|
896
1003
|
// A server held from an earlier run (`vx watch`) runs through this one
|
|
897
1004
|
// too, so it may write a mutable input while it is described.
|
|
898
1005
|
for (const id of heldBefore.kept.keys()) {
|
|
@@ -934,9 +1041,9 @@ async function runOnBus(
|
|
|
934
1041
|
const footers = executors.filter((e) => e.footer !== undefined)
|
|
935
1042
|
const placers = executors.filter((e) => e.running !== undefined)
|
|
936
1043
|
// The scheduler's dependents count, and every row's `blocks` (the live
|
|
937
|
-
// list's and the results'): one closure over the graph,
|
|
938
|
-
// first that asks.
|
|
939
|
-
let counted: ReadonlyMap<string, number> | undefined
|
|
1044
|
+
// list's and the results'): one closure over the graph, counted by
|
|
1045
|
+
// `prepareRun` beside git when it could, else on the first that asks.
|
|
1046
|
+
let counted: ReadonlyMap<string, number> | undefined = prepared.dependents
|
|
940
1047
|
const blocksNow = (): ReadonlyMap<string, number> =>
|
|
941
1048
|
(counted ??= graphCounts(nodes, hashCache.folds, hashCache.graph))
|
|
942
1049
|
// Every row's `usual` and the forecast's timings: history read once, on
|
|
@@ -1149,9 +1256,16 @@ async function runOnBus(
|
|
|
1149
1256
|
// dead runs on a failure the outcomes do not hold yet (the server's
|
|
1150
1257
|
// says `success` until the run ends), so it is seeded here, at its
|
|
1151
1258
|
// dispatch, and the tracker carries it to what is built on it.
|
|
1259
|
+
// A server can also die under a task that is running (`serverDiedUnder`),
|
|
1260
|
+
// which seeds it then, and so does a task that left a process running
|
|
1261
|
+
// (`leftRunning`).
|
|
1152
1262
|
const taintSeeds = new Set(excluded.seeds)
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1263
|
+
const hasServers = [...nodes.values()].some((n) => n.config.exec?.persistent !== undefined)
|
|
1264
|
+
const continueAlways = options.continueMode === 'always'
|
|
1265
|
+
let taint = taintTracker(continueAlways, taintSeeds, nodes, hasServers)
|
|
1266
|
+
let taintOn = continueAlways || taintSeeds.size > 0 || hasServers
|
|
1267
|
+
// Each task that ran tainted, and the task at the root of it: its
|
|
1268
|
+
// outcome's `taintedBy` once it finishes.
|
|
1155
1269
|
const taintedRan = new Map<string, string>()
|
|
1156
1270
|
const depended = dependedOn(nodes.values())
|
|
1157
1271
|
|
|
@@ -1181,6 +1295,7 @@ async function runOnBus(
|
|
|
1181
1295
|
fingerprintWatch,
|
|
1182
1296
|
keyedProjects: keyed,
|
|
1183
1297
|
readableDeps: readable,
|
|
1298
|
+
nestedDirsOf: nestedDirsOf(nodes, prepared.keyOnly, nestedDirsByProject),
|
|
1184
1299
|
outputDirSnapshots,
|
|
1185
1300
|
deferSave: saveLane.defer,
|
|
1186
1301
|
deferredSaves,
|
|
@@ -1188,6 +1303,40 @@ async function runOnBus(
|
|
|
1188
1303
|
stopSignal: stop.signal,
|
|
1189
1304
|
failFast: failFast.signal,
|
|
1190
1305
|
inputStability,
|
|
1306
|
+
serverReady: (id) => void sinceReady.set(id, []),
|
|
1307
|
+
}
|
|
1308
|
+
if (hasServers)
|
|
1309
|
+
scope.serverDiedUnder = async (node) => {
|
|
1310
|
+
const behind = (pick: (id: string) => boolean): string | undefined => {
|
|
1311
|
+
for (const d of node.deps) {
|
|
1312
|
+
const s = deadServerBehind(nodes, pick, d)
|
|
1313
|
+
if (s !== undefined) return s
|
|
1314
|
+
}
|
|
1315
|
+
return undefined
|
|
1316
|
+
}
|
|
1317
|
+
let dead = behind(serverDied)
|
|
1318
|
+
// A server that died just before the task ended may be heard after
|
|
1319
|
+
// it: both exits can land in one turn of the event loop, the task's
|
|
1320
|
+
// handled first and its save decided in that turn's microtasks. The
|
|
1321
|
+
// next turn has handled the rest.
|
|
1322
|
+
if (dead === undefined && behind((id) => persistentRegistry.has(id)) !== undefined) {
|
|
1323
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
1324
|
+
dead = behind(serverDied)
|
|
1325
|
+
}
|
|
1326
|
+
if (dead === undefined) return false
|
|
1327
|
+
taintSeeds.add(node.id)
|
|
1328
|
+
taintedRan.set(node.id, dead)
|
|
1329
|
+
return true
|
|
1330
|
+
}
|
|
1331
|
+
scope.leftRunning = (node) => {
|
|
1332
|
+
// The check stays off until the first leftover: it is seeded before
|
|
1333
|
+
// the task settles, so every outcome a dependent's judge reads is
|
|
1334
|
+
// recorded by the tracker made here.
|
|
1335
|
+
if (!taintOn) {
|
|
1336
|
+
taint = taintTracker(continueAlways, taintSeeds, nodes, true)
|
|
1337
|
+
taintOn = true
|
|
1338
|
+
}
|
|
1339
|
+
taintSeeds.add(node.id)
|
|
1191
1340
|
}
|
|
1192
1341
|
if (options.retries !== undefined) scope.retries = options.retries
|
|
1193
1342
|
scope.knownFlaky = (node, hash) => {
|
|
@@ -1212,13 +1361,14 @@ async function runOnBus(
|
|
|
1212
1361
|
}
|
|
1213
1362
|
if (explainMiss !== undefined) scope.explainMiss = explainMiss
|
|
1214
1363
|
if (keepRun !== undefined) scope.keepRun = keepRun
|
|
1364
|
+
if (options.watch === true) scope.watch = true
|
|
1215
1365
|
if (sandboxArmer !== null) scope.armSandbox = () => sandboxArmer.arm()
|
|
1216
1366
|
if (demand.any) scope.executing = demand.executing
|
|
1217
1367
|
const argsOf = executeArgsOf(scope, {
|
|
1218
1368
|
log: (node) => (node.config.exec?.persistent !== undefined ? serverLog(node.id) : log),
|
|
1219
1369
|
placements,
|
|
1220
1370
|
nestedDirsByProject,
|
|
1221
|
-
downloadModeOf: download
|
|
1371
|
+
downloadModeOf: download?.modeOf,
|
|
1222
1372
|
shortCircuit,
|
|
1223
1373
|
dependedOn: depended,
|
|
1224
1374
|
holders,
|
|
@@ -1287,7 +1437,7 @@ async function runOnBus(
|
|
|
1287
1437
|
const historyDb = indexDb(prepared.localCache)
|
|
1288
1438
|
const cacheOff = cachesNothing(policy)
|
|
1289
1439
|
const judgeFlaky = (o: TaskOutcome): void => {
|
|
1290
|
-
const candidates = flakyCandidates([o]
|
|
1440
|
+
const candidates = flakyCandidates([o])
|
|
1291
1441
|
if (candidates.length === 0) return
|
|
1292
1442
|
try {
|
|
1293
1443
|
const found = detectFlaky(historyDb, candidates)[0]
|
|
@@ -1303,7 +1453,6 @@ async function runOnBus(
|
|
|
1303
1453
|
heldBefore.kept.delete(node.id)
|
|
1304
1454
|
if (hasEnded(server.child)) return undefined
|
|
1305
1455
|
server.handOver()
|
|
1306
|
-
handed.add(server.child)
|
|
1307
1456
|
server.out.bus = bus
|
|
1308
1457
|
serverOuts.set(node.id, server.out)
|
|
1309
1458
|
persistentRegistry.set(node.id, server.child)
|
|
@@ -1319,11 +1468,15 @@ async function runOnBus(
|
|
|
1319
1468
|
}
|
|
1320
1469
|
let lockBroken: boolean | undefined
|
|
1321
1470
|
prepared.localCache.storeHeldBlobSizes()
|
|
1471
|
+
prepared.localCache.checkpointOffThread()
|
|
1322
1472
|
// A hit's row is whole when it finishes, so it is written while the
|
|
1323
1473
|
// graph runs (history-stream.ts); every other row waits for the bundle.
|
|
1324
1474
|
// A row with forward args is the bundle's too: only it digests them.
|
|
1325
1475
|
const historyAt = historyFiles(prepared.localCache)
|
|
1326
|
-
|
|
1476
|
+
let expectedHits = 0
|
|
1477
|
+
for (const probe of shortCircuit.preProbed.values()) if (probe.hit !== null) expectedHits++
|
|
1478
|
+
const stream =
|
|
1479
|
+
historyAt === undefined ? undefined : new HistoryStream<TaskOutcome>(historyAt, expectedHits)
|
|
1327
1480
|
const forwards = (options.forwardArgs?.length ?? 0) > 0
|
|
1328
1481
|
const streamHit = (o: TaskOutcome): void => {
|
|
1329
1482
|
if (stream === undefined || isGroupTask(o.node)) return
|
|
@@ -1333,7 +1486,6 @@ async function runOnBus(
|
|
|
1333
1486
|
stream.add(
|
|
1334
1487
|
o,
|
|
1335
1488
|
runRecordOf(o as RecordedOutcome, {
|
|
1336
|
-
tainted: taintedRan,
|
|
1337
1489
|
runId,
|
|
1338
1490
|
startedAtMs: endedAtMsAtStart,
|
|
1339
1491
|
endedAtMs: endedAtMsAtStart,
|
|
@@ -1342,6 +1494,50 @@ async function runOnBus(
|
|
|
1342
1494
|
}),
|
|
1343
1495
|
)
|
|
1344
1496
|
}
|
|
1497
|
+
// A dependency-only server stops once no task left uses it, not at the
|
|
1498
|
+
// end of the graph: it held a worker's cores for nothing. Not under a
|
|
1499
|
+
// hold, whose next cycle may reuse it.
|
|
1500
|
+
const unneeded =
|
|
1501
|
+
options.holdPersistent === true
|
|
1502
|
+
? undefined
|
|
1503
|
+
: unneededServers(nodes, options.log === undefined && (options.handleSignals ?? true))
|
|
1504
|
+
const serving = executors.filter((e) => e.servers === true && e.unneeded !== undefined)
|
|
1505
|
+
const released = new Set<string>()
|
|
1506
|
+
const stopping: Promise<void>[] = []
|
|
1507
|
+
// A server stops after the servers using it (`serverUsers`), which this
|
|
1508
|
+
// same release reached first.
|
|
1509
|
+
const usersOf = unneeded === undefined ? undefined : serverUsers(nodes)
|
|
1510
|
+
const stops = new Map<string, Promise<void>>()
|
|
1511
|
+
const release = (id: string): void => {
|
|
1512
|
+
// A stop signals every server once and waits out its grace: a second
|
|
1513
|
+
// signal here cut a server's cleanup short.
|
|
1514
|
+
if (stop.signal.aborted) return
|
|
1515
|
+
for (const s of unneeded?.(id) ?? []) {
|
|
1516
|
+
// Copies an executor started beside the dependants it ran.
|
|
1517
|
+
for (const e of serving)
|
|
1518
|
+
try {
|
|
1519
|
+
e.unneeded!(s)
|
|
1520
|
+
} catch {
|
|
1521
|
+
// Its copies stop at its teardown, as before.
|
|
1522
|
+
}
|
|
1523
|
+
const child = persistentRegistry.get(s)
|
|
1524
|
+
// One that died on its own stays: the end of the graph reports it.
|
|
1525
|
+
if (child === undefined || hasEnded(child)) continue
|
|
1526
|
+
released.add(s)
|
|
1527
|
+
persistentRegistry.delete(s)
|
|
1528
|
+
const users: Promise<void>[] = []
|
|
1529
|
+
for (const u of usersOf?.get(s) ?? []) {
|
|
1530
|
+
const p = stops.get(u)
|
|
1531
|
+
if (p !== undefined) users.push(p)
|
|
1532
|
+
}
|
|
1533
|
+
const p =
|
|
1534
|
+
users.length === 0
|
|
1535
|
+
? stopServer(child)
|
|
1536
|
+
: Promise.allSettled(users).then(() => stopServer(child))
|
|
1537
|
+
stops.set(s, p)
|
|
1538
|
+
stopping.push(p)
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1345
1541
|
const outcomes = await runGraph({
|
|
1346
1542
|
nodes,
|
|
1347
1543
|
concurrency,
|
|
@@ -1356,22 +1552,30 @@ async function runOnBus(
|
|
|
1356
1552
|
onFailFast: () => failFast.abort(),
|
|
1357
1553
|
signal: stop.signal,
|
|
1358
1554
|
onStart: (node) => {
|
|
1555
|
+
// A platform skip never starts, like a skip blocked upstream: its
|
|
1556
|
+
// start opened a single task's frame that nothing closed.
|
|
1557
|
+
if (placements.otherPlatform.has(node.id)) return
|
|
1359
1558
|
log.taskStart?.(node)
|
|
1360
1559
|
},
|
|
1361
1560
|
onFinish: (o) => {
|
|
1362
1561
|
if (cacheOff) o.cacheOff = true
|
|
1562
|
+
const root = taintedRan.get(o.node.id)
|
|
1563
|
+
if (root !== undefined && o.blockedBy === undefined) o.taintedBy = root
|
|
1363
1564
|
taint.settled(o)
|
|
1364
1565
|
judgeFlaky(o)
|
|
1365
1566
|
keeper?.taskDone(o)
|
|
1366
1567
|
log.taskComplete(o.node, o)
|
|
1568
|
+
if (sinceReady.size > 0) releaseSinceReady(o.node.id)
|
|
1367
1569
|
streamHit(o)
|
|
1368
1570
|
demand.finished(o.node.id)
|
|
1571
|
+
release(o.node.id)
|
|
1369
1572
|
// A server that dies while the graph runs is no longer running:
|
|
1370
1573
|
// its pin stayed `running` under the line naming its exit.
|
|
1371
1574
|
const server = o.status === 'success' ? persistentRegistry.get(o.node.id) : undefined
|
|
1372
1575
|
if (server !== undefined && o.serverExit === undefined)
|
|
1373
1576
|
void server.exited.then((code) => {
|
|
1374
|
-
if (code !== 0 && !graphOver && !stop.signal.aborted
|
|
1577
|
+
if (code !== 0 && !graphOver && !stop.signal.aborted && !released.has(o.node.id))
|
|
1578
|
+
o.serverExit = code
|
|
1375
1579
|
})
|
|
1376
1580
|
},
|
|
1377
1581
|
// A crash a hand-broken lock entry caused is named as that; asked
|
|
@@ -1443,6 +1647,14 @@ async function runOnBus(
|
|
|
1443
1647
|
stop.graphDone()
|
|
1444
1648
|
reprioritize = undefined
|
|
1445
1649
|
graphOver = true
|
|
1650
|
+
// Each task finishes once its save settles (`settledOf`): the list is whole.
|
|
1651
|
+
if (unsaved.length > 1) {
|
|
1652
|
+
const more = unsaved.slice(1)
|
|
1653
|
+
const named = more.slice(0, 5).join(', ') + (more.length > 5 ? ', …' : '')
|
|
1654
|
+
warnOf(log)(
|
|
1655
|
+
`[vx] cache save failed for ${more.length} more task${more.length === 1 ? '' : 's'}: ${named}`,
|
|
1656
|
+
)
|
|
1657
|
+
}
|
|
1446
1658
|
|
|
1447
1659
|
// Which persistent children outlive the graph, and the bounded SIGTERM
|
|
1448
1660
|
// of the rest, before the summary prints. Scoped to the real CLI
|
|
@@ -1459,6 +1671,7 @@ async function runOnBus(
|
|
|
1459
1671
|
// A stop already signalled every server once and waits out its grace:
|
|
1460
1672
|
// a SIGTERM here, while a server ran its cleanup, cut it short.
|
|
1461
1673
|
if (stop.signal.aborted) await stop.aborting?.catch(() => {})
|
|
1674
|
+
await Promise.all(stopping)
|
|
1462
1675
|
const { keepAlive, crashed: crashedPersistent } = await disposeServers({
|
|
1463
1676
|
registry: persistentRegistry,
|
|
1464
1677
|
nodes,
|
|
@@ -1551,7 +1764,6 @@ async function runOnBus(
|
|
|
1551
1764
|
const recordsOf = (final: TaskOutcome[], runOk: boolean) =>
|
|
1552
1765
|
assembleRunRecords({
|
|
1553
1766
|
outcomes: final,
|
|
1554
|
-
tainted: taintedRan,
|
|
1555
1767
|
runId,
|
|
1556
1768
|
startedAtMs: endedAtMsAtStart,
|
|
1557
1769
|
endedAtMs,
|
|
@@ -1591,6 +1803,11 @@ async function runOnBus(
|
|
|
1591
1803
|
// cache closes: its history is written once the wait has decided, or it
|
|
1592
1804
|
// read `ok` over the exit 1 and fed the flaky list a pass (X-23).
|
|
1593
1805
|
const historyAfterWait = keepAlive.children.length > 0 && !hold && stop.stoppedBy === undefined
|
|
1806
|
+
// The servers this run is done with end now; the kept ones once the wait decides.
|
|
1807
|
+
telemetry?.endServers(
|
|
1808
|
+
list,
|
|
1809
|
+
historyAfterWait ? new Set(keepAlive.nodes.map((n) => n.id)) : undefined,
|
|
1810
|
+
)
|
|
1594
1811
|
// A failed task's output, for `vx last` and an agent (run-failures.ts).
|
|
1595
1812
|
// A cache directory this user cannot write keeps none of the run's
|
|
1596
1813
|
// records: the run's one notice said so.
|
|
@@ -1611,7 +1828,7 @@ async function runOnBus(
|
|
|
1611
1828
|
// measured, or the next run knows none of it (2026-10-21).
|
|
1612
1829
|
const ended = list.filter((o) => o.status !== 'aborted' && o.status !== 'skipped')
|
|
1613
1830
|
stepSync('vx:history', () => {
|
|
1614
|
-
recordHistory(() => prepared.localCache.recordRuns(recordsOf(ended, false).runs))
|
|
1831
|
+
recordHistory(() => prepared.localCache.recordRuns(recordsOf(ended, false).runs, head))
|
|
1615
1832
|
recordFailures(ended)
|
|
1616
1833
|
})
|
|
1617
1834
|
}
|
|
@@ -1663,7 +1880,14 @@ async function runOnBus(
|
|
|
1663
1880
|
runId,
|
|
1664
1881
|
),
|
|
1665
1882
|
)
|
|
1666
|
-
|
|
1883
|
+
// A stop passes none (`VxPlugin.teardown`): the run did not reach its end.
|
|
1884
|
+
// Built and frozen only for a teardown that reads it.
|
|
1885
|
+
const report =
|
|
1886
|
+
stop.stoppedBy === undefined && prepared.plugins.some((p) => p.teardown !== undefined)
|
|
1887
|
+
? freezeDeep(
|
|
1888
|
+
reportOf(list, { runId, commit: head.commitSha, ciRun: ciContext, ok }, keeper),
|
|
1889
|
+
)
|
|
1890
|
+
: undefined
|
|
1667
1891
|
// Hand the per-run summary to the telemetry sinks + drain them. Only
|
|
1668
1892
|
// when a sink is active (telemetry !== undefined) — otherwise this
|
|
1669
1893
|
// whole block is skipped and the run is byte-identical to before.
|
|
@@ -1671,23 +1895,37 @@ async function runOnBus(
|
|
|
1671
1895
|
// the run; flush is the sink's last chance to ship buffered records.
|
|
1672
1896
|
// After the drain, so the summary says what the uploads moved; a live
|
|
1673
1897
|
// sink's task records went out as each task ended.
|
|
1674
|
-
|
|
1675
|
-
|
|
1898
|
+
const sinks = telemetry
|
|
1899
|
+
const summarize = async (
|
|
1900
|
+
final: TaskOutcome[],
|
|
1901
|
+
runOk: boolean,
|
|
1902
|
+
timed?: typeof step,
|
|
1903
|
+
ended = { at: endedAtMs, totalMs },
|
|
1904
|
+
): Promise<void> => {
|
|
1905
|
+
if (sinks === undefined || runContextRecord === undefined) return
|
|
1906
|
+
sinks.endServers(final)
|
|
1907
|
+
const tasks = final === list ? records.telemetryTasks : recordsOf(final, runOk).telemetryTasks
|
|
1908
|
+
const summary = assembleRunSummary(runContextRecord, tasks, {
|
|
1676
1909
|
startedAt: endedAtMsAtStart,
|
|
1677
|
-
endedAt:
|
|
1678
|
-
totalDurationMs: Math.round(totalMs),
|
|
1679
|
-
exitOk:
|
|
1680
|
-
|
|
1910
|
+
endedAt: ended.at,
|
|
1911
|
+
totalDurationMs: Math.round(ended.totalMs),
|
|
1912
|
+
exitOk: runOk,
|
|
1913
|
+
// As the terminal counts them: `1 aborted · 1 not run`.
|
|
1914
|
+
abortedCount: tasks.filter((t) => t.status === 'aborted').length,
|
|
1915
|
+
notRunCount: final.filter((o) => neverStarted(o) && !isGroupTask(o.node)).length,
|
|
1681
1916
|
stages: stageTimes(),
|
|
1682
1917
|
uploads: prepared.localCache.uploads,
|
|
1683
1918
|
machines: executors.flatMap((e) => executorMachines(e, fault)),
|
|
1684
1919
|
idealMs: executors.map((e) => executorIdeal(e, fault)).find((ms) => ms !== undefined),
|
|
1685
1920
|
advice: executors.flatMap((e) => executorAdvice(e, fault)),
|
|
1686
1921
|
})
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
await sinks.flush(step)
|
|
1922
|
+
sinks.emitSummary(summary)
|
|
1923
|
+
await sinks.flush(timed)
|
|
1690
1924
|
}
|
|
1925
|
+
// A kept server can still fail the run in the wait below: its summary
|
|
1926
|
+
// goes out once the wait has decided, as its history does, or every
|
|
1927
|
+
// sink (vx-ci's job summary) read a pass over the exit 1.
|
|
1928
|
+
if (!historyAfterWait) await summarize(list, ok, step)
|
|
1691
1929
|
// End-of-run plugin lifecycle: each plugin's teardown(). Crash-isolated
|
|
1692
1930
|
// + time-bounded inside teardownPlugins, so a faulty plugin can neither
|
|
1693
1931
|
// fail nor hang the run. Here on the normal path, after the drains
|
|
@@ -1798,6 +2036,28 @@ async function runOnBus(
|
|
|
1798
2036
|
local.close()
|
|
1799
2037
|
}
|
|
1800
2038
|
})
|
|
2039
|
+
// After the footer and the plugins' teardown: no step line there.
|
|
2040
|
+
// The run ended with the session, not with the summary above. A stop
|
|
2041
|
+
// reads as one, as a stop anywhere else does: the servers it took
|
|
2042
|
+
// down are aborted there. The history keeps them as a dev session's
|
|
2043
|
+
// usual end (item 854).
|
|
2044
|
+
const stopped = stop.signal.aborted
|
|
2045
|
+
const told = stopped
|
|
2046
|
+
? final.map((o) =>
|
|
2047
|
+
keepAlive.nodes.some((n) => n.id === o.node.id) && isPassStatus(o.status)
|
|
2048
|
+
? {
|
|
2049
|
+
...o,
|
|
2050
|
+
status: 'aborted' as const,
|
|
2051
|
+
exitCode:
|
|
2052
|
+
typeof first.code === 'number' ? first.code : signalExitCode(first.code),
|
|
2053
|
+
}
|
|
2054
|
+
: o,
|
|
2055
|
+
)
|
|
2056
|
+
: final
|
|
2057
|
+
await summarize(told, ok && !crashed && !stopped, undefined, {
|
|
2058
|
+
at: Date.now(),
|
|
2059
|
+
totalMs: Number(process.hrtime.bigint() - runStartHrTimeNs) / 1_000_000,
|
|
2060
|
+
})
|
|
1801
2061
|
}
|
|
1802
2062
|
return { ok: ok && !crashed, outcomes: final }
|
|
1803
2063
|
}
|
|
@@ -1853,16 +2113,38 @@ function noneAffected(prepared: PreparedRun, options: RunOptions): string {
|
|
|
1853
2113
|
: `No affected project declares task(s): ${options.tasks.join(', ')}.`
|
|
1854
2114
|
}
|
|
1855
2115
|
|
|
2116
|
+
/**
|
|
2117
|
+
* One line when a policy names the remote cache and no plugin supplies
|
|
2118
|
+
* one; a plan (`--dry`) gets it as the run does. The workspace's
|
|
2119
|
+
* `cache.policy` asks as a flag does: it was the one source never told.
|
|
2120
|
+
*/
|
|
2121
|
+
function warnNoRemote(prepared: PreparedRun, options: RunOptions, log: Logger): void {
|
|
2122
|
+
if (prepared.hasRemoteLayer) return
|
|
2123
|
+
const remoteNamed =
|
|
2124
|
+
options.remoteRequested ??
|
|
2125
|
+
(options.cache === undefined &&
|
|
2126
|
+
options.cacheFlags?.noCache !== true &&
|
|
2127
|
+
namesRemote(prepared.workspaceConfig?.cache?.policy)
|
|
2128
|
+
? 'cache.policy'
|
|
2129
|
+
: undefined)
|
|
2130
|
+
if (remoteNamed === undefined) return
|
|
2131
|
+
warnOf(log)(
|
|
2132
|
+
`vx: ${remoteNamed} named the remote cache, but no cache plugin in .vx/workspace.* supplies one — the remote axes are off for this run`,
|
|
2133
|
+
)
|
|
2134
|
+
}
|
|
2135
|
+
|
|
1856
2136
|
/**
|
|
1857
2137
|
* Planning mode. Same setup as `run()` — workspace discovery, config
|
|
1858
2138
|
* load, package graph, task graph — but stops short of execution.
|
|
1859
2139
|
* Returns a `RunPlan` predicting the cache hit/miss outcome of every
|
|
1860
2140
|
* task. Used by `--dry`.
|
|
1861
2141
|
*
|
|
1862
|
-
* Side-effects are limited to
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
2142
|
+
* Side-effects are limited to running `inputs.runtime` probe commands,
|
|
2143
|
+
* which key derivation requires: with no remote the local cache opens to
|
|
2144
|
+
* read only (`prepareRunLocked`'s `planning`); with one, the remote hits
|
|
2145
|
+
* found are kept in the index for the run after. Probing is the
|
|
2146
|
+
* byte-free `cache.has()` existence check — no artifact download, no
|
|
2147
|
+
* ingest, no accessed_at bump.
|
|
1866
2148
|
*/
|
|
1867
2149
|
export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
1868
2150
|
refuseRunOptions(options)
|
|
@@ -1870,7 +2152,13 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1870
2152
|
// what a stage says on the way goes to stderr (C-6).
|
|
1871
2153
|
const log = options.log ?? defaultLogger(undefined, undefined, process.stderr)
|
|
1872
2154
|
const reasons = new Map<string, AffectedReason>()
|
|
1873
|
-
const prepared = await
|
|
2155
|
+
const prepared = await prepareRunLocked(
|
|
2156
|
+
{ ...options, affectedReasons: reasons },
|
|
2157
|
+
log,
|
|
2158
|
+
undefined,
|
|
2159
|
+
false,
|
|
2160
|
+
true,
|
|
2161
|
+
)
|
|
1874
2162
|
try {
|
|
1875
2163
|
options = withRunSettings(options, prepared.workspaceConfig)
|
|
1876
2164
|
if (prepared.unresolvedTasks.length > 0) {
|
|
@@ -1890,6 +2178,7 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1890
2178
|
if (prepared.empty === 'none-affected')
|
|
1891
2179
|
return { tasks: [], noneAffected: noneAffected(prepared, options) }
|
|
1892
2180
|
if (prepared.empty !== null) return { tasks: [] }
|
|
2181
|
+
warnNoRemote(prepared, options, log)
|
|
1893
2182
|
// The plan orders nothing, but it refuses what the run would refuse.
|
|
1894
2183
|
await prepared.priorities()
|
|
1895
2184
|
terminalHolders(prepared.nodes, options.tty === true)
|
|
@@ -1980,7 +2269,7 @@ async function planRestorable(
|
|
|
1980
2269
|
/** The names a project outside the run's selection declares, and how to reach them. */
|
|
1981
2270
|
function elsewhereHint(names: readonly string[]): string {
|
|
1982
2271
|
if (names.length === 0) return ''
|
|
1983
|
-
return ` Only projects outside the selection declare ${names.join(', ')} — pass --all, or --filter
|
|
2272
|
+
return ` Only projects outside the selection declare ${names.join(', ')} — pass --all, or a --filter that includes them.`
|
|
1984
2273
|
}
|
|
1985
2274
|
|
|
1986
2275
|
/**
|
|
@@ -1989,6 +2278,7 @@ function elsewhereHint(names: readonly string[]): string {
|
|
|
1989
2278
|
* from: a task just added to a vx.config is not in it until `vx lock`.
|
|
1990
2279
|
*/
|
|
1991
2280
|
async function unresolvedHint(prepared: {
|
|
2281
|
+
workspaceRoot: string
|
|
1992
2282
|
unresolvedTasks: readonly string[]
|
|
1993
2283
|
hintProjects: ReadonlyMap<string, ProjectEntry>
|
|
1994
2284
|
declaredElsewhere: readonly string[]
|
|
@@ -2002,10 +2292,18 @@ async function unresolvedHint(prepared: {
|
|
|
2002
2292
|
prepared.hintProjects,
|
|
2003
2293
|
prepared.declaredElsewhere,
|
|
2004
2294
|
prepared.taskless,
|
|
2295
|
+
prepared.workspaceRoot,
|
|
2005
2296
|
)
|
|
2006
2297
|
const init = await initHint(prepared)
|
|
2298
|
+
// "No project declares a task yet" beside configs that declare one: the
|
|
2299
|
+
// lock holds none of them (a `vx lock` that failed), so say that.
|
|
2300
|
+
if (near.includes(NO_TASK)) {
|
|
2301
|
+
const unlocked = await unlockedAnywhere(prepared.workspaceRoot)
|
|
2302
|
+
if (unlocked !== undefined)
|
|
2303
|
+
return `${elsewhere}${near.replace(NO_TASK, `The lock holds no project for ${unlocked}: run \`vx lock\` (did it fail?).`)}${init}`
|
|
2304
|
+
}
|
|
2007
2305
|
const lock =
|
|
2008
|
-
init === '' && elsewhere === '' && !near.startsWith(' Did you mean')
|
|
2306
|
+
init === '' && elsewhere === '' && !near.startsWith(' Did you mean') && !near.includes(NO_TASK)
|
|
2009
2307
|
? ' A run reads the lock: after editing a vx.config, run `vx lock`.'
|
|
2010
2308
|
: ''
|
|
2011
2309
|
return `${elsewhere}${near}${init}${lock}`
|
|
@@ -2028,6 +2326,19 @@ async function initHint(prepared: {
|
|
|
2028
2326
|
return ` ${EMPTY_LOCK}.`
|
|
2029
2327
|
}
|
|
2030
2328
|
|
|
2329
|
+
/** A project config the lock holds no project for, found by the walk `vx lock` makes. */
|
|
2330
|
+
async function unlockedAnywhere(root: string): Promise<string | undefined> {
|
|
2331
|
+
const [{ configs }, lock] = await Promise.all([findProjectConfigs(root), readLockfile(root)])
|
|
2332
|
+
const held = new Set(Object.keys(lock?.projects ?? {}))
|
|
2333
|
+
const c = configs.find((x) => !held.has(lockKey(root, x.dir)))
|
|
2334
|
+
return c === undefined ? undefined : relPosix(root, c.configPath)
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// The state right after `vx init`: the lock is current, so the lock hint
|
|
2338
|
+
// alone sent a new user to a command that changed nothing.
|
|
2339
|
+
const NO_TASK =
|
|
2340
|
+
'No project declares a task yet: add one under `tasks` in a vx.config, then run `vx lock`.'
|
|
2341
|
+
|
|
2031
2342
|
/**
|
|
2032
2343
|
* A typo's nearest declared name, when one is within two edits — the
|
|
2033
2344
|
* message names the fix instead of only the mistake. A bare name is
|
|
@@ -2040,6 +2351,7 @@ function didYouMean(
|
|
|
2040
2351
|
projects: ReadonlyMap<string, ProjectEntry>,
|
|
2041
2352
|
elsewhere: readonly string[],
|
|
2042
2353
|
taskless: (name: string) => boolean,
|
|
2354
|
+
root: string,
|
|
2043
2355
|
): string {
|
|
2044
2356
|
// A default `build` is no declared task (X-102): neither a bare name's
|
|
2045
2357
|
// match nor a menu item or hint for `p#typo`, which led to `p#build`
|
|
@@ -2068,7 +2380,7 @@ function didYouMean(
|
|
|
2068
2380
|
// A name outside the selection is already said, with `--all`.
|
|
2069
2381
|
else if (allTasks.size > 0 && !elsewhere.includes(spec)) {
|
|
2070
2382
|
lists.add(`Tasks: ${listed(allTasks)}.`)
|
|
2071
|
-
}
|
|
2383
|
+
} else if (allTasks.size === 0 && elsewhere.length === 0) lists.add(NO_TASK)
|
|
2072
2384
|
continue
|
|
2073
2385
|
}
|
|
2074
2386
|
const [proj, task] = [spec.slice(0, at), spec.slice(at + 1)]
|
|
@@ -2094,7 +2406,20 @@ function didYouMean(
|
|
|
2094
2406
|
continue
|
|
2095
2407
|
}
|
|
2096
2408
|
if (!projects.has(proj)) {
|
|
2097
|
-
|
|
2409
|
+
// NFC typed, NFD on disk (or back): two edits apart, so no hint, and
|
|
2410
|
+
// the list named the project exactly as typed.
|
|
2411
|
+
const form = [...projects.keys()].find((n) => n.normalize('NFC') === proj.normalize('NFC'))
|
|
2412
|
+
if (form !== undefined) {
|
|
2413
|
+
lists.add(
|
|
2414
|
+
`${proj} is not the project ${form}, though they look the same: the two are written in different Unicode forms (NFC and NFD). Set \`name\` in its vx.config to the form you type.`,
|
|
2415
|
+
)
|
|
2416
|
+
continue
|
|
2417
|
+
}
|
|
2418
|
+
// A project's directory first: by edit distance, `p/a` hinted `p/b`.
|
|
2419
|
+
const p =
|
|
2420
|
+
projectNamed(proj, projects) ??
|
|
2421
|
+
projectAtDir(proj, root, projects.values()) ??
|
|
2422
|
+
nearest(proj, projects.keys())
|
|
2098
2423
|
if (p !== undefined) {
|
|
2099
2424
|
const tasks = tasksOf(projects.get(p))
|
|
2100
2425
|
const t = tasks.includes(task) ? task : nearest(task, tasks)
|
|
@@ -2150,11 +2475,22 @@ export function projectNamed(
|
|
|
2150
2475
|
return scoped.length === 1 ? scoped[0] : undefined
|
|
2151
2476
|
}
|
|
2152
2477
|
|
|
2478
|
+
/**
|
|
2479
|
+
* The project whose directory a typed name is, relative to the workspace
|
|
2480
|
+
* root, as `--filter p/a` reads it: `alpha` for `p/a`.
|
|
2481
|
+
*/
|
|
2482
|
+
export function projectAtDir(
|
|
2483
|
+
typed: string,
|
|
2484
|
+
root: string,
|
|
2485
|
+
projects: Iterable<{ name: string; dir: string }>,
|
|
2486
|
+
): string | undefined {
|
|
2487
|
+
const dir = path.resolve(root, typed)
|
|
2488
|
+
for (const p of projects) if (path.resolve(p.dir) === dir) return p.name
|
|
2489
|
+
return undefined
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2153
2492
|
/** The executed, keyed outcomes of a run — what flakiness is judged on. */
|
|
2154
|
-
function flakyCandidates(
|
|
2155
|
-
outcomes: readonly TaskOutcome[],
|
|
2156
|
-
tainted: ReadonlyMap<string, string>,
|
|
2157
|
-
): FlakyCandidate[] {
|
|
2493
|
+
function flakyCandidates(outcomes: readonly TaskOutcome[]): FlakyCandidate[] {
|
|
2158
2494
|
const out: FlakyCandidate[] = []
|
|
2159
2495
|
for (const o of outcomes) {
|
|
2160
2496
|
if (o.status !== 'success' && o.status !== 'failed') continue
|
|
@@ -2166,7 +2502,7 @@ function flakyCandidates(
|
|
|
2166
2502
|
// Behind a failed dependency the outcome is not the key's: a failure is
|
|
2167
2503
|
// the dependency's, and a pass built on bytes the key never named, so a
|
|
2168
2504
|
// later failure relapsed from it and read as a flake for thirty days.
|
|
2169
|
-
if (
|
|
2505
|
+
if (o.taintedBy !== undefined) continue
|
|
2170
2506
|
out.push({
|
|
2171
2507
|
project: o.node.projectName,
|
|
2172
2508
|
task: o.node.taskName,
|