3131 # namespace on top: untrusted fork runs must never share a cache with
3232 # push runs (whose caches feed production image builds) or with trusted
3333 # internal-PR runs.
34+ #
35+ # node_modules also keys on the lockfile hash: a sticky disk is a mutable
36+ # volume, and `bun install --frozen-lockfile` adds what the lockfile needs
37+ # without pruning what it dropped, so branches on different lockfiles were
38+ # contaminating each other (a stale @next/swc 16.2.6 outlived the 16.2.11
39+ # bump). The bun and Turbo caches are content/hash-addressed, so they stay
40+ # shared — that is what keeps a fresh node_modules disk cheap to fill.
3441 - name : Mount Bun cache
3542 uses : ./.github/actions/cache-mount
3643 with :
4249 uses : ./.github/actions/cache-mount
4350 with :
4451 provider : ${{ vars.CI_PROVIDER }}
45- key : ${{ github.repository }}-node-modules-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}
52+ key : ${{ github.repository }}-node-modules-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}-${{ hashFiles('bun.lock') }}
4653 path : ./node_modules
4754
4855 - name : Mount Turbo cache
@@ -193,19 +200,16 @@ jobs:
193200 # Next.js production build, in parallel with lint + tests. Sticky disks are
194201 # cloned from the last committed snapshot per job and committed last-writer-
195202 # wins, so concurrent mounts are safe. The bun/node_modules disks are shared
196- # with test-build (identical content from the same lockfile — LWW loss is
197- # harmless), but the Turbo cache gets its own key: with a shared key, only
198- # the last committer's new entries survive each run, so the test and build
199- # Turbo entries would evict each other nondeterministically.
200- # Same next build as the app image, so it needs a large runner. Peak RSS
201- # tracks Turbo/Turbopack cache warmth, and it is the COLD case that sizes the
202- # runner: warm peaks ~12 GB, partial ~28 GB, and a cold full rebuild peaked
203- # 51 GB. The 8vcpu tier only has 30.4 GB, so cold-cache runs OOM-killed the VM
204- # (oom_count 1, memory p100 ~30.5 GB, ~99% of the tier) — the job burned 8-15
205- # min and died, while warm runs finished under 8 min. NODE_OPTIONS'
206- # --max-old-space-size only caps Node's JS heap; the bulk is native Turbopack
207- # worker memory, so the cap cannot prevent this. 16vcpu = 60.8 GB fits the
208- # 51 GB cold peak with headroom. Keep the test job on 8vcpu; its memory is fine.
203+ # with test-build (the lockfile-hashed key means they only ever share when the
204+ # dependency tree really is identical, so LWW loss is harmless), but the Turbo
205+ # cache gets its own key: with a shared key, only the last committer's new
206+ # entries survive each run, so the test and build Turbo entries would evict
207+ # each other nondeterministically.
208+ #
209+ # Runner is sized for the COLD-cache build, which is what OOM-killed the 8vcpu
210+ # tier (23 kills / 1074 runs at 98% of its 30.4 GB): warm peaks ~12 GB, cold
211+ # peaked 51 GB. NODE_OPTIONS' --max-old-space-size caps only Node's JS heap,
212+ # not the native Turbopack workers that dominate, so it cannot prevent this.
209213 build :
210214 name : Build App
211215 runs-on : ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-16vcpu-ubuntu-2404' || 'linux-x64-8-core' }}
@@ -236,7 +240,7 @@ jobs:
236240 uses : ./.github/actions/cache-mount
237241 with :
238242 provider : ${{ vars.CI_PROVIDER }}
239- key : ${{ github.repository }}-node-modules-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}
243+ key : ${{ github.repository }}-node-modules-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}-${{ hashFiles('bun.lock') }}
240244 path : ./node_modules
241245
242246 - name : Mount Turbo cache
@@ -258,6 +262,21 @@ jobs:
258262 key : ${{ github.repository }}-nextjs-cache-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}
259263 path : ./apps/sim/.next/cache
260264
265+ # Running out of RAM kills the whole VM and surfaces only as "the runner
266+ # has received a shutdown signal" — no mention of memory, ~12 min in. Warn
267+ # with the real numbers so that failure is a one-line diagnosis instead of
268+ # a mystery. Warn, never fail: a warm build peaks ~12 GB and a partial one
269+ # ~28 GB, so a 32 GB runner still completes plenty of builds, and the
270+ # GitHub fallback is the break-glass path — degrading it to a guaranteed
271+ # failure would be worse than the risk this flags.
272+ - name : Check runner memory headroom
273+ run : |
274+ TOTAL_GB=$(awk '/MemTotal/ {printf "%d", $2/1048576}' /proc/meminfo)
275+ echo "Runner memory: ${TOTAL_GB} GB"
276+ if [ "$TOTAL_GB" -lt 40 ]; then
277+ echo "::warning::Runner has ${TOTAL_GB} GB. A cold-cache build peaks ~51 GB, so this run may be OOM-killed (reported only as 'the runner has received a shutdown signal'). Warm/partial builds should still fit."
278+ fi
279+
261280 - name : Install dependencies
262281 run : bun install --frozen-lockfile
263282
0 commit comments