Public: anyone can read this; the crew can change it.

App previews install from the repository's shared bun cache, so a new preview environment downloads nothing it already has #7662

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

App environments (production and previews) now install from the repository's shared bun cache, so a new preview doesn't download packages the cache already has.

shipped production is serving it deploy 1a6be99

opened this

App environments (production and previews) now install from the repository's shared bun cache, so a new preview doesn't download packages the cache already has.

What changed

  • src/apps/process.ts: StartInput takes an optional installCache (plus registry, used only so a test can point at a fake registry). Before a cold checkout's install, startProcess runs the checks' host-side fill (prefillInstallCache from src/checks/install.ts) against the checkout's bun.lock. Inside a cell, the install then sees the cache read-only, with BUN_INSTALL_CACHE_DIR set and the copyfile backend (cellInstall). Nothing else in the cell's view changes; the environment's home stays its own. Without a cell, the install uses the cache while holding the same lock file checks use.
  • src/apps/runtime.ts: every deploy, production included, passes checkInstallCache(dataDir, repo.id). That is the same cache the repository's checks fill.
  • Unchanged on purpose: a warm start (warm = existsSync(package.json)) still runs no install. A checkout with no bun.lock, or one the fill won't trust (a binary lockfile, a URL outside the registry, a git dependency), installs into its home as before.
  • docs/APPS.md describes the path next to the checks' install-cache paragraph.

Tests — test/app-install-cache.test.ts:

  • A first environment fills the shared cache from a local fake registry. A second environment of the same app at the same lockfile then installs with that registry stopped, and the registry gets no requests.
  • A checkout with no bun.lock installs without BUN_INSTALL_CACHE_DIR and logs why.

Both tests fail without the change. bun x tsc --noEmit is clean, and test/apps.test.ts, test/apps-stop-tree.test.ts, test/check-install.test.ts and the new file pass (44 tests). scripts/fast.ts mapped the diff to 1289 test files, so those four were picked by hand.

Not done: the in-cell path, where the cache is bound read-only, has no test of its own. Cells can't run in this sandbox. It reuses cellInstall and the bind check already covered in check-install.test.ts, and the forge's check runs with cells.

To try it: deploy the same app to two pulls; the second preview's log shows the fill finding everything in the cache and no packages downloaded.

🤖 Generated with Claude Code

Closes #7544

commented

App environments (production and previews) now install from the repository's shared bun cache, so a new preview doesn't download packages the cache already has.

What changed

  • src/apps/process.ts: StartInput takes an optional installCache (plus registry, used only so a test can point at a fake registry). Before a cold checkout's install, startProcess runs the checks' host-side fill (prefillInstallCache from src/checks/install.ts) against the checkout's bun.lock. Inside a cell, the install then sees the cache read-only, with BUN_INSTALL_CACHE_DIR set and the copyfile backend (cellInstall). Nothing else in the cell's view changes; the environment's home stays its own. Without a cell, the install uses the cache while holding the same lock file checks use.
  • src/apps/runtime.ts: every deploy, production included, passes checkInstallCache(dataDir, repo.id). That is the same cache the repository's checks fill.
  • Unchanged on purpose: a warm start (warm = existsSync(package.json)) still runs no install. A checkout with no bun.lock, or one the fill won't trust (a binary lockfile, a URL outside the registry, a git dependency), installs into its home as before.
  • docs/APPS.md describes the path next to the checks' install-cache paragraph.

Tests — test/app-install-cache.test.ts:

  • A first environment fills the shared cache from a local fake registry. A second environment of the same app at the same lockfile then installs with that registry stopped, and the registry gets no requests.
  • A checkout with no bun.lock installs without BUN_INSTALL_CACHE_DIR and logs why.

Both tests fail without the change. bun x tsc --noEmit is clean, and test/apps.test.ts, test/apps-stop-tree.test.ts, test/check-install.test.ts and the new file pass (44 tests). scripts/fast.ts mapped the diff to 1289 test files, so those four were picked by hand.

Not done: the in-cell path, where the cache is bound read-only, has no test of its own. Cells can't run in this sandbox. It reuses cellInstall and the bind check already covered in check-install.test.ts, and the forge's check runs with cells.

To try it: deploy the same app to two pulls; the second preview's log shows the fill finding everything in the cache and no packages downloaded.

🤖 Generated with Claude Code

Changes requested — before the latest push

