mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 19:27:02 +01:00
Build and publish a signed APK on every push to main
wake-alarm had no APK build at all; releases were not produced. Adds a standalone release-apk.yml plus the release signing config the app was missing. Signing follows the pattern proven in home_inventory: gradle reads android/key.properties when present and falls back to the debug key when absent, so local `flutter run --release` is unaffected. CI writes that file from repository secrets and shreds it afterwards, and apksigner fails the build closed if a debug-signed APK ever appears. This repo has no Flutter analyze/test job, so the release job is not gated on tests — a broken commit still publishes, with the build failing as the only backstop. Add `needs:` if a Flutter CI job is added later. versionCode is github.run_number, which is per-workflow-file — this file must never be renamed, or the counter resets and updates stop installing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dzgcZxmpS3VtmLci2sbxj
This commit is contained in:
parent
f878d48f11
commit
864fea5022
152
.github/workflows/release-apk.yml
vendored
Normal file
152
.github/workflows/release-apk.yml
vendored
Normal file
@ -0,0 +1,152 @@
|
||||
name: release-apk
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# Lets a release be rebuilt from the current main without a dummy commit.
|
||||
workflow_dispatch:
|
||||
|
||||
# gh release create needs write access to the repo's contents.
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
# Releases are tag-anchored; two concurrent runs would race on the same tag.
|
||||
concurrency:
|
||||
group: release-apk-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
# NOTE: versionCode is github.run_number, which is per-workflow-FILE. Never
|
||||
# rename or replace this file — the counter resets to 1, versionCode goes
|
||||
# backwards, and Android silently refuses every future update.
|
||||
#
|
||||
# This job is NOT gated on a test suite: this repo has no Flutter analyze/test
|
||||
# job to depend on, so a broken commit still publishes. The build failing is
|
||||
# the only backstop. Add `needs:` here if a Flutter CI job is ever added.
|
||||
jobs:
|
||||
release:
|
||||
name: release (APK)
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: phone_app
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: 3.44.8
|
||||
channel: stable
|
||||
cache: true
|
||||
|
||||
- name: Read version from pubspec
|
||||
id: ver
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# 1.0.0+1 -> 1.0.0; the +build part is replaced by the run number.
|
||||
raw=$(grep -m1 -E '^version:' pubspec.yaml | sed -E 's/^version:[[:space:]]*//')
|
||||
semver=${raw%%+*}
|
||||
if ! echo "$semver" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::pubspec version '$raw' is not X.Y.Z"; exit 1
|
||||
fi
|
||||
echo "semver=$semver" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# One step, no echoes, printf-into-file rather than echo: the key must
|
||||
# never reach the log. storeFile is ABSOLUTE because key.properties is
|
||||
# read from android/ while file() resolves against android/app/.
|
||||
- name: Materialise the release keystore
|
||||
env:
|
||||
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
KEY_PASS: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$KEYSTORE_B64" ]; then
|
||||
echo "::error::ANDROID_KEYSTORE_BASE64 is not set; refusing to publish."
|
||||
exit 1
|
||||
fi
|
||||
umask 077
|
||||
ks="$GITHUB_WORKSPACE/phone_app/android/app/kuhy-release.jks"
|
||||
printf '%s' "$KEYSTORE_B64" | base64 -d > "$ks"
|
||||
{
|
||||
printf 'storeFile=%s\n' "$ks"
|
||||
printf 'storePassword=%s\n' "$STORE_PASS"
|
||||
printf 'keyAlias=%s\n' "$KEY_ALIAS"
|
||||
printf 'keyPassword=%s\n' "$KEY_PASS"
|
||||
} > "$GITHUB_WORKSPACE/phone_app/android/key.properties"
|
||||
ls -l "$ks"
|
||||
|
||||
- run: flutter pub get
|
||||
|
||||
- name: Build signed APK
|
||||
run: |
|
||||
flutter build apk --release \
|
||||
--build-name="${{ steps.ver.outputs.semver }}" \
|
||||
--build-number="${{ github.run_number }}"
|
||||
|
||||
# Fail closed. `findByName("release") ?: debug` silently falls back to
|
||||
# the debug key if key.properties is misplaced, producing a green build
|
||||
# that can never be installed over. This is the check that catches it.
|
||||
- name: Verify the APK is release-signed
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apk=build/app/outputs/flutter-apk/app-release.apk
|
||||
signer=$(ls -d "$ANDROID_SDK_ROOT"/build-tools/* | sort -V | tail -1)/apksigner
|
||||
"$signer" verify --print-certs "$apk" | tee /tmp/certs.txt
|
||||
if grep -qi 'CN=Android Debug' /tmp/certs.txt; then
|
||||
echo "::error::APK is debug-signed; not publishing."; exit 1
|
||||
fi
|
||||
if ! grep -q 'CN=kuhy' /tmp/certs.txt; then
|
||||
echo "::error::APK is not signed with the kuhy release key."; exit 1
|
||||
fi
|
||||
|
||||
- name: Shred the signing material
|
||||
if: always()
|
||||
run: |
|
||||
rm -f "$GITHUB_WORKSPACE/phone_app/android/key.properties" \
|
||||
"$GITHUB_WORKSPACE/phone_app/android/app/kuhy-release.jks"
|
||||
|
||||
- name: Name the asset
|
||||
id: art
|
||||
run: |
|
||||
set -euo pipefail
|
||||
out="wake-alarm-${{ steps.ver.outputs.semver }}-build.${{ github.run_number }}.apk"
|
||||
cp build/app/outputs/flutter-apk/app-release.apk "$out"
|
||||
echo "path=$out" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish the per-commit prerelease
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="v${{ steps.ver.outputs.semver }}-build.${{ github.run_number }}"
|
||||
gh release create "$tag" "${{ steps.art.outputs.path }}" \
|
||||
--prerelease \
|
||||
--target "${{ github.sha }}" \
|
||||
--title "$tag" \
|
||||
--notes "Automated build of ${{ github.sha }} (versionCode ${{ github.run_number }})."
|
||||
|
||||
# Cut a clean vX.Y.Z the first time a pubspec version is seen. A failing
|
||||
# `gh release view` is the "not published yet" signal, which also covers
|
||||
# the first-ever run on a repo with no releases.
|
||||
- name: Publish the clean version release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="v${{ steps.ver.outputs.semver }}"
|
||||
if gh release view "$tag" >/dev/null 2>&1; then
|
||||
echo "$tag already exists; leaving it as-is."; exit 0
|
||||
fi
|
||||
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
|
||||
echo "Tag $tag exists without a release; refusing to clobber."; exit 0
|
||||
fi
|
||||
gh release create "$tag" "${{ steps.art.outputs.path }}" \
|
||||
--target "${{ github.sha }}" \
|
||||
--title "$tag" \
|
||||
--notes "Release $tag (versionCode ${{ github.run_number }})."
|
||||
@ -1,9 +1,21 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
// Release signing key, kept out of the repo. CI writes key.properties from
|
||||
// repository secrets; locally the file is absent and the build falls back to
|
||||
// the debug key so `flutter run --release` still works. Every release APK is
|
||||
// signed with the one shared key, so updates install over each other instead
|
||||
// of forcing an uninstall.
|
||||
val keystoreProperties = Properties().apply {
|
||||
val f = rootProject.file("key.properties")
|
||||
if (f.exists()) f.inputStream().use { load(it) }
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.kuhy.wake_alarm_sync"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@ -24,11 +36,23 @@ android {
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (keystoreProperties.getProperty("storeFile") != null) {
|
||||
create("release") {
|
||||
storeFile = file(keystoreProperties.getProperty("storeFile"))
|
||||
storePassword = keystoreProperties.getProperty("storePassword")
|
||||
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
// Falls back to the debug key only when key.properties is absent
|
||||
// (a fresh clone or a local run), so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.findByName("release")
|
||||
?: signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user