Skip to content
vxvx
GitHubBlueskydev.toRSS
Releases

Runs show their work, --force uploads less

A live run now shows what vx itself is doing and what each idle worker waits on, the footer counts remote failures and uploads in flight, --force no longer re-uploads what the cache holds, vx lock refuses fewer configs that were fine, and vx run sheds nine flags.

vx 0.0.639: Runs show their work, --force uploads less

A live run now shows what vx itself is doing and what each idle worker waits on. The footer counts remote failures and uploads still in flight, and --force no longer re-uploads what the cache holds. vx lock refuses fewer configs that were fine, and vx run sheds nine flags.

Read the breaking changes before you update: most are flags that are gone; plugin authors also get a new put rule and API changes.

Enjoy the release!

In this release

A run spends time on its own work too: reading the lock, keying tasks, setting up plugins, uploading. On a terminal that time showed as a frozen screen before the first task and after the last. And once a worker finished its task, its row sat there as a dim idle, so you saw what was running but not what was queued behind it.

Now each of vx’s own steps is a row beside your tasks, named <owner>:<step>: vx:keys, vx:uploads, @vzn/vx-otel:setup. It shows its elapsed time while it runs, and a step under 150 ms is not shown at all. A step is not a task: it has no count, history row or forecast. An idle worker row now holds a task that is blocked, and names the running task holding it back:

Terminal window
2.42s running ~18s solid-js#types blocks 2
blocked solid-js#build by solid-js#types
blocked solid-js#test by solid-js#types

When the blocked task starts, it runs in the row that held it. Each half of a task id also has a colour of its own now, so every #build looks the same across projects and runs, and no id uses red, green, yellow or cyan, the colours of an outcome.

These rows show only on a terminal. CI logs and piped output are unchanged. The output also behaves better around the edges:

  • On a terminal under 62 columns, such as a side panel, the footer’s bars and the frames’ rules shrink to fit instead of wrapping.
  • With TERM=dumb (Emacs M-x shell, some CI log viewers) vx prints plain rows, as it does to a pipe.
  • Warnings go to stderr, so vx run build > build.log keeps only the rows and the footer.
  • After a Ctrl-C, the aborted rows list above the failure frames, which close the log.

A failed remote read became a miss without a word in the footer, and uploads still running at the end of a run were invisible. The two cache dots, ● for a fresh hit and ○ for no cache, were also easy to mix up.

Now a fresh hit is ✓ and a task with no cache is ∅. The remote part of the cache line adds ✗ for tasks whose remote read or write failed, and ⇡ for uploads still running. A run that ends with uploads in flight keeps its footer on screen until they finish. A task saved here and uploaded counts once, as a remote ↑:

Terminal window
$ CI=1 vx run build lint --all
cache ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
local 1∅ · remote 1↑
$ vx run build lint --all
cache ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
local 1✓ 1∅

vx last names a remote failure on the task’s row, such as remote ✗ download: connection refused; upload: 503. --dry uses the same marks, so a plan and its run read alike.

vx run --force re-ran every task and then uploaded every result, even when the remote already held that key. On CI that was one upload per task for bytes the cache already had.

Now --force re-runs every task and keeps what is stored under the same key, locally and remotely. Each remote is asked which keys it holds: once for the whole run when its plugin has hasMany, else has per key just before that upload. A held key is not sent again. No task waits for those answers, and a failed probe means vx uploads, as before.

The same rule now binds cache plugins. vx no longer asks has before a put, so a remote’s put of a key it already holds must keep the stored bytes. turboCache() and reapi() do; Nx servers refuse the overwrite themselves. A bad entry is removed at the server, or locally with vx cache clean.

A stopped run also stops waiting on uploads. After a Ctrl-C or a CI cancellation it starts no more uploads, does not wait on the one in flight, prints its footer and exits with the signal’s code.

vx lock checks that no task’s key reads another task’s outputs, and that no two cached tasks write the same path. Both checks refused configs that were safe.

A key reads only what git lists, and git never lists an ignored file. So an output git ignores whole is no longer an overlap. A lint reading **/* beside a build writing a gitignored **/dist/** now locks:

app/vx.config.ts
import { defineProject } from '@vzn/vx/config'
export default defineProject({
tasks: {
build: {
exec: { command: 'mkdir -p dist && cp src/a.txt dist/a.txt' },
inputs: { files: ['src/**'] }, cache: { outputs: { files: ['**/dist/**'] } },
},
lint: {
dependsOn: ['build'],
exec: { command: 'eslint .' },
inputs: { files: ['**/*'] }, cache: { outputs: { files: [] } },
},
},
})
Terminal window
$ cat .gitignore
dist/
$ vx lock
✓ vx lock 1 project config → .vx/projects.lock.json

vx asks git rather than reading a .gitignore, so a rule in any .gitignore or in .git/info/exclude counts. Without the rule, vx lock still refuses and names the !**/dist/** entry to add.

A ! entry in a task’s outputs now counts too. A build writing **/dist with !coverage/** no longer clashes with a task writing coverage/**, because the two never touch the same file. bunx @vzn/vx-migrate uses this to keep such a task cached where it used to leave it uncached.

Nine vx run flags are gone. Most repeated what another command already does; the rest set a value nobody needed to change. A removed flag refuses and says what to use instead:

Terminal window
$ vx run build --all --summarize
✗ vx run --summarize (turbo): use `vx last --format json` for the run's JSON record
see vx run --help
  • --summarize and --format json → vx last --format json after the run.
  • --graph → vx run <task> --dry=json, which lists every planned task with its deps.
  • --report and --report-file → github() from @vzn/vx-ci, which writes the GitHub job summary.
  • --tag → nothing; no command ever read it back.
  • The ready notice, the kill grace and the teardown bound → fixed at 10 s, 2 s and 3 s.
  • A global vx upgrade inside a project moves both the project’s @vzn/vx and the global vx to the same version.
  • vx show in a workspace with no projects says so and names vx init.
  • vx watch names the project an edit is in, and says root only for a file no project owns.
  • The footer counts as affected only the projects with a task to run.
  • vx run --parallel build reads build as a task, not a count.
  • A ^build with a lockfile plugin but no lockfile yet says to install.
  • A sandboxed task’s failure counts only real denials, and names the directory a refused write wanted with a grant that works.
  • otel() refuses an endpoint with no http:// or https:// once, instead of failing every export.
  • reapi() says when a server refuses its token, instead of calling it unreachable.
  • scheduleHistoryPlugin({ file }) creates the file’s directory.
  • Every run checks for gitignored outputs, as above. That check now runs while git lists the files, which wins back about half of what it cost.

The first run resets vx’s local index once: run history starts over, and cached outputs still hit. Then:

  • --summarize, --format json, --graph, --report, --report-file, --tag, --ready-notice, --kill-grace and --teardown-timeout → removed, with run.readyNotice, run.killGrace, run.teardownTimeout and their VX_RUN_* variables. A leftover key in .vx/workspace.ts is refused.
  • --force → keeps an entry already stored under the same key. A cache plugin’s put of a key it holds must keep the stored bytes.
  • reapi() with an IP address over TLS → refused at startup. Name the server by host name, or use grpc:// for plaintext.
  • Footer ● / ○ → ✓ / ∅; the local ↑ no longer counts a task that was also uploaded.
  • Plugin API: RunContext.cache.uploads → traffic; CacheLayer.save loses skipLocalWrite; RunOptions.tags, summarize and json are removed; telemetry schema → 4.
  • Run warnings → stderr; their text is unchanged.

The flag and plugin API changes are also on Upgrading.

Terminal window
npm install -D @vzn/vx@latest
npx @vzn/vx lock

A standalone binary updates itself with vx upgrade.

Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.