Skip to content

Latest commit

 

History

History
142 lines (104 loc) · 6.74 KB

File metadata and controls

142 lines (104 loc) · 6.74 KB

Decisions

Why things are the way they are, and what it would take to change them. Add new entries here rather than burying rationale in commit messages.

Hand-rolled instead of using myoung34's image

myoung34/docker-github-actions-runner does everything this repo does, more robustly. It was not used as a base image because the goal was to learn the actual mechanism — the official runner tarball, the registration-token API call, the deregistration lifecycle — rather than a wrapper's env-var interface over it.

Its source is kept in reference/ (gitignored) and read directly. See reference.md.

Cost accepted: re-solving edge cases that image already handles (graceful shutdown, auto-update, token expiry). Where it is well-solved there, the approach was copied rather than reinvented — the trap → deregister pattern especially.

When to abandon this: if you reach org/enterprise scope or GitHub App auth, switch to that image. By then you will understand exactly what it does for you.

Standalone repo, not nested inside the target repo

Concrete friction avoided, not abstract tidiness:

  1. ci.yml triggers on pull_request — a Dockerfile change submitted as a PR would fire a full two-flavour lint build of an Android app to validate a shell script. (Weaker than it was: the push trigger has since been removed, so commits straight to main would no longer fire anything. The remaining three reasons stand on their own.)
  2. release.yml triggers on v* tags — app releases would also be tagging runner infra, one version number for two unrelated lifecycles.
  3. actions/checkout would clone the runner's own definition into the work dir, giving two copies on disk, one a build artifact.
  4. Cache volumes have to live outside the repo tree regardless, so nesting buys nothing there.

The runner is also barely repo-specific: the target-specific values are REPO_URL and labels, both in .env, zero lines of code. The toolchain (JDK/SDK/Gradle) applies to any Android project.

Pinned to linux/amd64

Android's build-tools (aapt2, zipalign) ship linux-x86_64 only — Google publishes no arm64 Linux build. On Apple Silicon the image therefore runs emulated; enable Docker Desktop → General → "Use Rosetta for x86_64/amd64 emulation" or builds crawl.

To change: on an x86_64 Linux host, drop --platform in the Dockerfile and platform: in docker-compose.yml.

--disableupdate, with versions pinned in the Dockerfile

GitHub otherwise replaces the runner binary in place while it is running. A hand-rolled entrypoint is much more likely to choke on that than a battle-tested wrapper. The trade is that version bumps are manual — see setup.md § Bumping versions.

Android SDK baked into the image, not bind-mounted

Licences are accepted once at build time, the first CI run does not pay a ten-minute download, and the toolchain is reproducible from the Dockerfile alone. It also lets the workflows drop android-actions/setup-android.

Cost: a fatter image, and a rebuild when compileSdk moves. Both rare and explicit. A missing component is one ARG/sdkmanager line away.

Named volumes, not host bind mounts

Gradle's cache is hundreds of thousands of small files. On macOS every bind-mount read crosses virtiofs; named volumes stay inside the Docker VM and are markedly faster.

Cost: no Finder-level visibility. docker compose exec runner du -sh /cache/gradle and docker volume rm cover inspection and reset.

Post-job pruning is size-gated

A persistent runner never gets a fresh disk, so build output accumulates forever. But deleting app/build after every job would throw away the incremental-build state that makes a warm runner worth having.

So hooks/post-job.sh prunes only once the workspace exceeds BUILD_CACHE_MAX_MB (default 5000). ACTIONS_RUNNER_HOOK_JOB_COMPLETED is a real GitHub Actions feature, not a wrapper invention.

No pre-job hook: there was nothing for it to do. Added only if a real need shows up.

Running as root

Avoids uid-mismatch pain on the cache volumes and keeps the entrypoint short. The reference implementation's dual root/gosu path was dropped.

Reconsider if this runner ever executes untrusted code — which is the same condition flagged in workflows.md § the pull_request trigger.

Memory ceiling on an 8 GB host

Open, not solved. Measured 2026-07-29 on the first real self-hosted run:

Host RAM 8 GB
Docker VM allocation 3.83 GB (already ~half the machine)
utilityvault requests -Xmx4g, parallel=true, workers.max=4
Result OOM-killed mid-build

Raising the VM allocation is largely closed off — macOS needs roughly 3 GB, so there is maybe 1 GB of headroom to hand over, and the two-flavour lint peaks near 3 GB by itself. Meanwhile ubuntu-latest on a private repo gets 2 cores and 7 GB, natively. The same workflow file passes there and fails here purely on headroom.

Consequence worth being blunt about: lintPlayDebug lintPersonalDebug is a bad fit for this runner. Lint's whole-program analysis is the most memory-hungry task in the project, so it was the worst possible first job to point at an emulated 3.83 GB container. Jobs that plausibly fit: a single-flavour assembleDebug, unit tests, anything without lint.

Not fixed in the image, because the lever is workflow-side (GRADLE_OPTS / org.gradle.jvmargs scoped to the self-hosted path) and the target repo's gradle.properties must keep -Xmx4g for local development. Symptoms and confirmation steps: troubleshooting.md.

Deliberately not built

Cut because none of it can be tested without the infrastructure it targets, and untested branches rot silently.

Feature What adding it involves
Org / enterprise scope A different registration-token endpoint (/orgs/{org}/…, /enterprises/{name}/…) and a different config.sh --url. ~12 lines across token.sh and entrypoint.sh
GitHub App auth Sign an RS256 JWT with the app private key → GET /app/installations to find the installation → POST for a 1-hour installation token that behaves like a PAT. Worth it when runners should outlive any individual's credentials
Ephemeral runners config.sh --ephemeral; one job per container, needs a supervisor to recreate them
Docker-in-docker Mount the docker socket, reconcile the docker group GID
GitHub Enterprise Server hosts Host/API-path normalisation (/api/v3)
Runner groups, gosu de-escalation

All of it exists in reference/ to copy from. See reference.md for a map of which file does what.