ci: build a test APK from any branch on demand
build-release.yml is tag-driven, builds three platforms and then creates a release -- none of which is what you want from a feature branch, and there was otherwise no way to get an installable build out of CI without cutting one. workflow_dispatch only, deliberately. The runner has a single slot shared with two other projects, so an APK on every feature-branch commit would starve them; dispatch it when you actually want to install something. Defaults to the R8-minified release build in the debug applicationId slot rather than a plain debug APK. Minification is where Android releases have actually broken here (R8 stripping JNI-loaded player and security classes), and a debug build cannot catch it. Neither variant needs the real signing key, and both install side by side with a real install. Builds through scripts/build-android.sh rather than a hand-rolled tauri invocation, so CI and a developer's machine produce the same thing and the script's applicationId assertion still runs. Shares the existing cargo registry cache key -- no fourth copy of the registry on a disk that has filled before.
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
name: '📱 Test APK'
|
||||
|
||||
# An installable APK from any branch, on demand, without cutting a release.
|
||||
#
|
||||
# Why this exists separately from build-release.yml: that workflow is tag-driven,
|
||||
# builds Linux + Windows + Android and then *creates a release*, which is not
|
||||
# what you want from a feature branch. This builds one Android APK from whatever
|
||||
# ref you dispatch it on and hands it back as an artifact.
|
||||
#
|
||||
# Deliberately `workflow_dispatch` only — no push trigger. The runner has a
|
||||
# single slot shared with two other projects, so a build on every feature-branch
|
||||
# commit would starve everything else. Dispatch it when you actually want to
|
||||
# install something.
|
||||
#
|
||||
# Both variants install as com.dtourolle.jellytau.debug ("JellyTau Debug"),
|
||||
# side by side with a real install and with their own data directory. Neither
|
||||
# needs the release signing key.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
variant:
|
||||
description: 'Which build to produce'
|
||||
required: true
|
||||
default: 'side-by-side-release'
|
||||
type: choice
|
||||
options:
|
||||
# R8-minified, exactly what ships, in the debug slot. Use this unless
|
||||
# you need stack traces: R8 stripping JNI-loaded classes has broken
|
||||
# release APKs here before, and a plain debug build cannot catch it.
|
||||
- side-by-side-release
|
||||
# Unminified. Faster, readable stack traces, but does not exercise
|
||||
# minification at all.
|
||||
- debug
|
||||
abi:
|
||||
description: 'Target ABI'
|
||||
required: true
|
||||
default: 'aarch64'
|
||||
type: choice
|
||||
options:
|
||||
- aarch64
|
||||
- armv7
|
||||
- x86_64
|
||||
|
||||
concurrency:
|
||||
# One test build at a time; a newer dispatch supersedes an in-flight one.
|
||||
group: build-test-apk
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Incremental state is never reused between CI runs -- pure disk cost.
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build test APK (${{ inputs.variant }}, ${{ inputs.abi }})
|
||||
runs-on: linux/amd64
|
||||
container:
|
||||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
|
||||
env:
|
||||
ANDROID_HOME: /opt/android-sdk
|
||||
ANDROID_SDK_ROOT: /opt/android-sdk
|
||||
ANDROID_NDK_HOME: /opt/android-sdk/ndk/27.0.11902837
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# set-version.sh derives a dev version from `git describe --tags`, so
|
||||
# the tags have to be here. A shallow checkout yields 0.0.0.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
# Registry only -- never src-tauri/target. Same reasoning (and the
|
||||
# same key) as every other job: that directory is ~16 GB and caching
|
||||
# it filled the runner's 74 GB disk. Sharing the key means this
|
||||
# workflow restores what the others saved rather than adding a
|
||||
# fourth copy of the registry.
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-registry-
|
||||
|
||||
- name: Cache Node dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
# Before `android init`: it derives the generated project (including the
|
||||
# initial versionCode) from tauri.conf.json.
|
||||
- name: Stamp a dev version
|
||||
run: ./scripts/set-version.sh
|
||||
|
||||
- name: Initialize Android project
|
||||
run: bun run tauri android init
|
||||
|
||||
# Again after init: tauri.properties only exists now, and its
|
||||
# autogenerated versionCode is neither large enough nor monotonic against
|
||||
# the 1000 floor already shipped. On a branch this derives from
|
||||
# `git describe`, so a test APK always sorts above the last release.
|
||||
- name: Pin a monotonic Android versionCode
|
||||
run: ./scripts/set-version.sh
|
||||
|
||||
# Built through the same script used locally, rather than a hand-rolled
|
||||
# gradle/tauri invocation. That is what keeps CI and a developer's machine
|
||||
# producing the same thing -- and the script asserts the applicationId the
|
||||
# APK actually carries, which has silently regressed before.
|
||||
- name: Build APK
|
||||
run: |
|
||||
if [ "${{ inputs.variant }}" = "side-by-side-release" ]; then
|
||||
./scripts/build-android.sh release --debug --abi "${{ inputs.abi }}"
|
||||
else
|
||||
./scripts/build-android.sh debug --abi "${{ inputs.abi }}"
|
||||
fi
|
||||
|
||||
- name: Collect APK
|
||||
id: collect
|
||||
run: |
|
||||
mkdir -p dist/test-apk
|
||||
if [ "${{ inputs.variant }}" = "side-by-side-release" ]; then
|
||||
PATTERN='*-release.apk'
|
||||
else
|
||||
PATTERN='*-debug.apk'
|
||||
fi
|
||||
APK=$(find src-tauri/gen/android/app/build/outputs/apk -name "$PATTERN" | head -1)
|
||||
if [ -z "$APK" ]; then
|
||||
echo "❌ No APK produced for variant ${{ inputs.variant }}"
|
||||
find src-tauri/gen/android/app/build/outputs/apk -name '*.apk' || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REF_NAME=$(echo "${GITHUB_REF#refs/heads/}" | tr '/' '-')
|
||||
OUT="dist/test-apk/jellytau-${REF_NAME}-${GITHUB_SHA::8}-${{ inputs.variant }}.apk"
|
||||
cp "$APK" "$OUT"
|
||||
|
||||
# Report what the thing actually is, not what it was meant to be.
|
||||
APKSIGNER=$(find "$ANDROID_SDK_ROOT/build-tools" -name apksigner | sort -V | tail -1)
|
||||
"$APKSIGNER" verify --print-certs "$OUT" || echo "⚠️ Could not verify signature"
|
||||
|
||||
{
|
||||
echo "### 📱 Test APK"
|
||||
echo ""
|
||||
echo "| | |"
|
||||
echo "|---|---|"
|
||||
echo "| Branch | \`${GITHUB_REF#refs/heads/}\` |"
|
||||
echo "| Commit | \`${GITHUB_SHA::8}\` |"
|
||||
echo "| Variant | \`${{ inputs.variant }}\` |"
|
||||
echo "| ABI | \`${{ inputs.abi }}\` |"
|
||||
echo "| Size | $(du -h "$OUT" | cut -f1) |"
|
||||
echo "| SHA256 | \`$(sha256sum "$OUT" | cut -d' ' -f1)\` |"
|
||||
echo ""
|
||||
echo "Installs as \`com.dtourolle.jellytau.debug\` — side by side with a real"
|
||||
echo "install, with its own data directory. Download the artifact, then:"
|
||||
echo ""
|
||||
echo '```'
|
||||
echo "adb install -r $(basename "$OUT")"
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
ls -lah dist/test-apk/
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jellytau-test-apk
|
||||
path: dist/test-apk/
|
||||
retention-days: 7
|
||||
@@ -92,6 +92,24 @@ That means:
|
||||
Follow the right log stream with `./scripts/logcat.sh [debug|release]`
|
||||
(defaults to debug).
|
||||
|
||||
### Getting a test APK out of CI
|
||||
|
||||
`.gitea/workflows/build-test-apk.yml` builds one from **any branch, on demand**
|
||||
— run it from Gitea's Actions tab (`workflow_dispatch`) against the ref you want.
|
||||
It is not a release: nothing is tagged, published, or signed with the real key.
|
||||
|
||||
Two variants, both installing into the `com.dtourolle.jellytau.debug` slot:
|
||||
|
||||
| Variant | What it is | When |
|
||||
|---------|-----------|------|
|
||||
| `side-by-side-release` (default) | R8-minified, exactly what ships, signed with the debug keystore | Almost always — a plain debug build cannot catch R8 stripping JNI-loaded classes, which has broken release APKs here before |
|
||||
| `debug` | Unminified | When you need readable stack traces |
|
||||
|
||||
There is deliberately **no push trigger**: the runner has one slot shared with
|
||||
two other projects, so building on every feature-branch commit would starve
|
||||
them. The APK lands as the `jellytau-test-apk` artifact (7-day retention), named
|
||||
for the branch and short SHA, with its size and SHA256 in the run summary.
|
||||
|
||||
### Key Files
|
||||
|
||||
Player-related Kotlin files:
|
||||
|
||||
Reference in New Issue
Block a user