🏗️ Build and Test JellyTau / Run Tests (push) Successful in 29m49s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 57s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 9m25s
Traceability Validation / Check Requirement Traces (push) Successful in 17s
🏗️ Build and Test JellyTau / Android Compile Check (push) Failing after 51s
Gitea artifacts need an account with read access to download, which makes them useless for handing a build to someone outside the project -- the actual reason a test APK gets built in the first place. An optional publish input attaches the APK to a pre-release instead, whose assets are a plain public URL on a public repo. No merge to master, no MR, no version tag, and the tester needs no account. Safe from a feature branch on two counts. The tag is test-<branch> rather than v*, and only v* triggers build-release.yml, so nothing else reacts to it. And it cannot reach existing users: the desktop updater reads a static latest.json from the updater branch, not the release list. Re-dispatching the same branch replaces the APK on the existing pre-release rather than accumulating one release per attempt.
267 lines
11 KiB
YAML
267 lines
11 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.
|
||
#
|
||
# 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
|