Compare commits

..

7 Commits

Author SHA1 Message Date
8a05323905 docs: add reusable Heluca Google Play publishing guide
Playbook for taking any Heluca app from repo to Google Play, learned from
shipping Dade to internal testing: one-time publisher account + upload keystore
setup, per-app checklist (signing via -Pandroid.injected.signing.*, AAB,
versionCode encoding, native symbols), internal-testing flow, gotchas, and
production promotion. Bakes in Heluca conventions and documents where Dade
deliberately diverges (custom signingConfig, UPLOAD_* secret names, :latest pin)
so the next app can follow the template instead.

Link it from the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 13:52:15 -04:00
605df9bdbf build: target amd64 — Google ships no arm64 Linux build-tools
All checks were successful
build-android-builder-image / build-image (push) Successful in 2m49s
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>
2026-06-21 22:53:07 -04:00
7556e9b547 build: install Node 24 so JS actions run in the job container
All checks were successful
build-android-builder-image / build-image (push) Successful in 2m30s
When this image is used as a Gitea Actions `container:`, the runner exec's
`node` inside it to run JS-based actions (actions/checkout, upload-artifact).
The image had no Node, so consuming app builds failed at the checkout step
with `exec: "node": executable file not found in $PATH`.

Pull Node 24 from NodeSource (Noble's apt nodejs is v18) and assert it in
the image sanity check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 17:58:54 -04:00
9f177c61f9 Merge pull request 'ci: wire up the builder-image workflow and fix repo layout' (#1) from ci/wire-up-builder-image into main
All checks were successful
build-android-builder-image / build-image (push) Successful in 1m44s
2026-06-16 12:54:14 +00:00
2e6cac3d5d ci: use PACKAGE_TOKEN for the registry login
GITEA_TOKEN is not set in these repos — the registry login sent a blank
password and Gitea's /v2/ endpoint returned unauthorized. Switch the
image-build login, the app template, and the README to PACKAGE_TOKEN, the
PAT the other repos already use (Syrinx pairs gitea.actor + PACKAGE_TOKEN
successfully). The PAT needs write:package here and read:package in app
repos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:53:31 -04:00
44064b6364 ci: wire up the builder-image workflow and fix repo layout
Some checks failed
build-android-builder-image / build-image (push) Failing after 9s
The repo was committed with the CI files misnamed/misplaced, so nothing
ran. Move them into place and fix the issues that surfaced against the
Syrinx reference and the Dade target:

- Dockerfile.yml -> Dockerfile (the workflow references ./Dockerfile)
- builder-image.yml -> .gitea/workflows/builder-image.yml (Gitea only
  runs files under .gitea/workflows)
- build.yml -> templates/build.yml (it's the app-repo template, not a
  workflow for this repo; keep it out of the run path)
- runs-on [self-hosted, arm64] -> ubuntu-24.04-arm64 (the real Gitea
  label, matching Syrinx) in the workflow, template, and README
- bump image SDK android-35/build-tools 35.0.0 -> android-36/36.0.0 to
  match Dade's compileSdk 36 (avoids a job-time SDK download)
- template upload-artifact@v4 -> @v3 (v4 is unsupported on Gitea)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:03:43 -04:00
f75be3b757 docs: rewrite README with comprehensive toolchain usage guide 2026-06-15 07:33:57 -04:00
6 changed files with 580 additions and 2 deletions

View File

@@ -0,0 +1,59 @@
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:
# amd64: Google ships the Linux build-tools (aapt2, d8) for x86_64 only,
# so the toolchain must be built for and run on amd64.
runs-on: ubuntu-24.04
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/amd64)
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
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
View 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

61
Dockerfile Normal file
View File

@@ -0,0 +1,61 @@
# Native linux/amd64 Android build toolchain for CI.
# Builds release artifacts only — no emulator, no test/system images.
# Must be amd64: Google publishes the Linux build-tools (aapt2, d8, …) for
# x86_64 only — there is no arm64 Linux build-tools package — so packaging
# cannot run natively on an aarch64 runner. The APK output is architecture-
# neutral regardless of the build host.
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/*
# Node is required for Gitea Actions JS actions (actions/checkout,
# upload-artifact, ...) to run when this image is used as a job container —
# the runner exec's `node` inside the container. Noble's apt nodejs is v18;
# pull the current Node 24 from NodeSource instead.
ARG NODE_MAJOR=24
RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - && \
apt-get install -y --no-install-recommends nodejs && \
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. The build-tools binaries
# (aapt2, d8) are x86_64 ELF — this is why the image must be amd64.
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.
# Execute aapt2 (not just --version a tool) so an arch mismatch in the
# build-tools binaries fails the image build here, not in every app build.
RUN java -version && sdkmanager --version && node --version && \
"${ANDROID_SDK_ROOT}/build-tools/${BUILD_TOOLS_VERSION}/aapt2" version && \
test -d "${ANDROID_SDK_ROOT}/platforms/${PLATFORM_VERSION}"
WORKDIR /workspace

107
README.md
View File

@@ -1,3 +1,106 @@
# android
# android-builder
Android build
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).
**Publishing to Google Play?** See [`docs/google-play-publishing.md`](docs/google-play-publishing.md)
— the reusable Heluca playbook (publisher setup, upload keystore, per-app
checklist, internal testing, promotion to production), learned from shipping Dade.
**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 job `container`
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` |
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.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
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.

View File

@@ -0,0 +1,240 @@
# Publishing a Heluca Android app to Google Play
The reusable playbook for taking a Heluca app from repo to Google Play, learned
from shipping **Dade** (`r/dade`) to internal testing. It covers the whole
lifecycle: the one-time publisher setup, the per-app checklist, and promotion to
production — with Heluca's conventions (this `r/android` toolchain, Gitea CI,
Play App Signing) baked in.
Authority order for anything ambiguous: **live app config > this guide > memory**.
When an app diverges from the convention here, that's allowed — but note it in the
app's own README so the next person knows it's deliberate. Dade's divergences are
called out throughout (see *Where Dade diverges*).
---
## 0. Concepts you need once
- **Two keys, one you hold.** Under **Play App Signing** (use it — it's the
default and lets Google reset a lost upload key), you generate an **upload key**
and sign your bundle with it; Google re-signs with the **app signing key** it
holds. You never see the app key.
- **The upload key is publisher-wide, not per-app.** One keystore signs every
Heluca app — Play keys an app by `applicationId`, not by signing key. Generate it
once (below), reuse it forever. Don't mint a new one per app.
- **Play wants an `.aab`, not an APK.** App Bundles only. That means
`bundleRelease` (or a flavored `bundle<Flavor>Release`), never `assembleRelease`,
for anything going to Play. (`assembleRelease` APKs remain useful for direct
`adb install` sideloading — Dade keeps a separate workflow for exactly that.)
- **`versionCode` is a bare integer Play compares; `versionName` is the human
label.** Every upload needs a strictly-greater `versionCode`. Heluca convention:
**encode the semver** as `MAJOR*10000 + MINOR*100 + PATCH` so the two track each
other (`0.0.1``1`, `1.2.3``10203`). Bump both together per release.
- **One listing per `applicationId`.** If an app ships per-environment flavors with
distinct ids (Dade: `ca.helu.daedalus` prod, `ca.helu.daedalus.dev` dev), each is
a **separate Play app** with its own listing, track, and opt-in link.
---
## 1. Publisher account — one-time for all of Heluca
Done once; every future app reuses it.
1. **Register** at https://play.google.com/console with a long-lived Google
account (not a throwaway). **$25 one-time** fee.
2. **Account type:** Personal is adequate — internal testing isn't gated either
way. Organization needs a D-U-N-S number and unlocks skipping the
new-personal-account production testing gate; only worth it if you'll push to
production broadly.
3. **Identity verification** (2026 rules): government ID, possibly a selfie and an
address document. Can take a few hours to a couple of days.
4. **Device verification** (personal accounts): install the **Play Console** app
on a real Android phone and confirm it before you can publish.
## 2. Generate the upload keystore — one-time for all of Heluca
Lives in `~/.android` (alongside the Android debug keystore) — out of your home
root, easy to include in backups. **Back it up and store the password in your
vault.** JDK 17+ `keytool` prompts for a single password and reuses it for both
the store and the key, so the two password secrets later hold the same value.
```bash
keytool -genkeypair -v -keystore ~/.android/heluca-upload.jks -alias heluca-upload \
-keyalg RSA -keysize 4096 -validity 10000 \
-dname "CN=Heluca, O=helu.ca, C=CA"
```
`-dname` is cosmetic under Play App Signing (Google re-signs), so `CN=Heluca` is
fine and consistent across apps.
Encode it once for CI secrets:
```bash
base64 -i ~/.android/heluca-upload.jks | pbcopy # macOS
# base64 -w0 ~/.android/heluca-upload.jks # Linux (one line, no wrap)
```
---
## 3. Per-app checklist — repeat for each new app
Assumes the account (§1) and upload keystore (§2) already exist.
### 3a. Gradle — signing + AAB + versionCode
The **Heluca convention** is the `r/android` template's approach: **inject signing
at job time via `-Pandroid.injected.signing.*`, no `signingConfig` in
`build.gradle.kts` at all.** This keeps the build file clean and the keystore
purely a CI concern. See `templates/build.yml` in this repo.
Set the versionCode encoding in `defaultConfig`:
```kotlin
defaultConfig {
// MAJOR*10000 + MINOR*100 + PATCH — versionName 0.0.1 -> versionCode 1
versionCode = 1
versionName = "0.0.1"
}
```
Nothing else is required for signing if you use `-Pandroid.injected.signing.*`.
### 3b. Native debug symbols (only if the app bundles `.so` libraries)
If the app ships native libraries (JNI, ONNX, a VAD lib, etc.), Play warns that
the `.aab` has no native debug symbols and native crashes show raw addresses. Fix
it in the release build type:
```kotlin
buildTypes {
release {
ndk { debugSymbolLevel = "SYMBOL_TABLE" } // function names; FULL adds line numbers
}
}
```
AGP embeds the symbols in the `.aab`; Play consumes them on upload. Use
`SYMBOL_TABLE` for prebuilt deps you don't build from source; `FULL` only if you
need line numbers. Pure-Kotlin/Java apps can skip this.
### 3c. Minification & the mapping file
If `isMinifyEnabled = true`, upload the R8 mapping file so Play deobfuscates
crash traces (AGP attaches it to the bundle automatically). If minification is
**off**, Play still prints a generic "no deobfuscation file" warning — **ignore
it**, there's nothing to upload.
### 3d. CI secrets (per app repo, or org-level)
Following the `r/android` convention, add these Gitea Action Secrets to the app
repo (**Settings → Actions → Secrets**). Org-level secrets avoid re-adding them
per app.
| Secret | Value |
|--------|-------|
| `KEYSTORE_BASE64` | `base64 -w0 ~/.android/heluca-upload.jks` (the whole `.jks` as text) |
| `KEYSTORE_PASSWORD` | the keystore password |
| `KEY_ALIAS` | `heluca-upload` |
| `KEY_PASSWORD` | same as the keystore password (JDK 17 unifies them) |
| `PACKAGE_TOKEN` | PAT with `read:package` (to pull the toolchain image) |
### 3e. CI workflow
Drop `templates/build.yml` from this repo into the app at
`.gitea/workflows/build.yml`. It runs inside the pinned toolchain image, decodes
the keystore, and builds the signed artifact. For a Play app, run it with the
`bundleRelease` task (the template exposes a `workflow_dispatch` choice; flavored
apps need `bundle<Flavor>Release` — adjust the task/matrix accordingly).
**Pin the toolchain to an immutable tag** (`git.helu.ca/r/android:2026.06`), never
`:latest`, for anything whose output you'll ship. See this repo's README →
*Tagging model*.
### 3f. Create the Play app(s) & upload
Per `applicationId` (one app, or one per flavor):
1. Play Console → **Create app** → name, language, app/game, free/paid, accept
declarations.
2. Complete the **App content** declarations (privacy policy URL, data safety, ads,
content rating, target audience). Internal testing tolerates a minimal honest
pass.
3. **Testing → Internal testing → Create new release** → upload the `.aab`
(download the CI artifact; it's zipped by Gitea's transport — unzip to get the
raw `app-*-release.aab`). Enable **Play App Signing** when prompted.
4. Add release notes ("What's new," ≤500 chars — describe the app for a first-time
tester, not the CI plumbing). **Save → Review release → Start rollout.**
### 3g. Add testers & install
1. Internal testing → **Testers** tab → create an email list → add the Google
account(s) on your phones.
2. **Copy link** (the opt-in URL) → open it **on the phone**, signed into that same
account → accept → the Play listing's **Install** appears.
3. Repeat the opt-in per app (each `applicationId` has its own link). Flavored apps
install side by side.
---
## 4. Gotchas (all learned the hard way on Dade)
- **"Item isn't available" right after opting in is normal propagation lag** —
1030 min typical, up to a couple of hours on a brand-new app's first release,
even when the console says "Available to internal testers." Not a
misconfiguration. Confirm the release status, confirm the **exact tester account**
is active in the phone's Play Store, then wait.
- **"Not reviewed" / "(unreviewed)" temporary app name does NOT block internal
testing** — that track is review-exempt. It clears on its own.
- **No install notification.** Google doesn't ping you that a testing app/build
exists. First install is always via the opt-in link. Updates auto-arrive (if
auto-update is on) or via Play Store → Manage apps & device → Update — no reliable
per-app notification for testing tracks.
- **Each upload needs a higher `versionCode`** or Play rejects the duplicate. The
encoding in §0 makes this mechanical.
- **Same Google account** on the device as in the tester list, or the opt-in link
404s / "not available."
- **The AAB is already a zip; Gitea's artifact download re-zips it** — the outer
`.zip` is ~the same size because you can't re-compress compressed data. Unzip once
and upload the inner `.aab`. Harmless.
---
## 5. Promoting to production
Internal testing (≤100 testers, no review gate) is where Heluca apps live for
personal use. To go public:
1. **Closed/open testing first if required.** New *personal* accounts must run a
closed test with **≥12 testers for ≥14 days** before production unlocks.
Organization accounts skip this. Internal testing does **not** count toward it.
2. **Full store listing + all App content declarations** must be complete (they're
optional-ish for internal, mandatory for production).
3. **Production track → Create release** → promote the same `.aab` (or a newer one)
→ it goes through Google **review** (hours to days) before rollout.
4. Consider a **staged rollout** (start at a small % of users).
For a personal, off-public-store app, you may never leave internal testing — that's
a valid end state, not a stepping stone.
---
## Where Dade diverges from this convention
Dade shipped before this guide was written and differs in a few deliberate ways.
Documented here so the next app can decide to follow the template (recommended) or
knowingly match Dade.
| Aspect | Convention (this guide / `templates/build.yml`) | Dade |
|--------|--------------------------------------------------|------|
| Signing injection | `-Pandroid.injected.signing.*` at job time, no `signingConfig` in Gradle | Custom `signingConfig` reading `DADE_KEYSTORE*` env vars |
| Secret names | `KEYSTORE_BASE64` / `KEYSTORE_PASSWORD` / `KEY_ALIAS` / `KEY_PASSWORD` | `UPLOAD_KEYSTORE_BASE64` / `UPLOAD_KEYSTORE_PASSWORD` / `UPLOAD_KEY_PASSWORD` |
| Toolchain pin | Immutable `:2026.06` | Moving `:latest` (README flags this to pin later) |
| Workflow | One `templates/build.yml`, task selectable | Two: `build-apks.yml` (unsigned APKs, sideload) + `build-bundles.yml` (signed AABs, Play) |
Dade's split (separate unsigned-APK and signed-AAB workflows) is worth keeping for
an app you also sideload during dev. A **new app that only targets Play** should
follow the single-template convention and standard secret names — less to maintain,
and org-level secrets work across repos when the names match.
**New app? Prefer the convention in §3.** Reach for Dade's pattern only when you
have Dade's reason (per-env flavors + a parallel sideload path).

83
templates/build.yml Normal file
View File

@@ -0,0 +1,83 @@
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:
# amd64: the toolchain image carries x86_64 Android build-tools (Google
# ships no arm64 Linux build-tools), so run it on an amd64 runner.
runs-on: ubuntu-24.04
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