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.
180 lines
6.7 KiB
YAML
180 lines
6.7 KiB
YAML
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
|