Skip to content

improve(ci): persist warm Vitest and Node caches#109425

Merged
steipete merged 4 commits into
mainfrom
codex/vitest-cache-hardening
Jul 17, 2026
Merged

improve(ci): persist warm Vitest and Node caches#109425
steipete merged 4 commits into
mainfrom
codex/vitest-cache-hardening

Conversation

@steipete

@steipete steipete commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Related: #109330

What Problem This Solves

Resolves a CI performance problem where every Node test shard repeatedly pays cold Vitest transform/import costs because its filesystem module cache does not survive the job. The original persistent-cache lane also depended on a cancellable protected writer, had no bounded eviction, and could not safely reuse protected seeds after partial configuration changes.

Why This Change Was Made

Persist Vitest transform data on the same Blacksmith sticky-disk mechanism as Node dependencies, with a single semantic writer, private per-job live directories, exact transform-generation validation, PR overlays, a non-cancelling default-branch warmer, GitHub-hosted fallback archives, and bounded pruning. Also update Vitest to 4.1.10 and persist Node's portable compile cache in independent test/build namespaces; trusted main builds publish build-specific bytecode without racing the test writer. Vitest children disable inherited bytecode caching so config-driven V8 coverage remains precise.

Trust boundaries stay explicit: pull requests cannot publish protected snapshots, protected seeds merge only when their transform fingerprints match, fork fallbacks remain PR-scoped, and on-demand warming uses default-branch-only repository dispatch rather than ref-selectable workflow dispatch.

User Impact

No product behavior changes. Maintainers should see faster warm Node CI, especially on partially changed pull requests that reuse most of the shared source graph. Cache growth is bounded at 2 GiB for Vitest transforms and 1 GiB per Node bytecode namespace, pruning back to 75%; closed PR fallback archives are deleted.

Evidence

  • Local Node 24 / Vitest 4.1.10 focused proof: 4 files, 171 passed, 1 skipped. Final cache-ownership guard rerun: 66 passed, 1 skipped.
  • Blacksmith changed-surface proof: formatting, dependency pins, all 80 shrinkwrap guards, Plugin SDK API/surface, boundary guards, full TypeScript graph, and core/extension lint passed. The final scripts-lint stop was an unrelated duplicate declaration briefly reintroduced on main; current main includes its fix.
  • Prior same-SHA hosted CI A/B on improve: reuse Vitest transform cache across CI runs #109330: active wall time 479.5s to 355.7s (-25.8%), summed Node job time 4816.8s to 3792.8s (-21.3%), transform time 2359.5s to 1430.8s (-39.4%), and import time 3593.9s to 2454.3s (-31.7%). 23 of 26 jobs improved, with no ENOTEMPTY/EEXIST/EBUSY/ENOSPC cache races.
  • Representative shard wins from that run: large-1 419.5s to 301.5s (-28.1%), large-2 226.0s to 182.8s (-19.1%), small-16 101.6s to 68.0s (-33.0%).
  • Isolated transform-cache A/B: 945ms cold to 779ms warm; transform 153ms to 40ms and import 235ms to 138ms.
  • Exact-head PR CI same-SHA rerun (run 29543572310): active Node matrix wall 350s to 301s (-14.0%), median Node job 155s to 147s (-5.2%), cumulative Vitest duration 3063.5s to 2845.3s (-7.1%), transform 1662.1s to 1436.5s (-13.6%), and import 2735.4s to 2412.9s (-11.8%).
  • Exact-head representatives: checks-node-compact-large-1 350s to 301s (-14.0%); checks-node-compact-small-16 75s to 65s (-13.3%). checks-node-compact-large-7 had 5s of job-level noise (91s to 96s), while its three Vitest plans still improved 66.03s to 62.79s (-4.9%), transform 123.51s to 109.54s (-11.3%), and import 140.92s to 122.63s (-13.0%).
  • Cold and warm logs confirm Vitest 4.1.10, PR sticky-disk restore/save, a committed 0.30 GiB writer snapshot, and no ENOTEMPTY/EEXIST/EBUSY/ENOSPC cache races.
  • Node compile-cache probe on the pnpm CLI graph: 0.81s cold versus 0.63-0.75s warm (about 20% median improvement).
  • Post-merge protected warmer proof: the first main seed committed 0.23 GiB of Vitest transforms and 0.04 GiB of Node test bytecode. A later partial-source-change run retained the same generation and cut cumulative transform work from 71.06s to 35.67s (-49.8%). A pinned exact-SHA pair remained green with 21-29s of transform work, although runner/import/test variance made the second job slower; cache persistence is proven, exact wall-clock benefit is workload- and fleet-sensitive.
  • Post-merge build-cache proof: main build job 87774785322 committed the first 0.03 GiB build snapshot; later job 87777911183 mounted and recommitted the same protected key. Both had per-SHA dist-cache misses, while Build dist improved from 58s to 44s (-24.1%). This is promising one-pair evidence rather than a guaranteed 24% gain because source and runner load also changed.

