@vzn/vx 0.0.135 → 0.0.137
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 +88 -108
- package/launcher.mjs +1 -1
- package/package.json +5 -5
- package/src/bin.ts +1 -1
- package/src/orchestrator/hit-restore.ts +12 -2
- package/src/orchestrator/run-artifacts.ts +40 -3
- package/src/orchestrator/run-report.ts +8 -1
- package/src/workspace/migration.ts +31 -1
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<h1 align="center">vx</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>A
|
|
9
|
-
|
|
8
|
+
<strong>A faster runner for your Turborepo or Nx repo.</strong><br>
|
|
9
|
+
Keep <code>turbo.json</code> or <code>nx.json</code> and run it unchanged.
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -16,29 +16,52 @@
|
|
|
16
16
|
<a href="https://vznjs.github.io/vx/"><img src="https://img.shields.io/badge/docs-vznjs.github.io%2Fvx-c6f84e" alt="Documentation"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
<picture>
|
|
21
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://vznjs.github.io/vx/bench-dark.svg">
|
|
22
|
-
<img src="https://vznjs.github.io/vx/bench-light.svg" width="760" alt="Bar chart: vx against Turborepo and Nx on a 3,270-task graph, cold and fully cached.">
|
|
23
|
-
</picture>
|
|
24
|
-
</p>
|
|
19
|
+
<!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
| 1,090 packages, 3,270 tasks | vx | Turborepo | Nx |
|
|
22
|
+
| -------------------------------- | ---------- | ---------- | -------- |
|
|
23
|
+
| Cold build: time the runner adds | **8 s** | 1 min 35 s | 31 min |
|
|
24
|
+
| Cold build: CPU burned | **35 s** | 1 min 13 s | 114 min |
|
|
25
|
+
| Fully cached run | **510 ms** | 760 ms | 3.59 s |
|
|
26
|
+
| Overhead per package | **8 ms** | 88 ms | 1,712 ms |
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
Time added is the wall time over the tasks' own ideal schedule (3 min 38 s).
|
|
29
|
+
Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
|
|
31
30
|
|
|
32
|
-
The runner adds seconds to a cold build where others add minutes. On a
|
|
33
|
-
1,090-package graph of 3,270 tasks whose ideal schedule is 3m 38s,
|
|
34
|
-
vx finishes in 3m 46s (+0:08), Turborepo in 5m 13s (+1:35) and Nx in
|
|
35
|
-
34m 44s (+31:06) — 8 ms of overhead per package against 88 ms and
|
|
36
|
-
1,712 ms, so the graph can grow and the runner stays in seconds. The cold build
|
|
37
|
-
burns 35 s of CPU in vx, 73 s in Turborepo and 114 minutes in Nx; a
|
|
38
|
-
fully cached run replays the graph in 510 ms.
|
|
39
31
|
<!-- bench:end -->
|
|
40
32
|
|
|
41
|
-
|
|
33
|
+
## Try it on your repo
|
|
34
|
+
|
|
35
|
+
In a Turborepo or Nx workspace, add one file:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// vx.workspace.ts
|
|
39
|
+
import { defineWorkspace } from '@vzn/vx'
|
|
40
|
+
import { turbo } from '@vzn/vx-migrate' // Nx: import { nx } and use nx()
|
|
41
|
+
|
|
42
|
+
export default defineWorkspace({ plugins: [turbo()] })
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npm install -D @vzn/vx @vzn/vx-migrate
|
|
49
|
+
npx vx run build --all # your turbo.json or Nx targets, run by vx
|
|
50
|
+
npx vx run build --all # again: up-to-date in milliseconds
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Nothing is rewritten. `@vzn/vx-migrate` is not on npm yet (first publish
|
|
54
|
+
pending); [`examples/turbo`](examples/turbo) runs this setup on every
|
|
55
|
+
commit.
|
|
56
|
+
|
|
57
|
+
## Why it is faster
|
|
58
|
+
|
|
59
|
+
- **Nothing boots per task.** No daemon, no Node process per task: vx
|
|
60
|
+
spawns your command directly.
|
|
61
|
+
- **Hashing without reading.** On a clean tree, cache keys come from
|
|
62
|
+
git's index; no file is read.
|
|
63
|
+
- **One pass over the repo.** One git walk per run; each stat and
|
|
64
|
+
lookup happens once.
|
|
42
65
|
|
|
43
66
|
## Install
|
|
44
67
|
|
|
@@ -93,48 +116,20 @@ runs on every commit. More: [Quickstart](https://vznjs.github.io/vx/quickstart/)
|
|
|
93
116
|
[CLI](packages/vx/docs/cli.md) · [every config field](packages/vx/docs/schema.md) ·
|
|
94
117
|
[playground](https://vznjs.github.io/vx/playground/) (the planner, in your browser).
|
|
95
118
|
|
|
96
|
-
##
|
|
97
|
-
|
|
98
|
-
Keep your `turbo.json`, Nx project graph or `.moon/`. Two lines run it
|
|
99
|
-
under vx, with nothing rewritten:
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
// vx.workspace.ts
|
|
103
|
-
import { defineWorkspace } from '@vzn/vx'
|
|
104
|
-
import { turbo } from '@vzn/vx-migrate' // or: nx, moon
|
|
119
|
+
## Configs of your own
|
|
105
120
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```sh
|
|
112
|
-
bunx @vzn/vx-migrate # vx.config.ts per package, from turbo.json, an Nx graph or .moon/
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
The written configs derive the same cache keys, so what `turbo()` cached
|
|
116
|
-
still hits: [`examples/turbo`](examples/turbo) runs both steps on every
|
|
117
|
-
commit.
|
|
118
|
-
|
|
119
|
-
`turboCache()` and `nxCache()` keep the remote cache you have (Turbo's
|
|
120
|
-
`/v8/artifacts` API, Vercel's included, or Nx's `/v1/cache`). What each
|
|
121
|
-
Turbo and Nx feature is in vx, pinned by a test:
|
|
122
|
-
[parity map](packages/vx/docs/parity.md) ·
|
|
121
|
+
When you want them, `bunx @vzn/vx-migrate` writes a `vx.config.ts` per
|
|
122
|
+
package from `turbo.json` or the Nx graph. They derive the same cache
|
|
123
|
+
keys, so what `turbo()` cached still hits. `turboCache()` and `nxCache()`
|
|
124
|
+
keep your Turbo or Nx remote cache. [Parity map](packages/vx/docs/parity.md) ·
|
|
123
125
|
[migration guide](https://vznjs.github.io/vx/guides/migrate/).
|
|
124
126
|
|
|
125
|
-
|
|
126
|
-
> npm yet; only `@vzn/vx` is.
|
|
127
|
-
|
|
128
|
-
## Why vx
|
|
127
|
+
## More than speed
|
|
129
128
|
|
|
130
129
|
- **A cache you can trust.** Configs are TypeScript, evaluated before
|
|
131
130
|
hashing, so imports and computed values are part of the key. Declared
|
|
132
131
|
outputs are wiped before every run and every restore: no stale file
|
|
133
|
-
survives.
|
|
134
|
-
read.
|
|
135
|
-
- **Fast where it counts.** Milliseconds of overhead per package, no
|
|
136
|
-
daemon, one git walk per run. Every optimization is recorded with
|
|
137
|
-
the invariant that keeps it valid ([ledger](packages/vx/docs/optimizations.md)).
|
|
132
|
+
survives.
|
|
138
133
|
- **Sandboxed tasks.** Opt in per task: a read the task did not
|
|
139
134
|
declare fails the build instead of poisoning the cache.
|
|
140
135
|
- **Clean exits.** Ctrl-C reaps every child. No orphaned dev servers.
|
|
@@ -145,27 +140,23 @@ Turbo and Nx feature is in vx, pinned by a test:
|
|
|
145
140
|
|
|
146
141
|
## Tried on real repos
|
|
147
142
|
|
|
148
|
-
|
|
149
|
-
and nothing rewritten, against the tool it ships with.
|
|
150
|
-
tasks per row, medians; vx first, theirs second.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
| [
|
|
156
|
-
| [
|
|
157
|
-
| [
|
|
158
|
-
| [
|
|
159
|
-
| [
|
|
160
|
-
| [
|
|
161
|
-
| [
|
|
162
|
-
| [
|
|
163
|
-
| [
|
|
164
|
-
| [
|
|
165
|
-
| [refinedev/refine](https://github.com/refinedev/refine) | Nx | 96.2 s / 105.9 s | 1.06 s / 1.19 s | 303 ms / 1.16 s |
|
|
166
|
-
| [kindspells/astro-shield](https://github.com/kindspells/astro-shield) | moon | 2.64 s / 3.78 s | 125 ms / 2.46 s | 111 ms / 2.37 s |
|
|
167
|
-
| [FormidableLabs/spectacle](https://github.com/FormidableLabs/spectacle) | wireit | 21.5 s / 22.2 s | 394 ms / 574 ms | 387 ms / 593 ms |
|
|
168
|
-
| [microsoft/lage](https://github.com/microsoft/lage) | lage | 32.4 s / **29.4 s** | 730 ms / 1.70 s | 284 ms / 1.56 s |
|
|
143
|
+
Eleven public monorepos, each run with its own `turbo.json` or Nx graph
|
|
144
|
+
and nothing rewritten, against the tool it ships with. Same machine and
|
|
145
|
+
tasks per row, medians; vx first, theirs second.
|
|
146
|
+
|
|
147
|
+
| Repo | Against | Cold build | Restore outputs | Nothing to do |
|
|
148
|
+
| ----------------------------------------------------------- | ------- | --------------------- | --------------- | ------------------- |
|
|
149
|
+
| [solidjs/solid](https://github.com/solidjs/solid) | Turbo | 40.6 s / 45.5 s | 66 ms / 127 ms | 51 ms / 95 ms |
|
|
150
|
+
| [withastro/astro](https://github.com/withastro/astro) | Turbo | 48.0 s / 57.9 s | 478 ms / 1.43 s | 249 ms / 1.38 s |
|
|
151
|
+
| [payloadcms/payload](https://github.com/payloadcms/payload) | Turbo | 126.9 s / 127.5 s | 3.44 s / 3.46 s | 256 ms / **237 ms** |
|
|
152
|
+
| [medusajs/medusa](https://github.com/medusajs/medusa) | Turbo | 308 s / 315 s | 3.86 s / 7.16 s | 947 ms / 3.29 s |
|
|
153
|
+
| [n8n-io/n8n](https://github.com/n8n-io/n8n) | Turbo | 137.2 s / **133.7 s** | 8.27 s / 12.5 s | 842 ms / 1.36 s |
|
|
154
|
+
| [calcom/cal.com](https://github.com/calcom/cal.com) | Turbo | 245.7 s / 250.7 s | 17.4 s / 19.9 s | 14.7 s / 18.5 s |
|
|
155
|
+
| [TanStack/query](https://github.com/TanStack/query) | Nx | 47.4 s / 55.1 s | 656 ms / 1.98 s | 174 ms / 1.88 s |
|
|
156
|
+
| [strapi/strapi](https://github.com/strapi/strapi) | Nx | 238.2 s / 245.7 s | 1.61 s / 3.94 s | 341 ms / 4.02 s |
|
|
157
|
+
| [novuhq/novu](https://github.com/novuhq/novu) | Nx | 291.2 s / 299.4 s | 3.10 s / 9.05 s | 655 ms / 8.55 s |
|
|
158
|
+
| [TanStack/router](https://github.com/TanStack/router) | Nx | 199.9 s / 226.7 s | 1.00 s / 3.33 s | 505 ms / 3.21 s |
|
|
159
|
+
| [refinedev/refine](https://github.com/refinedev/refine) | Nx | 96.2 s / 105.9 s | 1.06 s / 1.19 s | 303 ms / 1.16 s |
|
|
169
160
|
|
|
170
161
|
**Bold** marks the other tool winning. Versions, revisions, scopes and
|
|
171
162
|
harness per repo: [benchmarks](packages/vx/docs/benchmarks.md) (astro
|
|
@@ -173,46 +164,35 @@ and refine as re-measured on 2026-09-27, the rest 2026-09-10 and -11).
|
|
|
173
164
|
|
|
174
165
|
## How it compares
|
|
175
166
|
|
|
176
|
-
| | vx | Turborepo | Nx |
|
|
177
|
-
| ------------------------- | ----------------------------------------- | ------------------------------ | ---------------- |
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
|
|
|
181
|
-
|
|
|
182
|
-
|
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
¹ Warm, nothing to rebuild, the graph charted above. Every gap, and
|
|
189
|
-
when another tool is the better pick:
|
|
167
|
+
| | vx | Turborepo | Nx |
|
|
168
|
+
| ------------------------- | ----------------------------------------- | ------------------------------ | ---------------- |
|
|
169
|
+
| Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
|
|
170
|
+
| Output ownership | **Strict**: wiped before exec and restore | Additive (stale files survive) | Additive |
|
|
171
|
+
| Clean-tree hashing | **Zero reads** (git index OIDs) | git OIDs | re-hash / daemon |
|
|
172
|
+
| Daemon required for speed | **No** | Optional | Yes |
|
|
173
|
+
| Per-task sandbox | **Yes**: kernel-level, opt-in | No | No |
|
|
174
|
+
| Plugin API | **Yes**: every pipeline stage | No | Yes (TS-tied) |
|
|
175
|
+
| OTel CI/CD spans | **Yes**: `otel()` plugin, no OTel SDK | No | Paid |
|
|
176
|
+
| Install | **Single binary**; no Bun, Node optional | npm + Node | npm + Node |
|
|
177
|
+
|
|
178
|
+
Every gap, and when another tool is the better pick:
|
|
190
179
|
[comparison](packages/vx/docs/comparison.md) ·
|
|
191
180
|
[vx, Turborepo, Nx, Bazel](https://vznjs.github.io/vx/compare/).
|
|
192
181
|
|
|
193
|
-
² From moon's own docs (2026-09-28): [config](https://github.com/moonrepo/moon/blob/master/website/docs/config/overview.mdx),
|
|
194
|
-
[outputs](https://github.com/moonrepo/moon/blob/master/website/docs/config/project.mdx),
|
|
195
|
-
[hashing](https://github.com/moonrepo/moon/blob/master/website/docs/guides/debug-task.mdx),
|
|
196
|
-
[daemon](https://github.com/moonrepo/moon/blob/master/website/docs/guides/daemon.mdx),
|
|
197
|
-
[plugins](https://github.com/moonrepo/moon/blob/master/website/docs/guides/wasm-plugins.mdx),
|
|
198
|
-
[install](https://github.com/moonrepo/moon/blob/master/website/docs/install.mdx);
|
|
199
|
-
they describe no per-task sandbox and no OpenTelemetry export. Already on
|
|
200
|
-
moon: `moon()` runs it unchanged.
|
|
201
|
-
|
|
202
182
|
## Plugins
|
|
203
183
|
|
|
204
184
|
Declared in `vx.workspace.ts`. Without one, vx runs and caches on this
|
|
205
185
|
machine.
|
|
206
186
|
|
|
207
|
-
| Package | What it adds
|
|
208
|
-
| ---------------------------------------------------------- |
|
|
209
|
-
| [`@vzn/vx-migrate`](packages/vx-migrate) | Run a Turbo
|
|
210
|
-
| [`@vzn/vx-reapi`](packages/vx-reapi) | Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy)
|
|
211
|
-
| [`@vzn/vx-lockfile`](packages/vx-lockfile) | Key each project on its own lockfile slice: pnpm, bun, npm, yarn
|
|
212
|
-
| [`@vzn/vx-otel`](packages/vx-otel) | OpenTelemetry traces, metrics and logs, no SDK
|
|
213
|
-
| [`@vzn/vx-github`](packages/vx-github) | GitHub Actions job summary and Checks API
|
|
214
|
-
| [`@vzn/vx-mcp`](packages/vx-mcp) | `vx mcp`: a read-only MCP server for AI coding agents
|
|
215
|
-
| [`@vzn/vx-schedule-history`](packages/vx-schedule-history) | Schedule by the critical path learned from past runs
|
|
187
|
+
| Package | What it adds |
|
|
188
|
+
| ---------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
189
|
+
| [`@vzn/vx-migrate`](packages/vx-migrate) | Run a Turbo or Nx repo unchanged; write configs; keep its remote cache |
|
|
190
|
+
| [`@vzn/vx-reapi`](packages/vx-reapi) | Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy) |
|
|
191
|
+
| [`@vzn/vx-lockfile`](packages/vx-lockfile) | Key each project on its own lockfile slice: pnpm, bun, npm, yarn |
|
|
192
|
+
| [`@vzn/vx-otel`](packages/vx-otel) | OpenTelemetry traces, metrics and logs, no SDK |
|
|
193
|
+
| [`@vzn/vx-github`](packages/vx-github) | GitHub Actions job summary and Checks API |
|
|
194
|
+
| [`@vzn/vx-mcp`](packages/vx-mcp) | `vx mcp`: a read-only MCP server for AI coding agents |
|
|
195
|
+
| [`@vzn/vx-schedule-history`](packages/vx-schedule-history) | Schedule by the critical path learned from past runs |
|
|
216
196
|
|
|
217
197
|
Write your own: [plugin guide](https://vznjs.github.io/vx/guides/plugins/) ·
|
|
218
198
|
[architecture](packages/vx/docs/architecture.md).
|
package/launcher.mjs
CHANGED
|
@@ -105,7 +105,7 @@ const source = join(here, sourceEntry)
|
|
|
105
105
|
if (bin !== undefined) {
|
|
106
106
|
run(bin, args)
|
|
107
107
|
} else if (existsSync(source) && hasBun()) {
|
|
108
|
-
run('bun', [source, ...args])
|
|
108
|
+
run('bun', ['--no-env-file', '--no-install', source, ...args])
|
|
109
109
|
} else {
|
|
110
110
|
const supported = SUPPORTED.join(', ')
|
|
111
111
|
process.stderr.write(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.137",
|
|
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.137",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.137",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.137",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.137"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/bin.ts
CHANGED
|
@@ -20,7 +20,14 @@ import {
|
|
|
20
20
|
WORKSPACE_OUTPUT_PREFIX,
|
|
21
21
|
} from '../cache/index.js'
|
|
22
22
|
import type { TaskOutcome } from '../graph/index.js'
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
asTrees,
|
|
25
|
+
secretMask,
|
|
26
|
+
span,
|
|
27
|
+
splitNegations,
|
|
28
|
+
taskGlob,
|
|
29
|
+
wholeSubtreePrefixes,
|
|
30
|
+
} from '../util/index.js'
|
|
24
31
|
import type { ExecuteArgs } from './execute-task.js'
|
|
25
32
|
import { entryHolds } from './miss-save.js'
|
|
26
33
|
|
|
@@ -51,7 +58,10 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
51
58
|
const outputs = cacheCfg?.outputs.files ?? []
|
|
52
59
|
const wsOutputs = cacheCfg?.outputs.workspaceFiles ?? []
|
|
53
60
|
const anyOutputs = outputs.length > 0 || wsOutputs.length > 0
|
|
54
|
-
|
|
61
|
+
// The directories the positive globs cover whole. A `!` entry inside one
|
|
62
|
+
// leaves the snapshot sound: a file added or removed there still bumps a
|
|
63
|
+
// recorded directory, and the walk that follows applies the `!` (A-46).
|
|
64
|
+
const dirPrefixes = wholeSubtreePrefixes(splitNegations(outputs).positive)
|
|
55
65
|
const cleanArgs = {
|
|
56
66
|
projectDir: node.projectDir,
|
|
57
67
|
outputs,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import path from 'node:path'
|
|
17
17
|
import { isGroupTask, type TaskOutcome } from '../graph/index.js'
|
|
18
|
-
import { tallyOutcomes } from './tally.js'
|
|
18
|
+
import { tallyOutcomes, type Tally } from './tally.js'
|
|
19
19
|
import type { FlakyFinding } from './failure-mode.js'
|
|
20
20
|
|
|
21
21
|
export interface SummarizeArgs {
|
|
@@ -36,8 +36,45 @@ export interface SummarizeArgs {
|
|
|
36
36
|
flaky?: readonly FlakyFinding[]
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** One task of `--summarize`: the wire, stated by `schemas/summary.json`. */
|
|
40
|
+
export interface SummaryTaskJson {
|
|
41
|
+
id: string
|
|
42
|
+
project: string
|
|
43
|
+
task: string
|
|
44
|
+
status: TaskOutcome['status']
|
|
45
|
+
exitCode: number
|
|
46
|
+
durationMs: number
|
|
47
|
+
hash: string | null
|
|
48
|
+
noCache?: true
|
|
49
|
+
flaky?: { passes: number; failures: number; attempts: number }
|
|
50
|
+
cpuMs?: number
|
|
51
|
+
peakRssBytes?: number
|
|
52
|
+
storedCpuMs?: number
|
|
53
|
+
storedPeakRssBytes?: number
|
|
54
|
+
admissionHeldMs?: number
|
|
55
|
+
blockedBy?: string
|
|
56
|
+
timedOut?: true
|
|
57
|
+
sandboxViolations?: number
|
|
58
|
+
notReady?: 'timeout' | 'exited' | 'spawn'
|
|
59
|
+
wallclockStartNs?: string
|
|
60
|
+
wallclockEndNs?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The `--summarize` document. */
|
|
64
|
+
export interface RunSummaryJson {
|
|
65
|
+
runId: string
|
|
66
|
+
ok: boolean
|
|
67
|
+
exitCode: number
|
|
68
|
+
startedAt: string
|
|
69
|
+
endedAt: string
|
|
70
|
+
totalMs: number
|
|
71
|
+
tasks: SummaryTaskJson[]
|
|
72
|
+
aborted: SummaryTaskJson[]
|
|
73
|
+
summary: Tally
|
|
74
|
+
}
|
|
75
|
+
|
|
39
76
|
/** One task's entry. Shared by `tasks` and `aborted` so they read alike. */
|
|
40
|
-
function taskEntry(o: TaskOutcome, flaky?: FlakyFinding):
|
|
77
|
+
function taskEntry(o: TaskOutcome, flaky?: FlakyFinding): SummaryTaskJson {
|
|
41
78
|
return {
|
|
42
79
|
id: o.node.id,
|
|
43
80
|
project: o.node.projectName,
|
|
@@ -99,7 +136,7 @@ export async function writeRunSummary(args: SummarizeArgs): Promise<string> {
|
|
|
99
136
|
const aborted = args.outcomes.filter((o) => !isGroupTask(o.node) && o.status === 'aborted')
|
|
100
137
|
const flakyById = new Map((args.flaky ?? []).map((f) => [f.taskId, f]))
|
|
101
138
|
const exitCode = args.exitCode ?? (args.ok ? 0 : 1)
|
|
102
|
-
const payload = {
|
|
139
|
+
const payload: RunSummaryJson = {
|
|
103
140
|
runId: args.runId,
|
|
104
141
|
// The run-level verdict, first: a consumer gating on this artifact must
|
|
105
142
|
// not have to re-derive it by re-implementing the bucket rules.
|
|
@@ -113,9 +113,16 @@ function fmtDuration(ms: number): string {
|
|
|
113
113
|
* unvalidated names and shipped WITHOUT this escape, so a `|` in a task name
|
|
114
114
|
* or an output path shifted its columns. One definition, so the two
|
|
115
115
|
* cannot disagree about what a cell may contain.
|
|
116
|
+
*
|
|
117
|
+
* A pipe splits the row unless an ODD run of backslashes precedes it (GFM
|
|
118
|
+
* reads `\\` as one escaped backslash), so a pipe after an odd run is already
|
|
119
|
+
* escaped and one more backslash would free it: `a\|b` became `a\\|b`, two
|
|
120
|
+
* cells. A lone `\r` ends a line too.
|
|
116
121
|
*/
|
|
117
122
|
export function escapeMarkdownCell(value: string): string {
|
|
118
|
-
return value
|
|
123
|
+
return value
|
|
124
|
+
.replace(/(\\*)\|/g, (pipe, run: string) => (run.length % 2 === 0 ? `${run}\\|` : pipe))
|
|
125
|
+
.replace(/\r\n|\r|\n/g, ' ')
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
const cell = escapeMarkdownCell
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// tool wrote it. Core knows no source format here: a mapper returns a
|
|
6
6
|
// `MigrationPlan` and this file does the rest.
|
|
7
7
|
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
8
9
|
import { unlink } from 'node:fs/promises'
|
|
9
10
|
import path from 'node:path'
|
|
10
11
|
import { relPosix, UserError } from '../util/index.js'
|
|
@@ -304,11 +305,40 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
304
305
|
if (!cached && todos.has(CACHE_TODO)) {
|
|
305
306
|
report.push('', 'no task caches yet: add the cache block a TODO shows, and a second run hits')
|
|
306
307
|
}
|
|
307
|
-
|
|
308
|
+
const installed = existsSync(path.join(root, 'node_modules', '@vzn', 'vx', 'package.json'))
|
|
309
|
+
report.push(
|
|
310
|
+
'',
|
|
311
|
+
`next: ${vxInvocation(process.env['npm_config_user_agent'], installed)} run ${firstTask} --all`,
|
|
312
|
+
)
|
|
308
313
|
process.stdout.write(`${report.join('\n')}\n`)
|
|
309
314
|
return 0
|
|
310
315
|
}
|
|
311
316
|
|
|
317
|
+
/**
|
|
318
|
+
* How the user runs vx, for the report's `next:` line. After `npx vx init`
|
|
319
|
+
* or `bunx @vzn/vx init` a bare `vx` is on no PATH (the first-five-minutes
|
|
320
|
+
* walk, 2026-09-28), so the line names the runner that started this
|
|
321
|
+
* process — npx, pnpm, yarn and Bun each set `npm_config_user_agent` — and
|
|
322
|
+
* the spec it resolves: the installed bin, else the package, since a bare
|
|
323
|
+
* `bunx vx` or `npx vx` would fetch an unrelated package named `vx`. No
|
|
324
|
+
* runner (a global install, the compiled binary): `vx`.
|
|
325
|
+
*/
|
|
326
|
+
export function vxInvocation(userAgent: string | undefined, installed: boolean): string {
|
|
327
|
+
const runner = /^(npm|pnpm|yarn|bun)\//.exec(userAgent ?? '')?.[1]
|
|
328
|
+
switch (runner) {
|
|
329
|
+
case 'npm':
|
|
330
|
+
return installed ? 'npx vx' : 'npx @vzn/vx'
|
|
331
|
+
case 'bun':
|
|
332
|
+
return installed ? 'bunx vx' : 'bunx @vzn/vx'
|
|
333
|
+
case 'pnpm':
|
|
334
|
+
return installed ? 'pnpm vx' : 'pnpm dlx @vzn/vx'
|
|
335
|
+
case 'yarn':
|
|
336
|
+
return installed ? 'yarn vx' : 'yarn dlx @vzn/vx'
|
|
337
|
+
default:
|
|
338
|
+
return 'vx'
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
312
342
|
/** Ids named per shared TODO; the files carry each one (sveltejs/kit: 92 persistent tasks). */
|
|
313
343
|
const TODO_IDS_SHOWN = 5
|
|
314
344
|
|