diff --git a/.forgejo/workflows/flutter.yml b/.forgejo/workflows/flutter.yml index 0ff1066f..b677f62b 100644 --- a/.forgejo/workflows/flutter.yml +++ b/.forgejo/workflows/flutter.yml @@ -72,6 +72,25 @@ jobs: if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/') run: flutter build apk --debug + - name: Decode signing keystore + if: startsWith(github.ref, 'refs/tags/v') + # Reconstructs the release keystore from a base64-encoded + # CI secret so every tagged build is signed with the same + # key. Without this, each CI runner would generate its own + # debug keystore and Android would refuse to upgrade an + # existing install (signature mismatch). + shell: bash + env: + ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} + run: | + if [ -z "${ANDROID_KEYSTORE_B64}" ]; then + echo "::error::ANDROID_KEYSTORE_B64 secret is missing — release builds need a consistent signing key" + exit 1 + fi + KEYSTORE_PATH="${RUNNER_TEMP}/minstrel-release.keystore" + echo "${ANDROID_KEYSTORE_B64}" | base64 -d > "${KEYSTORE_PATH}" + echo "ANDROID_KEYSTORE_PATH=${KEYSTORE_PATH}" >> "${GITHUB_ENV}" + - name: Build release APK if: startsWith(github.ref, 'refs/tags/v') # Inject the tag (sans leading 'v') as the build's --build-name @@ -79,7 +98,15 @@ jobs: # reports — otherwise the in-app update banner would always # think the user is behind because pubspec.yaml's static # version drifts from the actual release tag. + # + # ANDROID_KEYSTORE_PATH is set by the previous step; + # build.gradle.kts picks it up and signs the release APK with + # the operator's persistent keystore. shell: bash + env: + ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | TAG="${GITHUB_REF#refs/tags/v}" flutter build apk --release --build-name="${TAG}" diff --git a/flutter_client/android/app/build.gradle.kts b/flutter_client/android/app/build.gradle.kts index afad756c..54f8d478 100644 --- a/flutter_client/android/app/build.gradle.kts +++ b/flutter_client/android/app/build.gradle.kts @@ -30,11 +30,38 @@ android { versionName = flutter.versionName } + // Real release signing config — populated only when CI exports + // ANDROID_KEYSTORE_PATH (decoded from a base64 secret) plus the + // matching password/alias env vars. Without these (local + // `flutter build apk --release` runs), the release build falls + // through to the debug keystore so local builds still work. + // + // Why this matters: Flutter's default `flutter run --release` and + // CI's earlier setup both signed with the per-machine debug + // keystore (~/.android/debug.keystore). Every CI runner generated + // its own debug key, so consecutive release APKs had different + // signatures and Android refused to upgrade an existing install. + val keystorePath: String? = System.getenv("ANDROID_KEYSTORE_PATH") + if (keystorePath != null && keystorePath.isNotEmpty()) { + signingConfigs { + create("release") { + storeFile = file(keystorePath) + storePassword = System.getenv("ANDROID_STORE_PASSWORD") + keyAlias = System.getenv("ANDROID_KEY_ALIAS") + keyPassword = System.getenv("ANDROID_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") + signingConfig = if (keystorePath != null && keystorePath.isNotEmpty()) { + signingConfigs.getByName("release") + } else { + // Local-only fallback so `flutter run --release` works + // without the keystore. CI must export ANDROID_KEYSTORE_PATH. + signingConfigs.getByName("debug") + } } } }