Compare commits

..
Author SHA1 Message Date
dtourolle c44070e720 fix(ci): don't use bash-only syntax in the APK workflow
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 24m28s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 55s
📱 Test APK / Build test APK (push) Successful in 51m15s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 9m8s
Traceability Validation / Check Requirement Traces (push) Successful in 15s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 7m9s
The runner hands `run:` blocks to sh (dash), where `${GITHUB_SHA::8}` is
not substring expansion but a syntax error. It failed as

    /var/run/act/workflow/9.sh: 29: Bad substitution

which names a temp file and no line of the workflow, after a 51-minute
build that had already produced a correctly signed APK and only needed to
write a summary table.

Two changes, either of which would have been enough, because this is a
silly way to lose an hour:

- The job declares `shell: bash`, so the rest of the file's assumptions
  hold and a future bash-ism does not resurface this.
- The short SHA is computed once with `cut` in the resolve step and read
  as a step output, so the two places that wanted it no longer depend on
  which shell runs them at all.

Everything before this point is confirmed working from the same run: the
SDK is found, the Rust cross-compile completes, gradle mints a debug
keystore, R8 runs, and apksigner reports "CN=Android Debug" -- the
side-by-side signing this workflow is supposed to produce.
2026-09-05 16:25:29 +02:00
dtourolle 8ecf74a2af fix(ci): let the Android build scripts respect a caller-set ANDROID_HOME
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 25m24s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 54s
📱 Test APK / Build test APK (push) Failing after 50m57s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 9m47s
Traceability Validation / Check Requirement Traces (push) Successful in 25s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 7m18s
build-android.sh hardcoded `export ANDROID_HOME="$HOME/Android/Sdk"`,
which discarded whatever the caller had set. In the builder image the SDK
is at /opt/android-sdk and the job exports exactly that, so the script
looked for an NDK under /root/Android/Sdk, found nothing, and the build
died a minute in with "Android SDK not found" -- the first automatic
`latest` APK build failed on this and nothing else.

Now a default rather than an override, matching what
test-player-conformance.sh already did. NDK_HOME likewise prefers an
explicitly pinned ANDROID_NDK_HOME over guessing with `ls | head -1`,
which is how CI pins an exact NDK revision.

A missing SDK now fails immediately and says which variable to set,
instead of letting `ls` print its own error and the real failure surface
a minute later inside the tauri CLI.

android-dev.sh had the same override and gets the same treatment.
2026-09-05 14:33:33 +02:00
dtourolle 5fb9c1ff3b ci: publish a rolling latest APK on every push to master
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 25m52s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 46s
📱 Test APK / Build test APK (push) Failing after 1m14s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 7m54s
Traceability Validation / Check Requirement Traces (push) Successful in 18s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 6m26s
The test-APK workflow was dispatch-only, so merging to master produced no
APK at all -- there was nothing to hand a tester without pressing a button
first, which is not what a "latest build" means.

Pushes to master now refresh a `latest` pre-release in place. Both the tag
and the asset name are stable, so the download URL never changes and a
link given to a tester once keeps serving the current build. Release
assets are public; Actions artifacts need an account, which is what made
them useless for this.

It stays the side-by-side variant: R8-minified like a real release, so it
still exercises the minification that has broken Android builds here
before, but signed with the debug keystore under the `.debug`
applicationId. A bad master commit therefore cannot replace anyone's
working install, and the production signing key stays in the tag-driven
release workflow.

Event handling is resolved in one step rather than read raw at each use.
A push carries no dispatch inputs -- every `github.event.inputs.*` is
empty on that event -- so the variant and ABI need real defaults, and the
publish decision differs by event. Doing it once means the build, collect
and publish steps cannot disagree about what the run is.

Known gap, documented rather than hidden: this builds in parallel with
build-and-test.yml, so `latest` can carry a commit whose tests later fail.
Cross-workflow dependencies are not reliably available here and
duplicating the test job would double an already hour-long queue on a
single-slot runner.
2026-09-05 13:42:33 +02:00
dtourolle b5a3a3b427 fix(ci): make the test-APK dispatch inputs work and stop it always publishing
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 26m0s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 4m3s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 8m59s
Traceability Validation / Check Requirement Traces (push) Successful in 28s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 10m45s
Two defects in a workflow that had never actually run.

