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