Skip to content

feat: persist per-runner cache volumes with surgical prune - #6

Merged
azlekov merged 2 commits into
mainfrom
fix/prune-package-caches
Aug 18, 2026
Merged

feat: persist per-runner cache volumes with surgical prune#6
azlekov merged 2 commits into
mainfrom
fix/prune-package-caches

Conversation

@azlekov

@azlekov azlekov commented Aug 18, 2026

Copy link
Copy Markdown

Rewrite — do not merge the old rm -rf

The previous commit on this branch wiped /root/.bun/install/cache (and npm/pnpm/yarn) after every job. Combined with cleanup already deleting Gradle build-cache-* / transforms-*, that would have thrown away the named volumes at the end of every job.

This rewrite lands the beelink layout and keeps the caches warm.

What landed

  • docker-compose.yml — replaces the one-service stub with the 3-runner X64 beelink compose (YAML anchor + per-runner work and cache volumes). Not a side compose.beelink.yml. Volumes are per runner, not shared: concurrent bun installs must not share one tree.
  • pre-job.sh (ACTIONS_RUNNER_HOOK_JOB_STARTED) — surgical prune only: bun *.tmp / *.part, Gradle modules-2.lock / journal-* / daemon/. Covers OOM / killed runner (JOB_COMPLETED does not run then). Does not rm -rf cache trees.
  • cleanup.sh (ACTIONS_RUNNER_HOOK_JOB_COMPLETED)
    • Keep: bun home, npm/pnpm/yarn, Gradle modules-2 / build-cache-* / transforms-*, Cargo registry
    • Delete: /tmp contents except $RUNNER_WORKDIR (and $RUNNER_TEMP if under /tmp) — workdir is /tmp/runner/work, so a blanket find /tmp -delete was wiping the work volume
    • Gradle daemon / journals / locks, runner _diag logs
    • Size cap ~20 GB per gradle-home / bun-home with LRU, not a wholesale wipe
  • Dockerfile — COPY pre-job.sh, set ACTIONS_RUNNER_HOOK_JOB_STARTED, keep JOB_COMPLETED cleanup
  • README — volumes, X64 class, and that workflows must not isolate GRADLE_USER_HOME / BUN_INSTALL_CACHE_DIR on these runners (setup-stack local-home mode)

The other half

GitHub actions/cache with restore-keys can still re-import a poisoned bun tarball. That is the shared-workflow / per-repo exact-key work, not this image.

Host follow-up (after merge + image publish)

On each beelink, from this compose file (existing runner-work* volumes keep their data; new cache volumes start empty and warm on first jobs):

docker compose pull && docker compose up -d

Do not tmpfs /tmp. Do not share one bun/gradle volume across the three containers. ARM64 / Pi: no cache volumes.

azlekov and others added 2 commits August 18, 2026 17:23
Bun is not in this image — setup-bun installs it per job — but
/root/.bun/install/cache survives between jobs in the same container. A job
killed mid-download leaves a partial tarball, and every later job on that
container then fails with:

  error: Fail extracting tarball for "next"

The failure is sticky and misleading: re-running does not clear it, and it reads
as a dependency problem rather than a runner problem. It was previously
attributed to the macOS runners specifically, and kodni.me's ci.yml still
prescribes pinning to Linux as the fix — but it just reproduced on a Linux X64
beelink. The cause is the shared cache on any persistent runner, not the OS.

Also prunes the npm/pnpm/yarn stores, which fail the same way.

NOTE: half a fix on its own. Workflows restoring ~/.bun/install/cache via
actions/cache with restore-keys can re-import a poisoned entry from the GitHub
cache, which outlives this container.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Named volumes only help if cleanup keeps bun/Gradle/Cargo homes and
spares RUNNER_WORKDIR; wholesale rm -rf was the wrong policy.

Co-authored-by: Cursor <cursoragent@cursor.com>
@azlekov azlekov changed the title fix: prune package caches so a killed job cannot poison the next one feat: persist per-runner cache volumes with surgical prune Aug 18, 2026
@azlekov
azlekov merged commit c00b12e into main Aug 18, 2026
11 checks passed
@azlekov
azlekov deleted the fix/prune-package-caches branch August 18, 2026 20:58
azlekov added a commit to dodi-smart/.github that referenced this pull request Aug 18, 2026
…e Pis

Three gaps between this action and the runner image's per-runner cache
volumes (dodi-smart/docker-github-actions-runner-android#6):

pnpm and yarn were never isolated. The image mounts and LRU-caps
/root/.cache/pnpm, and cleanup.sh keeps ~/.cache/yarn, but isolate mode set
only npm_config_cache. npm's `cache` is not pnpm's `cache-dir`, so a
deps-verify job on pnpm still read and wrote the shared store — it was not
verifying. pnpm reads npm_config_* as its own config, so npm_config_store_dir
and npm_config_cache_dir pin it; yarn takes YARN_CACHE_FOLDER and
YARN_GLOBAL_FOLDER, for classic and berry alike.

PUB_CACHE is now job-scoped in every mode, not just when isolated. A home dir
is only worth using if a volume backs it. The image mounts one for bun, npm,
pnpm, gradle and cargo and caps each at ~20 GB; it mounts none for pub. Left
on ~/.pub-cache, a persistent runner would grow it in the container's
writable layer, where the post-job LRU cap cannot reach.

The forced-isolate check is Linux ARM64, not ARM64. The rule exists because a
Pi has 8 GB and must not grow container volumes. The self-hosted Mac is ARM64
too, has no container volumes, and was being made to run every job cold for a
reason that does not describe it.

Also corrects the bun comment: setup-bun still caches the bun executable, a
small exact-keyed binary. What is never uploaded is the install cache — any
package STORE under restore-keys is the poison path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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