Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 8s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Skipped
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Skipped
Renovate / renovate (push) Successful in 52s
### What this changes Adds beta releases. Pushing a `release/*` branch whose committed `versionName` is `X.Y.Z-beta.N` makes the new `.gitea/workflows/beta.yaml` run the unit tests, build and sign the APK with the app key, and publish it as a **Codeberg pre-release** (APK + `.sha256`), plus a Gitea pre-release with the R8 mapping. F-Droid (self-hosted and official) and Play never get a beta; Obtainium only offers it with *Include prereleases* on. - **`scripts/version_info.sh`** is the single source for `versionName` → `versionCode`, used by `release.yaml`, `beta.yaml`, the changelog sync and the store-listing check. From 1.1.0: `X*1000000 + Y*10000 + Z*100 + N` for betas (N = 1–98), `+ 99` for stable, so `1.1.0-beta.1` → `1010001`, `1.1.0` → `1010099`. All 1.0.x versions keep the legacy formula, so `release/v1.0.1` (code `10001`) stays valid. - **Shared publish scripts:** `scripts/publish_codeberg_release.sh`, `scripts/publish_gitea_release.sh` and `scripts/release_notes.sh`, moved out of `release.yaml`. The stable path behaves as before. - **Guards:** - CI fails a PR whose committed `versionCode` doesn't match its `versionName`. - CI fails a PR into `main` that carries a beta version. - `release.yaml`'s `detect` refuses a beta on `main` as a backstop. - `beta.yaml` refuses a beta of a version that has already shipped as stable. - Betas get no store What's New file. - **Docs:** "Cutting a beta" and the versionCode table in `docs/RELEASING.md`; a note on beta tags in `docs/fdroid-official/README.md`; how to opt in to betas in the README. ### Why To ship a test build of an upcoming version (e.g. 1.1.0) to opted-in testers before the stable release, without it reaching F-Droid or Play users. ### How it was tested - `scripts/version_info.sh` against stable, beta, legacy and invalid version names. - Both publish scripts against a mock forge API: create, re-run (PATCH plus asset replacement), Codeberg's 500-then-retry path, and the skip when no token is set. - A scratch copy with `1.1.0-beta.1` committed: the version check passes, the changelog sync and `check_store_listing.py --complete` pass without a What's New, the PR-into-main guard trips, and a wrong `versionCode` is rejected. - `sync_changelog_to_fastlane.sh` and `check_store_listing.py` (with and without `--complete`) still pass on the current `1.0.0`. - All three workflow files parse as YAML. Not run on the real runners yet. The first beta push is the live test of `beta.yaml`, which assumes a mirrored branch push starts a workflow on Gitea, the same way pushes to `main` already do. ### Checklist - [x] No `versionName` / `versionCode` bump - [x] No `values-*/strings.xml` touched - [x] `CHANGELOG.md` not updated: this is release infrastructure, not a user-visible change Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de> Reviewed-on: https://codeberg.org/jlmakiola/agendula/pulls/38
509 lines
23 KiB
YAML
509 lines
23 KiB
YAML
name: Release — F-Droid repo + Gitea/Codeberg release + Play
|
|
|
|
# A release is cut by merging a release branch into main with a bumped
|
|
# versionName (see docs/RELEASING.md). This workflow reads that versionName and,
|
|
# if no matching tag exists yet, runs tests, builds + signs the APK, publishes
|
|
# it to the F-Droid repo, creates the vX.Y.Z tag + Gitea release, and publishes
|
|
# the release on Codeberg with the signed APK + a SHA-256 checksum as a
|
|
# direct-download channel — the tag is an output of the pipeline, not its
|
|
# trigger. Ordinary merges (no version bump) fall through `detect` and do
|
|
# nothing. Betas (X.Y.Z-beta.N) never come through here: beta.yaml cuts them
|
|
# from release/* branches as Codeberg-only pre-releases, and `detect` refuses
|
|
# one that reaches main.
|
|
#
|
|
# A trailing `play` job then uploads the App Bundle to Google Play. It is last
|
|
# and separate because Play can reject a good build for reasons the pipeline
|
|
# can't see, and that must not endanger a release which already shipped to
|
|
# F-Droid and Codeberg. It skips cleanly until PLAY_SERVICE_ACCOUNT_JSON exists.
|
|
#
|
|
# This file lives in .gitea/workflows on purpose: Codeberg is canonical for git,
|
|
# issues, PRs and releases, but every secret (app key, F-Droid repo key, Hetzner
|
|
# credentials) lives on the self-hosted Gitea instance, and this is the only
|
|
# directory Codeberg cannot see. Contributor-triggerable work lives in
|
|
# .forgejo/workflows and references no secret. See docs/RELEASING.md.
|
|
#
|
|
# A manual workflow_dispatch (from a branch) runs the re-sign-only recovery
|
|
# path: it re-signs the existing F-Droid index with the repo key and re-uploads,
|
|
# without building an APK or creating a release. Used for key rotation / repo
|
|
# recovery.
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: release
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
# Cheap gate: resolve the version from the committed build.gradle and decide
|
|
# whether this push actually cuts a new release (no tag for it yet). Keeps the
|
|
# heavy job from running on every merge to main.
|
|
detect:
|
|
# Gitea only. The workflow directory split already keeps this file invisible
|
|
# to Codeberg — Forgejo's lookup is first-match-wins, and .forgejo/workflows
|
|
# exists — but that only holds while .forgejo/ is non-empty. Move the last
|
|
# file out of it and Codeberg would fall back to .gitea/workflows and start
|
|
# running the release pipeline on the contributor-facing runner, with no
|
|
# secrets. repository_owner differs between the two forges regardless of
|
|
# URL, proxy or instance rename, so this closes it permanently.
|
|
if: github.repository_owner == 'makiolaj'
|
|
runs-on: docker
|
|
outputs:
|
|
is_release: ${{ steps.v.outputs.is_release }}
|
|
version: ${{ steps.v.outputs.version }}
|
|
version_code: ${{ steps.v.outputs.version_code }}
|
|
prerelease: ${{ steps.v.outputs.prerelease }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
|
|
- name: Resolve version and whether it is a new release
|
|
id: v
|
|
env:
|
|
# Tags are read from Codeberg, which is canonical — deliberately NOT
|
|
# from the Gitea API this workflow runs on. The Codeberg -> Gitea sync
|
|
# is a push mirror, i.e. `git push --mirror`, which deletes refs the
|
|
# source does not have. A tag minted here on Gitea is therefore wiped
|
|
# by the next sync (Codeberg does not have it yet) and only reappears
|
|
# once the tag push at the end of this workflow propagates back.
|
|
# Asking Gitea inside that window would report "no tag" for a release
|
|
# that already shipped, and cut it a second time.
|
|
# Public repo, so this read needs no token.
|
|
TAG_API: https://codeberg.org/api/v1/repos/jlmakiola/agendula
|
|
run: |
|
|
set -e
|
|
# versionName -> versionCode, channel and the pre-release flag (set
|
|
# while MAJOR was 0) all come from the one script beta.yaml uses too.
|
|
INFO=$(bash scripts/version_info.sh)
|
|
echo "$INFO"
|
|
echo "$INFO" >> "$GITHUB_OUTPUT"
|
|
VERSION=$(echo "$INFO" | sed -n 's/^version=//p')
|
|
CHANNEL=$(echo "$INFO" | sed -n 's/^channel=//p')
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
echo "Manual dispatch — re-sign path, not a release."
|
|
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
# Betas ship from release/* branches via beta.yaml and must never be
|
|
# cut here: this path publishes to F-Droid and Play. CI blocks such a
|
|
# PR into main; this is the backstop if one gets through anyway.
|
|
if [ "$CHANNEL" != "stable" ]; then
|
|
echo "versionName $VERSION on main is a beta. Set the stable version before merging to main." >&2
|
|
exit 1
|
|
fi
|
|
# A tag for this version already existing means the release shipped on
|
|
# an earlier push; do nothing. Absent => this merge cuts the release.
|
|
#
|
|
# Anything other than a clean 200/404 is treated as fatal rather than
|
|
# as "no tag". A Codeberg outage or a network blip would otherwise
|
|
# read as absent and re-cut a release that has already shipped —
|
|
# republishing to F-Droid. Failing here is recoverable; a duplicate
|
|
# release is not.
|
|
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$TAG_API/git/refs/tags/v$VERSION" || echo 000)
|
|
case "$STATUS" in
|
|
200)
|
|
echo "Tag v$VERSION already exists on Codeberg — nothing to release."
|
|
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
|
;;
|
|
404)
|
|
echo "No tag for v$VERSION on Codeberg yet — cutting the release."
|
|
echo "is_release=true" >> "$GITHUB_OUTPUT"
|
|
;;
|
|
*)
|
|
echo "Codeberg tag lookup for v$VERSION returned HTTP $STATUS." >&2
|
|
echo "Refusing to guess: treating this as 'no tag' could re-cut a shipped release." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# Before a single Gradle task runs: F-Droid truncates the in-client
|
|
# changelog, so an over-long one would reach users cut off mid-sentence.
|
|
# The script exits non-zero past the limit. Cheap enough to sit in the
|
|
# gate job, where failing costs nothing and publishes nothing — the step
|
|
# further down that regenerates the file for the repo would otherwise be
|
|
# the first thing to notice, after the build and the signing.
|
|
- name: Changelog fits the stores
|
|
if: steps.v.outputs.is_release == 'true'
|
|
run: bash scripts/sync_changelog_to_fastlane.sh
|
|
|
|
# Releases: build + sign + publish, then mint the tag and Gitea release.
|
|
# Also runs on manual dispatch, where it skips the build and just re-signs and
|
|
# re-uploads the existing index (recovery path).
|
|
release:
|
|
needs: detect
|
|
if: needs.detect.outputs.is_release == 'true' || github.event_name == 'workflow_dispatch'
|
|
runs-on: docker
|
|
env:
|
|
ANDROID_HOME: /opt/android-sdk
|
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
|
VERSION: ${{ needs.detect.outputs.version }}
|
|
VERSION_CODE: ${{ needs.detect.outputs.version_code }}
|
|
IS_RELEASE: ${{ needs.detect.outputs.is_release }}
|
|
PRERELEASE: ${{ needs.detect.outputs.prerelease }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
|
|
- name: Setup Java
|
|
uses: actions/setup-java@v4
|
|
with:
|
|
distribution: 'zulu'
|
|
java-version: '17'
|
|
|
|
- name: Setup Android SDK
|
|
uses: android-actions/setup-android@v3
|
|
with:
|
|
packages: ''
|
|
|
|
- name: Setup Android SDK cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: /opt/android-sdk
|
|
key: ${{ runner.os }}-android-sdk-37-36.0.0
|
|
|
|
- name: Install Android SDK packages
|
|
run: |
|
|
yes | sdkmanager --licenses >/dev/null || true
|
|
sdkmanager \
|
|
"platform-tools" \
|
|
"platforms;android-37.0" \
|
|
"build-tools;36.0.0"
|
|
|
|
- name: Setup Gradle cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-gradle-
|
|
|
|
- name: Install jq
|
|
run: |
|
|
set -e
|
|
SUDO=""
|
|
if command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
$SUDO apt-get update
|
|
$SUDO apt-get install -y jq
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
$SUDO apk add --no-cache jq
|
|
fi
|
|
|
|
- name: Grant execute permission for gradlew
|
|
run: chmod +x ./gradlew
|
|
|
|
# The committed versionName is the source of truth. Pin versionCode to the
|
|
# value scripts/version_info.sh derives from it, so the published APK's
|
|
# code follows the scheme even if the committed code was forgotten.
|
|
- name: Pin versionCode to versionName
|
|
if: env.IS_RELEASE == 'true'
|
|
run: |
|
|
set -e
|
|
sed -i "s/versionCode = .*/versionCode = $VERSION_CODE/" app/build.gradle.kts
|
|
grep -E 'versionName|versionCode' app/build.gradle.kts
|
|
|
|
# Test the exact commit being shipped (only on a real release).
|
|
- name: Unit tests
|
|
if: env.IS_RELEASE == 'true'
|
|
run: ./gradlew testDebugUnitTest
|
|
|
|
- name: Setup Android Keystore
|
|
if: env.IS_RELEASE == 'true'
|
|
env:
|
|
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
|
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
|
run: |
|
|
mkdir -p app
|
|
echo "$KEYSTORE_BASE64" | base64 --decode > app/upload-keystore.jks
|
|
cat > key.properties <<EOF
|
|
storePassword=$KEY_PASSWORD
|
|
keyPassword=$KEY_PASSWORD
|
|
keyAlias=$KEY_ALIAS
|
|
storeFile=upload-keystore.jks
|
|
EOF
|
|
|
|
- name: Build release APK
|
|
if: env.IS_RELEASE == 'true'
|
|
run: ./gradlew assembleRelease
|
|
|
|
- name: Setup F-Droid Server Tools
|
|
run: |
|
|
SUDO=""
|
|
if command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi
|
|
$SUDO apt-get update
|
|
$SUDO apt-get install -y sshpass python3-pip
|
|
pip3 install --break-system-packages --upgrade fdroidserver
|
|
|
|
- name: Fetch existing F-Droid repo from Hetzner
|
|
env:
|
|
HOST: ${{ secrets.HETZNER_HOST }}
|
|
USER: ${{ secrets.HETZNER_USER }}
|
|
PASS: ${{ secrets.HETZNER_PASS }}
|
|
run: |
|
|
set -euo pipefail
|
|
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=20"
|
|
mkdir -p fdroid
|
|
# Pull only the published repo/ (all apps' APKs), any per-app
|
|
# metadata, and the repo icon — enough to rebuild the index without
|
|
# dropping the other apps. The signing key is deliberately NOT pulled
|
|
# from the box; it comes from CI secrets in the next step so it never
|
|
# has to live in the web-served tree.
|
|
sshpass -p "$PASS" scp $SSH_OPTS -r "$USER@$HOST:dev/fdroid/repo" fdroid/ 2>/dev/null || true
|
|
sshpass -p "$PASS" scp $SSH_OPTS -r "$USER@$HOST:dev/fdroid/metadata" fdroid/ 2>/dev/null || true
|
|
sshpass -p "$PASS" scp $SSH_OPTS "$USER@$HOST:dev/fdroid/icon.png" fdroid/ 2>/dev/null || true
|
|
mkdir -p fdroid/repo fdroid/metadata
|
|
|
|
- name: Restore F-Droid signing key and config from secrets
|
|
env:
|
|
FDROID_KEYSTORE_BASE64: ${{ secrets.FDROID_KEYSTORE_BASE64 }}
|
|
FDROID_CONFIG_BASE64: ${{ secrets.FDROID_CONFIG_BASE64 }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Fail loudly if the repo key is not configured. NEVER auto-generate
|
|
# one: a fresh key changes the repo fingerprint and breaks every
|
|
# user's pinned repo.
|
|
if [ -z "${FDROID_KEYSTORE_BASE64:-}" ] || [ -z "${FDROID_CONFIG_BASE64:-}" ]; then
|
|
echo "ERROR: FDROID_KEYSTORE_BASE64 / FDROID_CONFIG_BASE64 secrets are not set." >&2
|
|
echo "Refusing to continue — will not auto-generate a new repo key." >&2
|
|
exit 1
|
|
fi
|
|
echo "$FDROID_KEYSTORE_BASE64" | base64 --decode > fdroid/keystore.p12
|
|
echo "$FDROID_CONFIG_BASE64" | base64 --decode > fdroid/config.yml
|
|
test -s fdroid/keystore.p12
|
|
test -s fdroid/config.yml
|
|
mkdir -p fdroid/repo/icons
|
|
|
|
- name: Copy new APK to repo
|
|
if: env.IS_RELEASE == 'true'
|
|
run: |
|
|
set -e
|
|
mkdir -p fdroid/repo
|
|
cp app/build/outputs/apk/release/app-release.apk "fdroid/repo/agendula_v${VERSION}.apk"
|
|
|
|
# Per-version "What's New": ensure this version's changelog exists in the
|
|
# fastlane tree (committed at release-cut time for the official repo; this
|
|
# regenerates it from CHANGELOG.md so the self-hosted repo never depends on
|
|
# the commit having happened). The transform below then carries it across.
|
|
- name: Ensure this version's changelog is in the fastlane tree
|
|
if: env.IS_RELEASE == 'true'
|
|
run: bash scripts/sync_changelog_to_fastlane.sh
|
|
|
|
- name: Build F-Droid metadata from fastlane (single source of truth)
|
|
run: |
|
|
mkdir -p fdroid/metadata
|
|
# App-level control file (Categories/License/links) for the self-hosted
|
|
# repo's `fdroid update`.
|
|
cp fdroid-metadata/de.jeanlucmakiola.agendula.yml fdroid/metadata/
|
|
# Localized text + graphics + per-version changelogs come from the SAME
|
|
# fastlane tree the official F-Droid repo harvests from source,
|
|
# transformed into the F-Droid repo "localized" layout. One source of
|
|
# truth, both channels.
|
|
bash scripts/fastlane_to_fdroid_localized.sh \
|
|
fastlane/metadata/android \
|
|
fdroid/metadata/de.jeanlucmakiola.agendula
|
|
|
|
- name: Generate F-Droid Index
|
|
run: |
|
|
cd fdroid
|
|
fdroid update -c
|
|
|
|
- name: Upload repo/ to Hetzner
|
|
env:
|
|
HOST: ${{ secrets.HETZNER_HOST }}
|
|
USER: ${{ secrets.HETZNER_USER }}
|
|
PASS: ${{ secrets.HETZNER_PASS }}
|
|
run: |
|
|
set -euo pipefail
|
|
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=20"
|
|
sshpass -p "$PASS" sftp $SSH_OPTS "$USER@$HOST" <<'SFTP'
|
|
-mkdir dev
|
|
-mkdir dev/fdroid
|
|
SFTP
|
|
# Publish the signed repo/ plus metadata/ (descriptions, screenshots,
|
|
# per-version changelogs) so changelog history survives across
|
|
# releases. keystore.p12 and config.yml are NEVER uploaded.
|
|
sshpass -p "$PASS" scp $SSH_OPTS -r fdroid/repo fdroid/metadata "$USER@$HOST:dev/fdroid/"
|
|
|
|
# The APK is published and the index re-signed — now record the release.
|
|
# Creating it with target_commitish makes Gitea create the vX.Y.Z tag at
|
|
# this commit, so the tag only ever marks a fully-shipped release (and a
|
|
# failure before here leaves no tag, so re-running the workflow retries).
|
|
# Also attaches the R8 mapping (best-effort) so user crash stacktraces
|
|
# stay deobfuscatable. Notes = this version's CHANGELOG section.
|
|
- name: Create tag + Gitea release
|
|
if: env.IS_RELEASE == 'true'
|
|
env:
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
|
SHA: ${{ github.sha }}
|
|
run: |
|
|
set -e
|
|
bash scripts/release_notes.sh "$VERSION" > release-notes.md
|
|
TAG="v$VERSION" NOTES_FILE=release-notes.md \
|
|
MAPPING=app/build/outputs/mapping/release/mapping.txt \
|
|
bash scripts/publish_gitea_release.sh
|
|
|
|
# Publish the release on Codeberg, which is canonical for tags and
|
|
# releases (see docs/RELEASING.md): push the tag there and attach the
|
|
# signed APK plus a SHA-256 checksum as the direct-download channel for
|
|
# users who don't want F-Droid. The APK is identical to the F-Droid one
|
|
# (same app key), so this adds no trust surface. Needs the
|
|
# CODEBERG_RELEASE_TOKEN secret; skips cleanly if unset.
|
|
- name: Publish release to Codeberg
|
|
if: env.IS_RELEASE == 'true'
|
|
# NOT continue-on-error: this step reported green through 0.2.1, 0.2.2,
|
|
# 0.3.0, 0.3.1 and 0.3.2 while never once publishing, which is how a
|
|
# crash-fix release reached F-Droid but not the Codeberg/Obtainium
|
|
# users who needed it. A broken mirror must fail the release loudly.
|
|
env:
|
|
TOKEN: ${{ secrets.CODEBERG_RELEASE_TOKEN }}
|
|
API: https://codeberg.org/api/v1/repos/jlmakiola/agendula
|
|
SHA: ${{ github.sha }}
|
|
run: |
|
|
set -e
|
|
[ -s release-notes.md ] || bash scripts/release_notes.sh "$VERSION" > release-notes.md
|
|
TAG="v$VERSION" NOTES_FILE=release-notes.md \
|
|
APK=app/build/outputs/apk/release/app-release.apk \
|
|
bash scripts/publish_codeberg_release.sh
|
|
|
|
# Play takes an App Bundle, not the APK: a second artifact from the same
|
|
# source and signing config. Play treats the release key only as the
|
|
# upload key and re-signs with its own (Play App Signing), so Play and
|
|
# F-Droid installs carry different signatures and can't update each other.
|
|
#
|
|
# Built last and continue-on-error: everything above has already shipped,
|
|
# and nothing Play-related may take it down. The AAB never touches the
|
|
# F-Droid repo or the releases. AGP embeds the R8 mapping in the bundle,
|
|
# so Play gets deobfuscated stacktraces without a separate upload.
|
|
- name: Build release AAB
|
|
if: env.IS_RELEASE == 'true'
|
|
continue-on-error: true
|
|
run: ./gradlew bundleRelease
|
|
|
|
# NOT actions/upload-artifact@v4: its client refuses any non-github.com
|
|
# server as unsupported GHES (go-gitea/gitea#36024). This fork drops that
|
|
# check. Pinned to a commit — a third-party action in the signing
|
|
# pipeline must not change under us.
|
|
- name: Hand the AAB to the Play job
|
|
if: env.IS_RELEASE == 'true'
|
|
continue-on-error: true
|
|
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7 # v4
|
|
with:
|
|
name: release-aab-${{ needs.detect.outputs.version }}
|
|
path: app/build/outputs/bundle/release/app-release.aab
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
# Google Play channel. A separate job after the F-Droid publish and both forge
|
|
# releases, so a Play rejection (policy review, API outage, listing rules)
|
|
# shows up as one red job next to a release that already shipped.
|
|
#
|
|
# Not a `container:` job: act_runner provides no node inside custom job
|
|
# containers, so JavaScript actions (checkout, download-artifact) can't run.
|
|
play:
|
|
needs: [detect, release]
|
|
# workflow_dispatch is the F-Droid re-sign recovery path; never touch Play.
|
|
if: needs.detect.outputs.is_release == 'true'
|
|
runs-on: docker
|
|
env:
|
|
VERSION: ${{ needs.detect.outputs.version }}
|
|
VERSION_CODE: ${{ needs.detect.outputs.version_code }}
|
|
# The release itself is the gate (a bumped versionName only reaches main
|
|
# after on-device review), so it goes straight to production. Override
|
|
# with repo variables to stage instead.
|
|
PLAY_TRACK: ${{ vars.PLAY_TRACK || 'production' }}
|
|
PLAY_RELEASE_STATUS: ${{ vars.PLAY_RELEASE_STATUS || 'completed' }}
|
|
# true validates the edit against the API and discards it.
|
|
PLAY_DRY_RUN: ${{ vars.PLAY_DRY_RUN || 'false' }}
|
|
BUNDLE_PATH: vendor/bundle
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
# Skip cleanly when Play isn't configured yet, same contract as the
|
|
# Codeberg publish.
|
|
- name: Write the Play service-account key
|
|
id: key
|
|
env:
|
|
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${PLAY_SERVICE_ACCOUNT_JSON:-}" ]; then
|
|
echo "PLAY_SERVICE_ACCOUNT_JSON not set — skipping the Play upload."
|
|
echo "configured=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
printf '%s' "$PLAY_SERVICE_ACCOUNT_JSON" > play-service-account.json
|
|
python3 -c "import json,sys; d=json.load(open('play-service-account.json')); sys.exit(0 if d.get('type')=='service_account' else 1)" \
|
|
|| { echo "PLAY_SERVICE_ACCOUNT_JSON is not a valid service-account JSON." >&2; exit 1; }
|
|
echo "configured=true" >> "$GITHUB_OUTPUT"
|
|
|
|
# Same GHES-detection fix as the upload side.
|
|
- name: Download the AAB
|
|
if: steps.key.outputs.configured == 'true'
|
|
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7 # v4
|
|
with:
|
|
name: release-aab-${{ needs.detect.outputs.version }}
|
|
path: dist
|
|
|
|
- name: Install Ruby
|
|
if: steps.key.outputs.configured == 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
SUDO=""
|
|
if command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi
|
|
$SUDO apt-get update
|
|
# Several fastlane dependencies build native extensions.
|
|
$SUDO apt-get install -y ruby-full ruby-dev build-essential
|
|
ruby -v
|
|
|
|
- name: Cache bundled gems
|
|
if: steps.key.outputs.configured == 'true'
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: vendor/bundle
|
|
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-gems-
|
|
|
|
- name: Install fastlane
|
|
if: steps.key.outputs.configured == 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
gem install bundler --no-document
|
|
bundle config set --local path vendor/bundle
|
|
bundle install --jobs 4
|
|
bundle exec fastlane --version
|
|
|
|
- name: Upload to Play
|
|
if: steps.key.outputs.configured == 'true'
|
|
env:
|
|
SUPPLY_JSON_KEY: play-service-account.json
|
|
FASTLANE_SKIP_UPDATE_CHECK: '1'
|
|
FASTLANE_HIDE_CHANGELOG: '1'
|
|
run: |
|
|
set -euo pipefail
|
|
# Absolute: a lane body runs from fastlane/, not the workspace root.
|
|
AAB="$GITHUB_WORKSPACE/dist/app-release.aab"
|
|
test -f "$AAB" || { echo "No AAB at $AAB — the artifact handoff failed." >&2; ls -la dist || true; exit 1; }
|
|
bundle exec fastlane deploy \
|
|
aab:"$AAB" \
|
|
track:"$PLAY_TRACK" \
|
|
release_status:"$PLAY_RELEASE_STATUS" \
|
|
dry_run:"$PLAY_DRY_RUN"
|
|
echo "Uploaded $VERSION (code $VERSION_CODE) to the '$PLAY_TRACK' track."
|
|
|
|
# The workspace is reused on a self-hosted runner; the key must not
|
|
# outlive the job.
|
|
- name: Shred the service-account key
|
|
if: always()
|
|
run: shred -u play-service-account.json 2>/dev/null || rm -f play-service-account.json
|