feat(runner): add Android-augmented job image (#1)

Adds opt-in Android job image so workflows like exifcleaner-web's APK build skip the ~3-5 min cold Android SDK install per run.

- runner-image-android/Dockerfile inherits forgejo-stack/job:latest, adds JDK 17 Temurin + Android cmdline-tools + platforms;android-35 + build-tools;35.0.0
- setup.sh builds forgejo-stack/job-android:latest after the main image (gated on RUNNER_BUILD_ANDROID_IMAGE, default true)
- .env.example documents the new vars; .gitignore adds .worktrees/

Consumers opt in via container: forgejo-stack/job-android:latest at the job level. Default forgejo-stack/job:latest stays slim.
This commit is contained in:
forgejo_admin 2026-05-17 15:41:18 +04:00
parent f363db42c7
commit 5c3e33ced3
4 changed files with 115 additions and 0 deletions

View file

@ -32,6 +32,8 @@ RUNNER_IMAGE=code.forgejo.org/forgejo/runner:6
RUNNER_NAME=local-runner
# Override job container image (must be available locally; see runner-image/).
# RUNNER_JOB_IMAGE=forgejo-stack/job:latest
# RUNNER_BUILD_ANDROID_IMAGE=true # set false to skip the ~5 min android image build
# RUNNER_ANDROID_JOB_IMAGE=forgejo-stack/job-android:latest
# Concurrent jobs per runner. Each slot can saturate a CPU core during build —
# size to host capacity. Default 6; set to `nproc` on a dedicated CI host, or
# lower if the box is shared with workstation use.

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ data/
docker-compose.caddy.yml
docker-compose.*.local.yml
*.swp
.worktrees/

View file

@ -0,0 +1,92 @@
# Job container image for Android workflows. Inherits forgejo-stack/job:latest
# (Node 22 + yarn + Playwright stack) and adds JDK 17 + Android SDK so
# Capacitor / Gradle workflows skip the ~3-5 min SDK install on every run.
#
# Built locally by setup.sh and tagged forgejo-stack/job-android:latest.
# Workflows opt in via `container: forgejo-stack/job-android:latest` at the
# job level — the main job image stays slim for non-Android jobs.
#
# Cold-build win vs. installing setup-java@v4 + setup-android@v3 per job:
# - JDK 17 install: ~15s saved
# - cmdline-tools download: ~30s saved
# - platforms;android-35: ~1m saved
# - build-tools;35.0.0: ~1m saved
# - License acceptance: ~30s saved
# Total saved per cold run: ~3-5 min
#
# Bump ANDROID_API_LEVEL + ANDROID_BUILD_TOOLS_VERSION when targeting newer
# Android SDKs; bump CMDLINE_TOOLS_VERSION + CMDLINE_TOOLS_REVISION when
# upstream Android cmdline-tools ships a new revision.
FROM forgejo-stack/job:latest
USER root
# JDK 17 (Temurin) via Eclipse Adoptium apt repo. Capacitor 7 / AGP 8+
# require JDK 17; JDK 21 also works but 17 is the LTS we standardise on.
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
wget gnupg ca-certificates apt-transport-https \
&& wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public \
| gpg --dearmor -o /usr/share/keyrings/adoptium.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" \
> /etc/apt/sources.list.d/adoptium.list \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
temurin-17-jdk unzip \
&& rm -rf /var/lib/apt/lists/*
ENV JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64
ENV PATH=$JAVA_HOME/bin:$PATH
# Sanity check — fail the build if JDK isn't reachable.
RUN java -version && javac -version
# Android SDK. Path matches GitHub-hosted runner convention so workflows
# written against that environment work without modification.
ENV ANDROID_HOME=/usr/local/lib/android/sdk
ENV ANDROID_SDK_ROOT=$ANDROID_HOME
# Pin the cmdline-tools revision so image rebuilds are reproducible. Bump
# both values when refreshing — the URL changes alongside the in-archive
# version label.
ARG CMDLINE_TOOLS_VERSION=11076708
ARG ANDROID_API_LEVEL=35
ARG ANDROID_BUILD_TOOLS_VERSION=35.0.0
RUN mkdir -p "$ANDROID_HOME/cmdline-tools" \
&& cd /tmp \
&& wget -q "https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip" -O cmdline-tools.zip \
&& unzip -q cmdline-tools.zip -d "$ANDROID_HOME/cmdline-tools" \
&& mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest" \
&& rm cmdline-tools.zip
ENV PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH
# Accept all SDK licenses non-interactively, then install the platform +
# build-tools the Capacitor 7 default targets (compileSdk 35).
# `yes |` pipes acceptance to every license prompt sdkmanager raises.
RUN yes | sdkmanager --licenses >/dev/null \
&& sdkmanager --update >/dev/null \
&& sdkmanager \
"platform-tools" \
"platforms;android-${ANDROID_API_LEVEL}" \
"build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \
&& chmod -R a+rX "$ANDROID_HOME"
# Sanity check — fail the build if any of the three Android tools aren't
# reachable. (aapt2 lives in build-tools; adb in platform-tools.)
RUN sdkmanager --list_installed | grep -E "platforms;android-${ANDROID_API_LEVEL}|build-tools;${ANDROID_BUILD_TOOLS_VERSION}|platform-tools"
# Note on Gradle: we don't bake Gradle itself or AGP plugins into the image.
# Consumer projects ship a Gradle wrapper (`gradlew`) that downloads the
# project-pinned Gradle version on first use, and AGP plugins are pulled
# transitively by `assembleDebug`. Workflows cache `~/.gradle` via
# actions/cache@v4 — second runs on the same host are warm. We could
# pre-warm AGP plugin metadata here, but doing it well requires coupling
# the image to a specific AGP version; not worth the maintenance burden.
# Document the toolchain versions baked in (handy for `docker inspect`).
LABEL org.metascrub.runner.jdk="17"
LABEL org.metascrub.runner.android-api-level="35"
LABEL org.metascrub.runner.android-build-tools="35.0.0"
LABEL org.metascrub.runner.cmdline-tools="11076708"

View file

@ -79,6 +79,26 @@ else
log_info "runner job image already present: $JOB_IMAGE_TAG"
fi
# 4-android. Build the Android-augmented job image. Inherits the main image
# (Node 22 + yarn + Playwright) and adds JDK 17 + Android SDK so workflows
# that opt in via `container: forgejo-stack/job-android:latest` skip the
# ~3-5 min cold Android SDK install per run. Default is opt-out for the
# base job image — non-Android workflows pay no disk cost.
#
# Set RUNNER_BUILD_ANDROID_IMAGE=false to skip (e.g. CI hosts that don't
# serve Android workflows).
ANDROID_JOB_IMAGE_TAG="${RUNNER_ANDROID_JOB_IMAGE:-forgejo-stack/job-android:latest}"
if [[ "${RUNNER_BUILD_ANDROID_IMAGE:-true}" == "true" ]]; then
if ! docker image inspect "$ANDROID_JOB_IMAGE_TAG" >/dev/null 2>&1; then
log_info "building android runner job image: $ANDROID_JOB_IMAGE_TAG (this takes ~5 min)"
docker build -t "$ANDROID_JOB_IMAGE_TAG" runner-image-android/
else
log_info "android runner job image already present: $ANDROID_JOB_IMAGE_TAG"
fi
else
log_info "skipping android runner job image (RUNNER_BUILD_ANDROID_IMAGE=false)"
fi
# 4a. Seed the hostedtoolcache volume from the job image's baked
# /opt/hostedtoolcache. Docker only auto-initialises a named volume from
# the FIRST container that mounts it; runner-1 (image forgejo/runner:6)