CI: cache Android SDK/NDK, Gradle, and npm across APK builds
Persist three Docker volumes into the build job (android-sdk, gradle home, npm cache) and enable the Gradle build cache, so repeat builds skip the ~2GB NDK download, Maven dependency resolution, and unchanged native/Kotlin compilation — cutting builds from ~1h to a few minutes after the first run. The runner must whitelist these volumes via config.yaml (container.valid_volumes); deploy-runner.sh now writes that config, pre-creates the volumes, and starts the daemon with --config. Requires re-running deploy-runner.sh on the runner host before the next tag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1ca2c38fbf
commit
3583c2e15f
5 changed files with 89 additions and 12 deletions
|
|
@ -11,14 +11,27 @@ jobs:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: node:22-bookworm
|
image: node:22-bookworm
|
||||||
|
# Persistent caches across runs (Docker named volumes). These must be
|
||||||
|
# allowed in the runner's config.yaml `container.valid_volumes` — the
|
||||||
|
# deploy-runner.sh script sets that up. First run populates them (~1h);
|
||||||
|
# later runs reuse the SDK/NDK, Gradle deps + build cache, and npm cache,
|
||||||
|
# dropping the build to a few minutes.
|
||||||
|
volumes:
|
||||||
|
- camptickets-android-sdk:/opt/android-sdk
|
||||||
|
- camptickets-gradle:/root/.gradle
|
||||||
|
- camptickets-npm:/root/.npm
|
||||||
env:
|
env:
|
||||||
ANDROID_HOME: /opt/android-sdk
|
ANDROID_HOME: /opt/android-sdk
|
||||||
ANDROID_SDK_ROOT: /opt/android-sdk
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
||||||
|
GRADLE_USER_HOME: /root/.gradle
|
||||||
# Force IPv4 for all JVMs (Gradle launcher, daemon, Kotlin/CMake workers).
|
# Force IPv4 for all JVMs (Gradle launcher, daemon, Kotlin/CMake workers).
|
||||||
# The runner host has no working IPv6 route, so Maven Central (which has
|
# The runner host has no working IPv6 route, so Maven Central (which has
|
||||||
# AAAA records) was unreachable — this makes Java ignore AAAA and use IPv4.
|
# AAAA records) was unreachable — this makes Java ignore AAAA and use IPv4.
|
||||||
JAVA_TOOL_OPTIONS: -Djava.net.preferIPv4Stack=true
|
JAVA_TOOL_OPTIONS: -Djava.net.preferIPv4Stack=true
|
||||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false
|
# org.gradle.caching enables the local build cache (persisted in
|
||||||
|
# GRADLE_USER_HOME), so unchanged native/Kotlin tasks are restored instead
|
||||||
|
# of recompiled.
|
||||||
|
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dorg.gradle.caching=true
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
@ -36,16 +49,25 @@ jobs:
|
||||||
apt-get install -y --no-install-recommends openjdk-17-jdk-headless unzip wget git
|
apt-get install -y --no-install-recommends openjdk-17-jdk-headless unzip wget git
|
||||||
echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> "$GITHUB_ENV"
|
echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Install Android SDK
|
- name: Install Android SDK (cached)
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eu
|
||||||
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
# Skip the whole install when the cache volume already has the SDK.
|
||||||
cd /tmp
|
# (NDK + CMake are auto-installed by Gradle into the same volume on
|
||||||
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O cmdtools.zip
|
# the first build, so they persist too.)
|
||||||
unzip -q cmdtools.zip -d "$ANDROID_HOME/cmdline-tools"
|
if [ ! -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
|
||||||
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
|
echo "Installing Android command-line tools..."
|
||||||
|
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||||||
|
cd /tmp
|
||||||
|
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O cmdtools.zip
|
||||||
|
unzip -q cmdtools.zip -d "$ANDROID_HOME/cmdline-tools"
|
||||||
|
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
|
||||||
|
else
|
||||||
|
echo "Android SDK found in cache volume — skipping download."
|
||||||
|
fi
|
||||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
|
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
|
||||||
yes | sdkmanager --licenses >/dev/null || true
|
yes | sdkmanager --licenses >/dev/null 2>&1 || true
|
||||||
|
# sdkmanager is a no-op for packages already present in the volume.
|
||||||
sdkmanager --install "platform-tools" \
|
sdkmanager --install "platform-tools" \
|
||||||
"platforms;android-36" "platforms;android-35" \
|
"platforms;android-36" "platforms;android-35" \
|
||||||
"build-tools;36.0.0" "build-tools;35.0.0" >/dev/null
|
"build-tools;36.0.0" "build-tools;35.0.0" >/dev/null
|
||||||
|
|
@ -73,7 +95,7 @@ jobs:
|
||||||
CAMPSCAN_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
CAMPSCAN_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./gradlew
|
chmod +x ./gradlew
|
||||||
./gradlew assembleRelease --init-script ../../ci/signing.gradle --no-daemon
|
./gradlew assembleRelease --init-script ../../ci/signing.gradle --no-daemon --build-cache
|
||||||
mkdir -p "$GITHUB_WORKSPACE/artifacts"
|
mkdir -p "$GITHUB_WORKSPACE/artifacts"
|
||||||
cp app/build/outputs/apk/release/app-release.apk \
|
cp app/build/outputs/apk/release/app-release.apk \
|
||||||
"$GITHUB_WORKSPACE/artifacts/camp-scan-${{ steps.ver.outputs.tag }}.apk"
|
"$GITHUB_WORKSPACE/artifacts/camp-scan-${{ steps.ver.outputs.tag }}.apk"
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,27 @@ The runner advertises the `docker` label; the build workflow (`.forgejo/workflow
|
||||||
2. Push a tag: `git tag v0.1.0 && git push origin v0.1.0`.
|
2. Push a tag: `git tag v0.1.0 && git push origin v0.1.0`.
|
||||||
3. The runner builds `camp-scan-v0.1.0.apk` and attaches it to a Forgejo release; Obtainium picks it up.
|
3. The runner builds `camp-scan-v0.1.0.apk` and attaches it to a Forgejo release; Obtainium picks it up.
|
||||||
|
|
||||||
|
## Build caching (fast repeat builds)
|
||||||
|
|
||||||
|
The first APK build takes ~1h (downloads Gradle + the ~2GB Android NDK, resolves
|
||||||
|
all Maven deps, compiles native modules). After that it should drop to a few
|
||||||
|
minutes because three persistent Docker volumes are reused across runs:
|
||||||
|
|
||||||
|
| Volume | Holds |
|
||||||
|
|---|---|
|
||||||
|
| `camptickets-android-sdk` → `/opt/android-sdk` | SDK, NDK, CMake |
|
||||||
|
| `camptickets-gradle` → `/root/.gradle` | Gradle dist, Maven deps, local build cache |
|
||||||
|
| `camptickets-npm` → `/root/.npm` | npm download cache |
|
||||||
|
|
||||||
|
For this to work the runner must **allow** these volumes via `config.yaml`
|
||||||
|
(`container.valid_volumes`) — `deploy-runner.sh` writes that config, pre-creates
|
||||||
|
the volumes, and starts the daemon with `--config /data/config.yaml`. If you set
|
||||||
|
the runner up by hand, copy `config.yaml` next to the compose file and add
|
||||||
|
`--config /data/config.yaml` to the daemon command.
|
||||||
|
|
||||||
|
To force a clean rebuild, remove the volumes:
|
||||||
|
`docker volume rm camptickets-android-sdk camptickets-gradle camptickets-npm`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The runner I initially registered on the app host has been removed. If Forgejo
|
- The runner I initially registered on the app host has been removed. If Forgejo
|
||||||
|
|
@ -37,3 +58,6 @@ The runner advertises the `docker` label; the build workflow (`.forgejo/workflow
|
||||||
*Settings → Actions → Runners*.
|
*Settings → Actions → Runners*.
|
||||||
- `DOCKER_GID` must match the roomy server's docker socket group, or the runner
|
- `DOCKER_GID` must match the roomy server's docker socket group, or the runner
|
||||||
can't reach the Docker daemon.
|
can't reach the Docker daemon.
|
||||||
|
- After updating to a caching-enabled runner, re-run `deploy-runner.sh` (or
|
||||||
|
`docker compose up -d` in the runner dir) so the new `config.yaml` + volumes
|
||||||
|
take effect, then push a fresh tag.
|
||||||
|
|
|
||||||
9
runner/config.yaml
Normal file
9
runner/config.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Forgejo runner config. The important bit is valid_volumes: it whitelists the
|
||||||
|
# named Docker volumes the build-apk workflow mounts as persistent caches
|
||||||
|
# (Android SDK/NDK, Gradle home, npm cache). Without these listed, the runner
|
||||||
|
# rejects the workflow's `volumes:` and the job fails.
|
||||||
|
container:
|
||||||
|
valid_volumes:
|
||||||
|
- camptickets-android-sdk
|
||||||
|
- camptickets-gradle
|
||||||
|
- camptickets-npm
|
||||||
|
|
@ -11,6 +11,9 @@ services:
|
||||||
- "${DOCKER_GID:-988}"
|
- "${DOCKER_GID:-988}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
|
# config.yaml allows the build workflow to mount the persistent cache
|
||||||
|
# volumes (valid_volumes); without it those mounts are rejected.
|
||||||
|
- ./config.yaml:/data/config.yaml:ro
|
||||||
# Runner spawns job containers via the host Docker daemon.
|
# Runner spawns job containers via the host Docker daemon.
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
|
@ -31,4 +34,4 @@ services:
|
||||||
--name "${RUNNER_NAME:-camptickets-runner}" \
|
--name "${RUNNER_NAME:-camptickets-runner}" \
|
||||||
--labels "docker:docker://node:22-bookworm"
|
--labels "docker:docker://node:22-bookworm"
|
||||||
fi
|
fi
|
||||||
exec forgejo-runner daemon
|
exec forgejo-runner daemon --config /data/config.yaml
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,24 @@ mkdir -p "$INSTALL_DIR/data"
|
||||||
# The runner image runs as uid/gid 1000 and must own its config dir.
|
# The runner image runs as uid/gid 1000 and must own its config dir.
|
||||||
chown -R 1000:1000 "$INSTALL_DIR/data"
|
chown -R 1000:1000 "$INSTALL_DIR/data"
|
||||||
|
|
||||||
|
# Runner config: allow the build workflow to mount the persistent cache volumes
|
||||||
|
# (Android SDK/NDK, Gradle home, npm cache) so APK builds don't redownload and
|
||||||
|
# recompile everything each run. Without valid_volumes listed, the runner
|
||||||
|
# rejects the workflow's `volumes:` and the job fails.
|
||||||
|
cat > "$INSTALL_DIR/config.yaml" <<'CONFIG'
|
||||||
|
container:
|
||||||
|
valid_volumes:
|
||||||
|
- camptickets-android-sdk
|
||||||
|
- camptickets-gradle
|
||||||
|
- camptickets-npm
|
||||||
|
CONFIG
|
||||||
|
chown 1000:1000 "$INSTALL_DIR/config.yaml"
|
||||||
|
|
||||||
|
# Pre-create the named cache volumes (idempotent).
|
||||||
|
docker volume create camptickets-android-sdk >/dev/null
|
||||||
|
docker volume create camptickets-gradle >/dev/null
|
||||||
|
docker volume create camptickets-npm >/dev/null
|
||||||
|
|
||||||
cat > "$INSTALL_DIR/docker-compose.yml" <<COMPOSE
|
cat > "$INSTALL_DIR/docker-compose.yml" <<COMPOSE
|
||||||
services:
|
services:
|
||||||
runner:
|
runner:
|
||||||
|
|
@ -72,6 +90,7 @@ services:
|
||||||
- "\${DOCKER_GID}"
|
- "\${DOCKER_GID}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
|
- ./config.yaml:/data/config.yaml:ro
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
env_file: .env
|
env_file: .env
|
||||||
command:
|
command:
|
||||||
|
|
@ -86,7 +105,7 @@ services:
|
||||||
--name "\${RUNNER_NAME}" \\
|
--name "\${RUNNER_NAME}" \\
|
||||||
--labels "${JOB_LABEL}"
|
--labels "${JOB_LABEL}"
|
||||||
fi
|
fi
|
||||||
exec forgejo-runner daemon
|
exec forgejo-runner daemon --config /data/config.yaml
|
||||||
COMPOSE
|
COMPOSE
|
||||||
|
|
||||||
# Preserve an existing token line if re-running without a new token.
|
# Preserve an existing token line if re-running without a new token.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue