Compare commits
4 Commits
87a010d857
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f177c61f9 | |||
| 2e6cac3d5d | |||
| 44064b6364 | |||
| f75be3b757 |
57
.gitea/workflows/builder-image.yml
Normal file
57
.gitea/workflows/builder-image.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
name: build-android-builder-image
|
||||
|
||||
# Builds the Android toolchain image and pushes it to the Gitea registry.
|
||||
# Push a tag like `2026.06` to cut an immutable toolchain; pushes to main
|
||||
# refresh the moving `latest` tag.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["*"]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: git.helu.ca
|
||||
IMAGE: git.helu.ca/r/android
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubuntu-24.04-arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Determine which tags to push:
|
||||
# - a git tag -> use it verbatim (e.g. 2026.06) + latest
|
||||
# - main branch -> latest only
|
||||
- name: Compute tags
|
||||
id: tags
|
||||
run: |
|
||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
||||
REF="${{ gitea.ref_name }}"
|
||||
echo "tags=${IMAGE}:${REF},${IMAGE}:latest" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "tags=${IMAGE}:latest" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Log in to Gitea registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.PACKAGE_TOKEN }}
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push (linux/arm64)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
# Bump these here if you want to override the Dockerfile defaults
|
||||
# build-args: |
|
||||
# BUILD_TOOLS_VERSION=35.0.0
|
||||
# PLATFORM_VERSION=android-35
|
||||
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# --- Secrets / signing (must never be committed) ---
|
||||
*.keystore
|
||||
*.jks
|
||||
*.p12
|
||||
*.pem
|
||||
*.key
|
||||
keystore*
|
||||
*.base64
|
||||
key.properties
|
||||
local.properties
|
||||
|
||||
# --- Build / scratch ---
|
||||
*.zip
|
||||
*.tmp
|
||||
tmp/
|
||||
out/
|
||||
dist/
|
||||
|
||||
# --- OS cruft ---
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# --- Editors / IDEs ---
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# --- Logs ---
|
||||
*.log
|
||||
48
Dockerfile
Normal file
48
Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
||||
# Native linux/arm64 Android build toolchain for CI.
|
||||
# Builds release artifacts only — no emulator, no test/system images.
|
||||
# eclipse-temurin publishes a native linux/arm64 manifest, so this runs
|
||||
# without emulation on an OCI Ampere (aarch64) runner.
|
||||
FROM eclipse-temurin:21-jdk-noble
|
||||
|
||||
# --- Version pins (bump deliberately; this is what your app repos trust) ---
|
||||
# cmdline-tools: find the current build number at
|
||||
# https://developer.android.com/studio#command-line-tools-only
|
||||
ARG CMDLINE_TOOLS_VERSION=13114758
|
||||
ARG BUILD_TOOLS_VERSION=36.0.0
|
||||
ARG PLATFORM_VERSION=android-36
|
||||
|
||||
ENV ANDROID_SDK_ROOT=/opt/android-sdk \
|
||||
ANDROID_HOME=/opt/android-sdk \
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# git is needed by some Gradle plugins; unzip/curl for SDK install.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl unzip git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install the command-line tools into the canonical "latest" location.
|
||||
RUN mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools && \
|
||||
curl -fsSL -o /tmp/tools.zip \
|
||||
"https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip" && \
|
||||
unzip -q /tmp/tools.zip -d ${ANDROID_SDK_ROOT}/cmdline-tools && \
|
||||
mv ${ANDROID_SDK_ROOT}/cmdline-tools/cmdline-tools ${ANDROID_SDK_ROOT}/cmdline-tools/latest && \
|
||||
rm /tmp/tools.zip
|
||||
|
||||
ENV PATH=${PATH}:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/platform-tools
|
||||
|
||||
# Accept licenses and bake the SDK packages into the image so prod builds
|
||||
# don't depend on Google's endpoint at job time.
|
||||
# aapt2 and the build-tools binaries ship native arm64 for 35.x, so APK/AAB
|
||||
# packaging runs natively on aarch64.
|
||||
RUN yes | sdkmanager --licenses >/dev/null && \
|
||||
sdkmanager --install \
|
||||
"platform-tools" \
|
||||
"platforms;${PLATFORM_VERSION}" \
|
||||
"build-tools;${BUILD_TOOLS_VERSION}" && \
|
||||
rm -rf ${ANDROID_SDK_ROOT}/.android
|
||||
|
||||
# Sanity: fail the image build if the toolchain isn't actually usable.
|
||||
RUN java -version && sdkmanager --version && \
|
||||
test -d "${ANDROID_SDK_ROOT}/platforms/${PLATFORM_VERSION}"
|
||||
|
||||
WORKDIR /workspace
|
||||
96
README.md
96
README.md
@@ -1,3 +1,95 @@
|
||||
# android
|
||||
# android-builder
|
||||
|
||||
Android build
|
||||
Native `linux/arm64` 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). The OCI Ampere (aarch64) runner has no `/dev/kvm` (the guest VM
|
||||
boots at EL1, so KVM can't access HYP/EL2), so there's no accelerated emulator
|
||||
here — and we don't need one.
|
||||
|
||||
## What's in the image
|
||||
|
||||
- Eclipse Temurin JDK 21 (native arm64)
|
||||
- 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`
|
||||
|
||||
Pinned versions live as `ARG`s 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` |
|
||||
|
||||
`aapt2` and the build-tools binaries ship native arm64 for 36.x, so packaging
|
||||
runs without emulation.
|
||||
|
||||
## 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.06` produces `git.helu.ca/r/android:2026.06`.
|
||||
This is what app repos pin to.
|
||||
- **Moving** — pushes to `main` (and any tag build) refresh
|
||||
`git.helu.ca/r/android:latest` for convenience. Don't pin prod builds to this.
|
||||
|
||||
## Cutting a new toolchain
|
||||
|
||||
1. Update the `ARG` defaults in the `Dockerfile` if you're moving SDK/tool
|
||||
versions. Verify `CMDLINE_TOOLS_VERSION` against the link above — a stale
|
||||
number 404s the image build.
|
||||
2. Commit to `main`. The workflow builds and pushes `:latest`; confirm it's green.
|
||||
3. Tag the release with the year-month you want app repos to reference:
|
||||
```
|
||||
git tag 2026.06
|
||||
git push origin 2026.06
|
||||
```
|
||||
This publishes the immutable `git.helu.ca/r/android:2026.06`.
|
||||
4. 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:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm64
|
||||
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.
|
||||
81
templates/build.yml
Normal file
81
templates/build.yml
Normal file
@@ -0,0 +1,81 @@
|
||||
name: build-release
|
||||
|
||||
# Drop this into any Android app repo at .gitea/workflows/build.yml
|
||||
# It runs the release build inside the pinned toolchain image and publishes
|
||||
# the signed artifact. Instrumented tests are NOT run here — by design,
|
||||
# prod builds are promotions of code already tested in Dev.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"] # build prod artifacts on version tags
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gradle_task:
|
||||
description: "Release task"
|
||||
type: choice
|
||||
default: assembleRelease # APK. Use bundleRelease for a Play AAB.
|
||||
options: [assembleRelease, bundleRelease]
|
||||
|
||||
env:
|
||||
# Pin the toolchain. Bump deliberately when you roll forward.
|
||||
BUILDER_IMAGE: git.helu.ca/r/android:2026.06
|
||||
# Default task for tag-triggered builds (workflow_dispatch overrides via input)
|
||||
DEFAULT_TASK: assembleRelease
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm64
|
||||
container:
|
||||
image: git.helu.ca/r/android:2026.06
|
||||
credentials:
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.PACKAGE_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve gradle task
|
||||
id: task
|
||||
run: |
|
||||
TASK="${{ inputs.gradle_task }}"
|
||||
[ -z "$TASK" ] && TASK="${DEFAULT_TASK}"
|
||||
echo "task=$TASK" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# --- Signing ---------------------------------------------------------
|
||||
# Store the keystore as a base64-encoded Gitea Actions secret and the
|
||||
# passwords as separate secrets. Nothing sensitive lives in the repo
|
||||
# or the image. Decode at job time into a path Gradle reads.
|
||||
#
|
||||
# Create the base64 secret yourself with:
|
||||
# base64 -w0 release.keystore (copy output into secret KEYSTORE_BASE64)
|
||||
#
|
||||
# Reference KEYSTORE_PASSWORD / KEY_ALIAS / KEY_PASSWORD from your
|
||||
# signingConfig (or via -Pandroid.injected.signing.* as below).
|
||||
- name: Decode keystore
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/release.keystore"
|
||||
|
||||
- name: Build release
|
||||
run: |
|
||||
./gradlew --no-daemon ${{ steps.task.outputs.task }} \
|
||||
-Pandroid.injected.signing.store.file="$RUNNER_TEMP/release.keystore" \
|
||||
-Pandroid.injected.signing.store.password="${{ secrets.KEYSTORE_PASSWORD }}" \
|
||||
-Pandroid.injected.signing.key.alias="${{ secrets.KEY_ALIAS }}" \
|
||||
-Pandroid.injected.signing.key.password="${{ secrets.KEY_PASSWORD }}"
|
||||
|
||||
# Collects whichever artifact the task produced — apk or aab.
|
||||
- name: Collect artifact
|
||||
run: |
|
||||
mkdir -p out
|
||||
find app/build/outputs -type f \( -name "*-release.apk" -o -name "*-release.aab" \) \
|
||||
-exec cp {} out/ \;
|
||||
ls -l out
|
||||
|
||||
# Pinned to v3: upload-artifact@v4 requires a backend Gitea Actions does
|
||||
# not implement and fails with "not supported on GHES".
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: release-artifacts
|
||||
path: out/*
|
||||
if-no-files-found: error
|
||||
Reference in New Issue
Block a user