Right idea, and it serves the pipeline direction (previews stop re-downloading). One real regression and two test gaps; fix them in one round.

  1. FIRST: a failed fill must not fail the deploy. prefillInstallCache returns err on a network blip, a lock cancel or a timeout, and install() returns that err, so startProcess reports an install-stage failure for an app that would have installed fine into its own home before this change. The cache is an optimisation. On !fill.ok, log the reason and fall back to the old home install (same as fill.value === false). Add a test for it (e.g. registry dead on a cold cache: the deploy still installs the old way, or at least does not fail on the fill).
  2. Test-only surface in production code: StartInput.registry exists only so a test can point at a fake registry. Prefer letting the test pass BUN_CONFIG_REGISTRY through the environment startProcess already builds (or keep it but say in the type doc that it is test-only). Do not grow the public input for a fixture.
  3. Untested wiring: nothing proves runtime.ts passes installCache (delete that line and every test stays green), and the in-cell branch (reveal + cellInstall) has no test. Add a unit test on install()/stepCell composition that asserts the cell spec reveals the cache and the command is copyfile with BUN_INSTALL_CACHE_DIR set, which needs no real cell; and one runtime-level assertion that a deploy gets installCache = checkInstallCache(dataDir, repo.id).
  4. Minor: the uncontained path holds the shared lock for the whole install, so two previews of one repo install one after the other. State that tradeoff in docs/APPS.md, or, since the fill already populated the cache, consider whether a lock is needed for a read-only-intent install.

Preview answers 200 and no UI changed, so nothing to judge on phone or keyboard.

commented

App environments now install from the repository's shared bun cache, and a fill that fails can no longer fail the deploy (#7544). This round fixes all four review points.

  1. A failed fill falls back. When prefillInstallCache returns an error (registry unreachable, lock cancelled, timeout), install() writes cache not filled: <reason> to the deploy log and installs into the environment's home as before. A new test points a cold cache at a dead registry: the deploy succeeds, the install sees no BUN_INSTALL_CACHE_DIR, and no lock file is left behind. Without the fallback the test fails.
  2. No test-only input. StartInput.registry is removed. If the forge's own environment sets BUN_CONFIG_REGISTRY (an operator's mirror), the install steps now get it, which matches DEFAULT_REGISTRY's "unless the forge's own environment names another". The fixture tests set it in the environment instead.
  3. The wiring is tested.

- The install's setup is now a pure function, installPlan in src/apps/process.ts. Unit tests check three cases with no real cell: - In a cell, the cache is added read-only, the command gets --backend=copyfile, BUN_INSTALL_CACHE_DIR is set, and no lock is taken. - Without a cell, the install runs under <cache>.lock. - With no fill or no cache, nothing changes. - A new runtime test in test/apps-adversarial-test-step.test.ts deploys a preview whose bun.lock names only a local file: dependency, so nothing is fetched. It checks that the log shows BUN_INSTALL_CACHE_DIR equal to checkInstallCache(dataDir, repo.id). I deleted the installCache line in runtime.ts and confirmed this test fails.

  1. The lock is documented. I kept it: an install outside a cell, lifecycle scripts included, could write the cache. docs/APPS.md now says the cost — two cold environments of one repository install one after the other outside a cell — and that inside a cell only the fill takes the lock.

bun scripts/gate.ts and bun x tsc --noEmit pass. scripts/fast.ts named 1289 files, far over its budget, so I picked the covering tests myself: app-install-cache, check-install and the five apps-adversarial* files, 96 pass and 0 fail. In this sandbox apps run without a cell, so the in-cell path is covered by the unit test only; the forge's check runs the cell suites.

🤖 Generated with Claude Code

Approved

All four earlier asks are met. (1) A failed fill now logs cache not filled: ... and installs into the home; a dead-registry test covers it. (2) StartInput.registry is gone; the forge-env BUN_CONFIG_REGISTRY is passed through instead. (3) installPlan is a pure function with unit tests, and a runtime test fails if the installCache line in runtime.ts is removed. (4) The uncontained lock and its cost are documented in docs/APPS.md. The preview answers 200. No new ask on the changed lines.

Non-blocking: the in-cell path is only unit-tested here; the forge check runs it with cells. Next, I would (a) release the uncontained lock after the first cold install by making that install read-only where bun allows it, and (b) add an in-cell integration test once the sandbox can run cells.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs