@vzn/vx 0.0.255 → 0.0.258
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/package.json +5 -5
- package/src/cache/cache.ts +3 -2
- package/src/cache/key-fold.ts +9 -0
- package/src/cache/layer.ts +4 -3
- package/src/cache/layered-cache.ts +2 -2
- package/src/cache/policy.ts +5 -3
- package/src/cli/init.ts +8 -2
- package/src/cli/plugin-templates.ts +2 -2
- package/src/config.ts +4 -2
- package/src/exec/sandbox-runtime.ts +5 -5
- package/src/orchestrator/options.ts +3 -2
- package/src/orchestrator/plan.ts +1 -1
- package/src/orchestrator/plugin.ts +5 -7
- package/src/orchestrator/sandbox-request.ts +3 -2
- package/src/util/task-id.ts +4 -3
- package/src/workspace/affected.ts +7 -2
- package/src/workspace/config-schema.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.258",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.258",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.258",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.258",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.258"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/cache.ts
CHANGED
|
@@ -100,8 +100,9 @@ import { CACHE_VERSION, foldKey } from './key-fold.js'
|
|
|
100
100
|
/**
|
|
101
101
|
* An artifact or temp file without an `entries` row is reaped by
|
|
102
102
|
* `prune()` only once it is this old. A save renames the artifact into
|
|
103
|
-
* place
|
|
104
|
-
*
|
|
103
|
+
* place inside its `BEGIN IMMEDIATE` row transaction (A-3), so a live
|
|
104
|
+
* artifact without a row is one whose commit failed and was taken back
|
|
105
|
+
* out; a crashed save's temp is stale long before this. Generous on purpose — a false orphan costs a
|
|
105
106
|
* re-run, a leaked temp costs disk.
|
|
106
107
|
*/
|
|
107
108
|
const ORPHAN_GRACE_MS = 60 * 60 * 1000
|
package/src/cache/key-fold.ts
CHANGED
|
@@ -87,6 +87,15 @@ import type { CacheKeyInput } from './layer.js'
|
|
|
87
87
|
// never folded and saved what it built. The fix withholds that link unless
|
|
88
88
|
// the key answers for the package, but an entry saved before it still hits
|
|
89
89
|
// after the sibling changes (probed: the old output replayed).
|
|
90
|
+
// v32: an artifact saved before non-UTF-8 names were refused lacks that
|
|
91
|
+
// output under an unchanged key (item 739).
|
|
92
|
+
// v33: an entry saved after an input changed under a running task sits
|
|
93
|
+
// under the key the fixed code derives once the tree is back (item 743).
|
|
94
|
+
// v34: the same for an in-run lockfile rewrite (item 750).
|
|
95
|
+
// v35: the sidecar records the key and ingest refuses a mismatch; a v34
|
|
96
|
+
// artifact records none (item 943).
|
|
97
|
+
// v36: each artifact ends in a `.vx-sum` CRC-32 of its entries, checked on
|
|
98
|
+
// scan and restore; a v35 artifact has none (L-19).
|
|
90
99
|
export const CACHE_VERSION = 'vx-cache-v36'
|
|
91
100
|
|
|
92
101
|
/**
|
package/src/cache/layer.ts
CHANGED
|
@@ -498,8 +498,8 @@ export class ArtifactVanishedError extends Error {
|
|
|
498
498
|
}
|
|
499
499
|
|
|
500
500
|
/**
|
|
501
|
-
* The shape every cache implementation honors. `Cache` (the local
|
|
502
|
-
*
|
|
501
|
+
* The shape every cache implementation honors. `Cache` (the local store)
|
|
502
|
+
* and `LayeredCache` both `implements` this so the
|
|
503
503
|
* orchestrator's `executeTask` can take either without a discriminated
|
|
504
504
|
* union and we get a compile-time guarantee the surfaces stay congruent.
|
|
505
505
|
*/
|
|
@@ -561,7 +561,8 @@ export interface CacheLayer {
|
|
|
561
561
|
* Lightweight existence probe. `'local'` / `'remote'` names the layer
|
|
562
562
|
* that holds the artifact; `null` is a miss. NEVER moves bytes: no
|
|
563
563
|
* artifact read, no remote download, no local ingest, no accessed_at
|
|
564
|
-
* bump (the
|
|
564
|
+
* bump (on the remote side the layer decides the call: an HTTP HEAD, or
|
|
565
|
+
* REAPI's `getActionResult` + `findMissingBlobs`). Planning
|
|
565
566
|
* (`--dry` / `--graph`) predicts hits with this instead of `get` so a
|
|
566
567
|
* dry run can't pull N artifacts over the network. Remote errors
|
|
567
568
|
* degrade to `null` — an existence probe never fails anything.
|
|
@@ -396,8 +396,8 @@ export class LayeredCache implements CacheLayer {
|
|
|
396
396
|
const remoteBody = remoteResult as { body: Blob | Response; durationMs?: number }
|
|
397
397
|
|
|
398
398
|
// Ingest the remote body into local using the caller-supplied
|
|
399
|
-
// taskId/command plus the remote-reported durationMs
|
|
400
|
-
// layer
|
|
399
|
+
// taskId/command plus the remote-reported durationMs, which `get`
|
|
400
|
+
// returns beside the body (the layer decides where it reads it from);
|
|
401
401
|
// taskId + command come from the orchestrator's TaskNode in scope.
|
|
402
402
|
// Without `ctx`, we can't populate a meaningful entries row, so
|
|
403
403
|
// ingest with placeholders — caller-side typing nudges everyone
|
package/src/cache/policy.ts
CHANGED
|
@@ -9,9 +9,11 @@ import { UserError } from '../util/index.js'
|
|
|
9
9
|
* toggled on its own so `--force` (re-execute but still refresh the
|
|
10
10
|
* cache) is distinct from `--no-cache` (disable everything).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The task-artifact get/save path is gated, and the local axes also gate
|
|
13
|
+
* the local store's config-evaluation reads and writes and its file-hash
|
|
14
|
+
* writes (`cache.ts`). `recordRun`, `stats`, `prune`, key derivation, and
|
|
15
|
+
* prefetch-ingest are never affected — they are bookkeeping/analytics
|
|
16
|
+
* that a run policy has no business disabling.
|
|
15
17
|
*/
|
|
16
18
|
export interface CachePolicy {
|
|
17
19
|
localRead: boolean
|
package/src/cli/init.ts
CHANGED
|
@@ -216,11 +216,17 @@ function remoteCacheSignal(root: string, runner: 'turbo' | 'nx'): string | undef
|
|
|
216
216
|
} catch {
|
|
217
217
|
continue
|
|
218
218
|
}
|
|
219
|
-
if (typeof rc === 'object' && rc !== null
|
|
219
|
+
if (typeof rc === 'object' && rc !== null) {
|
|
220
|
+
// Turned off in turbo.json is off, whatever CI sets.
|
|
221
|
+
if ((rc as { enabled?: unknown }).enabled === false) return undefined
|
|
220
222
|
return `${f} names a remoteCache`
|
|
223
|
+
}
|
|
221
224
|
}
|
|
222
225
|
}
|
|
223
226
|
const variable = REMOTE_CACHE_ENV[runner]
|
|
227
|
+
// A line that sets it (`TURBO_TOKEN: …`, `TURBO_TOKEN=…`), not one that
|
|
228
|
+
// names it: a comment or a doc line counted as "sets" (J's lead).
|
|
229
|
+
const sets = new RegExp(`^(?!\\s*#).*\\b${variable}\\s*[:=]`, 'm')
|
|
224
230
|
const workflows = new Bun.Glob('.github/workflows/*.{yml,yaml}')
|
|
225
231
|
const files = [...workflows.scanSync({ cwd: root, dot: true })].sort().concat(CI_FILES)
|
|
226
232
|
for (const f of files) {
|
|
@@ -230,7 +236,7 @@ function remoteCacheSignal(root: string, runner: 'turbo' | 'nx'): string | undef
|
|
|
230
236
|
} catch {
|
|
231
237
|
continue
|
|
232
238
|
}
|
|
233
|
-
if (
|
|
239
|
+
if (sets.test(text)) return `${f} sets ${variable}`
|
|
234
240
|
}
|
|
235
241
|
return undefined
|
|
236
242
|
}
|
|
@@ -20,8 +20,8 @@ export const PLUGIN_TEMPLATES: Readonly<Record<string, { plugin: string; test: s
|
|
|
20
20
|
},
|
|
21
21
|
executor: {
|
|
22
22
|
plugin:
|
|
23
|
-
"// `executor`: where a task's command runs. This one runs it here, in a\n// shell of its own, and stops the shell's whole process group when core\n// aborts the request (a stop, or `exec.timeout`): core cannot reach a\n// process an executor spawned.\nimport { definePlugin, type TaskExecutor, type VxPlugin } from '@vzn/vx'\n\nexport function shellExecutor(): VxPlugin {\n const executor: TaskExecutor = {\n name: 'shell',\n async execute(req) {\n const started = Date.now()\n const child = Bun.spawn([
|
|
24
|
-
test: "// A test for the example `executor` plugin beside it: a throwaway workspace\n// declares the plugin and vx's own run() drives it. `bun test` runs it.\nimport { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { afterEach, beforeEach, expect, it } from 'bun:test'\nimport { run, type Logger } from '@vzn/vx'\n\nlet root: string\nbeforeEach(async () => {\n root = await mkdtemp(path.join(os.tmpdir(), 'vx-plugin-executor-'))\n await writeFile(path.join(root, 'package.json'), JSON.stringify({ name: 'ws', private: true }))\n await writeFile(path.join(root, 'pnpm-workspace.yaml'), 'packages:\\n - \"packages/*\"\\n')\n Bun.spawnSync({ cmd: ['git', 'init', '-q'], cwd: root })\n})\nafterEach(async () => {\n await rm(root, { recursive: true, force: true })\n})\n\n/** A package `name` whose `vx.config.mjs` is `config`. */\nasync function pkg(\n name: string,\n config: string,\n scripts?: Record<string, string>,\n): Promise<string> {\n const dir = path.join(root, 'packages', name)\n await mkdir(dir, { recursive: true })\n await writeFile(\n path.join(dir, 'package.json'),\n JSON.stringify({ name, version: '1.0.0', ...(scripts ? { scripts } : {}) }),\n )\n await writeFile(path.join(dir, 'vx.config.mjs'), config)\n return dir\n}\n\n/** A `vx.workspace.mjs` declaring `call`, e.g. `shellExecutor()`, from the plugin file beside this test. */\nasync function workspace(call: string): Promise<void> {\n const from = JSON.stringify(path.join(import.meta.dir, 'executor.ts'))\n await writeFile(\n path.join(root, 'vx.workspace.mjs'),\n `import { shellExecutor } from ${from}\\nexport default { plugins: [${call}] }\\n`,\n )\n}\n\nfunction log(order: string[] = []): Logger {\n return {\n status() {},\n taskStart(node) {\n order.push(`start ${node.id}`)\n },\n taskStdout() {},\n taskStderr() {},\n taskComplete(node) {\n order.push(`end ${node.id}`)\n },\n }\n}\n\nit('runs the command and says where; stops it on exec.timeout', async () => {\n const dir = await pkg(\n 'a',\n `export default { tasks: {\n hello: { exec: { command: 'echo hi > out.txt' } },\n slow: { exec: { command: 'exec sleep 30', timeout: 300 } },\n } }`,\n )\n await workspace('shellExecutor()')\n const ok = await run({ cwd: root, tasks: ['hello'], log: log(), handleSignals: false })\n expect(ok.outcomes.map((o) => [o.node.id, o.status, o.where])).toEqual([\n ['a#hello', 'success', 'shell'],\n ])\n expect(await Bun.file(path.join(dir, 'out.txt')).text()).toBe('hi\\n')\n const started = Date.now()\n const slow = await run({ cwd: root, tasks: ['slow'], log: log(), handleSignals: false })\n expect(slow.outcomes.map((o) => [o.node.id, o.status, o.timedOut === true])).toEqual([\n ['a#slow', 'failed', true],\n ])\n expect(Date.now() - started).toBeLessThan(10_000)\n}, 20_000)\n",
|
|
23
|
+
"// `executor`: where a task's command runs. This one runs it here, in a\n// shell of its own, and stops the shell's whole process group when core\n// aborts the request (a stop, or `exec.timeout`): core cannot reach a\n// process an executor spawned.\nimport { definePlugin, type TaskExecutor, type VxPlugin } from '@vzn/vx'\n\nexport function shellExecutor(): VxPlugin {\n const executor: TaskExecutor = {\n name: 'shell',\n async execute(req) {\n const started = Date.now()\n // vx's own `sh`: a bare name is looked up on `req.env`'s PATH, which\n // leads with the project's node_modules/.bin, where a dependency's\n // `sh` would run every command.\n const sh = Bun.which('sh') ?? '/bin/sh'\n const child = Bun.spawn([sh, '-c', [req.command, ...req.forwardArgs].join(' ')], {\n cwd: req.cwd,\n env: req.env,\n stdout: 'pipe',\n stderr: 'pipe',\n detached: true,\n })\n const stop = (): void => {\n try {\n process.kill(-child.pid, 'SIGTERM')\n } catch {\n // already gone\n }\n }\n req.signal?.addEventListener('abort', stop, { once: true })\n const drain = async (stream: ReadableStream<Uint8Array>, on: (s: string) => void) => {\n const decoder = new TextDecoder()\n let all = ''\n for await (const chunk of stream) {\n const text = decoder.decode(chunk, { stream: true })\n all += text\n on(text)\n }\n return all\n }\n const [stdout, stderr, exitCode] = await Promise.all([\n drain(child.stdout, req.onStdout),\n drain(child.stderr, req.onStderr),\n child.exited,\n ])\n req.signal?.removeEventListener('abort', stop)\n return {\n exitCode,\n durationMs: Date.now() - started,\n stdout,\n stderr,\n violations: [],\n where: 'shell',\n }\n },\n }\n return definePlugin(import.meta, { executor: () => executor })\n}\n",
|
|
24
|
+
test: "// A test for the example `executor` plugin beside it: a throwaway workspace\n// declares the plugin and vx's own run() drives it. `bun test` runs it.\nimport { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { afterEach, beforeEach, expect, it } from 'bun:test'\nimport { run, type Logger } from '@vzn/vx'\n\nlet root: string\nbeforeEach(async () => {\n root = await mkdtemp(path.join(os.tmpdir(), 'vx-plugin-executor-'))\n await writeFile(path.join(root, 'package.json'), JSON.stringify({ name: 'ws', private: true }))\n await writeFile(path.join(root, 'pnpm-workspace.yaml'), 'packages:\\n - \"packages/*\"\\n')\n Bun.spawnSync({ cmd: ['git', 'init', '-q'], cwd: root })\n})\nafterEach(async () => {\n await rm(root, { recursive: true, force: true })\n})\n\n/** A package `name` whose `vx.config.mjs` is `config`. */\nasync function pkg(\n name: string,\n config: string,\n scripts?: Record<string, string>,\n): Promise<string> {\n const dir = path.join(root, 'packages', name)\n await mkdir(dir, { recursive: true })\n await writeFile(\n path.join(dir, 'package.json'),\n JSON.stringify({ name, version: '1.0.0', ...(scripts ? { scripts } : {}) }),\n )\n await writeFile(path.join(dir, 'vx.config.mjs'), config)\n return dir\n}\n\n/** A `vx.workspace.mjs` declaring `call`, e.g. `shellExecutor()`, from the plugin file beside this test. */\nasync function workspace(call: string): Promise<void> {\n const from = JSON.stringify(path.join(import.meta.dir, 'executor.ts'))\n await writeFile(\n path.join(root, 'vx.workspace.mjs'),\n `import { shellExecutor } from ${from}\\nexport default { plugins: [${call}] }\\n`,\n )\n}\n\nfunction log(order: string[] = []): Logger {\n return {\n status() {},\n taskStart(node) {\n order.push(`start ${node.id}`)\n },\n taskStdout() {},\n taskStderr() {},\n taskComplete(node) {\n order.push(`end ${node.id}`)\n },\n }\n}\n\nit('runs the command and says where; stops it on exec.timeout', async () => {\n const dir = await pkg(\n 'a',\n `export default { tasks: {\n hello: { exec: { command: 'echo hi > out.txt' } },\n slow: { exec: { command: 'exec sleep 30', timeout: 300 } },\n } }`,\n )\n await workspace('shellExecutor()')\n const ok = await run({ cwd: root, tasks: ['hello'], log: log(), handleSignals: false })\n expect(ok.outcomes.map((o) => [o.node.id, o.status, o.where])).toEqual([\n ['a#hello', 'success', 'shell'],\n ])\n expect(await Bun.file(path.join(dir, 'out.txt')).text()).toBe('hi\\n')\n const started = Date.now()\n const slow = await run({ cwd: root, tasks: ['slow'], log: log(), handleSignals: false })\n expect(slow.outcomes.map((o) => [o.node.id, o.status, o.timedOut === true])).toEqual([\n ['a#slow', 'failed', true],\n ])\n expect(Date.now() - started).toBeLessThan(10_000)\n}, 20_000)\n\nit(\"runs the command through this machine's sh, not a project's node_modules/.bin sh\", async () => {\n const dir = await pkg(\n 'a',\n `export default { tasks: { hello: { exec: { command: 'echo real' } } } }`,\n )\n await mkdir(path.join(dir, 'node_modules', '.bin'), { recursive: true })\n await writeFile(path.join(dir, 'node_modules', '.bin', 'sh'), '#!/bin/sh\\necho planted\\n', {\n mode: 0o755,\n })\n await workspace('shellExecutor()')\n const out: string[] = []\n const r = await run({\n cwd: root,\n tasks: ['hello'],\n log: { ...log(), taskStdout: (_n, chunk) => void out.push(chunk) },\n handleSignals: false,\n })\n expect([r.outcomes.map((o) => o.status), out.join('')]).toEqual([['success'], 'real\\n'])\n}, 20_000)\n",
|
|
25
25
|
},
|
|
26
26
|
graph: {
|
|
27
27
|
plugin:
|
package/src/config.ts
CHANGED
|
@@ -261,8 +261,10 @@ export interface SandboxGrants {
|
|
|
261
261
|
*/
|
|
262
262
|
write?: string[]
|
|
263
263
|
/**
|
|
264
|
-
* Reachable domains
|
|
265
|
-
*
|
|
264
|
+
* Reachable domains, `['*.example.com']` a pattern. The proxy is the
|
|
265
|
+
* run's: it filters every sandboxed task against the union of the lists
|
|
266
|
+
* the run's tasks declare, so `true` adds no domain and reaches only
|
|
267
|
+
* those (none in a run with no list). Omitted adds none either.
|
|
266
268
|
*/
|
|
267
269
|
network?: true | string[]
|
|
268
270
|
/**
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// This module adds nothing implicit on top — no /tmp, no project dir —
|
|
10
10
|
// so what a task may touch is its vx.config.ts plus those baselines.
|
|
11
11
|
//
|
|
12
|
-
// Network is
|
|
13
|
-
//
|
|
12
|
+
// Network is a domain list, and the run's: SRT's one proxy filters every
|
|
13
|
+
// sandboxed task against the union of the lists the run's tasks declare.
|
|
14
|
+
// With no list, no domain is reachable; `sandbox.network: true` adds none.
|
|
14
15
|
//
|
|
15
16
|
// Platform reality:
|
|
16
17
|
// macOS — `SandboxViolationStore` is populated from the system log
|
|
@@ -520,9 +521,8 @@ function hostHasIpv6(): boolean {
|
|
|
520
521
|
* servers + (on macOS) the violation log monitor. Safe to call repeatedly
|
|
521
522
|
* — SRT itself returns early on the second call.
|
|
522
523
|
*
|
|
523
|
-
* The base config sets network to "block everything" (empty allowedDomains)
|
|
524
|
-
*
|
|
525
|
-
* tasks with `sandbox.network: true`.
|
|
524
|
+
* The base config sets network to "block everything" (empty allowedDomains)
|
|
525
|
+
* unless `allowedDomains` names some; `sandbox.network: true` adds none.
|
|
526
526
|
*
|
|
527
527
|
* @param opts.allowedDomains every domain any sandboxed task in this run
|
|
528
528
|
* declared. SRT's filtering proxy is per-RUN and reads its allowlist from
|
|
@@ -176,8 +176,9 @@ export interface RunOptions {
|
|
|
176
176
|
*/
|
|
177
177
|
signal?: AbortSignal
|
|
178
178
|
/**
|
|
179
|
-
* Hand the requested persistent tasks still running
|
|
180
|
-
*
|
|
179
|
+
* Hand the requested persistent tasks still running, and the persistent
|
|
180
|
+
* tasks they depend on (C-46), back on `RunSummary.persistent` instead
|
|
181
|
+
* of stopping them when the graph ends.
|
|
181
182
|
* The caller owns them from then on and stops them with its `stop()`.
|
|
182
183
|
* The watch loop sets this: a dev server stays up while watch idles and
|
|
183
184
|
* is replaced only when the next cycle starts.
|
package/src/orchestrator/plan.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// No TASK execution happens: no task spawn, no cleanOutputs, no
|
|
6
6
|
// restoreOutputs. Cache probing uses the byte-free `cache.has()`
|
|
7
|
-
// existence probe (a remote layer answers
|
|
7
|
+
// existence probe (a remote layer answers it its own way — no
|
|
8
8
|
// artifact download, no local ingest), so the plan is read-only — with
|
|
9
9
|
// one deliberate exception: `cache.inputs.runtime` / `workspaceRuntime`
|
|
10
10
|
// probe commands DO run, because predicting a task's key requires
|
|
@@ -28,12 +28,10 @@ import type { TelemetryContext, TelemetrySink } from './telemetry.js'
|
|
|
28
28
|
* executed. Registered explicitly in vx.workspace.ts via
|
|
29
29
|
* defineWorkspace({ plugins: [...] }). No auto-discovery.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* (src/plugins/), declared by the workspace — there is no fallback
|
|
36
|
-
* outside the list.
|
|
31
|
+
* Made by `definePlugin(import.meta, hooks)` only: the loader refuses a
|
|
32
|
+
* plain object. The capabilities are consulted by `plugin-host.ts`. Core
|
|
33
|
+
* names no plugin; the local executor and the local cache are the floor
|
|
34
|
+
* under the list, taking what every plugin declines.
|
|
37
35
|
*/
|
|
38
36
|
export interface VxPlugin {
|
|
39
37
|
/**
|
|
@@ -47,7 +45,7 @@ export interface VxPlugin {
|
|
|
47
45
|
// --- PIPELINE stages (shape the run before it executes — opt-in) ----------
|
|
48
46
|
//
|
|
49
47
|
// Each receives the object core is about to use and edits it IN PLACE;
|
|
50
|
-
// core re-validates after
|
|
48
|
+
// core re-validates after each plugin, so a plugin cannot produce what
|
|
51
49
|
// the loader would refuse from a user. Declaration order is the order.
|
|
52
50
|
// Whatever a stage changes reaches the cache key by construction: the key
|
|
53
51
|
// hashes the task config AFTER `project` ran. See
|
|
@@ -81,9 +81,10 @@ export function sandboxRunUnion(nodes: Iterable<TaskNode>): SandboxRunUnion | nu
|
|
|
81
81
|
for (const n of sandboxed) {
|
|
82
82
|
const allow = n.config.exec?.sandbox?.allow
|
|
83
83
|
const net = allow?.network
|
|
84
|
-
// `network: true`
|
|
85
|
-
// proxy rather than going through it, so folding it in as `*` would
|
|
84
|
+
// `network: true` adds nothing to the union: folded in as `*` it would
|
|
86
85
|
// widen the allowlist every OTHER task in the run is filtered against.
|
|
86
|
+
// It does not skip the proxy either, so it reaches only the domains
|
|
87
|
+
// some task of the run lists (schema.md § network is per-RUN).
|
|
87
88
|
if (Array.isArray(net)) for (const d of net) domains.add(d)
|
|
88
89
|
for (const d of n.config.exec?.sandbox?.deny?.network ?? []) denied.add(d)
|
|
89
90
|
const sockets = allow?.unixSockets
|
package/src/util/task-id.ts
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* about the identity of the same task: a lookup either found nothing or, worse,
|
|
11
11
|
* answered with a different task's history.
|
|
12
12
|
*
|
|
13
|
-
* A
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
13
|
+
* A config's task name may not hold `#` (`taskNameProblem`), but an id also
|
|
14
|
+
* arrives from run history and plugins, so the first-`#` rule is the one
|
|
15
|
+
* the graph uses. The cache's run history read the same rule from a
|
|
16
|
+
* private copy until item 646; one rule, one place.
|
|
16
17
|
*/
|
|
17
18
|
export function splitTaskId(id: string): [project: string, task: string] {
|
|
18
19
|
const i = id.indexOf('#')
|
|
@@ -594,10 +594,15 @@ export async function defaultAffectedBase(workspaceRoot: string): Promise<string
|
|
|
594
594
|
'ignore',
|
|
595
595
|
)
|
|
596
596
|
const out = new TextDecoder().decode(probe.stdout).trim()
|
|
597
|
-
|
|
597
|
+
// An origin/HEAD naming a branch the remote deleted (a pruned fetch keeps
|
|
598
|
+
// the symref) is no base either: it failed as `git ref "origin/master" did
|
|
599
|
+
// not resolve`, a ref nobody typed, where an unset one falls back.
|
|
600
|
+
if (probe.exitCode === 0 && out.length > 0 && revParse(workspaceRoot, out) !== undefined) {
|
|
601
|
+
return out
|
|
602
|
+
}
|
|
598
603
|
if (revParse(workspaceRoot, 'HEAD~1') === undefined) {
|
|
599
604
|
throw new UserError(
|
|
600
|
-
'--affected has no base here: origin/HEAD is not set and HEAD has no parent to compare ' +
|
|
605
|
+
'--affected has no base here: origin/HEAD is not set (or names a branch that is gone) and HEAD has no parent to compare ' +
|
|
601
606
|
'with — a shallow clone? Fetch history (actions/checkout: fetch-depth: 0) or name the ' +
|
|
602
607
|
'base: --affected=origin/main',
|
|
603
608
|
)
|
|
@@ -993,8 +993,10 @@ function assertFilterNamesDeclaredDeps(
|
|
|
993
993
|
if (raw.startsWith('!')) continue
|
|
994
994
|
const f = specForm(raw)
|
|
995
995
|
if (f.task.length === 0 || f.task.includes('*') || named(f)) continue
|
|
996
|
+
// `where` opens with the config's path; the message names it once.
|
|
997
|
+
const task = where.slice(where.lastIndexOf(': tasks.') + 2)
|
|
996
998
|
throw new UserError(
|
|
997
|
-
`${where}.cache.inputs.tasks: "${raw}" names no task in ${
|
|
999
|
+
`${where}.cache.inputs.tasks: "${raw}" names no task in ${task}.dependsOn ` +
|
|
998
1000
|
`(${declared.length === 0 ? 'none declared' : dependsOn!.map((d) => `'${d}'`).join(', ')}) — ` +
|
|
999
1001
|
`it would match nothing and fold no upstream hash, decoupling the task from its ` +
|
|
1000
1002
|
`dependencies. \`name\` is this project's task, \`^name\` its dependencies', ` +
|