AI-assisted: yes. I reviewed the implementation, dependency contracts, trust boundaries, and validation results.

@steipete
steipete requested a review from a team as a code owner July 16, 2026 23:42
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation app: web-ui App: web-ui scripts Repository scripts size: L maintainer Maintainer-authored PR labels Jul 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20387d5d65

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +229 to +230
echo "NODE_COMPILE_CACHE=$cache_root" >> "$GITHUB_ENV"
echo "NODE_COMPILE_CACHE_PORTABLE=1" >> "$GITHUB_ENV"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Disable compile cache for test-projects Vitest children

When node-compile-cache is enabled for the Node shard job, this exports NODE_COMPILE_CACHE into every later command, including scripts/ci-run-node-test-shard.mjs which invokes node scripts/test-projects.mjs; that path spawns Vitest from scripts/test-projects.mjs without going through the new resolveRunVitestSpawnEnv stripping in scripts/run-vitest.mjs. In CI shards that generate V8 coverage, those Vitest processes now inherit the compile cache, while the Node module docs recommend disabling it for precise V8 coverage, so coverage results can become less accurate even though the direct run-vitest wrapper is protected.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot added the dependencies-changed PR changes dependency-related files label Jul 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Guard

This PR changes dependency-related files. Maintainers should confirm these changes are intentional.

Changed files:

  • package.json
  • pnpm-lock.yaml
  • ui/package.json

Maintainer follow-up:

  • Review whether the dependency changes are intentional.
  • Inspect resolved package deltas when lockfile, shrinkwrap, or workspace dependency policy changes are present.
  • Treat package-lock.json and npm-shrinkwrap.json diffs as security-review surfaces.
  • Run pnpm deps:changes:report -- --base-ref origin/main --markdown /tmp/dependency-changes.md --json /tmp/dependency-changes.json locally for detailed release-style evidence.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Dependency graph changes noted

This PR includes dependency graph changes. The dependency guard is informational because the PR author is a repository admin or a member of @openclaw/openclaw-secops.

  • Current SHA: 1518138d5be01ef3e51298edbeecae65e132ce24
  • Trusted actor: @steipete
  • Trusted role: pull request author; openclaw-secops

Security review is still recommended before merge when the dependency graph change is intentional.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​vitest/​browser@​4.1.10991007898100
Addednpm/​vitest@​4.1.10981007998100
Addednpm/​@​vitest/​coverage-v8@​4.1.10991007999100
Addednpm/​@​vitest/​browser-playwright@​4.1.101001008698100

View full report

@steipete
steipete merged commit b78cf56 into main Jul 17, 2026
186 checks passed
@steipete
steipete deleted the codex/vitest-cache-hardening branch July 17, 2026 00:22
@steipete

Copy link
Copy Markdown
Contributor Author

Merged via squash.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 17, 2026
* perf(ci): harden persistent test caches

* ci: document trusted cache cleanup trigger

* ci: harden composite cache inputs

* perf(ci): isolate Node build bytecode cache
steipete added a commit that referenced this pull request Jul 17, 2026
…109929)

