mirror of https://github.com/ospab/ostp.git
fix(android): sign releases with a stable upload key, not the debug keystore
Published APKs could never be updated over - users hit "App not installed" or "unable to parse the package" and had to uninstall first. The cause was not the version code (verified: local.properties carries flutter.versionCode=23 and gha.ps1 bumps pubspec's build number every release, so it increments correctly). It was the signing key: app/build.gradle.kts still had the stock Flutter template TODO and pointed the release build type at signingConfigs["debug"]. Android identifies an app by applicationId + signing key and refuses to update across a key change, and the debug keystore is generated per machine - on ephemeral CI runners that means every single published build was signed with a different random key. Release builds now take their key from android/key.properties or the OSTP_KEYSTORE_* environment variables, falling back to debug (with a loud warning) only so local `flutter build apk --release` keeps working. CI materialises the keystore from repository secrets, refuses to build at all if the secret is absent, and re-verifies the finished APK is not debug-signed rather than ever shipping an un-updatable build again. NOTE: existing installs are signed with a now-unreproducible random key, so users must uninstall once more for THIS release. Every update after it works.
This commit is contained in:
parent
a8aba8f4b8
commit
32c36afc3b
|
|
@ -593,21 +593,50 @@ jobs:
|
|||
- name: Build Android APK
|
||||
shell: bash
|
||||
working-directory: ostp-flutter
|
||||
env:
|
||||
OSTP_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
OSTP_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
OSTP_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
OSTP_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: |
|
||||
# 1. Compile JNI
|
||||
set -euo pipefail
|
||||
|
||||
# 1. Materialise the upload keystore from secrets. Android keys an app
|
||||
# by applicationId + signing key and refuses to update across a key
|
||||
# change, so every published build MUST use this one key. Releases
|
||||
# used to fall through to the per-machine debug keystore, which on
|
||||
# ephemeral CI runners meant a different random key every build -
|
||||
# hence "App not installed" on upgrade.
|
||||
if [ -z "${OSTP_KEYSTORE_B64:-}" ]; then
|
||||
echo "::error::ANDROID_KEYSTORE_BASE64 secret is not set. Refusing to publish a"
|
||||
echo "::error::debug-signed APK: users could not update over it and the key is"
|
||||
echo "::error::not reproducible. See docs for the one-time keystore setup."
|
||||
exit 1
|
||||
fi
|
||||
export OSTP_KEYSTORE_PATH="$RUNNER_TEMP/ostp-upload.jks"
|
||||
echo "$OSTP_KEYSTORE_B64" | base64 -d > "$OSTP_KEYSTORE_PATH"
|
||||
|
||||
# 2. Compile JNI
|
||||
mkdir -p android/app/src/main/jniLibs/${{ matrix.arch }}
|
||||
|
||||
|
||||
cd ../ostp-jni
|
||||
cargo ndk -t ${{ matrix.arch }} -o "../ostp-flutter/android/app/src/main/jniLibs" build --release
|
||||
cd ../ostp-flutter
|
||||
|
||||
|
||||
|
||||
# 3. Build Flutter APK
|
||||
flutter build apk --release --target-platform ${{ matrix.flutter_target }}
|
||||
|
||||
# 4. Copy to output
|
||||
cp build/app/outputs/flutter-apk/app-release.apk ostp-android-${{ matrix.arch }}.apk
|
||||
|
||||
# 4. Fail loudly if the APK somehow still came out debug-signed, rather
|
||||
# than shipping another un-updatable build.
|
||||
APK=build/app/outputs/flutter-apk/app-release.apk
|
||||
if "$ANDROID_HOME"/build-tools/*/apksigner verify --print-certs "$APK" 2>/dev/null \
|
||||
| grep -qi "CN=Android Debug"; then
|
||||
echo "::error::APK is signed with the Android debug certificate - aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. Copy to output
|
||||
cp "$APK" ostp-android-${{ matrix.arch }}.apk
|
||||
|
||||
- name: Upload to GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
|
|
@ -5,6 +8,30 @@ plugins {
|
|||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
// ── Release signing material ────────────────────────────────────────────────
|
||||
// Supplied out-of-band and never committed: either an `android/key.properties`
|
||||
// file (local release builds) or OSTP_KEYSTORE_* environment variables (CI).
|
||||
//
|
||||
// This exists because the release build used to be signed with the DEBUG
|
||||
// keystore (the stock Flutter template TODO). Android identifies an app by
|
||||
// applicationId + signing key, and refuses to update across a key change. The
|
||||
// debug keystore is auto-generated per machine, and CI runners are ephemeral,
|
||||
// so every published build carried a different random key — which is why
|
||||
// updating on top of a previous install failed with "App not installed" /
|
||||
// "unable to parse the package" and only a full uninstall+reinstall worked.
|
||||
val keystoreProperties = Properties().apply {
|
||||
val propsFile = rootProject.file("key.properties")
|
||||
if (propsFile.exists()) {
|
||||
FileInputStream(propsFile).use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun signingSetting(propKey: String, envKey: String): String? =
|
||||
keystoreProperties.getProperty(propKey) ?: System.getenv(envKey)
|
||||
|
||||
val releaseStorePath: String? = signingSetting("storeFile", "OSTP_KEYSTORE_PATH")
|
||||
val hasReleaseSigning: Boolean = !releaseStorePath.isNullOrBlank()
|
||||
|
||||
android {
|
||||
namespace = "com.ospab.ostp_client"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
|
|
@ -34,11 +61,35 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
if (hasReleaseSigning) {
|
||||
storeFile = file(releaseStorePath!!)
|
||||
storePassword = signingSetting("storePassword", "OSTP_KEYSTORE_PASSWORD")
|
||||
keyAlias = signingSetting("keyAlias", "OSTP_KEY_ALIAS")
|
||||
keyPassword = signingSetting("keyPassword", "OSTP_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
// Use the real upload key when one was supplied; otherwise fall back to
|
||||
// the debug keystore so a plain local `flutter build apk --release`
|
||||
// still works for development. Anything PUBLISHED must take the first
|
||||
// branch — a debug-signed build cannot be updated over, and its key is
|
||||
// machine-local, so it also can't be reproduced later.
|
||||
if (hasReleaseSigning) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
} else {
|
||||
logger.warn(
|
||||
"OSTP: no release keystore configured (android/key.properties or " +
|
||||
"OSTP_KEYSTORE_PATH) - falling back to the DEBUG keystore. This APK " +
|
||||
"is for local use only: users cannot update over it, and the key is " +
|
||||
"not reproducible on another machine."
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue