All checks were successful
PR Checks / android-build (pull_request) Successful in 10m23s
The push-to-main release model kept failing: the job builds the signed APK fine, but the final `git push origin HEAD:main` (version-bump commit) is rejected by main's branch protection — "pre-receive hook declined / Internal Server Error" — across runs #187, #194. main is deliberately protected (allowlist push, required approvals, required status checks), which is fundamentally incompatible with a CI job pushing a fresh commit to it. Flip the trigger: the workflow now runs on pushing a `v*` tag (or via workflow_dispatch with a tag input). The tag *is* the release input, so: - version/versionCode are derived from the tag name (no version-planning engine); - app/build.gradle.kts is set for the build only, never committed back; - no `git push` to main, no tag creation, no REGISTRY_USER needed — only REGISTRY_TOKEN, to create the Gitea release + upload the APK/SHA256SUMS. To cut a release now: `git tag v0.1.0 && git push origin v0.1.0`. Keeps the speed fixes from #13 (trimmed setup-android, no Gradle cache, timeout-minutes). Changelog is still generated from conventional-commit subjects since the previous tag. Co-Authored-By: Claude <noreply@anthropic.com>
195 lines
8.7 KiB
YAML
195 lines
8.7 KiB
YAML
# Automated release for the Runic Gateway Android app.
|
|
#
|
|
# Trigger: pushing a version tag `v*` (e.g. `v0.1.0`). Tag-driven on purpose — the
|
|
# build never has to push to protected `main`; the tag *is* the release input.
|
|
#
|
|
# To cut a release:
|
|
# git tag v0.1.0 && git push origin v0.1.0
|
|
# (or create the tag from the Gitea UI). Re-build/re-release an existing tag via
|
|
# the workflow_dispatch input below.
|
|
#
|
|
# versionName = the tag without its leading `v`; versionCode = major*10000 +
|
|
# minor*100 + patch (deterministic + monotonic, PLAN.md §10). Both are injected
|
|
# into app/build.gradle.kts for the build only — nothing is committed back to main.
|
|
#
|
|
# Prerequisites (Settings -> Actions -> Secrets on RunicGateway/Android-app):
|
|
# REGISTRY_TOKEN — Gitea access token with `write:repository` (create the release)
|
|
# ANDROID_KEYSTORE_BASE64 — base64 of the release .jks (single line)
|
|
# ANDROID_KEYSTORE_PASSWORD — keystore password
|
|
# ANDROID_KEY_ALIAS — key alias (e.g. runicgateway)
|
|
# ANDROID_KEY_PASSWORD — key password (== store password for a PKCS12 keystore)
|
|
#
|
|
# Runner handling matches pr-checks.yml (self-hosted `ubuntu-latest`): the container
|
|
# lacks git/curl/unzip and can't reach api.adoptium.net, so we apt-install the base
|
|
# tools + JDK 17 (not actions/setup-java), install the exact SDK packages, and
|
|
# `chmod +x ./gradlew` in-step (checkout drops the exec bit).
|
|
|
|
name: Release APK
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Existing v* tag to (re)build and release'
|
|
required: true
|
|
|
|
concurrency:
|
|
group: release-apk-${{ github.event.inputs.tag || github.ref_name }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
GITEA_HOST: gitea.whitlocktech.com
|
|
REPO: RunicGateway/Android-app
|
|
GRADLE_MODULE: app
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
# Fail fast on a genuinely wedged run (e.g. a stalled SDK/network download on
|
|
# the self-hosted runner) instead of hanging forever and — because concurrency
|
|
# is `cancel-in-progress: false` — blocking every later release behind it.
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Install base tools + JDK 17
|
|
run: |
|
|
apt-get update
|
|
apt-get install -y git curl unzip jq openjdk-17-jdk-headless
|
|
echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> "$GITHUB_ENV"
|
|
|
|
- name: Check out the release tag (full history for the changelog)
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.inputs.tag || github.ref_name }}
|
|
fetch-depth: 0
|
|
|
|
# ── Derive version + changelog straight from the tag ─────────────────
|
|
- name: Plan the release (version + changelog from the tag)
|
|
id: plan
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p dist
|
|
git fetch --tags --force >/dev/null 2>&1 || true
|
|
|
|
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
|
case "$TAG" in
|
|
v[0-9]*) : ;;
|
|
*) echo "::error::expected a v* version tag, got '$TAG'"; exit 1 ;;
|
|
esac
|
|
VERSION="${TAG#v}"
|
|
|
|
# versionCode: deterministic + monotonic from the semver (PLAN.md §10).
|
|
IFS=. read -r MA MI PA <<< "$VERSION"
|
|
: "${MA:=0}"; : "${MI:=0}"; : "${PA:=0}"
|
|
VERSION_CODE=$(( MA*10000 + MI*100 + PA ))
|
|
|
|
# Changelog: conventional-commit subjects since the previous v* tag.
|
|
PREV_TAG="$(git describe --tags --match 'v*' --abbrev=0 "${TAG}^" 2>/dev/null || true)"
|
|
if [ -n "$PREV_TAG" ]; then RANGE="${PREV_TAG}..${TAG}"; else RANGE="${TAG}"; fi
|
|
SUBJECTS="$(git log --no-merges --format='%s' $RANGE || true)"
|
|
|
|
{
|
|
echo "## Runic Gateway Android ${TAG}"
|
|
echo
|
|
FEATS="$(echo "$SUBJECTS" | grep -E '^feat' || true)"
|
|
FIXES="$(echo "$SUBJECTS" | grep -E '^(fix|perf)' || true)"
|
|
[ -n "$FEATS" ] && { echo "### Features"; echo "$FEATS" | sed 's/^/- /'; echo; }
|
|
[ -n "$FIXES" ] && { echo "### Fixes"; echo "$FIXES" | sed 's/^/- /'; echo; }
|
|
echo "### All changes"
|
|
if [ -n "$PREV_TAG" ]; then echo "Since ${PREV_TAG}:"; fi
|
|
echo "$SUBJECTS" | sed 's/^/- /'
|
|
echo
|
|
echo "---"
|
|
echo "Signed APK — sideload on Android 10+ (§10). The app self-configures its shard site on first run."
|
|
} > dist/CHANGELOG.md
|
|
|
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "versionCode=${VERSION_CODE}" >> "$GITHUB_OUTPUT"
|
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
echo "==> tag=${TAG} version=${VERSION} code=${VERSION_CODE} prev_tag=${PREV_TAG:-<none>}"
|
|
|
|
# ── SDK + signing keystore ───────────────────────────────────────────
|
|
- name: Set up Android SDK
|
|
uses: android-actions/setup-android@v3
|
|
with:
|
|
# Only put cmdline-tools on PATH. The action's default package set drags in
|
|
# the whole emulator + the legacy `tools` package (hundreds of MB, network-
|
|
# bound on this runner) that a headless APK build never uses. The next step
|
|
# installs exactly the packages we need.
|
|
packages: ''
|
|
|
|
- name: Install Android SDK packages
|
|
run: |
|
|
set +o pipefail
|
|
yes | sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"
|
|
|
|
- name: Decode signing keystore
|
|
env:
|
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${ANDROID_KEYSTORE_BASE64:-}" ]; then
|
|
echo "::error::ANDROID_KEYSTORE_BASE64 secret is not set — cannot build a signed release."
|
|
exit 1
|
|
fi
|
|
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > "${RUNNER_TEMP}/release.jks"
|
|
echo "ANDROID_KEYSTORE_FILE=${RUNNER_TEMP}/release.jks" >> "$GITHUB_ENV"
|
|
|
|
# ── Set the version, build the signed APK ────────────────────────────
|
|
- name: Set the app version to match the tag
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION="${{ steps.plan.outputs.version }}"
|
|
VERSION_CODE="${{ steps.plan.outputs.versionCode }}"
|
|
# Replace only the version defaults (the `?: "x.y.z"` / `?: N` fallbacks).
|
|
sed -i -E "s/(\?: )\"[0-9]+\.[0-9]+\.[0-9]+\"/\1\"${VERSION}\"/" "${GRADLE_MODULE}/build.gradle.kts"
|
|
sed -i -E "s/(toIntOrNull\(\) \?: )[0-9]+/\1${VERSION_CODE}/" "${GRADLE_MODULE}/build.gradle.kts"
|
|
grep -nE "versionCode = |versionName = " "${GRADLE_MODULE}/build.gradle.kts"
|
|
|
|
- name: Unit tests + signed release APK
|
|
env:
|
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
chmod +x ./gradlew
|
|
./gradlew --no-daemon :${GRADLE_MODULE}:testDebugUnitTest :${GRADLE_MODULE}:assembleRelease
|
|
|
|
- name: Package APK + SHA256SUMS
|
|
run: |
|
|
set -euo pipefail
|
|
SRC="${GRADLE_MODULE}/build/outputs/apk/release/app-release.apk"
|
|
test -f "$SRC" || { echo "::error::release APK not found at $SRC"; exit 1; }
|
|
cp "$SRC" "dist/runic-gateway-${{ steps.plan.outputs.version }}.apk"
|
|
( cd dist && sha256sum "runic-gateway-${{ steps.plan.outputs.version }}.apk" > SHA256SUMS )
|
|
ls -l dist && cat dist/SHA256SUMS
|
|
|
|
# ── Create the Gitea release + upload assets (no push to main) ───────
|
|
- name: Create Gitea release and upload assets
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${{ steps.plan.outputs.tag }}"
|
|
API="https://${GITEA_HOST}/api/v1/repos/${REPO}"
|
|
BODY="$(cat dist/CHANGELOG.md)"
|
|
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
|
|
|
|
REL_ID="$(curl -sSf -X POST "${API}/releases" \
|
|
-H "Authorization: token ${CI_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$(jq -n --arg tag "$TAG" --arg body "$BODY" \
|
|
'{tag_name:$tag, name:$tag, body:$body, draft:false, prerelease:false}')" \
|
|
| jq -r '.id')"
|
|
echo "Created release ${TAG} (id=${REL_ID})"
|
|
|
|
for f in "runic-gateway-${{ steps.plan.outputs.version }}.apk" SHA256SUMS; do
|
|
curl -sSf -X POST "${API}/releases/${REL_ID}/assets?name=${f}" \
|
|
-H "Authorization: token ${CI_TOKEN}" \
|
|
-F "attachment=@dist/${f}" >/dev/null
|
|
echo " uploaded ${f}"
|
|
done
|