The installation hit Blacksmith's backing-disk cap because sticky keys
embedded PR numbers and content hashes, minting a new disk per PR and
per input change until every mount 429-failed fleet-wide. node-deps
(#109752) and ext-boundary (#109804) were already re-keyed; this
converts the last two minters and audits the third suspect.

Keys changed:
- Vitest fs transform cache: `vitest-fs-v2-<pr-N|protected>-...` ->
  the one existing `vitest-fs-v2-protected-<os>-<arch>-node-<ver>`
  disk. Entries are content-hash keyed (vitest sha1 over
  id+content+NODE_ENV+version+env config), so cross-PR sharing is safe
  by construction: PRs now mount the protected snapshot directly
  (read-only, commit false) and the separate PR-only seed mount is
  deleted. Writers (non-PR shard writer + scheduled warm run) keep
  explicit commit true. Disk count: 1 + O(open PRs) -> 1.
- Gradle: `gradle-v1-<task>-<pr-N|protected>-<hashFiles(deps)>` ->
  `gradle-v2-<task>`. Task scope stays (light ktlint must not seed
  heavy build lanes); PR number and dependency hash leave the key. The
  dependency hash moved to an in-job fingerprint marker that makes the
  non-PR writer rebuild its snapshot cold when inputs change, bounding
  disk growth; PR mounts are read-only and safely reuse stale
  snapshots because Gradle caches are content-addressed. Disk count:
  O(tasks x PRs x dependency bumps) -> O(tasks) (7 today).
- Docker builder (useblacksmith/setup-docker-builder): audited, out of
  scope. The action owns its key internally and always uses the repo
  name only (src/setup_builder.ts getStickyDisk), so it is already one
  disk per installation repo and cannot mint per-PR disks.

PR warm-path behavior changes:
- Vitest: PRs lose only PR-local warm entries for files the PR itself
  changed (previously persisted on the pr-N disk between pushes);
  changed files re-transform once per push, unchanged files still hit
  the protected snapshot. PRs touching transform inputs
  (lockfile/tsconfig/package.json) run cold per push since the
  generation wipe is now local to the discarded clone.
- Gradle: dependency-bump PRs get warmer (stale-but-valid protected
  snapshot instead of a cold fresh key); other PRs are unchanged.

Deleted `.github/workflows/pr-cache-cleanup.yml`: it existed for the
per-PR cache layer (#109425) and only deleted GitHub actions/cache
archives, which GitHub's own LRU/TTL eviction already handles for the
remaining fork/Windows per-PR archive paths.

Guards: pinned the O(1) vitest key + non-PR commit gate, added a
Gradle per-task key/writer/fingerprint guard, added a repo-wide scan
asserting no useblacksmith/stickydisk key ever contains
github.event.pull_request.number or hashFiles(), and replaced the
cleanup-workflow assertions with a stays-deleted check.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 18, 2026
…penclaw#109929)

The installation hit Blacksmith's backing-disk cap because sticky keys
embedded PR numbers and content hashes, minting a new disk per PR and
per input change until every mount 429-failed fleet-wide. node-deps
(openclaw#109752) and ext-boundary (openclaw#109804) were already re-keyed; this
converts the last two minters and audits the third suspect.

Keys changed:
- Vitest fs transform cache: `vitest-fs-v2-<pr-N|protected>-...` ->
  the one existing `vitest-fs-v2-protected-<os>-<arch>-node-<ver>`
  disk. Entries are content-hash keyed (vitest sha1 over
  id+content+NODE_ENV+version+env config), so cross-PR sharing is safe
  by construction: PRs now mount the protected snapshot directly
  (read-only, commit false) and the separate PR-only seed mount is
  deleted. Writers (non-PR shard writer + scheduled warm run) keep
  explicit commit true. Disk count: 1 + O(open PRs) -> 1.
- Gradle: `gradle-v1-<task>-<pr-N|protected>-<hashFiles(deps)>` ->
  `gradle-v2-<task>`. Task scope stays (light ktlint must not seed
  heavy build lanes); PR number and dependency hash leave the key. The
  dependency hash moved to an in-job fingerprint marker that makes the
  non-PR writer rebuild its snapshot cold when inputs change, bounding
  disk growth; PR mounts are read-only and safely reuse stale
  snapshots because Gradle caches are content-addressed. Disk count:
  O(tasks x PRs x dependency bumps) -> O(tasks) (7 today).
- Docker builder (useblacksmith/setup-docker-builder): audited, out of
  scope. The action owns its key internally and always uses the repo
  name only (src/setup_builder.ts getStickyDisk), so it is already one
  disk per installation repo and cannot mint per-PR disks.

PR warm-path behavior changes:
- Vitest: PRs lose only PR-local warm entries for files the PR itself
  changed (previously persisted on the pr-N disk between pushes);
  changed files re-transform once per push, unchanged files still hit
  the protected snapshot. PRs touching transform inputs
  (lockfile/tsconfig/package.json) run cold per push since the
  generation wipe is now local to the discarded clone.
- Gradle: dependency-bump PRs get warmer (stale-but-valid protected
  snapshot instead of a cold fresh key); other PRs are unchanged.

Deleted `.github/workflows/pr-cache-cleanup.yml`: it existed for the
per-PR cache layer (openclaw#109425) and only deleted GitHub actions/cache
archives, which GitHub's own LRU/TTL eviction already handles for the
remaining fork/Windows per-PR archive paths.

Guards: pinned the O(1) vitest key + non-PR commit gate, added a
Gradle per-task key/writer/fingerprint guard, added a repo-wide scan
asserting no useblacksmith/stickydisk key ever contains
github.event.pull_request.number or hashFiles(), and replaced the
cleanup-workflow assertions with a stays-deleted check.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui dependencies-changed PR changes dependency-related files docs Improvements or additions to documentation maintainer Maintainer-authored PR scripts Repository scripts size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant