Skip to content

feat: let an operator scope cache mounts per app - #18

Open
dviejokfs wants to merge 1 commit into
mainfrom
feat/cache-scope
Open

feat: let an operator scope cache mounts per app#18
dviejokfs wants to merge 1 commit into
mainfrom
feat/cache-scope

Conversation

@dviejokfs

Copy link
Copy Markdown
Contributor

Refs #13.

The situation

cache_id is autopack-{name}-{sanitize(directory)} — the cache name and its mount point, both constants per ecosystem. Nothing app-specific enters it, so every project built on a worker shares one volume:

default:  id=autopack-npm-store-cache-npm      # identical for every app

Why this is not simply a bug

Sharing a content-addressed package store is the point of one. It is most of the value of caching at all, and per-app keys mean every project pays a cold install. So shared stays the default — this PR does not change what anyone gets today.

What is worth fixing is that the mount is not a tenant boundary and nothing said so. Install steps run app-controlled code as root — lifecycle scripts, and pnpm run build in the build step — and the install step is the one that holds the store read-write. On a worker shared between projects that are not mutually trusting, one build writes to a store the others read. pnpm's integrity checking limits what a consumer accepts but does not cover index/metadata files or anything a consumer's lockfile does not integrity-pin.

AUTOPACK_CACHE_SCOPE=app gives operators in that position a switch:

default:  id=autopack-npm-store-cache-npm
scoped:   id=autopack-b2cd6d01c7a2c3df-npm-store-cache-npm

#12 corrected the comment on that line, which claimed the id already keyed the cache to the app's directory. This is the behaviour half.

Choices worth reviewing

  • Opt-in, not opt-out. Option 3 of the three in Package-manager cache is shared across every project on a builder #13. Single-tenant users — the common case for a self-hosted builder — should not pay a cold cache for a threat model they do not have.
  • FNV-1a, not DefaultHasher. Rust does not promise DefaultHasher's output is stable across releases, and a cache id that shifted when autopack was rebuilt would silently discard every cache on upgrade. Twelve lines, no new dependency — sha2 was deliberately dropped in build(deps): drop the unused sha2 dependency #7 and this is not worth bringing it back for.
  • Digest of the path, not the path. Keeps the id short and free of characters the mount syntax would object to. It does mean two checkouts of the same repo at different paths get different caches, which is the correct reading of "app" but will look surprising in CI.
  • Unknown values fall back to shared rather than failing the build. A typo in a cache setting should not stop a deploy.

Test plan

  • cache_ids_are_shared_by_default_and_isolated_on_request — default id unchanged; two apps differ under scoping; two caches within one app still differ
  • cache_scope_is_opt_in_and_stable — digest is deterministic, distinct per path, fixed width, and pinned to a known value so a change to the hash is a test failure rather than a silent cache wipe
  • Verified against the built CLI, output above
  • cargo test --workspace → 231 passed
  • cargo fmt --all -- --check and cargo clippy --workspace --all-targets -- -D warnings clean

Left open in #13

Whether Temps Cloud specifically should default to app. That is a deployment decision rather than a builder default, and it belongs with whoever operates the fleet.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

📓 Changelog preview

This is what your commits will add to the generated CHANGELOG.md at release time (via git-cliff). Do not edit CHANGELOG.md by hand — it is generated from your Conventional Commit messages.

## [Unreleased]

### Added

- Let an operator scope cache mounts per app

Cache mount ids are derived from the cache name and its mount point, both
constants per ecosystem, so every project built on a worker shares one
/cache/npm, one /cache/pnpm, one /cache/yarn.

For a content-addressed package store that is the point, and it is most
of the value of caching at all, so it stays the default. But the mount is
not a tenant boundary: install steps run app-controlled code as root and
hold the store read-write, so on a worker shared between projects that do
not trust each other, one build writes to a store the others read.

AUTOPACK_CACHE_SCOPE=app mixes a digest of the app path into every id for
operators who need that isolation, at the cost of a cold store per
project. FNV-1a rather than DefaultHasher, whose output Rust does not
promise to keep stable — an id that moved between autopack builds would
silently discard every cache on upgrade.

Refs #13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant