Skip to content

feat(run): add --dry-run to preview cache hits - #792

Closed
lifeiscontent wants to merge 9 commits into
voidzero-dev:mainfrom
lifeiscontent:feat/run-dry-run
Closed

lifeiscontent wants to merge 9 commits into
voidzero-dev:mainfrom
lifeiscontent:feat/run-dry-run

Conversation

@lifeiscontent

Copy link
Copy Markdown
Contributor

Motivation

Closes #791. There's no way to see which tasks a run would take from the cache without running them.

Changes

  • vp run --dry-run <task> plans the run as usual and prints, per task, a cache hit, a miss with the summary's reason, cache disabled, or unknown when something it runs after isn't a hit.
  • Nothing runs and nothing is written. The cache database is opened read-only, and the last run summary used by --last-details is left alone. The remote cache isn't queried.
  • Recording the execution key on a hit moved from try_hit_local into try_hit, so normal runs behave the same and the dry run can share the lookup.
  • The miss reason wording moved into a Display impl so the summary and the dry run use the same text.

`vp run --dry-run <task>` plans the run as usual and then reports, for each
task, whether the local cache would hit, why it would miss, or that caching
is disabled. A task that runs after something that isn't a hit is reported
as unknown, since that earlier task could change its inputs.

Nothing runs and nothing is written: the cache database is opened read-only
(or skipped if it doesn't exist), the local lookup no longer records the
execution key itself, and the last run summary is left alone. The remote
cache is not queried.
main added an uploads field to ExecutionCache, and the extra dry run branch
in main_inner put it over clippy's line limit, so the reporter builder for
the log mode moves into its own function.
Upstream voidzero-dev#821 changed the run summary to also show the count of successful tasks. The real runs in the dry_run fixture now print that count.
@jasonkuhrt

Copy link
Copy Markdown
Contributor

Thanks for this. We'd like to use --dry-run to plan CI (skip booting runners when a whole job would be replays), and it covers almost everything we need. Two additions we'd find useful, which we're happy to send as a follow-up PR on top of this one once it lands, or for you to fold in if you prefer:

  1. A machine-readable form, --dry-run=json: one document listing each planned command with package_name, task_name, command, cwd and a tagged result (hit / miss with the summary's reason / disabled / in_process / unknown with runs_after). CI can then decide per job without parsing text.
  2. Restored outputs make dependents unknown. A cached command whose entry has an output archive will restore files when it runs, so a dependent's inputs read now (before restoration) may not match what it would see. On a fresh CI checkout that's the common case; we report those dependents as unknown (runs after the restoring command) rather than predicting from the pre-restore filesystem.

We have both implemented with snapshot tests against this design (same read-only open and the same association move), and can rebase them onto your branch. Let us know which you'd prefer.

@lifeiscontent

Copy link
Copy Markdown
Contributor Author

@jasonkuhrt thanks, both additions sound useful. Please send them as a follow-up PR once this one lands. The maintainers have asked to keep PRs small so they can review and merge them one at a time. Treating dependents of a restoring command as unknown is the right call, since predicting from the filesystem before the restore can be wrong on a fresh checkout.

@jasonkuhrt

Copy link
Copy Markdown
Contributor

@lifeiscontent is there anything I can do to help get this one merged?

@lifeiscontent

Copy link
Copy Markdown
Contributor Author

@jasonkuhrt yeah, get me a job at cloud flare haha 😆

@wan9chi wan9chi added the read-write overlap Caching tasks that read and write the same paths label Nov 4, 2026
wan9chi added a commit that referenced this pull request Nov 7, 2026
There was no way to ask whether a run would hit the cache, or what it
would run, without running it (#791, vite-plus#2848).

`vp run --dry-run` plans the run and checks the cache as a real run
does, the remote cache included, and prints each command's line and the
summary. It runs nothing and records nothing. The cache is opened as in
a real run, but no entry is written, a hit's outputs aren't downloaded
or restored, and `last-summary.json` isn't written, so `--last-details`
still shows the last real run.

- Each line is a real run's, without `, executing` or `, replaying`, and
  with no blank line between commands. `--log` is ignored, since it only
  shapes task output.
- The summary follows a real run's rules, with "nothing was run" in place
  of the successful count and saved time, and no `--last-details` hint.
  The detailed summary (`-v`) has no exit icons.
- A cache lookup error is printed under its command, and stops the dry
  run as it stops a real run. Otherwise a dry run exits 0 once planning
  succeeds.
- A nested `vp run --dry-run` isn't inlined: it's started as a process,
  which prints its own dry run, so an enclosing dry run doesn't start it.
- Without a task, `--dry-run` is an error, like `vp run -v`.

A dry run goes through the same lookup and spawn path as a real run,
which skips each step that would write or run: recording a local hit,
downloading a remote one, replaying it, and spawning the command.

This supersedes #792, whose write-free lookup and test scenario it
builds on.

Co-authored-by: Aaron Reisman <aaron@lifeiscontent.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wan9chi

wan9chi commented Nov 7, 2026

Copy link
Copy Markdown
Member

Superseded by #960, which is a part of the whole --json --dry-run feature.

I took some of implementation and test cases and will credit you in #960. Thanks for working on this!

@wan9chi wan9chi closed this Nov 7, 2026
wan9chi added a commit that referenced this pull request Nov 7, 2026
There was no way to ask whether a run would hit the cache, or what it
would run, without running it (#791, vite-plus#2848).

`vp run --dry-run` plans the run and checks the cache as a real run
does, the remote cache included, and prints each command's line and the
summary. It runs nothing and records nothing. The cache is opened as in
a real run, but no entry is written, a hit's outputs aren't downloaded
or restored, and `last-summary.json` isn't written, so `--last-details`
still shows the last real run.

- Each line is a real run's, without `, executing` or `, replaying`, and
  with no blank line between commands. `--log` is ignored, since it only
  shapes task output.
- The summary follows a real run's rules, with "nothing was run" in place
  of the successful count and saved time, and no `--last-details` hint.
  The detailed summary (`-v`) has no exit icons.
- A cache lookup error is printed under its command, and stops the dry
  run as it stops a real run. Otherwise a dry run exits 0 once planning
  succeeds.
- A nested `vp run --dry-run` isn't inlined: it's started as a process,
  which prints its own dry run, so an enclosing dry run doesn't start it.
- Without a task, `--dry-run` is an error, like `vp run -v`.

A dry run goes through the same lookup and spawn path as a real run,
which skips each step that would write or run: recording a local hit,
downloading a remote one, replaying it, and spawning the command.

This supersedes #792, whose write-free lookup and test scenario it
builds on.

Co-authored-by: Aaron Reisman <aaron@lifeiscontent.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wan9chi added a commit that referenced this pull request Nov 7, 2026
There was no way to ask whether a run would hit the cache, or what it
would run, without running it (#791, vite-plus#2848).

`vp run --dry-run` plans the run and checks the cache as a real run
does, the remote cache included, and prints each command's line and the
summary. It runs nothing and records nothing. The cache is opened as in
a real run, but no entry is written, a hit's outputs aren't downloaded
or restored, and `last-summary.json` isn't written, so `--last-details`
still shows the last real run.

- Each line is a real run's, without `, executing` or `, replaying`, and
  with no blank line between commands. `--log` is ignored, since it only
  shapes task output: `--dry-run` and `--log` move from `RunFlags`, which
  selects tasks, to `RunCommand`, and resolve to a `RunMode` that is
  `Real { log }` or `Dry`, so a dry run carries no log mode.
- The summary follows a real run's rules, with "nothing was run" in place
  of the successful count and saved time, and no `--last-details` hint.
  The detailed summary (`-v`) has no exit icons.
- A cache lookup error is printed under its command, and stops the dry
  run as it stops a real run. Otherwise a dry run exits 0 once planning
  succeeds.
- A nested `vp run --dry-run` isn't inlined: it's started as a process,
  which prints its own dry run, so an enclosing dry run doesn't start it.
- Without a task, `--dry-run` is an error, like `vp run -v`.

A dry run goes through the same lookup and spawn path as a real run,
which skips each step that would write or run: recording a local hit,
downloading a remote one, replaying it, and spawning the command.

This supersedes #792, whose write-free lookup and test scenario it
builds on.

Co-authored-by: Aaron Reisman <aaron@lifeiscontent.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wan9chi added a commit that referenced this pull request Nov 7, 2026
There was no way to ask whether a run would hit the cache, or what it
would run, without running it (#791, vite-plus#2848).

`vp run --dry-run` plans the run and checks the cache as a real run
does, the remote cache included, and prints each command's line and the
summary. It runs nothing and records nothing. The cache is opened as in
a real run, but no entry is written, a hit's outputs aren't downloaded
or restored, and `last-summary.json` isn't written, so `--last-details`
still shows the last real run.

- Each line is a real run's, without `, executing` or `, replaying`, and
  with no blank line between commands. `--log` is ignored, since it only
  shapes task output: `--dry-run` and `--log` move from `RunFlags`, which
  selects tasks, to `RunCommand`, and resolve to a `RunMode` that is
  `Real { log }` or `Dry`, so a dry run carries no log mode.
- The summary follows a real run's rules, with "nothing was run" in place
  of the successful count and saved time, and no `--last-details` hint.
  The detailed summary (`-v`) has no exit icons.
- A cache lookup error is printed under its command, and stops the dry
  run as it stops a real run. Otherwise a dry run exits 0 once planning
  succeeds.
- Both now exit 1 after a failed lookup: the summary reporter records
  only tasks that started, so a task that failed before it started
  didn't count, and the run exited 0. A shared flag now makes such a
  task fail the run.
- A nested `vp run --dry-run` isn't inlined: it's started as a process,
  which prints its own dry run, so an enclosing dry run doesn't start it.
- Without a task, `--dry-run` is an error, like `vp run -v`.

A dry run goes through the same lookup and spawn path as a real run,
which skips each step that would write or run: recording a local hit,
downloading a remote one, replaying it, and spawning the command.

This supersedes #792, whose write-free lookup and test scenario it
builds on.

Co-authored-by: Aaron Reisman <aaron@lifeiscontent.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wan9chi added a commit that referenced this pull request Nov 9, 2026
There was no way to ask whether a run would hit the cache, or what it
would run, without running it (#791, vite-plus#2848).

`vp run --dry-run` plans the run and checks the cache as a real run
does, the remote cache included, and prints each command's line and the
summary. It runs nothing and records nothing. The cache is opened as in
a real run, but no entry is written, a hit's outputs aren't downloaded
or restored, and `last-summary.json` isn't written, so `--last-details`
still shows the last real run.

- Each line is a real run's, without `, executing` or `, replaying`, and
  with no blank line between commands. `--log` is ignored, since it only
  shapes task output: `--dry-run` and `--log` move from `RunFlags`, which
  selects tasks, to `RunCommand`, and resolve to a `RunMode` that is
  `Real { log }` or `Dry`, so a dry run carries no log mode.
- The summary follows a real run's rules, with "nothing was run" in place
  of the successful count and saved time, and no `--last-details` hint.
  The detailed summary (`-v`) has no exit icons.
- A cache lookup error is printed under its command, and stops the dry
  run as it stops a real run. Otherwise a dry run exits 0 once planning
  succeeds.
- Both now exit 1 after a failed lookup: the summary reporter records
  only tasks that started, so a task that failed before it started
  didn't count, and the run exited 0. A shared flag now makes such a
  task fail the run.
- A nested `vp run --dry-run` isn't inlined: it's started as a process,
  which prints its own dry run, so an enclosing dry run doesn't start it.
- Without a task, `--dry-run` is an error, like `vp run -v`.

A dry run goes through the same lookup and spawn path as a real run,
which skips each step that would write or run: recording a local hit,
downloading a remote one, replaying it, and spawning the command.

This supersedes #792, whose write-free lookup and test scenario it
builds on.

Co-authored-by: Aaron Reisman <aaron@lifeiscontent.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wan9chi added a commit that referenced this pull request Nov 9, 2026
## Motivation

Before starting a long run, there's no way to see which tasks would come
from the cache and which would run, or why one would miss, without
running them (#791, voidzero-dev/vite-plus#2848). [The dry-run
RFC](https://github.com/voidzero-dev/vite-task/blob/main/docs/rfcs/dry-run.md)
specifies `vp run --dry-run` for this. This is the first PR of the stack
that implements it: the text output. The PRs above it add every change
of a miss (#961), `-v` (#963), `--json` (#966, #967), `unknown` (#968)
and `-vv` (#969).

This supersedes #792 by @lifeiscontent, whose write-free lookup and test
scenario it builds on, so it credits them as co-author.

Closes #791.

## Changes

`vp run --dry-run` plans the run and checks the cache as a real run
does, the remote cache included, then prints each command's line and the
summary. It runs nothing and records nothing. The cache is opened as in
a real run, but no entry is written, a hit's outputs aren't downloaded
or restored, and `last-summary.json` isn't written, so `--last-details`
still shows the last real run.

```
$ vp run --dry-run test
$ vtt print-file src/a.txt ◉ cache hit
$ vtt print tested

---
vp run: 1/2 cache hit (50%), nothing was run.
```

- A dry run goes through the real run's lookup and spawn path, which
skips each step that would write or run: recording a local hit,
downloading a remote hit, replaying a hit, and spawning the command.
- Each line is a real run's, without `, executing` or `, replaying`, and
with no blank line between commands. `--log` is ignored, since it only
shapes task output: `--dry-run` and `--log` move from `RunFlags`, which
selects tasks, to `RunCommand`, and resolve to a `RunMode` of `Real {
log }` or `Dry`, so a dry run can't carry a log mode.
- The summary follows a real run's rules, with "nothing was run" in
place of the successful count and the time saved, and without the
`--last-details` hint. The detailed summary (`-v`) has no ✓ or exit
codes.

  Real run, unchanged:

  ```
vp run: 1/2 cache hit (50%), 2/2 successful. (Run `vp run
--last-details` for full details)
          │ │            │     │ │
          │ │            │     │ └─ tasks
          │ │            │     └─ tasks that succeeded
          │ │            └─ cache hit rate
          │ └─ tasks
          └─ cache hits
  ```

  Dry run:

  ```
  vp run: 1/2 cache hit (50%), nothing was run.
          │ │            │
          │ │            └─ cache hit rate
          │ └─ tasks
          └─ cache hits
  ```

- A cache lookup error is printed under its command and stops the dry
run, as it stops a real run, and both exit 1. Real runs stopped but
exited 0 before, since the summary only counted tasks that started; a
task that fails before it starts now fails the run. Otherwise a dry run
exits 0 once planning succeeds.
- A nested `vp run --dry-run` isn't inlined. It's started as a process
that prints its own dry run, so an enclosing dry run doesn't start it.
- Without a task, `--dry-run` is an error, like `vp run -v`. After the
task name, it's passed to the task, like any other flag.
- RFC: the summary and cache-error rules are written down, and the
sentence about the first changed file being the first in path order is
removed. Without `-v`, the file check stops at whichever changed file it
finds first.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches cache lookup, remote fetch, spawn, and exit-code paths across
the runner; mistakes could skip real execution side effects or
mis-report failures, though behavior is gated on `RunKind::Dry`.
> 
> **Overview**
> Adds **`vp run --dry-run`**, which plans the task graph and performs
full cache lookups (including remote) but **does not** spawn processes,
replay outputs, download remote archives, write cache entries, or
persist `last-summary.json`.
> 
> The CLI introduces **`RunMode`/`RunKind`**: `--dry-run` and `--log`
move onto `RunCommand` (dry runs always use interleaved output).
Execution and cache layers thread `RunKind` through lookup, spawn, and
reporters so dry runs short-circuit after reporting cache status
(`WouldSpawn` / `DryRun` not-updated reasons).
> 
> **Output and summary** match a real run’s command lines and cache
indicators, but omit `, executing` / `, replaying`, skip extra blank
lines, use **“nothing was run”** instead of success/saved time, and omit
the `--last-details` hint. **`-v`** detailed summaries drop exit icons
and saved-time wording for hits.
> 
> **Bugfix:** runs now **exit 1** when a task’s cache cannot be checked
(e.g. DB unreadable), including failures before `start()`; nested `vp
run --dry-run` is not inlined. Changelog and dry-run RFC/e2e snapshots
document the behavior.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3010605. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Aaron Reisman <aaron@lifeiscontent.net>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

read-write overlap Caching tasks that read and write the same paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Preview cache hits without running tasks

3 participants