Merge pull request 'ci(release): make the release tag-driven (stop pushing to protected main)' (#14) from ci/tag-driven-release into main
All checks were successful
Release APK / release (push) Successful in 8m44s
All checks were successful
Release APK / release (push) Successful in 8m44s
Reviewed-on: #14 Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
@@ -1,37 +1,23 @@
|
|||||||
# Automated build + release for the Runic Gateway Android app.
|
# Automated release for the Runic Gateway Android app.
|
||||||
#
|
#
|
||||||
# Trigger: every push to `main` (i.e. every merged PR).
|
# 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.
|
||||||
#
|
#
|
||||||
# Flow (two conceptual halves, kept separate on purpose) — mirrors link/'s engine:
|
# 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.
|
||||||
#
|
#
|
||||||
# ┌── RELEASE ENGINE (language-agnostic) ─────────────────────────────┐
|
# versionName = the tag without its leading `v`; versionCode = major*10000 +
|
||||||
# │ reads: latest v* git tag + conventional-commit subjects │
|
# minor*100 + patch (deterministic + monotonic, PLAN.md §10). Both are injected
|
||||||
# │ produces: next version, changelog, and (at the end) the release │
|
# into app/build.gradle.kts for the build only — nothing is committed back to main.
|
||||||
# └───────────────────────────────────────────────────────────────────┘
|
|
||||||
# ┌── ANDROID ADAPTER (the only Android-specific part) ───────────────┐
|
|
||||||
# │ consumes: the version │
|
|
||||||
# │ produces: the artifact (a signed release APK + SHA256SUMS) │
|
|
||||||
# └───────────────────────────────────────────────────────────────────┘
|
|
||||||
#
|
|
||||||
# Version bump (conventional commits since the last v* tag):
|
|
||||||
# feat!: / BREAKING CHANGE -> major feat: -> minor fix|perf: -> patch
|
|
||||||
# nothing releasable -> no release is cut
|
|
||||||
# (first ever run, no tag) -> releases the current build.gradle.kts version as-is
|
|
||||||
# versionName is the semver; versionCode is derived major*10000+minor*100+patch so
|
|
||||||
# it is deterministic + monotonic (PLAN.md §10). The bump is committed back to
|
|
||||||
# app/build.gradle.kts, then tagged.
|
|
||||||
#
|
#
|
||||||
# Prerequisites (Settings -> Actions -> Secrets on RunicGateway/Android-app):
|
# Prerequisites (Settings -> Actions -> Secrets on RunicGateway/Android-app):
|
||||||
# REGISTRY_USER — Gitea username the token below belongs to
|
# REGISTRY_TOKEN — Gitea access token with `write:repository` (create the release)
|
||||||
# REGISTRY_TOKEN — Gitea access token with `write:repository` (push the bump
|
|
||||||
# commit + tag and create the release)
|
|
||||||
# ANDROID_KEYSTORE_BASE64 — base64 of the release .jks (single line)
|
# ANDROID_KEYSTORE_BASE64 — base64 of the release .jks (single line)
|
||||||
# ANDROID_KEYSTORE_PASSWORD — keystore password
|
# ANDROID_KEYSTORE_PASSWORD — keystore password
|
||||||
# ANDROID_KEY_ALIAS — key alias (e.g. runicgateway)
|
# ANDROID_KEY_ALIAS — key alias (e.g. runicgateway)
|
||||||
# ANDROID_KEY_PASSWORD — key password (== store password for a PKCS12 keystore)
|
# ANDROID_KEY_PASSWORD — key password (== store password for a PKCS12 keystore)
|
||||||
# Also: `main` must accept a direct push from the REGISTRY_USER account (disable
|
|
||||||
# branch protection for it, or add it as an exception) — the bump commit lands on
|
|
||||||
# main. The bump commit carries `[skip ci]`, so it does not re-trigger this workflow.
|
|
||||||
#
|
#
|
||||||
# Runner handling matches pr-checks.yml (self-hosted `ubuntu-latest`): the container
|
# 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
|
# lacks git/curl/unzip and can't reach api.adoptium.net, so we apt-install the base
|
||||||
@@ -42,11 +28,16 @@ name: Release APK
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
tags:
|
||||||
workflow_dispatch: {}
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Existing v* tag to (re)build and release'
|
||||||
|
required: true
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-apk
|
group: release-apk-${{ github.event.inputs.tag || github.ref_name }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -61,10 +52,6 @@ jobs:
|
|||||||
# the self-hosted runner) instead of hanging forever and — because concurrency
|
# the self-hosted runner) instead of hanging forever and — because concurrency
|
||||||
# is `cancel-in-progress: false` — blocking every later release behind it.
|
# is `cancel-in-progress: false` — blocking every later release behind it.
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
# Don't loop on our own bump commit (belt-and-suspenders with [skip ci]).
|
|
||||||
# Quoted because the expression contains a colon (`chore(release):`), which an
|
|
||||||
# unquoted YAML scalar would misparse as a mapping value.
|
|
||||||
if: "${{ !contains(github.event.head_commit.message, 'chore(release): bump version') }}"
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install base tools + JDK 17
|
- name: Install base tools + JDK 17
|
||||||
run: |
|
run: |
|
||||||
@@ -72,70 +59,46 @@ jobs:
|
|||||||
apt-get install -y git curl unzip jq openjdk-17-jdk-headless
|
apt-get install -y git curl unzip jq openjdk-17-jdk-headless
|
||||||
echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> "$GITHUB_ENV"
|
echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Check out full history (need tags + commit log for the bump)
|
- name: Check out the release tag (full history for the changelog)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# ── RELEASE ENGINE: decide the next version + changelog ──────────────
|
# ── Derive version + changelog straight from the tag ─────────────────
|
||||||
- name: Plan the release (version + changelog)
|
- name: Plan the release (version + changelog from the tag)
|
||||||
id: plan
|
id: plan
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
git fetch --tags --force >/dev/null 2>&1 || true
|
git fetch --tags --force >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# Current committed version (the `?: "x.y.z"` default in build.gradle.kts).
|
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||||||
MANIFEST_VERSION="$(sed -nE 's/.*\?: "([0-9]+\.[0-9]+\.[0-9]+)".*/\1/p' "${GRADLE_MODULE}/build.gradle.kts" | head -1)"
|
case "$TAG" in
|
||||||
LAST_TAG="$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)"
|
v[0-9]*) : ;;
|
||||||
if [ -n "$LAST_TAG" ]; then RANGE="${LAST_TAG}..HEAD"; else RANGE="HEAD"; fi
|
*) echo "::error::expected a v* version tag, got '$TAG'"; exit 1 ;;
|
||||||
|
|
||||||
SUBJECTS="$(git log --no-merges --format='%s' $RANGE || true)"
|
|
||||||
BODIES="$(git log --no-merges --format='%B' $RANGE || true)"
|
|
||||||
|
|
||||||
BUMP=none
|
|
||||||
if echo "$BODIES" | grep -qE 'BREAKING[ -]CHANGE' ; then BUMP=major; fi
|
|
||||||
if echo "$SUBJECTS" | grep -qE '^[a-z]+(\([^)]+\))?!:' ; then BUMP=major; fi
|
|
||||||
if [ "$BUMP" = none ] && echo "$SUBJECTS" | grep -qE '^feat(\([^)]+\))?:' ; then BUMP=minor; fi
|
|
||||||
if [ "$BUMP" = none ] && echo "$SUBJECTS" | grep -qE '^(fix|perf)(\([^)]+\))?:'; then BUMP=patch; fi
|
|
||||||
|
|
||||||
bump() { # <x.y.z> <major|minor|patch> -> bumped
|
|
||||||
IFS=. read -r MA MI PA <<< "$1"
|
|
||||||
case "$2" in
|
|
||||||
major) echo "$((MA+1)).0.0" ;;
|
|
||||||
minor) echo "${MA}.$((MI+1)).0" ;;
|
|
||||||
patch) echo "${MA}.${MI}.$((PA+1))" ;;
|
|
||||||
esac
|
esac
|
||||||
}
|
VERSION="${TAG#v}"
|
||||||
|
|
||||||
RELEASE=true
|
# versionCode: deterministic + monotonic from the semver (PLAN.md §10).
|
||||||
if [ -z "$LAST_TAG" ]; then
|
|
||||||
VERSION="$MANIFEST_VERSION" # first release: ship what's committed
|
|
||||||
elif [ "$BUMP" = none ]; then
|
|
||||||
RELEASE=false # no feat/fix/breaking since last tag
|
|
||||||
VERSION="${LAST_TAG#v}"
|
|
||||||
else
|
|
||||||
VERSION="$(bump "${LAST_TAG#v}" "$BUMP")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
|
|
||||||
echo "Tag v${VERSION} already exists — nothing to release."
|
|
||||||
RELEASE=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Derive a deterministic, monotonic versionCode from the semver.
|
|
||||||
IFS=. read -r MA MI PA <<< "$VERSION"
|
IFS=. read -r MA MI PA <<< "$VERSION"
|
||||||
|
: "${MA:=0}"; : "${MI:=0}"; : "${PA:=0}"
|
||||||
VERSION_CODE=$(( MA*10000 + MI*100 + PA ))
|
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 v${VERSION}"
|
echo "## Runic Gateway Android ${TAG}"
|
||||||
echo
|
echo
|
||||||
FEATS="$(echo "$SUBJECTS" | grep -E '^feat' || true)"
|
FEATS="$(echo "$SUBJECTS" | grep -E '^feat' || true)"
|
||||||
FIXES="$(echo "$SUBJECTS" | grep -E '^(fix|perf)' || true)"
|
FIXES="$(echo "$SUBJECTS" | grep -E '^(fix|perf)' || true)"
|
||||||
[ -n "$FEATS" ] && { echo "### Features"; echo "$FEATS" | sed 's/^/- /'; echo; }
|
[ -n "$FEATS" ] && { echo "### Features"; echo "$FEATS" | sed 's/^/- /'; echo; }
|
||||||
[ -n "$FIXES" ] && { echo "### Fixes"; echo "$FIXES" | sed 's/^/- /'; echo; }
|
[ -n "$FIXES" ] && { echo "### Fixes"; echo "$FIXES" | sed 's/^/- /'; echo; }
|
||||||
echo "### All changes"
|
echo "### All changes"
|
||||||
if [ -n "$LAST_TAG" ]; then echo "Since ${LAST_TAG}:"; fi
|
if [ -n "$PREV_TAG" ]; then echo "Since ${PREV_TAG}:"; fi
|
||||||
echo "$SUBJECTS" | sed 's/^/- /'
|
echo "$SUBJECTS" | sed 's/^/- /'
|
||||||
echo
|
echo
|
||||||
echo "---"
|
echo "---"
|
||||||
@@ -144,14 +107,11 @@ jobs:
|
|||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "versionCode=${VERSION_CODE}" >> "$GITHUB_OUTPUT"
|
echo "versionCode=${VERSION_CODE}" >> "$GITHUB_OUTPUT"
|
||||||
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
echo "release=${RELEASE}" >> "$GITHUB_OUTPUT"
|
echo "==> tag=${TAG} version=${VERSION} code=${VERSION_CODE} prev_tag=${PREV_TAG:-<none>}"
|
||||||
echo "bump=${BUMP}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "==> release=${RELEASE} version=${VERSION} code=${VERSION_CODE} bump=${BUMP} last_tag=${LAST_TAG:-<none>}"
|
|
||||||
|
|
||||||
# ── ANDROID ADAPTER: SDK + signing keystore ──────────────────────────
|
# ── SDK + signing keystore ───────────────────────────────────────────
|
||||||
- name: Set up Android SDK
|
- name: Set up Android SDK
|
||||||
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v3
|
||||||
with:
|
with:
|
||||||
# Only put cmdline-tools on PATH. The action's default package set drags in
|
# Only put cmdline-tools on PATH. The action's default package set drags in
|
||||||
@@ -161,13 +121,11 @@ jobs:
|
|||||||
packages: ''
|
packages: ''
|
||||||
|
|
||||||
- name: Install Android SDK packages
|
- name: Install Android SDK packages
|
||||||
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
||||||
run: |
|
run: |
|
||||||
set +o pipefail
|
set +o pipefail
|
||||||
yes | sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"
|
yes | sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"
|
||||||
|
|
||||||
- name: Decode signing keystore
|
- name: Decode signing keystore
|
||||||
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
||||||
env:
|
env:
|
||||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
run: |
|
run: |
|
||||||
@@ -179,9 +137,8 @@ jobs:
|
|||||||
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > "${RUNNER_TEMP}/release.jks"
|
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > "${RUNNER_TEMP}/release.jks"
|
||||||
echo "ANDROID_KEYSTORE_FILE=${RUNNER_TEMP}/release.jks" >> "$GITHUB_ENV"
|
echo "ANDROID_KEYSTORE_FILE=${RUNNER_TEMP}/release.jks" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
# ── ANDROID ADAPTER: set the version, gate, build the signed APK ─────
|
# ── Set the version, build the signed APK ────────────────────────────
|
||||||
- name: Set the app version to match the release
|
- name: Set the app version to match the tag
|
||||||
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
VERSION="${{ steps.plan.outputs.version }}"
|
VERSION="${{ steps.plan.outputs.version }}"
|
||||||
@@ -192,7 +149,6 @@ jobs:
|
|||||||
grep -nE "versionCode = |versionName = " "${GRADLE_MODULE}/build.gradle.kts"
|
grep -nE "versionCode = |versionName = " "${GRADLE_MODULE}/build.gradle.kts"
|
||||||
|
|
||||||
- name: Unit tests + signed release APK
|
- name: Unit tests + signed release APK
|
||||||
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
||||||
env:
|
env:
|
||||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
@@ -203,7 +159,6 @@ jobs:
|
|||||||
./gradlew --no-daemon :${GRADLE_MODULE}:testDebugUnitTest :${GRADLE_MODULE}:assembleRelease
|
./gradlew --no-daemon :${GRADLE_MODULE}:testDebugUnitTest :${GRADLE_MODULE}:assembleRelease
|
||||||
|
|
||||||
- name: Package APK + SHA256SUMS
|
- name: Package APK + SHA256SUMS
|
||||||
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SRC="${GRADLE_MODULE}/build/outputs/apk/release/app-release.apk"
|
SRC="${GRADLE_MODULE}/build/outputs/apk/release/app-release.apk"
|
||||||
@@ -212,37 +167,8 @@ jobs:
|
|||||||
( cd dist && sha256sum "runic-gateway-${{ steps.plan.outputs.version }}.apk" > SHA256SUMS )
|
( cd dist && sha256sum "runic-gateway-${{ steps.plan.outputs.version }}.apk" > SHA256SUMS )
|
||||||
ls -l dist && cat dist/SHA256SUMS
|
ls -l dist && cat dist/SHA256SUMS
|
||||||
|
|
||||||
# ── RELEASE ENGINE: commit the bump, tag, push ───────────────────────
|
# ── Create the Gitea release + upload assets (no push to main) ───────
|
||||||
- name: Commit version bump and push tag
|
|
||||||
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
||||||
env:
|
|
||||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
||||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
TAG="${{ steps.plan.outputs.tag }}"
|
|
||||||
# Secrets can arrive with a trailing newline; a stray CR/LF corrupts the
|
|
||||||
# remote URL / auth header. Strip line breaks before use.
|
|
||||||
CI_USER="$(printf '%s' "${REGISTRY_USER}" | tr -d '\r\n')"
|
|
||||||
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
|
|
||||||
git config user.name "android-app-ci"
|
|
||||||
git config user.email "ci@whitlocktech.com"
|
|
||||||
git remote set-url origin \
|
|
||||||
"https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${REPO}.git"
|
|
||||||
|
|
||||||
git add "${GRADLE_MODULE}/build.gradle.kts"
|
|
||||||
if ! git diff --cached --quiet; then
|
|
||||||
git commit -m "chore(release): bump version to ${TAG} [skip ci]"
|
|
||||||
git push origin "HEAD:main"
|
|
||||||
else
|
|
||||||
echo "Version unchanged (first release) — no bump commit needed."
|
|
||||||
fi
|
|
||||||
git tag "${TAG}"
|
|
||||||
git push origin "${TAG}"
|
|
||||||
|
|
||||||
# ── RELEASE ENGINE: create the Gitea release + upload assets ─────────
|
|
||||||
- name: Create Gitea release and upload assets
|
- name: Create Gitea release and upload assets
|
||||||
if: ${{ steps.plan.outputs.release == 'true' }}
|
|
||||||
env:
|
env:
|
||||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
Reference in New Issue
Block a user