The publish guard was `if: ${{ inputs.publish }}`. A dispatch input arrives
as a *string*, and every non-empty string is truthy in the expression
language, so "false" is truthy too -- the workflow would have published a
public pre-release on every run, including the ones where the box was
deliberately left unticked. Now compared against 'true' explicitly.

The bare `inputs.*` context is also newer than `github.event.inputs.*` and
no other workflow here uses either, so nothing proved the short form works
on this Gitea. Switched to the form both Gitea and GitHub have supported
throughout; an unresolved context would have silently expanded to an empty
string, sending every build down the `debug` branch and then failing to
find a `*-debug.apk`.
2026-09-03 20:19:59 +02:00
4 changed files with 214 additions and 99 deletions
+152 -70
View File
@@ -1,27 +1,39 @@
name: '📱 Test APK'
# An installable APK from any branch, on demand, without cutting a release.
# Installable Android builds that are not releases.
#
# 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.
# Two ways in:
#
# 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.
# push to master -> refreshes the rolling `latest` pre-release, so there is
# always a current APK behind one stable URL that can be
# handed to a tester once and never re-sent.
# workflow_dispatch -> builds any branch on demand, optionally publishing it
# as `test-<branch>`.
#
# 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.
# Why this is separate from build-release.yml: that workflow is tag-driven,
# builds Linux + Windows + Android and creates a real release. This produces one
# APK and never touches the release channel.
#
# What comes out installs as com.dtourolle.jellytau.debug ("JellyTau Debug"),
# side by side with a real install and with its own data directory. It is a
# fully R8-minified release build -- minification is where Android builds have
# actually broken here (R8 stripping JNI-loaded player and security classes),
# and a plain debug build cannot catch that -- but it is signed with the debug
# keystore rather than the store key. So a bad master commit can never replace
# somebody's working install, and the production signing key stays in the
# tag-driven workflow where it belongs.
#
# 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.
# access to download, so published builds are attached 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:
push:
branches:
- master
paths-ignore:
- '**/*.md'
workflow_dispatch:
inputs:
variant:
@@ -30,9 +42,7 @@ on:
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.
# R8-minified, exactly what ships, in the debug slot.
- side-by-side-release
# Unminified. Faster, readable stack traces, but does not exercise
# minification at all.
@@ -47,13 +57,15 @@ on:
- armv7
- x86_64
publish:
description: 'Also publish as a pre-release, for testers with no Gitea account'
description: 'Also publish as a pre-release (automatic on master)'
required: false
default: false
type: boolean
concurrency:
# One test build at a time; a newer dispatch supersedes an in-flight one.
# One APK build at a time, and a newer push supersedes an in-flight one — so a
# burst of commits to master costs one build, not one per commit. This matters:
# the runner has a single slot shared with two other projects.
group: build-test-apk
cancel-in-progress: true
@@ -63,8 +75,15 @@ env:
jobs:
build:
name: Build test APK (${{ inputs.variant }}, ${{ inputs.abi }})
name: Build test APK
runs-on: linux/amd64
defaults:
run:
# This runner executes `run:` blocks with `sh` (dash) unless told
# otherwise, so bash-only syntax fails with a bare "Bad substitution"
# naming a temp file and no line of your workflow. Say bash explicitly.
# The short-SHA output below avoids depending on it regardless.
shell: bash
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
env:
@@ -79,6 +98,69 @@ jobs:
# the tags have to be here. A shallow checkout yields 0.0.0.
fetch-depth: 0
# One place decides what this run is, so the build, the collect step and
# the publish step cannot disagree about it. A push carries no dispatch
# inputs at all -- every `github.event.inputs.*` is empty on that event --
# so each value needs an explicit default rather than being read raw.
- name: Resolve build parameters
id: cfg
run: |
set -e
VARIANT="${{ github.event.inputs.variant }}"
ABI="${{ github.event.inputs.abi }}"
PUBLISH="${{ github.event.inputs.publish }}"
BRANCH="${GITHUB_REF#refs/heads/}"
VARIANT="${VARIANT:-side-by-side-release}"
ABI="${ABI:-aarch64}"
# A push to master always publishes -- that is the whole point of a
# rolling `latest`. A dispatch publishes only if asked. Compared
# against the string 'true' rather than used as a bare truthiness
# test: dispatch inputs arrive as strings, and every non-empty string
# is truthy, so `if: inputs.publish` would publish even when the box
# was deliberately left unticked.
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
PUBLISH=true
elif [ "$PUBLISH" = "true" ]; then
PUBLISH=true
else
PUBLISH=false
fi
# Master is the rolling channel and keeps one stable tag, so the
# download URL a tester was given keeps working. Anything else gets
# its own branch-scoped tag.
if [ "$BRANCH" = "master" ]; then
TAG="latest"
RELEASE_NAME="Latest build (master)"
else
TAG="test-$(echo "$BRANCH" | tr '/' '-')"
RELEASE_NAME="Test build: $BRANCH"
fi
# Stable asset name for the same reason the tag is stable.
ASSET="jellytau-${TAG}.apk"
# Computed once, with `cut` rather than `${GITHUB_SHA::8}`. The
# substring form is bash-only and this runner may hand a step to
# `sh`; that cost a 51-minute build which produced a perfectly good
# APK and then died formatting the summary table.
SHORT_SHA=$(printf '%s' "$GITHUB_SHA" | cut -c1-8)
{
echo "variant=$VARIANT"
echo "abi=$ABI"
echo "publish=$PUBLISH"
echo "tag=$TAG"
echo "release_name=$RELEASE_NAME"
echo "asset=$ASSET"
echo "branch=$BRANCH"
echo "short_sha=$SHORT_SHA"
} >> "$GITHUB_OUTPUT"
echo "variant=$VARIANT abi=$ABI publish=$PUBLISH tag=$TAG asset=$ASSET"
- name: Cache Rust dependencies
uses: actions/cache@v3
with:
@@ -129,30 +211,29 @@ jobs:
# 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 }}"
if [ "${{ steps.cfg.outputs.variant }}" = "side-by-side-release" ]; then
./scripts/build-android.sh release --debug --abi "${{ steps.cfg.outputs.abi }}"
else
./scripts/build-android.sh debug --abi "${{ inputs.abi }}"
./scripts/build-android.sh debug --abi "${{ steps.cfg.outputs.abi }}"
fi
- name: Collect APK
id: collect
run: |
set -e
mkdir -p dist/test-apk
if [ "${{ inputs.variant }}" = "side-by-side-release" ]; then
if [ "${{ steps.cfg.outputs.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 }}"
echo "❌ No APK produced for variant ${{ steps.cfg.outputs.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"
OUT="dist/test-apk/${{ steps.cfg.outputs.asset }}"
cp "$APK" "$OUT"
# Report what the thing actually is, not what it was meant to be.
@@ -160,37 +241,32 @@ jobs:
"$APKSIGNER" verify --print-certs "$OUT" || echo "⚠️ Could not verify signature"
{
echo "### 📱 Test APK"
echo "### 📱 ${{ steps.cfg.outputs.release_name }}"
echo ""
echo "| | |"
echo "|---|---|"
echo "| Branch | \`${GITHUB_REF#refs/heads/}\` |"
echo "| Commit | \`${GITHUB_SHA::8}\` |"
echo "| Variant | \`${{ inputs.variant }}\` |"
echo "| ABI | \`${{ inputs.abi }}\` |"
echo "| Branch | \`${{ steps.cfg.outputs.branch }}\` |"
echo "| Commit | \`${{ steps.cfg.outputs.short_sha }}\` |"
echo "| Variant | \`${{ steps.cfg.outputs.variant }}\` |"
echo "| ABI | \`${{ steps.cfg.outputs.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.
# real release. `latest` and `test-*` carry no version, so nothing else
# reacts to them.
#
# 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 }}
# This also cannot reach existing users by itself. 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 who does
# not have the link -- and the APK installs under a different
# applicationId anyway.
- name: Publish pre-release
if: ${{ steps.cfg.outputs.publish == 'true' }}
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -200,26 +276,26 @@ jobs:
API="${GITHUB_SERVER_URL}/api/v1"
REPO="${GITHUB_REPOSITORY}"
TOKEN="${GITEA_TOKEN:-$AUTO_TOKEN}"
BRANCH="${GITHUB_REF#refs/heads/}"
TAG="test-$(echo "$BRANCH" | tr '/' '-')"
TAG="${{ steps.cfg.outputs.tag }}"
ASSET="${{ steps.cfg.outputs.asset }}"
# 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**." \
"Automatic build of \`${{ steps.cfg.outputs.branch }}\` at \`${{ steps.cfg.outputs.short_sha }}\` — **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." \
"normal install and with its own separate data. It cannot replace or upgrade a" \
"real install, and uninstalling it does not touch one." \
"" \
"Variant: \`${{ inputs.variant }}\` · ABI: \`${{ inputs.abi }}\`" \
"R8-minified like a real release, but signed with a debug key — so Android will" \
"warn about an unknown source. That is expected." \
"" \
"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.")
"Variant: \`${{ steps.cfg.outputs.variant }}\` · ABI: \`${{ steps.cfg.outputs.abi }}\`" \
"" \
"This release is refreshed on every push; the download link stays the same.")
PAYLOAD=$(jq -n \
--arg tag "$TAG" \
--arg name "Test build: $BRANCH" \
--arg name "${{ steps.cfg.outputs.release_name }}" \
--arg body "$BODY" \
--arg target "$GITHUB_SHA" \
'{tag_name:$tag, target_commitish:$target, name:$name, body:$body, draft:false, prerelease:true}')
@@ -230,11 +306,14 @@ jobs:
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"
# The rolling case: reuse the release, refresh its body to name the
# new commit, and clear the old asset so `latest` means latest.
RELEASE_ID=$(curl -fsS "$API/repos/$REPO/releases/tags/$TAG" \
-H "Authorization: token $TOKEN" | jq -r '.id')
echo "️ Refreshing existing pre-release $TAG (id=$RELEASE_ID)"
curl -fsS -X PATCH "$API/repos/$REPO/releases/$RELEASE_ID" \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "$PAYLOAD" >/dev/null
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" \
@@ -244,21 +323,24 @@ jobs:
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
# The tag moves with the branch, so an old tag object would otherwise
# keep `latest` pointing at a stale commit.
curl -fsS -X POST \
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$ASSET" \
-H "Authorization: token $TOKEN" -F "attachment=@dist/test-apk/$ASSET" >/dev/null
URL="${GITHUB_SERVER_URL}/${REPO}/releases/download/${TAG}/${ASSET}"
{
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."
echo "Direct download (stable link, no account needed):"
echo ""
echo " $URL"
} >> "$GITHUB_STEP_SUMMARY"
echo "✅ Published $TAG -> $URL"
- name: Upload APK
- name: Upload APK artifact
uses: actions/upload-artifact@v3
with:
name: jellytau-test-apk
+1 -1
View File
@@ -7,7 +7,7 @@ echo "======================================"
# Setup environment
echo "Setting up environment..."
source "$HOME/.cargo/env.fish" 2>/dev/null || source "$HOME/.cargo/env" || true
export ANDROID_HOME="$HOME/Android/Sdk"
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}"
export NDK_HOME="$ANDROID_HOME/ndk/$(ls $ANDROID_HOME/ndk 2>/dev/null | head -1)"
# Check prerequisites
+20 -3
View File
@@ -6,9 +6,26 @@ set -e
# Source Rust environment
source "$HOME/.cargo/env.fish" 2>/dev/null || source "$HOME/.cargo/env" 2>/dev/null || true
# Set Android environment variables
export ANDROID_HOME="$HOME/Android/Sdk"
export NDK_HOME="$ANDROID_HOME/ndk/$(ls "$ANDROID_HOME/ndk" | head -1)"
# Set Android environment variables.
#
# Defaults, not overrides. A developer's SDK is at ~/Android/Sdk, but CI runs in
# the builder image where it lives at /opt/android-sdk and the job sets
# ANDROID_HOME accordingly — hardcoding the home-directory path here silently
# discarded that and the build died with "Android SDK not found" a minute in.
# `test-player-conformance.sh` already had this right; this script did not.
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}"
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
if [ ! -d "$ANDROID_HOME/ndk" ]; then
echo "❌ No NDK directory at $ANDROID_HOME/ndk" >&2
echo " Set ANDROID_HOME to your SDK location, or install the NDK." >&2
exit 1
fi
# Respect an NDK the caller has already picked (CI pins an exact revision via
# ANDROID_NDK_HOME); otherwise take whatever is installed.
export NDK_HOME="${NDK_HOME:-${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/$(ls "$ANDROID_HOME/ndk" | head -1)}}"
export ANDROID_NDK_HOME="$NDK_HOME"
echo "🤖 Building Android APK..."
echo "Android SDK: $ANDROID_HOME"
+41 -25
View File
@@ -94,40 +94,56 @@ Follow the right log stream with `./scripts/logcat.sh [debug|release]`
### 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.
`.gitea/workflows/build-test-apk.yml` produces installable APKs that are **not
releases**. Two ways in:
Two variants, both installing into the `com.dtourolle.jellytau.debug` slot:
| Trigger | Result |
|---------|--------|
| **push to `master`** | Refreshes the rolling **`latest`** pre-release automatically |
| **`workflow_dispatch`** | Builds any branch on demand; optionally publishes it as `test-<branch>` |
#### The rolling `latest` build
Every push to `master` (bar doc-only ones) rebuilds and replaces the APK on the
`latest` pre-release. Both the tag and the asset name are stable, so the
download URL never changes:
```
https://gitea.tourolle.paris/dtourolle/jellytau/releases/download/latest/jellytau-latest.apk
```
Send that link to a tester once and it keeps serving the current build. No
account needed — release assets are public, unlike Actions artifacts.
#### What you get, and why it is safe
Both variants install 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 |
| `side-by-side-release` (default, and what `latest` always is) | 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.
Three properties make an automatic build on every master push safe:
#### Sending a build to an outside tester
- **It cannot replace a real install.** The applicationId is suffixed `.debug`,
so it sits beside the store build with its own data. A broken master commit
can never take out somebody's working app.
- **The production signing key is not involved.** That stays in the tag-driven
`build-release.yml`. This workflow needs no secrets beyond the API token.
- **The tag is `latest`/`test-*`, never `v*`.** Only `v*` triggers
`build-release.yml`. And the desktop updater reads a static `latest.json` from
the `updater` branch rather than the release list, so nothing here is offered
to existing users.
Gitea **artifacts require an account** with read access to download, so an
artifact is no use to someone outside the project. Tick **`publish`** on the
dispatch and the APK is also attached to a **pre-release**, whose assets are a
plain public URL on a public repo — no account, no MR, no merge to `master`.
**Known gap:** the APK builds in parallel with `build-and-test.yml`, not after
it, so `latest` can carry a commit whose tests later fail. Cross-workflow
dependencies are not reliably available here, and duplicating the test job would
double an already hour-long queue on a single-slot runner. Check the commit's
CI status before handing the link to somebody.
Two things make that safe to do from a feature branch:
- The tag is `test-<branch>`, **not** `v*`. Only `v*` triggers
`build-release.yml`, so nothing else reacts to it.
- It 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 this way is invisible to anyone without the link.
Re-dispatching for the same branch replaces the APK on the existing
pre-release rather than piling up one release per attempt. Delete the release
when testing is over.
Runs are serialised and `cancel-in-progress` is on, so a burst of pushes to
master collapses into one build rather than one per commit.
### Key Files