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
🏗️ 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.
This commit is contained in:
@@ -1,27 +1,39 @@
|
|||||||
name: '📱 Test APK'
|
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,
|
# Two ways in:
|
||||||
# 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
|
# push to master -> refreshes the rolling `latest` pre-release, so there is
|
||||||
# single slot shared with two other projects, so a build on every feature-branch
|
# always a current APK behind one stable URL that can be
|
||||||
# commit would starve everything else. Dispatch it when you actually want to
|
# handed to a tester once and never re-sent.
|
||||||
# install something.
|
# workflow_dispatch -> builds any branch on demand, optionally publishing it
|
||||||
|
# as `test-<branch>`.
|
||||||
#
|
#
|
||||||
# Both variants install as com.dtourolle.jellytau.debug ("JellyTau Debug"),
|
# Why this is separate from build-release.yml: that workflow is tag-driven,
|
||||||
# side by side with a real install and with their own data directory. Neither
|
# builds Linux + Windows + Android and creates a real release. This produces one
|
||||||
# needs the release signing key.
|
# 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
|
# 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
|
# access to download, so published builds are attached to a pre-release, whose
|
||||||
# whose assets are a plain public URL. That is the only way an outside tester
|
# assets are a plain public URL. That is the only way an outside tester gets the
|
||||||
# gets the file without being given an account.
|
# file without being given an account.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
variant:
|
variant:
|
||||||
@@ -30,9 +42,7 @@ on:
|
|||||||
default: 'side-by-side-release'
|
default: 'side-by-side-release'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
# R8-minified, exactly what ships, in the debug slot. Use this unless
|
# R8-minified, exactly what ships, in the debug slot.
|
||||||
# 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
|
- side-by-side-release
|
||||||
# Unminified. Faster, readable stack traces, but does not exercise
|
# Unminified. Faster, readable stack traces, but does not exercise
|
||||||
# minification at all.
|
# minification at all.
|
||||||
@@ -47,13 +57,15 @@ on:
|
|||||||
- armv7
|
- armv7
|
||||||
- x86_64
|
- x86_64
|
||||||
publish:
|
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
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
concurrency:
|
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
|
group: build-test-apk
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
@@ -63,7 +75,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build test APK (${{ github.event.inputs.variant }}, ${{ github.event.inputs.abi }})
|
name: Build test APK
|
||||||
runs-on: linux/amd64
|
runs-on: linux/amd64
|
||||||
container:
|
container:
|
||||||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
|
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
|
||||||
@@ -79,6 +91,62 @@ jobs:
|
|||||||
# the tags have to be here. A shallow checkout yields 0.0.0.
|
# the tags have to be here. A shallow checkout yields 0.0.0.
|
||||||
fetch-depth: 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"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "variant=$VARIANT"
|
||||||
|
echo "abi=$ABI"
|
||||||
|
echo "publish=$PUBLISH"
|
||||||
|
echo "tag=$TAG"
|
||||||
|
echo "release_name=$RELEASE_NAME"
|
||||||
|
echo "asset=$ASSET"
|
||||||
|
echo "branch=$BRANCH"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
echo "variant=$VARIANT abi=$ABI publish=$PUBLISH tag=$TAG asset=$ASSET"
|
||||||
|
|
||||||
- name: Cache Rust dependencies
|
- name: Cache Rust dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
@@ -129,30 +197,29 @@ jobs:
|
|||||||
# APK actually carries, which has silently regressed before.
|
# APK actually carries, which has silently regressed before.
|
||||||
- name: Build APK
|
- name: Build APK
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event.inputs.variant }}" = "side-by-side-release" ]; then
|
if [ "${{ steps.cfg.outputs.variant }}" = "side-by-side-release" ]; then
|
||||||
./scripts/build-android.sh release --debug --abi "${{ github.event.inputs.abi }}"
|
./scripts/build-android.sh release --debug --abi "${{ steps.cfg.outputs.abi }}"
|
||||||
else
|
else
|
||||||
./scripts/build-android.sh debug --abi "${{ github.event.inputs.abi }}"
|
./scripts/build-android.sh debug --abi "${{ steps.cfg.outputs.abi }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Collect APK
|
- name: Collect APK
|
||||||
id: collect
|
|
||||||
run: |
|
run: |
|
||||||
|
set -e
|
||||||
mkdir -p dist/test-apk
|
mkdir -p dist/test-apk
|
||||||
if [ "${{ github.event.inputs.variant }}" = "side-by-side-release" ]; then
|
if [ "${{ steps.cfg.outputs.variant }}" = "side-by-side-release" ]; then
|
||||||
PATTERN='*-release.apk'
|
PATTERN='*-release.apk'
|
||||||
else
|
else
|
||||||
PATTERN='*-debug.apk'
|
PATTERN='*-debug.apk'
|
||||||
fi
|
fi
|
||||||
APK=$(find src-tauri/gen/android/app/build/outputs/apk -name "$PATTERN" | head -1)
|
APK=$(find src-tauri/gen/android/app/build/outputs/apk -name "$PATTERN" | head -1)
|
||||||
if [ -z "$APK" ]; then
|
if [ -z "$APK" ]; then
|
||||||
echo "❌ No APK produced for variant ${{ github.event.inputs.variant }}"
|
echo "❌ No APK produced for variant ${{ steps.cfg.outputs.variant }}"
|
||||||
find src-tauri/gen/android/app/build/outputs/apk -name '*.apk' || true
|
find src-tauri/gen/android/app/build/outputs/apk -name '*.apk' || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REF_NAME=$(echo "${GITHUB_REF#refs/heads/}" | tr '/' '-')
|
OUT="dist/test-apk/${{ steps.cfg.outputs.asset }}"
|
||||||
OUT="dist/test-apk/jellytau-${REF_NAME}-${GITHUB_SHA::8}-${{ github.event.inputs.variant }}.apk"
|
|
||||||
cp "$APK" "$OUT"
|
cp "$APK" "$OUT"
|
||||||
|
|
||||||
# Report what the thing actually is, not what it was meant to be.
|
# Report what the thing actually is, not what it was meant to be.
|
||||||
@@ -160,42 +227,32 @@ jobs:
|
|||||||
"$APKSIGNER" verify --print-certs "$OUT" || echo "⚠️ Could not verify signature"
|
"$APKSIGNER" verify --print-certs "$OUT" || echo "⚠️ Could not verify signature"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### 📱 Test APK"
|
echo "### 📱 ${{ steps.cfg.outputs.release_name }}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "| | |"
|
echo "| | |"
|
||||||
echo "|---|---|"
|
echo "|---|---|"
|
||||||
echo "| Branch | \`${GITHUB_REF#refs/heads/}\` |"
|
echo "| Branch | \`${{ steps.cfg.outputs.branch }}\` |"
|
||||||
echo "| Commit | \`${GITHUB_SHA::8}\` |"
|
echo "| Commit | \`${GITHUB_SHA::8}\` |"
|
||||||
echo "| Variant | \`${{ github.event.inputs.variant }}\` |"
|
echo "| Variant | \`${{ steps.cfg.outputs.variant }}\` |"
|
||||||
echo "| ABI | \`${{ github.event.inputs.abi }}\` |"
|
echo "| ABI | \`${{ steps.cfg.outputs.abi }}\` |"
|
||||||
echo "| Size | $(du -h "$OUT" | cut -f1) |"
|
echo "| Size | $(du -h "$OUT" | cut -f1) |"
|
||||||
echo "| SHA256 | \`$(sha256sum "$OUT" | cut -d' ' -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"
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
ls -lah dist/test-apk/
|
ls -lah dist/test-apk/
|
||||||
|
|
||||||
# Deliberately NOT tagged `v*`: that pattern triggers build-release.yml,
|
# Deliberately NOT tagged `v*`: that pattern triggers build-release.yml,
|
||||||
# which would run the whole three-platform release matrix and publish a
|
# 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
|
# real release. `latest` and `test-*` carry no version, so nothing else
|
||||||
# branch name and carries no version, so nothing else reacts to it.
|
# reacts to them.
|
||||||
#
|
#
|
||||||
# This also cannot reach existing users. The desktop updater reads a
|
# This also cannot reach existing users by itself. The desktop updater
|
||||||
# static latest.json from the `updater` branch, not the release list, so a
|
# reads a static latest.json from the `updater` branch, not the release
|
||||||
# pre-release published here is invisible to anyone without the link.
|
# list, so a pre-release published here is invisible to anyone who does
|
||||||
- name: Publish as a pre-release
|
# not have the link -- and the APK installs under a different
|
||||||
# Compared against the string 'true', not used as a bare truthiness
|
# applicationId anyway.
|
||||||
# test. A dispatch input arrives as a *string*, and every non-empty
|
- name: Publish pre-release
|
||||||
# string is truthy in the expression language — so `if: inputs.publish`
|
if: ${{ steps.cfg.outputs.publish == 'true' }}
|
||||||
# would publish a pre-release on every run, including the ones where the
|
|
||||||
# box was deliberately left unticked.
|
|
||||||
if: ${{ github.event.inputs.publish == 'true' }}
|
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -205,26 +262,26 @@ jobs:
|
|||||||
API="${GITHUB_SERVER_URL}/api/v1"
|
API="${GITHUB_SERVER_URL}/api/v1"
|
||||||
REPO="${GITHUB_REPOSITORY}"
|
REPO="${GITHUB_REPOSITORY}"
|
||||||
TOKEN="${GITEA_TOKEN:-$AUTO_TOKEN}"
|
TOKEN="${GITEA_TOKEN:-$AUTO_TOKEN}"
|
||||||
BRANCH="${GITHUB_REF#refs/heads/}"
|
TAG="${{ steps.cfg.outputs.tag }}"
|
||||||
TAG="test-$(echo "$BRANCH" | tr '/' '-')"
|
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' \
|
BODY=$(printf '%s\n' \
|
||||||
"Test build of \`$BRANCH\` at \`${GITHUB_SHA::8}\` — **not a release**." \
|
"Automatic build of \`${{ steps.cfg.outputs.branch }}\` at \`${GITHUB_SHA::8}\` — **not a release**." \
|
||||||
"" \
|
"" \
|
||||||
"Installs as **JellyTau Debug** (\`com.dtourolle.jellytau.debug\`), alongside a" \
|
"Installs as **JellyTau Debug** (\`com.dtourolle.jellytau.debug\`), alongside a" \
|
||||||
"normal install and with its own separate data. Uninstalling it does not touch" \
|
"normal install and with its own separate data. It cannot replace or upgrade a" \
|
||||||
"the real app." \
|
"real install, and uninstalling it does not touch one." \
|
||||||
"" \
|
"" \
|
||||||
"Variant: \`${{ github.event.inputs.variant }}\` · ABI: \`${{ github.event.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" \
|
"Variant: \`${{ steps.cfg.outputs.variant }}\` · ABI: \`${{ steps.cfg.outputs.abi }}\`" \
|
||||||
"for a build signed with a debug key rather than the store key.")
|
"" \
|
||||||
|
"This release is refreshed on every push; the download link stays the same.")
|
||||||
|
|
||||||
PAYLOAD=$(jq -n \
|
PAYLOAD=$(jq -n \
|
||||||
--arg tag "$TAG" \
|
--arg tag "$TAG" \
|
||||||
--arg name "Test build: $BRANCH" \
|
--arg name "${{ steps.cfg.outputs.release_name }}" \
|
||||||
--arg body "$BODY" \
|
--arg body "$BODY" \
|
||||||
--arg target "$GITHUB_SHA" \
|
--arg target "$GITHUB_SHA" \
|
||||||
'{tag_name:$tag, target_commitish:$target, name:$name, body:$body, draft:false, prerelease:true}')
|
'{tag_name:$tag, target_commitish:$target, name:$name, body:$body, draft:false, prerelease:true}')
|
||||||
@@ -235,11 +292,14 @@ jobs:
|
|||||||
if [ "$HTTP" = "201" ]; then
|
if [ "$HTTP" = "201" ]; then
|
||||||
RELEASE_ID=$(jq -r '.id' resp.json)
|
RELEASE_ID=$(jq -r '.id' resp.json)
|
||||||
elif [ "$HTTP" = "409" ]; then
|
elif [ "$HTTP" = "409" ]; then
|
||||||
# Re-dispatching for the same branch replaces the previous APK rather
|
# The rolling case: reuse the release, refresh its body to name the
|
||||||
# than accumulating one release per attempt.
|
# new commit, and clear the old asset so `latest` means latest.
|
||||||
echo "ℹ️ Pre-release $TAG exists; reusing it"
|
|
||||||
RELEASE_ID=$(curl -fsS "$API/repos/$REPO/releases/tags/$TAG" \
|
RELEASE_ID=$(curl -fsS "$API/repos/$REPO/releases/tags/$TAG" \
|
||||||
-H "Authorization: token $TOKEN" | jq -r '.id')
|
-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" \
|
for id in $(curl -fsS "$API/repos/$REPO/releases/$RELEASE_ID/assets" \
|
||||||
-H "Authorization: token $TOKEN" | jq -r '.[].id'); do
|
-H "Authorization: token $TOKEN" | jq -r '.[].id'); do
|
||||||
curl -fsS -X DELETE "$API/repos/$REPO/releases/$RELEASE_ID/assets/$id" \
|
curl -fsS -X DELETE "$API/repos/$REPO/releases/$RELEASE_ID/assets/$id" \
|
||||||
@@ -249,21 +309,24 @@ jobs:
|
|||||||
echo "❌ Failed to create pre-release (HTTP $HTTP):"; cat resp.json; exit 1
|
echo "❌ Failed to create pre-release (HTTP $HTTP):"; cat resp.json; exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for f in dist/test-apk/*.apk; do
|
# The tag moves with the branch, so an old tag object would otherwise
|
||||||
echo "⬆️ $(basename "$f")"
|
# keep `latest` pointing at a stale commit.
|
||||||
curl -fsS -X POST \
|
curl -fsS -X POST \
|
||||||
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$(basename "$f")" \
|
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$ASSET" \
|
||||||
-H "Authorization: token $TOKEN" -F "attachment=@$f" >/dev/null
|
-H "Authorization: token $TOKEN" -F "attachment=@dist/test-apk/$ASSET" >/dev/null
|
||||||
done
|
|
||||||
|
|
||||||
|
URL="${GITHUB_SERVER_URL}/${REPO}/releases/download/${TAG}/${ASSET}"
|
||||||
{
|
{
|
||||||
echo ""
|
echo ""
|
||||||
echo "**Published:** ${GITHUB_SERVER_URL}/${REPO}/releases/tag/${TAG}"
|
echo "**Published:** ${GITHUB_SERVER_URL}/${REPO}/releases/tag/${TAG}"
|
||||||
echo ""
|
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"
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "✅ Published $TAG -> $URL"
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: jellytau-test-apk
|
name: jellytau-test-apk
|
||||||
|
|||||||
@@ -94,40 +94,56 @@ Follow the right log stream with `./scripts/logcat.sh [debug|release]`
|
|||||||
|
|
||||||
### Getting a test APK out of CI
|
### Getting a test APK out of CI
|
||||||
|
|
||||||
`.gitea/workflows/build-test-apk.yml` builds one from **any branch, on demand**
|
`.gitea/workflows/build-test-apk.yml` produces installable APKs that are **not
|
||||||
— run it from Gitea's Actions tab (`workflow_dispatch`) against the ref you want.
|
releases**. Two ways in:
|
||||||
It is not a release: nothing is tagged, published, or signed with the real key.
|
|
||||||
|
|
||||||
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 |
|
| 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 |
|
| `debug` | Unminified | When you need readable stack traces |
|
||||||
|
|
||||||
There is deliberately **no push trigger**: the runner has one slot shared with
|
Three properties make an automatic build on every master push safe:
|
||||||
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.
|
|
||||||
|
|
||||||
#### 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
|
**Known gap:** the APK builds in parallel with `build-and-test.yml`, not after
|
||||||
artifact is no use to someone outside the project. Tick **`publish`** on the
|
it, so `latest` can carry a commit whose tests later fail. Cross-workflow
|
||||||
dispatch and the APK is also attached to a **pre-release**, whose assets are a
|
dependencies are not reliably available here, and duplicating the test job would
|
||||||
plain public URL on a public repo — no account, no MR, no merge to `master`.
|
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:
|
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.
|
||||||
- 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.
|
|
||||||
|
|
||||||
### Key Files
|
### Key Files
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user