One command to start, branch caches the server enforces
vx init installs vx for you, a cache server keeps branches apart by the CI token instead of trusting the client, a slow download gives way to running the task, and a local hit copies its files out unchecked.
Starting with vx is now one command: vx init installs vx for you.
A cache server, not the client, now keeps a branch’s builds out of
main’s entries. A remote hit whose download would outlast the task
gives way and the task runs here, and a local hit copies its files out
without checking them first.
This release is breaking: cache.store.verify is gone, task keys reach
the remote plain, and vx init maps only Turbo and Nx repos.
Read the breaking changes first.
Starting meant installing @vzn/vx first, then running vx init.
Now vx init installs vx itself: globally, with the package manager
that started it, so a bare vx is on your PATH (skipped when that vx
is already this version), and as a root devDependency with the repo’s
own manager. Beside a turbo.json or nx.json it hands the repo to
@vzn/vx-migrate; anywhere else it writes .vx/workspace.ts and an
empty root vx.config.ts, then locks.
A pull request should read what main built and never write where main
reads. vx used to decide that on the client, with a second, scoped key
per task, so only the client kept a branch out of main’s entries.
Task keys now reach every remote plain. The CI OIDC token, checked
against the cache server’s trust policy, decides where a write lands:
main’s entries, the token’s own branch, or nowhere. turboCache() only
steers reads: every HEAD, GET and batch POST names the run’s refs,
its own branch first, in x-artifact-read-scopes. On CI the refs come
from its env; on a laptop from git (the current branch, then
origin/HEAD).
flowchart LR
PR[CI on feat-x] -- "reads feat-x, then main" --> S[cache server]
PR -- "writes, OIDC token: feat-x" --> S
S -- "files the write under" --> B[feat-x entries]
M[CI on main] -- "writes, OIDC token: main" --> S
S --> T[main entries]
For a server that ignores the header, such as Vercel’s,
turboCache({ scope: 'key' }) folds the ref into each key and asks for
every ref’s key in one batch; the first hit wins. How a server
implements its half:
Branch caches a server enforces.
A remote hit always waited for its whole body, even when running the
task would have finished first: a large artifact on a slow link held its
task longer than the task takes.
A remote that says an entry’s size and the task’s stored time (Turbo’s
API does, in its batch query and HEAD) now lets vx weigh each download
while a task waits on it. When the rest of the body would take longer
than twice the task’s stored time, room for the run and its save, vx
gives the download up and runs the task here. It uploads nothing: the
remote already holds the key. A remote that says neither is awaited, as
before. With 500 tasks 30 ms away at 4 MB/s per
connection, 20 of them 8 MB, a run went
from 3,070 ms to 1,807 ms (min of 5 runs, before and after the
change).
The run’s footer now says how many bytes each remote direction moved,
and --dry says what restoring the remote hits would download:
A local hit copies its stored files out without checking their size,
mtime or bytes first, and a save no longer reads its outputs back to
write a checksum. The local store is vx’s own, so the check caught
nothing. A remote hit is still checked against its archive’s checksum
when it is unpacked.
cache.store.verify and VX_CACHE_STORE_VERIFY are removed: delete
them from .vx/workspace.ts and the environment.
Scoped task keys are removed: task keys are plain on every remote.
The x-vx-cache-scope header (turboCache(), nxCache()) is
removed; turboCache() sends x-artifact-read-scopes on reads.
VX_CACHE_SCOPE, github({ cacheScope }) and gitlab({ cacheScope })
now scope only plugin entries (ctx.cache, run records).
vx init in a repo with only a package.json writes one empty root
project and no longer maps scripts: run bunx @vzn/vx-migrate for
that.
Plugin API: a remote cache layer’s methods take no RemoteKey, and
RemoteKey is removed. has resolves ArtifactInfo or null, not a
boolean; hasMany resolves a Map of the held keys to ArtifactInfo,
not a Set.
A local cache hit copies plain files instead of unpacking an archive, a remote hit is decompressed once straight into the store, remote downloads and uploads set their own pace, a cache server learns each request's scope, GitLab CI gets branch scopes, [...] in a task glob is a character class, and vx upgrade re-locks.
A remote cache scope is now the branch, named owner/repo:branch, so a laptop on a branch reads what CI built there. Each task asks the remote one key, and the local cache never splits by scope. --cache-scope is gone and run.affectedBase is now baseBranch.
Editing a file: or link: dependency, or patching one, now rebuilds what uses it. A hit brings back empty folders and symlinks as the build left them. A remote cache token lives in your keychain or comes from GitHub OIDC, warm runs are faster at scale, and Ctrl-C stops at once.
Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.