@vzn/vx 0.0.21 → 0.0.23
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 +21 -18
- package/package.json +6 -6
- package/src/bin.ts +52 -3
- package/src/cache/archive.ts +170 -21
- package/src/cache/cache.ts +667 -288
- package/src/cache/chained-cache.ts +14 -2
- package/src/cache/config-evals.ts +29 -6
- package/src/cache/file-hashes.ts +63 -35
- package/src/cache/git-inputs.ts +398 -69
- package/src/cache/index.ts +8 -1
- package/src/cache/inputs.ts +403 -102
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +81 -11
- package/src/cache/layered-cache.ts +199 -32
- package/src/cache/output-index.ts +109 -10
- package/src/cache/run-history.ts +8 -3
- package/src/cache/zstd.ts +53 -30
- package/src/cli/cache.ts +51 -21
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +13 -3
- package/src/cli/index.ts +35 -9
- package/src/cli/info.ts +49 -7
- package/src/cli/init.ts +17 -5
- package/src/cli/last.ts +96 -21
- package/src/cli/lock.ts +34 -17
- package/src/cli/plan-format.ts +4 -4
- package/src/cli/plugin-commands.ts +20 -5
- package/src/cli/run.ts +55 -12
- package/src/cli/select.ts +23 -13
- package/src/cli/show.ts +9 -4
- package/src/cli/upgrade.ts +134 -14
- package/src/cli/watch.ts +622 -102
- package/src/cli/why.ts +8 -16
- package/src/cli/workspace-config.ts +21 -9
- package/src/config.ts +46 -9
- package/src/exec/executor.ts +51 -5
- package/src/exec/index.ts +10 -0
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/runner.ts +327 -82
- package/src/exec/sandbox-binds.ts +34 -5
- package/src/exec/sandbox-paths.ts +22 -2
- package/src/exec/sandbox-runtime.ts +627 -102
- package/src/exec/sandbox-violations.ts +36 -23
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +12 -2
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +182 -134
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +16 -2
- package/src/orchestrator/admission.ts +41 -17
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +22 -7
- package/src/orchestrator/doctor.ts +122 -14
- package/src/orchestrator/events.ts +71 -2
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +331 -37
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +81 -14
- package/src/orchestrator/history.ts +6 -7
- package/src/orchestrator/hit-restore.ts +82 -18
- package/src/orchestrator/index.ts +7 -1
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +152 -32
- package/src/orchestrator/logger.ts +116 -20
- package/src/orchestrator/metrics.ts +156 -60
- package/src/orchestrator/miss-save.ts +106 -43
- package/src/orchestrator/options.ts +30 -4
- package/src/orchestrator/persistent.ts +41 -21
- package/src/orchestrator/placement.ts +55 -25
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +17 -23
- package/src/orchestrator/plugin.ts +31 -8
- package/src/orchestrator/prepare.ts +99 -13
- package/src/orchestrator/projects.ts +19 -8
- package/src/orchestrator/remote-prefetch.ts +21 -1
- package/src/orchestrator/run-artifacts.ts +9 -0
- package/src/orchestrator/run-context.ts +19 -4
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +6 -23
- package/src/orchestrator/run-report.ts +4 -2
- package/src/orchestrator/run.ts +264 -57
- package/src/orchestrator/sandbox-request.ts +388 -62
- package/src/orchestrator/save-lane.ts +4 -7
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +86 -30
- package/src/orchestrator/stable-keys.ts +202 -10
- package/src/orchestrator/status-line.ts +33 -1
- package/src/orchestrator/summary.ts +49 -2
- package/src/orchestrator/task-hash.ts +87 -7
- package/src/orchestrator/task-log-buffer.ts +7 -7
- package/src/orchestrator/telemetry-host.ts +16 -7
- package/src/orchestrator/telemetry.ts +69 -28
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +32 -3
- package/src/util/paths.ts +147 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +4 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +272 -64
- package/src/workspace/config-cache.ts +262 -32
- package/src/workspace/config-eval.ts +49 -12
- package/src/workspace/config-imports.ts +126 -5
- package/src/workspace/config-schema.ts +237 -41
- package/src/workspace/filter.ts +33 -18
- package/src/workspace/fingerprint.ts +29 -11
- package/src/workspace/index.ts +7 -0
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +33 -11
- package/src/workspace/migrate-scripts.ts +60 -7
- package/src/workspace/migration.ts +40 -1
- package/src/workspace/package-graph.ts +154 -16
- package/src/workspace/project-loader.ts +227 -77
- package/src/workspace/workspace.ts +173 -23
|
@@ -103,8 +103,14 @@ export interface VxPlugin {
|
|
|
103
103
|
* fingerprint every key sees (the config-evaluation cache still keys on
|
|
104
104
|
* it — a config may import a dependency), and `--affected` asks
|
|
105
105
|
* `affected` which projects a change to it touches instead of selecting
|
|
106
|
-
* every project. A file has one claimant
|
|
107
|
-
*
|
|
106
|
+
* every project. A file has one claimant.
|
|
107
|
+
*
|
|
108
|
+
* A root file core does NOT fold is claimed the same way when the
|
|
109
|
+
* plugin's stages read it: `turbo()` shapes every task from
|
|
110
|
+
* `turbo.json`, so an edit to it re-keys tasks through their resolved
|
|
111
|
+
* configs while no project owns the path. Nothing is taken out;
|
|
112
|
+
* `--affected` asks `affected` (undefined: every project) and `vx watch`
|
|
113
|
+
* re-runs on an edit (item 961). A claim is a bare name at the root.
|
|
108
114
|
*/
|
|
109
115
|
readonly fingerprint?: FingerprintClaim
|
|
110
116
|
|
|
@@ -187,9 +193,13 @@ export interface VxPlugin {
|
|
|
187
193
|
| Promise<TelemetrySink | TelemetrySink[] | undefined>
|
|
188
194
|
|
|
189
195
|
/**
|
|
190
|
-
* Optional one-time setup
|
|
191
|
-
*
|
|
192
|
-
*
|
|
196
|
+
* Optional one-time setup per run (validate the workspace, open a
|
|
197
|
+
* connection, read a token). Throwing aborts the run with a clean
|
|
198
|
+
* UserError naming the plugin. It is not a guard over the pipeline: the
|
|
199
|
+
* config, project, cache, graph, key and schedule stages have run by the
|
|
200
|
+
* time it is called, and a plan (`--dry`, `planRun`) never calls it. It
|
|
201
|
+
* precedes the executors, the telemetry sinks, admission and the first
|
|
202
|
+
* task (tests/plugin-e2e.test.ts).
|
|
193
203
|
*/
|
|
194
204
|
setup?(ctx: PluginSetupContext): void | Promise<void>
|
|
195
205
|
|
|
@@ -252,7 +262,7 @@ export interface KeyHookContext extends BaseContext {}
|
|
|
252
262
|
|
|
253
263
|
/** A plugin's claim on workspace fingerprint files — see `VxPlugin.fingerprint`. */
|
|
254
264
|
export interface FingerprintClaim {
|
|
255
|
-
/**
|
|
265
|
+
/** Bare names at the workspace root: a lockfile core folds (`pnpm-lock.yaml`) or a file the plugin reads (`turbo.json`). */
|
|
256
266
|
readonly files: readonly string[]
|
|
257
267
|
/**
|
|
258
268
|
* The projects (package names) a change to a claimed file affects. Called
|
|
@@ -308,7 +318,10 @@ export interface PluginContext {
|
|
|
308
318
|
readonly workspaceRoot: string
|
|
309
319
|
/** Where vx's cache lives — read-only as far as the plugin is concerned. */
|
|
310
320
|
readonly cacheDir: string
|
|
311
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* The run event bus. A plugin can subscribe directly if its needs exceed
|
|
323
|
+
* the hooks; the subscription ends with the run, as a hook's does.
|
|
324
|
+
*/
|
|
312
325
|
readonly bus: EventBus
|
|
313
326
|
/**
|
|
314
327
|
* Convenience: register a typed handler keyed off `RunEvent.kind`.
|
|
@@ -460,7 +473,17 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
460
473
|
const ctx: PluginContext = {
|
|
461
474
|
workspaceRoot,
|
|
462
475
|
cacheDir,
|
|
463
|
-
|
|
476
|
+
// A subscription made past the hooks leaves with the run like a
|
|
477
|
+
// hook's does: the bus can outlive the run (`RunOptions.bus`), and
|
|
478
|
+
// one left behind hears every later run on it (item 635).
|
|
479
|
+
bus: {
|
|
480
|
+
emit: (event) => bus.emit(event),
|
|
481
|
+
subscribe(subscriber) {
|
|
482
|
+
const dispose = bus.subscribe(subscriber)
|
|
483
|
+
disposers.push(dispose)
|
|
484
|
+
return dispose
|
|
485
|
+
},
|
|
486
|
+
},
|
|
464
487
|
on(hook, handler) {
|
|
465
488
|
const dispose = bus.subscribe((event) => {
|
|
466
489
|
if (disabled.has(plugin.name)) return
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
computeNestedProjectDirs,
|
|
29
29
|
computeWorkspaceFingerprints,
|
|
30
30
|
findWorkspaceRoot,
|
|
31
|
+
type LoadReads,
|
|
31
32
|
listProjects,
|
|
32
33
|
loadWorkspace,
|
|
33
34
|
FROZEN_WITHOUT_LOCK,
|
|
@@ -37,8 +38,10 @@ import {
|
|
|
37
38
|
} from '../workspace/index.js'
|
|
38
39
|
import {
|
|
39
40
|
buildTaskGraph,
|
|
41
|
+
excludeDependencies,
|
|
40
42
|
expandRequested,
|
|
41
43
|
type TaskNode,
|
|
44
|
+
undeclaredDepsError,
|
|
42
45
|
unresolvedRequests,
|
|
43
46
|
} from '../graph/index.js'
|
|
44
47
|
import {
|
|
@@ -50,6 +53,8 @@ import {
|
|
|
50
53
|
resolveCache,
|
|
51
54
|
} from './plugin-host.js'
|
|
52
55
|
import { loadProjects, loadWorkspacePlugins, type LoadedProjects } from './projects.js'
|
|
56
|
+
import { keyExcludedDependencies } from './excluded-keys.js'
|
|
57
|
+
import { FingerprintWatch } from './fingerprint-watch.js'
|
|
53
58
|
import type { VxPlugin } from './plugin.js'
|
|
54
59
|
import { createHashCache, type HashCache } from './task-hash.js'
|
|
55
60
|
import type { Logger } from './logger.js'
|
|
@@ -99,6 +104,8 @@ export interface PreparedRun {
|
|
|
99
104
|
*/
|
|
100
105
|
anyProjectConfig: boolean
|
|
101
106
|
workspaceFingerprint: string
|
|
107
|
+
/** Whether a task has since rewritten what that fingerprint folded (fingerprint-watch.ts). */
|
|
108
|
+
fingerprintWatch: FingerprintWatch
|
|
102
109
|
nestedDirsByProject: Map<string, string[]>
|
|
103
110
|
/**
|
|
104
111
|
* Per-run memo for `git ls-files` output, keyed by project dir.
|
|
@@ -143,7 +150,10 @@ export interface PreparedRun {
|
|
|
143
150
|
*/
|
|
144
151
|
export async function prepareRun(options: RunOptions, log: Logger): Promise<PreparedRun> {
|
|
145
152
|
mark('startup')
|
|
146
|
-
|
|
153
|
+
// The root manifest is read once for the root, the globs and the
|
|
154
|
+
// fingerprint. A watch cycle is a new run and reads it afresh.
|
|
155
|
+
const reads: LoadReads = new Map()
|
|
156
|
+
const workspaceRoot = await findWorkspaceRoot(options.cwd, reads)
|
|
147
157
|
// An UNSCOPED run (no explicit scope, at least one bare task name)
|
|
148
158
|
// enumerates the whole tree whatever the configs say, so git starts
|
|
149
159
|
// HERE — the walk needs only the root — and overlaps the workspace
|
|
@@ -160,7 +170,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
160
170
|
// handler keeps that from surfacing as an unhandled rejection (the real
|
|
161
171
|
// await further down still sees the error).
|
|
162
172
|
earlyGit?.catch(() => {})
|
|
163
|
-
const workspace = await loadWorkspace(workspaceRoot)
|
|
173
|
+
const workspace = await loadWorkspace(workspaceRoot, reads)
|
|
164
174
|
const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, (m) =>
|
|
165
175
|
log.status(m),
|
|
166
176
|
)
|
|
@@ -191,6 +201,11 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
191
201
|
}
|
|
192
202
|
const seeds: 'all' | string[] =
|
|
193
203
|
options.projects !== undefined ? [...options.projects, ...anchored] : hasBare ? 'all' : anchored
|
|
204
|
+
// Its own mark: the package graph is the bulk of what used to be charged
|
|
205
|
+
// to `open cache` (measured on a 1,000-project tree, 2026-09-20 — the
|
|
206
|
+
// graph 3-8 ms against the cache open's ~1 ms and the fingerprints' ~0.3),
|
|
207
|
+
// so a reader profiling that stage was sent to the wrong code.
|
|
208
|
+
mark('package graph')
|
|
194
209
|
|
|
195
210
|
// The local cache opens BEFORE the configs load: it is also where their
|
|
196
211
|
// cached evaluations live. `--cache-dir <path>` (RunOptions.cacheDir)
|
|
@@ -200,14 +215,22 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
200
215
|
const cacheDir = options.cacheDir
|
|
201
216
|
? path.resolve(options.cwd, options.cacheDir)
|
|
202
217
|
: resolveCacheDir(workspaceRoot, workspaceConfig)
|
|
203
|
-
const localCache = new Cache(
|
|
218
|
+
const localCache = new Cache(
|
|
219
|
+
cacheDir,
|
|
220
|
+
{ read: policy.localRead, write: policy.localWrite },
|
|
221
|
+
workspaceRoot,
|
|
222
|
+
options.artifactCeiling,
|
|
223
|
+
)
|
|
224
|
+
localCache.assertWritable()
|
|
204
225
|
noteSchemaReset(localCache, (m) => log.status(m))
|
|
205
226
|
// Two digests from one read: the config-evaluation cache keys on every
|
|
206
227
|
// file (a config may import a dependency), the task keys on the files no
|
|
207
228
|
// plugin claims (`VxPlugin.fingerprint`).
|
|
229
|
+
const fingerprintsAt = Date.now()
|
|
208
230
|
const fingerprints = await computeWorkspaceFingerprints(
|
|
209
231
|
workspaceRoot,
|
|
210
232
|
new Set(fingerprintClaims(plugins).keys()),
|
|
233
|
+
reads,
|
|
211
234
|
)
|
|
212
235
|
const workspaceFingerprint = fingerprints.unclaimed
|
|
213
236
|
mark('open cache')
|
|
@@ -224,19 +247,22 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
224
247
|
throw new UserError(FROZEN_WITHOUT_LOCK)
|
|
225
248
|
}
|
|
226
249
|
|
|
250
|
+
const loadArgs = {
|
|
251
|
+
workspaceRoot,
|
|
252
|
+
cacheDir,
|
|
253
|
+
plugins,
|
|
254
|
+
projectMetas,
|
|
255
|
+
packageGraph,
|
|
256
|
+
lock,
|
|
257
|
+
evalCache: { store: localCache, workspaceFingerprint: fingerprints.all },
|
|
258
|
+
warn: (m: string) => log.status(m),
|
|
259
|
+
}
|
|
227
260
|
let loaded: LoadedProjects
|
|
228
261
|
try {
|
|
229
262
|
loaded = await loadProjects({
|
|
230
|
-
|
|
231
|
-
cacheDir,
|
|
232
|
-
plugins,
|
|
233
|
-
projectMetas,
|
|
234
|
-
packageGraph,
|
|
263
|
+
...loadArgs,
|
|
235
264
|
seeds,
|
|
236
265
|
closure: true,
|
|
237
|
-
lock,
|
|
238
|
-
evalCache: { store: localCache, workspaceFingerprint: fingerprints.all },
|
|
239
|
-
warn: (m) => log.status(m),
|
|
240
266
|
...(options.staged !== undefined ? { staged: options.staged } : {}),
|
|
241
267
|
})
|
|
242
268
|
} catch (err) {
|
|
@@ -319,6 +345,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
319
345
|
applyGitEnumeration(enumeration, workspaceRoot, projectDirs, gitFilesCache, usesWorkspaceInputs)
|
|
320
346
|
mark('git enumeration')
|
|
321
347
|
const hashCache = createHashCache()
|
|
348
|
+
const fingerprintWatch = new FingerprintWatch(workspaceRoot, fingerprints, fingerprintsAt)
|
|
322
349
|
|
|
323
350
|
// Empty-cases bookkeeping. We still construct the cache + fingerprint
|
|
324
351
|
// so the caller's try/finally pattern can close it uniformly.
|
|
@@ -337,6 +364,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
337
364
|
projects,
|
|
338
365
|
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
339
366
|
workspaceFingerprint,
|
|
367
|
+
fingerprintWatch,
|
|
340
368
|
nestedDirsByProject,
|
|
341
369
|
gitFilesCache,
|
|
342
370
|
hashCache,
|
|
@@ -345,14 +373,26 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
345
373
|
}
|
|
346
374
|
}
|
|
347
375
|
|
|
376
|
+
// The whole graph, whatever `--exclude-dependencies` says: the stages
|
|
377
|
+
// below shape and key the tasks it drops as a full run would, because
|
|
378
|
+
// a dropped task is still keyed (excluded-keys.ts). A scoped load is not
|
|
379
|
+
// the whole workspace: a `^name` nothing loaded declares may be declared
|
|
380
|
+
// by a config the scope left out, so the builder hands it back instead of
|
|
381
|
+
// refusing it, and the rest decide.
|
|
382
|
+
const unproven: Array<[taskId: string, name: string]> = []
|
|
348
383
|
const nodes = buildTaskGraph({
|
|
349
384
|
projects,
|
|
350
385
|
packageGraph,
|
|
351
386
|
requested,
|
|
352
|
-
...(
|
|
353
|
-
? {
|
|
387
|
+
...(projects.size < projectsWithConfigs.length
|
|
388
|
+
? { undeclaredDeps: (id: string, name: string) => void unproven.push([id, name]) }
|
|
354
389
|
: {}),
|
|
355
390
|
})
|
|
391
|
+
if (unproven.length > 0) {
|
|
392
|
+
await refuseUndeclaredDeps(unproven, () =>
|
|
393
|
+
loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
|
|
394
|
+
)
|
|
395
|
+
}
|
|
356
396
|
// The graph is built; what follows is the plugins' (graph, key,
|
|
357
397
|
// schedule). Two rows, so a plugin's key stage reads as its own cost
|
|
358
398
|
// and not as graph building — a lockfile plugin's 1000 stats per run
|
|
@@ -369,6 +409,24 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
369
409
|
if (hasHook(plugins, 'key')) {
|
|
370
410
|
await applyKeyHooks(plugins, nodes, { workspaceRoot, cacheDir, warn: (m) => log.status(m) })
|
|
371
411
|
}
|
|
412
|
+
const exclude = options.excludeDependencies
|
|
413
|
+
if (exclude !== undefined && (exclude === 'all' || exclude.length > 0)) {
|
|
414
|
+
const { keyOnly, dropped } = excludeDependencies(nodes, exclude)
|
|
415
|
+
if (dropped.size > 0) {
|
|
416
|
+
await keyExcludedDependencies({
|
|
417
|
+
nodes,
|
|
418
|
+
keyOnly,
|
|
419
|
+
dropped,
|
|
420
|
+
cache,
|
|
421
|
+
workspaceRoot,
|
|
422
|
+
workspaceFingerprint,
|
|
423
|
+
forwardArgs: options.forwardArgs,
|
|
424
|
+
nestedDirsByProject,
|
|
425
|
+
gitFilesCache,
|
|
426
|
+
hashCache,
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
}
|
|
372
430
|
let priorities: ReadonlyMap<string, number> = new Map()
|
|
373
431
|
if (hasHook(plugins, 'schedule')) {
|
|
374
432
|
priorities = await applyScheduleHooks(plugins, nodes, {
|
|
@@ -393,6 +451,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
393
451
|
projects,
|
|
394
452
|
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
395
453
|
workspaceFingerprint,
|
|
454
|
+
fingerprintWatch,
|
|
396
455
|
nestedDirsByProject,
|
|
397
456
|
gitFilesCache,
|
|
398
457
|
hashCache,
|
|
@@ -400,3 +459,30 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
400
459
|
empty: nodes.size === 0 ? 'empty-graph' : null,
|
|
401
460
|
}
|
|
402
461
|
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Refuses the first `^name` in `unproven` that no project in the whole
|
|
465
|
+
* workspace declares. `load` evaluates the configs a scoped run left out;
|
|
466
|
+
* it is reached only when a `^name` found no holder and nothing loaded
|
|
467
|
+
* declares it, so a run that names only declared tasks never pays for it.
|
|
468
|
+
* The rest failing to load leaves the names unjudged: an out-of-scope
|
|
469
|
+
* broken config does not fail a scoped run.
|
|
470
|
+
*/
|
|
471
|
+
async function refuseUndeclaredDeps(
|
|
472
|
+
unproven: ReadonlyArray<readonly [taskId: string, name: string]>,
|
|
473
|
+
load: () => Promise<LoadedProjects>,
|
|
474
|
+
): Promise<void> {
|
|
475
|
+
let all: LoadedProjects
|
|
476
|
+
try {
|
|
477
|
+
all = await load()
|
|
478
|
+
} catch {
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
const declared = new Set<string>()
|
|
482
|
+
for (const p of all.projects.values()) {
|
|
483
|
+
for (const t of Object.keys(p.config.tasks ?? {})) declared.add(t)
|
|
484
|
+
}
|
|
485
|
+
for (const [id, name] of unproven) {
|
|
486
|
+
if (!declared.has(name)) throw undeclaredDepsError(id, name)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
loadProjectConfigs,
|
|
11
11
|
validateProjectConfig,
|
|
12
12
|
type LoadProjectConfigOptions,
|
|
13
|
+
type LoadReads,
|
|
13
14
|
type Lockfile,
|
|
14
15
|
type PackageGraph,
|
|
15
16
|
type ProjectEntry,
|
|
@@ -113,19 +114,28 @@ export async function loadProjects(args: LoadProjectsArgs): Promise<LoadedProjec
|
|
|
113
114
|
const metaByName = new Map<string, ProjectMeta>(configured.map((m) => [m.name, m]))
|
|
114
115
|
const needed = new Set<string>()
|
|
115
116
|
const pending: ProjectMeta[] = []
|
|
117
|
+
// Counted, not read off `pending`: a round empties `pending`, and a guard
|
|
118
|
+
// on its length let every cross edge after the first round walk its
|
|
119
|
+
// whole closure again (1,500 walks at 300 projects, item 932).
|
|
120
|
+
let neededConfigured = 0
|
|
116
121
|
const consider = (name: string): void => {
|
|
117
122
|
if (needed.has(name)) return
|
|
118
123
|
needed.add(name)
|
|
119
124
|
const meta = metaByName.get(name)
|
|
120
|
-
if (meta)
|
|
125
|
+
if (meta) {
|
|
126
|
+
pending.push(meta)
|
|
127
|
+
neededConfigured++
|
|
128
|
+
}
|
|
121
129
|
}
|
|
130
|
+
const walked = new Set<string>()
|
|
122
131
|
const considerWithDeps = (name: string): void => {
|
|
123
132
|
consider(name)
|
|
124
|
-
// Every config-bearing project already
|
|
125
|
-
// nothing, and asking for it would build the package graph's
|
|
133
|
+
// Every config-bearing project already considered: the closure can
|
|
134
|
+
// add nothing, and asking for it would build the package graph's
|
|
126
135
|
// transitive bitsets — a cost the unscoped run (every project a seed)
|
|
127
|
-
// otherwise never pays.
|
|
128
|
-
if (
|
|
136
|
+
// otherwise never pays. A closure already walked adds nothing either.
|
|
137
|
+
if (neededConfigured === metaByName.size || walked.has(name)) return
|
|
138
|
+
walked.add(name)
|
|
129
139
|
for (const dep of packageGraph.transitiveDeps(name)) consider(dep)
|
|
130
140
|
}
|
|
131
141
|
const seeds = args.seeds === 'all' ? metaByName.keys() : [...args.seeds]
|
|
@@ -231,10 +241,11 @@ export async function loadResolvedProjects(
|
|
|
231
241
|
opts: { scope?: 'all' | readonly string[]; warn?: (message: string) => void } = {},
|
|
232
242
|
): Promise<Map<string, ProjectEntry>> {
|
|
233
243
|
const warn = opts.warn ?? ((): void => {})
|
|
234
|
-
const
|
|
244
|
+
const reads: LoadReads = new Map()
|
|
245
|
+
const metas = await listProjects(await loadWorkspace(workspaceRoot, reads))
|
|
235
246
|
const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, warn)
|
|
236
247
|
const cacheDir = resolveCacheDir(workspaceRoot, workspaceConfig)
|
|
237
|
-
const cache = new Cache(cacheDir)
|
|
248
|
+
const cache = new Cache(cacheDir, { read: true, write: true }, workspaceRoot)
|
|
238
249
|
noteSchemaReset(cache, warn)
|
|
239
250
|
try {
|
|
240
251
|
const loaded = await loadProjects({
|
|
@@ -248,7 +259,7 @@ export async function loadResolvedProjects(
|
|
|
248
259
|
lock: null,
|
|
249
260
|
evalCache: {
|
|
250
261
|
store: cache,
|
|
251
|
-
workspaceFingerprint: await computeWorkspaceFingerprint(workspaceRoot),
|
|
262
|
+
workspaceFingerprint: await computeWorkspaceFingerprint(workspaceRoot, reads),
|
|
252
263
|
},
|
|
253
264
|
warn,
|
|
254
265
|
})
|
|
@@ -78,6 +78,11 @@ async function runPrefetch(args: PrefetchArgs): Promise<void> {
|
|
|
78
78
|
const present = (await args.cache.remoteHasMany?.(uniqueHashes)) ?? null
|
|
79
79
|
if (present !== null) {
|
|
80
80
|
args.cache.markRemoteAbsent?.(uniqueHashes.filter((h) => !present.has(h)))
|
|
81
|
+
// The pre-mark above already answers a pull for an absent hash from
|
|
82
|
+
// the in-flight map, so on the wire this filter is the pump's
|
|
83
|
+
// economy: deleting it alone GETs nothing extra, deleting both GETs
|
|
84
|
+
// every absent key. What it decides is which hashes the pump asks
|
|
85
|
+
// the layer for, and that is what the row pins (item 643, 645).
|
|
81
86
|
toPrefetch = stableKeys.filter((k) => present.has(k.hash))
|
|
82
87
|
if (toPrefetch.length === 0) return
|
|
83
88
|
}
|
|
@@ -92,9 +97,24 @@ async function runPrefetch(args: PrefetchArgs): Promise<void> {
|
|
|
92
97
|
while (next < toPrefetch.length) {
|
|
93
98
|
const { hash, node } = toPrefetch[next++]!
|
|
94
99
|
await args.cache
|
|
95
|
-
.prefetch(hash,
|
|
100
|
+
.prefetch(hash, getContext(node, node.config.exec?.command ?? ''))
|
|
96
101
|
.catch(() => false)
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
await Promise.all(Array.from({ length: workers }, () => pump()))
|
|
100
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* What a cache lookup is told about the task: its id and command for the
|
|
109
|
+
* row a remote hit inserts, and its declared outputs, the only names a
|
|
110
|
+
* remote artifact may carry (item 942). Shared with execute-task's own
|
|
111
|
+
* lookup, the other one that can reach a remote.
|
|
112
|
+
*/
|
|
113
|
+
export function getContext(node: TaskNode, command: string) {
|
|
114
|
+
const outputs = node.config.cache?.outputs
|
|
115
|
+
return {
|
|
116
|
+
taskId: node.id,
|
|
117
|
+
command,
|
|
118
|
+
outputs: { files: outputs?.files ?? [], workspaceFiles: outputs?.workspaceFiles ?? [] },
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -64,6 +64,15 @@ function taskEntry(o: TaskOutcome, flaky?: FlakyFinding): Record<string, unknown
|
|
|
64
64
|
...(o.storedPeakRssBytes !== undefined ? { storedPeakRssBytes: o.storedPeakRssBytes } : {}),
|
|
65
65
|
// Present only when an `admit` policy held the task with a worker free.
|
|
66
66
|
...(o.admissionHeldMs !== undefined ? { admissionHeldMs: o.admissionHeldMs } : {}),
|
|
67
|
+
// A skipped row names the failed (or aborted) task at the root of what
|
|
68
|
+
// blocked it; a fail-fast skip has none.
|
|
69
|
+
...(o.blockedBy !== undefined ? { blockedBy: o.blockedBy } : {}),
|
|
70
|
+
// A failure vx's own `timeout` killed; absent on every other row.
|
|
71
|
+
...(o.timedOut === true ? { timedOut: true } : {}),
|
|
72
|
+
// The sandbox's violation count; present only on a task it recorded any for.
|
|
73
|
+
...(o.sandboxViolations !== undefined ? { sandboxViolations: o.sandboxViolations } : {}),
|
|
74
|
+
// A persistent task that never became ready, and why.
|
|
75
|
+
...(o.notReady !== undefined ? { notReady: o.notReady } : {}),
|
|
67
76
|
// hrtime spans are bigints → emit as strings so JSON.parse on
|
|
68
77
|
// the consumer side doesn't truncate the ns precision.
|
|
69
78
|
...(o.wallclockStartNs !== undefined ? { wallclockStartNs: String(o.wallclockStartNs) } : {}),
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import fs from 'node:fs'
|
|
12
12
|
import os from 'node:os'
|
|
13
13
|
import path from 'node:path'
|
|
14
|
-
import { ulid, xxh3hex } from '../util/index.js'
|
|
14
|
+
import { executablePath, ulid, xxh3hex } from '../util/index.js'
|
|
15
15
|
|
|
16
16
|
export interface GitContext {
|
|
17
17
|
/** `git rev-parse HEAD`, or null outside a repo / on failure. */
|
|
@@ -65,7 +65,15 @@ export function captureGitContext(
|
|
|
65
65
|
}
|
|
66
66
|
try {
|
|
67
67
|
const proc = Bun.spawnSync({
|
|
68
|
-
cmd: [
|
|
68
|
+
cmd: [
|
|
69
|
+
executablePath('git'),
|
|
70
|
+
'-C',
|
|
71
|
+
workspaceRoot,
|
|
72
|
+
'rev-parse',
|
|
73
|
+
'HEAD',
|
|
74
|
+
'--abbrev-ref',
|
|
75
|
+
'HEAD',
|
|
76
|
+
],
|
|
69
77
|
stdout: 'pipe',
|
|
70
78
|
stderr: 'ignore',
|
|
71
79
|
})
|
|
@@ -205,7 +213,14 @@ export function captureDefaultBranch(
|
|
|
205
213
|
|
|
206
214
|
try {
|
|
207
215
|
const proc = Bun.spawnSync({
|
|
208
|
-
cmd: [
|
|
216
|
+
cmd: [
|
|
217
|
+
executablePath('git'),
|
|
218
|
+
'-C',
|
|
219
|
+
workspaceRoot,
|
|
220
|
+
'symbolic-ref',
|
|
221
|
+
'--short',
|
|
222
|
+
'refs/remotes/origin/HEAD',
|
|
223
|
+
],
|
|
209
224
|
stdout: 'pipe',
|
|
210
225
|
stderr: 'ignore',
|
|
211
226
|
})
|
|
@@ -310,7 +325,7 @@ export function captureWorkspaceIdentity(workspaceRoot: string): WorkspaceIdenti
|
|
|
310
325
|
// rewrites, so two developers mirroring the same repo through
|
|
311
326
|
// different proxies would derive different workspace ids. The raw
|
|
312
327
|
// configured URL is the identity.
|
|
313
|
-
cmd: ['git', '-C', workspaceRoot, 'config', '--get', 'remote.origin.url'],
|
|
328
|
+
cmd: [executablePath('git'), '-C', workspaceRoot, 'config', '--get', 'remote.origin.url'],
|
|
314
329
|
stdout: 'pipe',
|
|
315
330
|
stderr: 'ignore',
|
|
316
331
|
})
|