@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
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// A bounded number run at once (memory: each pack holds an artifact's
|
|
7
7
|
// bytes); the scheduler finishes a task only once its save has settled
|
|
8
8
|
// (`settledOf`), so a run ends with every save in the cache; and a save
|
|
9
|
-
// that fails degrades to a miss next time,
|
|
10
|
-
// ran.
|
|
9
|
+
// that fails degrades to a miss next time, reported with its task — the
|
|
10
|
+
// task's work ran.
|
|
11
11
|
|
|
12
12
|
export interface SaveLane {
|
|
13
13
|
/**
|
|
@@ -17,34 +17,45 @@ export interface SaveLane {
|
|
|
17
17
|
* task's outcome, and a duplicate of the same task in another run
|
|
18
18
|
* (admission's in-flight join).
|
|
19
19
|
*/
|
|
20
|
-
defer(save: () => Promise<void
|
|
20
|
+
defer(save: () => Promise<void>, taskId: string): Promise<boolean>
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
interface Queued {
|
|
24
|
+
save: () => Promise<void>
|
|
25
|
+
taskId: string
|
|
26
|
+
settle: (stored: boolean) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createSaveLane(
|
|
30
|
+
cap: number,
|
|
31
|
+
onError: (err: unknown, taskId: string) => void,
|
|
32
|
+
): SaveLane {
|
|
33
|
+
const queue: Queued[] = []
|
|
25
34
|
let running = 0
|
|
26
|
-
const start = (
|
|
27
|
-
void
|
|
35
|
+
const start = (q: Queued): void => {
|
|
36
|
+
void q
|
|
37
|
+
.save()
|
|
28
38
|
.then(
|
|
29
39
|
() => true,
|
|
30
40
|
(err: unknown) => {
|
|
31
|
-
onError(err)
|
|
41
|
+
onError(err, q.taskId)
|
|
32
42
|
return false
|
|
33
43
|
},
|
|
34
44
|
)
|
|
35
45
|
.then((stored) => {
|
|
36
46
|
running--
|
|
37
|
-
settle(stored)
|
|
47
|
+
q.settle(stored)
|
|
38
48
|
const next = queue.shift()
|
|
39
|
-
if (next !== undefined) start(next
|
|
49
|
+
if (next !== undefined) start(next)
|
|
40
50
|
})
|
|
41
51
|
running++
|
|
42
52
|
}
|
|
43
53
|
return {
|
|
44
|
-
defer(save) {
|
|
54
|
+
defer(save, taskId) {
|
|
45
55
|
return new Promise<boolean>((settle) => {
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
const q = { save, taskId, settle }
|
|
57
|
+
if (running < cap) start(q)
|
|
58
|
+
else queue.push(q)
|
|
48
59
|
})
|
|
49
60
|
},
|
|
50
61
|
}
|
|
@@ -25,9 +25,11 @@ export function shellVerdict(args: {
|
|
|
25
25
|
signal?: string | number | undefined
|
|
26
26
|
/** A sandboxed task's view: whether a file is hidden from it (no grant reads it). */
|
|
27
27
|
hidden?: ((file: string) => boolean) | undefined
|
|
28
|
+
/** What the step wrote to stderr: the shell's own "not found" line names the word a compound command missed. */
|
|
29
|
+
stderr?: string | undefined
|
|
28
30
|
}): string | undefined {
|
|
29
31
|
if (args.code !== 127 && args.code !== 126) return signalVerdict(args.code, args.signal)
|
|
30
|
-
const word = execWord(args.command)
|
|
32
|
+
const word = execWord(args.command) ?? (args.code === 127 ? notFoundWord(args.stderr) : undefined)
|
|
31
33
|
const what = word ?? 'a command in this task'
|
|
32
34
|
if (word !== undefined && word.includes('/')) {
|
|
33
35
|
const file = path.resolve(args.cwd, word)
|
|
@@ -59,6 +61,23 @@ export function shellVerdict(args: {
|
|
|
59
61
|
return `[vx] exit 127 is the shell's "command not found": ${what} is not on this task's PATH — vx puts ${args.bins.join(' and ')} first and never a sibling project's bin; install it in this package or at the workspace root`
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The word the shell's last "not found" line names: dash's `sh: 1: cat:
|
|
66
|
+
* not found`, bash's `bash: line 1: cat: command not found`. A command with
|
|
67
|
+
* a redirect or a `&&` has no one word to read from the command itself, and
|
|
68
|
+
* a hermetic task's hidden `cat` was told to be installed (almost every
|
|
69
|
+
* real command has one).
|
|
70
|
+
*/
|
|
71
|
+
function notFoundWord(stderr: string | undefined): string | undefined {
|
|
72
|
+
if (stderr === undefined) return undefined
|
|
73
|
+
const lines = stderr.trimEnd().split('\n')
|
|
74
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
75
|
+
const m = /: ([^:]+): (?:command )?not found$/.exec(lines[i]!.trimEnd())
|
|
76
|
+
if (m !== null) return m[1]
|
|
77
|
+
}
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
/** Why the shell could not run a file, read from the file. */
|
|
63
82
|
function fileVerdict(file: string): string {
|
|
64
83
|
if (!existsSync(file)) {
|
|
@@ -99,6 +99,103 @@ export async function terminateChildren(
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The signals a run hears from the moment it goes for its run lock until
|
|
104
|
+
* `forwardSignals` takes over. A plugin's setup runs in between, so a
|
|
105
|
+
* slow one held that window open for seconds, and a Ctrl-C in it killed
|
|
106
|
+
* vx by the default action: no plugin's teardown, no sink's flush, the
|
|
107
|
+
* lock's entry left in the temp dir. The first signal is noted and aborts
|
|
108
|
+
* `signal` (the lock's wait reads it); `forwardSignals` replays it once
|
|
109
|
+
* installed, or `run()` exits of it on a way out before that. A second
|
|
110
|
+
* exits at once, as a second does later.
|
|
111
|
+
*/
|
|
112
|
+
export interface HeldSignals {
|
|
113
|
+
readonly signal: AbortSignal
|
|
114
|
+
/** Removes the listeners, once; the signal heard so far, if any. */
|
|
115
|
+
takeOver(): StopSignal | undefined
|
|
116
|
+
/**
|
|
117
|
+
* From here a stop is held for the run's own end (plugin setup has
|
|
118
|
+
* begun, so teardown is owed); before it, a stop leaves at once, its
|
|
119
|
+
* exit hooks releasing the run lock: nothing earlier (a git listing, a
|
|
120
|
+
* remote's open) waits on the stop, so holding it held the run.
|
|
121
|
+
*/
|
|
122
|
+
arm(): void
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function holdSignals(): HeldSignals {
|
|
126
|
+
const controller = new AbortController()
|
|
127
|
+
let heard: StopSignal | undefined
|
|
128
|
+
let armed = false
|
|
129
|
+
const onSignal = (signal: StopSignal): void => {
|
|
130
|
+
if (heard !== undefined) exitOf(heard, false)
|
|
131
|
+
if (!armed) {
|
|
132
|
+
claimExitForSignal()
|
|
133
|
+
exitOf(signal, false)
|
|
134
|
+
}
|
|
135
|
+
heard = signal
|
|
136
|
+
claimExitForSignal()
|
|
137
|
+
controller.abort(signal)
|
|
138
|
+
}
|
|
139
|
+
const onSigint = (): void => onSignal('SIGINT')
|
|
140
|
+
const onSigterm = (): void => onSignal('SIGTERM')
|
|
141
|
+
const onSighup = (): void => onSignal('SIGHUP')
|
|
142
|
+
process.on('SIGINT', onSigint)
|
|
143
|
+
process.on('SIGTERM', onSigterm)
|
|
144
|
+
if (!hangupIgnored()) process.on('SIGHUP', onSighup)
|
|
145
|
+
let taken = false
|
|
146
|
+
return {
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
arm: () => {
|
|
149
|
+
armed = true
|
|
150
|
+
},
|
|
151
|
+
takeOver: () => {
|
|
152
|
+
if (taken) return undefined
|
|
153
|
+
taken = true
|
|
154
|
+
process.off('SIGINT', onSigint)
|
|
155
|
+
process.off('SIGTERM', onSigterm)
|
|
156
|
+
process.off('SIGHUP', onSighup)
|
|
157
|
+
return heard
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* `work`, or a rejection once `signal` aborts. A plugin's setup or
|
|
164
|
+
* executor factory that never settles held a held signal with it: the
|
|
165
|
+
* first was noted and waited, and only a second, which tears nothing
|
|
166
|
+
* down, ended vx.
|
|
167
|
+
*/
|
|
168
|
+
export function unlessStopped<T>(work: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
|
169
|
+
if (signal === undefined) return work
|
|
170
|
+
if (signal.aborted) return Promise.reject(new Error(`stopped by ${String(signal.reason)}`))
|
|
171
|
+
const { promise, resolve, reject } = Promise.withResolvers<T>()
|
|
172
|
+
const onAbort = (): void => reject(new Error(`stopped by ${String(signal.reason)}`))
|
|
173
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
174
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort))
|
|
175
|
+
return promise
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A held signal no `forwardSignals` took over (the run returned or threw
|
|
180
|
+
* before its tasks) leaves here, once the run's own end is done: stdout
|
|
181
|
+
* drained, bounded, then `exitOf`.
|
|
182
|
+
*/
|
|
183
|
+
export async function exitOfSignal(signal: StopSignal): Promise<never> {
|
|
184
|
+
await settleWithin(new Promise<void>((r) => process.stdout.end(() => r())), 2_000)
|
|
185
|
+
return exitOf(signal, false)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Die of the signal once the exit hooks have run: a shell running vx in
|
|
190
|
+
* a script stops it on Ctrl-C only when its child died of SIGINT, and a
|
|
191
|
+
* 130 exit ran the script's next line. The code stays the fallback
|
|
192
|
+
* should the signal not land.
|
|
193
|
+
*/
|
|
194
|
+
function exitOf(signal: StopSignal, handsTerminal: boolean): never {
|
|
195
|
+
if (!handsTerminal) process.once('exit', () => dieOf(signal))
|
|
196
|
+
process.exit(signalExitCode(signal))
|
|
197
|
+
}
|
|
198
|
+
|
|
102
199
|
export interface SignalForwarding {
|
|
103
200
|
/** Detach the handlers — in `run()`'s finally, so repeated runs never stack listeners. */
|
|
104
201
|
remove(): void
|
|
@@ -130,6 +227,8 @@ export function forwardSignals(args: {
|
|
|
130
227
|
* the signal's code rather than dying of the signal.
|
|
131
228
|
*/
|
|
132
229
|
handsTerminal?: boolean
|
|
230
|
+
/** The run's held signals: taken over here, and one already heard replayed. */
|
|
231
|
+
held?: HeldSignals | undefined
|
|
133
232
|
/** In-flight children; the runner adds and removes each around its spawn. */
|
|
134
233
|
liveChildren: ReadonlySet<Child>
|
|
135
234
|
/** Ready persistent tasks the orchestrator owns until the graph finishes. */
|
|
@@ -155,14 +254,7 @@ export function forwardSignals(args: {
|
|
|
155
254
|
} catch {
|
|
156
255
|
// double-close race with the normal path; we're exiting anyway
|
|
157
256
|
}
|
|
158
|
-
|
|
159
|
-
// in a script stops it on Ctrl-C only when its child died of SIGINT,
|
|
160
|
-
// and a 130 exit ran the script's next line. The code stays the
|
|
161
|
-
// fallback should the signal not land.
|
|
162
|
-
if (args.handsTerminal !== true) {
|
|
163
|
-
process.once('exit', () => dieOf(signal))
|
|
164
|
-
}
|
|
165
|
-
process.exit(signalExitCode(signal))
|
|
257
|
+
return exitOf(signal, args.handsTerminal === true)
|
|
166
258
|
}
|
|
167
259
|
let stopping: StopSignal | undefined
|
|
168
260
|
const onSignal = (signal: StopSignal): void => {
|
|
@@ -209,6 +301,9 @@ export function forwardSignals(args: {
|
|
|
209
301
|
process.on('SIGTSTP', onSigtstp)
|
|
210
302
|
process.on('SIGCONT', onSigcont)
|
|
211
303
|
}
|
|
304
|
+
// After this module's own listeners are on, so no signal finds none.
|
|
305
|
+
const heard = args.held?.takeOver()
|
|
306
|
+
if (heard !== undefined) onSignal(heard)
|
|
212
307
|
return {
|
|
213
308
|
remove: () => {
|
|
214
309
|
process.off('SIGINT', onSigint)
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
foldedDeps,
|
|
16
16
|
isGroupTask,
|
|
17
17
|
outputTakenBack,
|
|
18
|
+
standInsOf,
|
|
18
19
|
type TaskNode,
|
|
19
20
|
type TaskOutcome,
|
|
20
21
|
} from '../graph/index.js'
|
|
@@ -518,13 +519,13 @@ const probesAfterWritesOf = new WeakMap<ReadonlyMap<string, TaskNode>, ReadonlyS
|
|
|
518
519
|
* its upstream ran (`ComputeHashArgs.probesAfterWrites`).
|
|
519
520
|
*
|
|
520
521
|
* A writer is any upstream whose command may write (`commandWriteReach`)
|
|
521
|
-
* or that declares outputs. A key that folds
|
|
522
|
-
* writes, as for a rewriter in `deriveStableKeys`: an early answer then
|
|
522
|
+
* or that declares outputs. A key that folds a cached writer's covers what
|
|
523
|
+
* it writes, as for a rewriter in `deriveStableKeys`: an early answer then
|
|
523
524
|
* costs a spurious miss on the run after the bytes change, never a stale
|
|
524
525
|
* hit, and such a probe keeps the run's shared answer — one spawn per run,
|
|
525
526
|
* not per task. So only a writer the key does not fold, transitively
|
|
526
|
-
* (`tasks: []`, a filter that leaves it out, an order-only edge),
|
|
527
|
-
* the probe wait.
|
|
527
|
+
* (`tasks: []`, a filter that leaves it out, an order-only edge), or an
|
|
528
|
+
* uncached one, makes the probe wait.
|
|
528
529
|
*
|
|
529
530
|
* Once per graph: run, admission and both up-front passes ask the same map.
|
|
530
531
|
*/
|
|
@@ -577,7 +578,9 @@ export function probesAfterWrites(
|
|
|
577
578
|
if (unfolded || !depWrites) continue
|
|
578
579
|
const folded =
|
|
579
580
|
foldsAll || (folds ??= new Set(foldedDeps(node, nodeOf).map((c) => c.node.id))).has(dep)
|
|
580
|
-
|
|
581
|
+
// An uncached writer's writes are not a function of its key.
|
|
582
|
+
const covers = folded && (!writers.has(dep) || nodeOf(dep).config.cache === true)
|
|
583
|
+
unfolded = covers ? unfoldedBelow.has(dep) : true
|
|
581
584
|
}
|
|
582
585
|
if (writer) writerBelow.add(id)
|
|
583
586
|
if (unfolded) unfoldedBelow.add(id)
|
|
@@ -588,8 +591,11 @@ export function probesAfterWrites(
|
|
|
588
591
|
|
|
589
592
|
/**
|
|
590
593
|
* Does this task's key fold the dependency's? What `computeTaskHash` folds
|
|
591
|
-
* (`filterUpstreamHashes` over the same synthetic outcomes);
|
|
592
|
-
* every keyed member.
|
|
594
|
+
* (`filterUpstreamHashes` over the same synthetic outcomes); an unkeyed
|
|
595
|
+
* group, which has no filter, folds every keyed member. A keyed group is
|
|
596
|
+
* keyed by `computeTaskHash`, filter and all: read as folding everything, its
|
|
597
|
+
* `tasks: []` hid a cached rewriter below it, and its reader was restored
|
|
598
|
+
* under the key taken before the rewrite.
|
|
593
599
|
*/
|
|
594
600
|
function foldedBy(
|
|
595
601
|
node: TaskNode,
|
|
@@ -597,9 +603,11 @@ function foldedBy(
|
|
|
597
603
|
keyById: Map<string, string>,
|
|
598
604
|
): (dep: string) => boolean {
|
|
599
605
|
const filter = node.config.inputs?.tasks
|
|
600
|
-
if (
|
|
606
|
+
if (filter === undefined) return (dep) => keyById.has(dep)
|
|
601
607
|
const hashes = new Set(
|
|
602
|
-
filterUpstreamHashes(upstream, filter, node.projectName, node.id).map(
|
|
608
|
+
filterUpstreamHashes(upstream, filter, node.projectName, node.id, standInsOf(node)).map(
|
|
609
|
+
([, h]) => h,
|
|
610
|
+
),
|
|
603
611
|
)
|
|
604
612
|
return (dep) => {
|
|
605
613
|
const h = keyById.get(dep)
|
|
@@ -69,7 +69,10 @@ export interface TaskArtifacts {
|
|
|
69
69
|
restore(key: string, task: ArtifactTask): Promise<CapturedChunk[] | null>
|
|
70
70
|
/**
|
|
71
71
|
* Entry `key`'s output alone, laying no file; null when no cache holds
|
|
72
|
-
* it. An `exec.remote: 'only'` task's files stay off this disk.
|
|
72
|
+
* it. An `exec.remote: 'only'` task's files stay off this disk. An entry
|
|
73
|
+
* read off a remote is kept in the local store, so a later `restore`
|
|
74
|
+
* downloads nothing; naming no task, its row reads `<key>#unknown` with
|
|
75
|
+
* no command, and its file names are not checked against any outputs.
|
|
73
76
|
*/
|
|
74
77
|
output(key: string): Promise<CapturedChunk[] | null>
|
|
75
78
|
/**
|
|
@@ -129,7 +132,17 @@ export function taskArtifacts(cache: CacheLayer, workspaceRoot: string): TaskArt
|
|
|
129
132
|
},
|
|
130
133
|
|
|
131
134
|
async restore(key, task) {
|
|
132
|
-
|
|
135
|
+
// The task names the row a remote read writes, and the files its
|
|
136
|
+
// artifact may carry (`CacheGetContext.outputs`). Its command is
|
|
137
|
+
// masked as a save's is, read only by that write.
|
|
138
|
+
const ctx = {
|
|
139
|
+
taskId: task.id,
|
|
140
|
+
get command() {
|
|
141
|
+
return secretMask([process.env])?.mask(task.command) ?? task.command
|
|
142
|
+
},
|
|
143
|
+
outputs: task.outputs,
|
|
144
|
+
}
|
|
145
|
+
const entry = await (cache.getSaved ? cache.getSaved(key, ctx) : cache.get(key, ctx))
|
|
133
146
|
if (entry === null) return null
|
|
134
147
|
await cache.restoreOutputs(key, task.projectDir, workspaceRoot, task.outputs)
|
|
135
148
|
return decodeOutputLog(entry.stdout)
|