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. # # Getting the APK to somebody else: Gitea artifacts need an account with read # access to download, so `publish: true` also attaches the APK to a pre-release # whose assets are a plain public URL. That is the only way an outside tester # gets the file without being given an account. 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 publish: description: 'Also publish as a pre-release, for testers with no Gitea account' required: false default: false type: boolean 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/ # Deliberately NOT tagged `v*`: that pattern triggers build-release.yml, # which would run the whole three-platform release matrix and publish a # real release off a feature branch. The tag here is derived from the # branch name and carries no version, so nothing else reacts to it. # # This also cannot reach existing users. The desktop updater reads a # static latest.json from the `updater` branch, not the release list, so a # pre-release published here is invisible to anyone without the link. - name: Publish as a pre-release if: ${{ inputs.publish }} env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -e command -v jq >/dev/null || { echo "โŒ jq is required on the runner"; exit 1; } API="${GITHUB_SERVER_URL}/api/v1" REPO="${GITHUB_REPOSITORY}" TOKEN="${GITEA_TOKEN:-$AUTO_TOKEN}" BRANCH="${GITHUB_REF#refs/heads/}" TAG="test-$(echo "$BRANCH" | tr '/' '-')" # printf, not a heredoc: inside a YAML block scalar every line is # indented, and a heredoc terminator has to sit at column 0. BODY=$(printf '%s\n' \ "Test build of \`$BRANCH\` at \`${GITHUB_SHA::8}\` โ€” **not a release**." \ "" \ "Installs as **JellyTau Debug** (\`com.dtourolle.jellytau.debug\`), alongside a" \ "normal install and with its own separate data. Uninstalling it does not touch" \ "the real app." \ "" \ "Variant: \`${{ inputs.variant }}\` ยท ABI: \`${{ inputs.abi }}\`" \ "" \ "Android will warn about installing from an unknown source; that is expected" \ "for a build signed with a debug key rather than the store key.") PAYLOAD=$(jq -n \ --arg tag "$TAG" \ --arg name "Test build: $BRANCH" \ --arg body "$BODY" \ --arg target "$GITHUB_SHA" \ '{tag_name:$tag, target_commitish:$target, name:$name, body:$body, draft:false, prerelease:true}') HTTP=$(curl -sS -o resp.json -w '%{http_code}' -X POST "$API/repos/$REPO/releases" \ -H "Authorization: token $TOKEN" -H "Content-Type: application/json" -d "$PAYLOAD") if [ "$HTTP" = "201" ]; then RELEASE_ID=$(jq -r '.id' resp.json) elif [ "$HTTP" = "409" ]; then # Re-dispatching for the same branch replaces the previous APK rather # than accumulating one release per attempt. echo "โ„น๏ธ Pre-release $TAG exists; reusing it" RELEASE_ID=$(curl -fsS "$API/repos/$REPO/releases/tags/$TAG" \ -H "Authorization: token $TOKEN" | jq -r '.id') for id in $(curl -fsS "$API/repos/$REPO/releases/$RELEASE_ID/assets" \ -H "Authorization: token $TOKEN" | jq -r '.[].id'); do curl -fsS -X DELETE "$API/repos/$REPO/releases/$RELEASE_ID/assets/$id" \ -H "Authorization: token $TOKEN" >/dev/null done else echo "โŒ Failed to create pre-release (HTTP $HTTP):"; cat resp.json; exit 1 fi for f in dist/test-apk/*.apk; do echo "โฌ†๏ธ $(basename "$f")" curl -fsS -X POST \ "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$(basename "$f")" \ -H "Authorization: token $TOKEN" -F "attachment=@$f" >/dev/null done { echo "" echo "**Published:** ${GITHUB_SERVER_URL}/${REPO}/releases/tag/${TAG}" echo "" echo "Public link โ€” no Gitea account needed. Delete the release when testing is done." } >> "$GITHUB_STEP_SUMMARY" - name: Upload APK uses: actions/upload-artifact@v3 with: name: jellytau-test-apk path: dist/test-apk/ retention-days: 7