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
Check · Review · Land · Live done — Landed and live in production.
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
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:StartInputtakes an optionalinstallCache(plusregistry, used only so a test can point at a fake registry). Before a cold checkout'sinstall,startProcessruns the checks' host-side fill (prefillInstallCachefromsrc/checks/install.ts) against the checkout'sbun.lock. Inside a cell, the install then sees the cache read-only, withBUN_INSTALL_CACHE_DIRset and the copyfile backend (cellInstall). Nothing else in the cell's view changes; the environment'shomestays 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, passescheckInstallCache(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 nobun.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.mddescribes 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.lockinstalls withoutBUN_INSTALL_CACHE_DIRand 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
Right idea, and it serves the pipeline direction (previews stop re-downloading). One real regression and two test gaps; fix them in one round.
- 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).
- 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.
- 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).
- 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.
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.
- A failed fill falls back. When
prefillInstallCachereturns an error (registry unreachable, lock cancelled, timeout),install()writescache 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 noBUN_INSTALL_CACHE_DIR, and no lock file is left behind. Without the fallback the test fails. - No test-only input.
StartInput.registryis removed. If the forge's own environment setsBUN_CONFIG_REGISTRY(an operator's mirror), the install steps now get it, which matchesDEFAULT_REGISTRY's "unless the forge's own environment names another". The fixture tests set it in the environment instead. - 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.
- The lock is documented. I kept it: an install outside a cell, lifecycle scripts included, could write the cache.
docs/APPS.mdnow 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
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.
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:StartInputtakes an optionalinstallCache(plusregistry, used only so a test can point at a fake registry). Before a cold checkout'sinstall,startProcessruns the checks' host-side fill (prefillInstallCachefromsrc/checks/install.ts) against the checkout'sbun.lock. Inside a cell, the install then sees the cache read-only, withBUN_INSTALL_CACHE_DIRset and the copyfile backend (cellInstall). Nothing else in the cell's view changes; the environment'shomestays 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, passescheckInstallCache(dataDir, repo.id). That is the same cache the repository's checks fill.warm = existsSync(package.json)) still runs no install. A checkout with nobun.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.mddescribes the path next to the checks' install-cache paragraph.Tests —
test/app-install-cache.test.ts:bun.lockinstalls withoutBUN_INSTALL_CACHE_DIRand logs why.Both tests fail without the change.
bun x tsc --noEmitis clean, andtest/apps.test.ts,test/apps-stop-tree.test.ts,test/check-install.test.tsand the new file pass (44 tests).scripts/fast.tsmapped 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
cellInstalland the bind check already covered incheck-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