feat(release): m6 release mechanics — signed APK, R8, version guard, icons
All checks were successful
PR Checks / android-build (pull_request) Successful in 9m52s
All checks were successful
PR Checks / android-build (pull_request) Successful in 9m52s
Release-hardening pass (PLAN.md §9 M6, §10, §12). No architecture, data-flow, or endpoint changes; the app remains a pure API client. App icons (default brand assets): - New gateway-medallion launcher icon set (all densities, adaptive fg/bg, round, Play Store icon) + an RG notification icon staged for M7 push. - Replace the Image Asset wizard's default green-grid adaptive background with the deep-indigo brand fill (@color/ic_launcher_background #1B1033); recomposite the legacy square/round webps and the 512 Play icon over indigo so the whole set is coherent (the green never shipped). Restore the SPDX headers the wizard stripped; drop the orphaned placeholder foreground vector. No <monochrome> layer — the full-colour medallion has no clean silhouette, so themed mode falls back to the standard icon rather than a tinted blob. Version-mismatch guard (§3): - The connect probe now refuses a Runic Gateway backend whose API version this build can't speak (e.g. a future v2) with a clear "app out of date" message, instead of mis-rendering; lenient on a blank api (older backend). Decision logic extracted to a pure ConnectionRepository.evaluateVersion() with unit tests. Release build hardening (§7, §12): - Enable R8 full-mode minify + resource shrink for release (~31 MB debug -> 4.2 MB signed release). ProGuard keep-rules for kotlinx.serialization serializers + our wire DTOs, Retrofit service interfaces, and a -dontwarn for Tink's compile-only Error Prone annotations (EncryptedSharedPreferences). - Release signingConfig reads keystore material from a gitignored keystore.properties or env vars; absent -> unsigned (debug + PR gate unaffected). Keystore never in repo. - versionName/versionCode overridable via -P so the release tag + CI run number drive them (§10). CI: - release.yml: on a `v*` tag, build a SIGNED release APK (keystore from a base64 Gitea secret) and attach it + SHA256SUMS to a Gitea release; workflow_dispatch is a signing dry run. Mirrors pr-checks.yml's self-hosted-runner handling (apt JDK 17, explicit sdkmanager, in-step chmod +x gradlew). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
150
.gitea/workflows/release.yml
Normal file
150
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,150 @@
|
||||
# Build a SIGNED release APK and attach it to a Gitea release (PLAN.md §10, §12).
|
||||
#
|
||||
# Trigger: pushing a semver tag `v*` (e.g. `v1.0.0`). Cutting an APK is a
|
||||
# deliberate act — we do NOT release on every merge to main — so the tag is the
|
||||
# source of truth for the version. `workflow_dispatch` builds the signed APK too
|
||||
# but skips publishing (a dry run to smoke-test signing without cutting a release).
|
||||
#
|
||||
# Version: the tag drives versionName (`v1.2.3` -> `1.2.3`); the workflow run
|
||||
# number is the monotonic versionCode. Both are passed to Gradle as -P overrides.
|
||||
#
|
||||
# Signing (Settings -> Actions -> Secrets on RunicGateway/Android-app). The
|
||||
# keystore never lives in the repo — it is a base64 secret decoded at build time.
|
||||
# Secret names deliberately avoid the reserved GITEA_/GITHUB_ prefixes:
|
||||
# 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 (equals the store password for a
|
||||
# PKCS12 keystore)
|
||||
# The release itself is created with the runner's built-in ${{ github.token }},
|
||||
# so no extra API token secret is required.
|
||||
#
|
||||
# Runner notes are identical to 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 rather than using 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: {}
|
||||
|
||||
concurrency:
|
||||
group: release-apk-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GITEA_HOST: gitea.whitlocktech.com
|
||||
REPO: RunicGateway/Android-app
|
||||
|
||||
jobs:
|
||||
release-apk:
|
||||
runs-on: ubuntu-latest
|
||||
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"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Install Android SDK packages
|
||||
run: |
|
||||
set +o pipefail
|
||||
yes | sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', 'gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-
|
||||
|
||||
# Derive versionName from the tag (dispatch runs get a 0.0.0-dev placeholder,
|
||||
# since they don't publish) and a monotonic versionCode from the run number.
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||
VN="${GITHUB_REF_NAME#v}"
|
||||
else
|
||||
VN="0.0.0-dev"
|
||||
fi
|
||||
echo "versionName=${VN}" >> "$GITHUB_OUTPUT"
|
||||
echo "versionCode=${{ github.run_number }}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "==> versionName=${VN} versionCode=${{ github.run_number }} ref=${GITHUB_REF_NAME}"
|
||||
|
||||
# Decode the keystore secret to a file the build reads via ANDROID_KEYSTORE_FILE.
|
||||
# `base64 -d` tolerates the trailing newline a pasted secret may carry.
|
||||
- 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"
|
||||
|
||||
- name: Build 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 :app:assembleRelease \
|
||||
-PversionName="${{ steps.ver.outputs.versionName }}" \
|
||||
-PversionCode="${{ steps.ver.outputs.versionCode }}"
|
||||
|
||||
- name: Stage APK
|
||||
id: stage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SRC="app/build/outputs/apk/release/app-release.apk"
|
||||
test -f "$SRC" || { echo "::error::release APK not found at $SRC"; exit 1; }
|
||||
mkdir -p dist
|
||||
OUT="dist/runic-gateway-${{ steps.ver.outputs.versionName }}.apk"
|
||||
cp "$SRC" "$OUT"
|
||||
( cd dist && sha256sum "$(basename "$OUT")" > SHA256SUMS )
|
||||
echo "apk=${OUT}" >> "$GITHUB_OUTPUT"
|
||||
ls -l dist && cat dist/SHA256SUMS
|
||||
|
||||
# Publish only for a real tag push; a manual dispatch stops after the signed
|
||||
# build above (dry run).
|
||||
- name: Create Gitea release and upload APK
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ steps.ver.outputs.tag }}"
|
||||
API="https://${GITEA_HOST}/api/v1/repos/${REPO}"
|
||||
TOKEN="$(printf '%s' "${RELEASE_TOKEN}" | tr -d '\r\n')"
|
||||
REL_ID="$(curl -sSf -X POST "${API}/releases" \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg tag "$TAG" \
|
||||
'{tag_name:$tag, name:$tag, body:("Signed release APK for " + $tag + ". Sideload on Android 10+ (§10); the app self-configures its shard site on first run."), draft:false, prerelease:false}')" \
|
||||
| jq -r '.id')"
|
||||
echo "Created release ${TAG} (id=${REL_ID})"
|
||||
for f in "$(basename "${{ steps.stage.outputs.apk }}")" SHA256SUMS; do
|
||||
curl -sSf -X POST "${API}/releases/${REL_ID}/assets?name=${f}" \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-F "attachment=@dist/${f}" >/dev/null
|
||||
echo " uploaded ${f}"
|
||||
done
|
||||
Reference in New Issue
Block a user