The image pinned build-tools 36.0.0, whose aapt2/d8 are x86_64-only (Google publishes no arm64 Linux build-tools). Built for and run on arm64, packaging died with `aapt2: Syntax error: newline unexpected` (an x86_64 binary the kernel can't exec). Build the image for linux/amd64 on an amd64 runner, and point the template + README at ubuntu-24.04. The APK output is architecture-neutral, so this costs nothing downstream. Also execute aapt2 in the image sanity check so an arch mismatch fails the image build, not every app build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
android-builder
Native linux/amd64 Android build toolchain for CI, published to the Gitea
container registry at git.helu.ca/r/android.
App repos consume this image to build signed release artifacts. Instrumented tests are not part of this toolchain — by design, CI builds are promotions of code already tested in Dev (on Apple silicon, where the emulator runs natively).
Why amd64: Google publishes the Linux Android build-tools (aapt2, d8,
…) for x86_64 only — there is no arm64 Linux build-tools package. Running
the toolchain on an aarch64 runner fails at resource packaging
(aapt2: Syntax error: newline unexpected, an x86_64 binary the kernel can't
exec). The produced APK/AAB is architecture-neutral regardless of build host,
so building on amd64 costs nothing on the output side.
What's in the image
- Eclipse Temurin JDK 21 (native amd64)
- Android cmdline-tools, platform, and build-tools — baked in, so prod builds don't depend on Google's download endpoint at job time
git,curl,unzip- Node.js 24 — so Gitea Actions JS actions (
actions/checkout,upload-artifact, …) run when this image is used as a jobcontainer
Pinned versions live as ARGs at the top of the Dockerfile:
| ARG | Default | Where to check |
|---|---|---|
CMDLINE_TOOLS_VERSION |
13114758 |
https://developer.android.com/studio#command-line-tools-only |
BUILD_TOOLS_VERSION |
36.0.0 |
SDK Manager / release notes |
PLATFORM_VERSION |
android-36 |
your app's compileSdk |
The build-tools binaries (aapt2, d8) are x86_64 ELF — this is why the
image is amd64 (see "Why amd64" above).
Tagging model
The whole point of owning this image is that app repos pin a tag and a new toolchain never lands in a prod build by accident.
- Immutable — a git tag like
2026.06producesgit.helu.ca/r/android:2026.06. This is what app repos pin to. - Moving — pushes to
main(and any tag build) refreshgit.helu.ca/r/android:latestfor convenience. Don't pin prod builds to this.
Cutting a new toolchain
- Update the
ARGdefaults in theDockerfileif you're moving SDK/tool versions. VerifyCMDLINE_TOOLS_VERSIONagainst the link above — a stale number 404s the image build. - Commit to
main. The workflow builds and pushes:latest; confirm it's green. - Tag the release with the year-month you want app repos to reference:
This publishes the immutable
git tag 2026.06 git push origin 2026.06git.helu.ca/r/android:2026.06. - Roll app repos forward deliberately by bumping their pinned tag (see below) — one at a time if you want to validate, all at once if you trust the bump.
Using it in an app repo
Drop .gitea/workflows/build.yml (the template alongside this repo) into the
app and pin the toolchain:
jobs:
build:
runs-on: ubuntu-24.04
container:
image: git.helu.ca/r/android:2026.06
credentials:
username: ${{ gitea.actor }}
password: ${{ secrets.PACKAGE_TOKEN }}
The build task is selectable: assembleRelease (APK, the default) or
bundleRelease (AAB — only needed for Google Play distribution).
Required secrets in each app repo (or org-level)
Signing happens at job time; nothing sensitive lives in the repo or the image.
| Secret | What it is |
|---|---|
KEYSTORE_BASE64 |
release keystore, base64-encoded: base64 -w0 release.keystore |
KEYSTORE_PASSWORD |
keystore password |
KEY_ALIAS |
signing key alias |
KEY_PASSWORD |
key password |
PACKAGE_TOKEN (a PAT, the same secret the other repos here use) needs
write:package in this repo (to push the image) and read:package in app
repos (to pull it). Set it as a repo or org-level Actions secret. The
built-in gitea.token is not used because it isn't scoped for the registry.
First-run sequencing
The image must exist in the registry before any app workflow can pull it.
So: cut and push 2026.06 from this repo first, confirm it's in the registry,
then the app workflows that pin it will resolve.