From edc6efc600583a590bc0cc9b344ee07c4150a5ea Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Sat, 28 Jun 2025 12:47:01 -0400 Subject: [PATCH] [UPDATE] Enable android app release signing from CI --- .github/workflows/android-release.yml | 7 ++ .github/workflows/test-keystore-clean.yml | 114 ++++++++++++++++++++++ KEYSTORE_RESOLUTION.md | 0 app/build.gradle.kts | 23 +++-- keystore/README.md | 38 ++++++-- 5 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/test-keystore-clean.yml create mode 100644 KEYSTORE_RESOLUTION.md diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 2fc439b..23e3661 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -33,8 +33,15 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + - name: Decode keystore from base64 + run: | + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore/trmnl-app-release.keystore + - name: Build Release APK run: ./gradlew assembleStandardRelease + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} - name: Extract version name id: version diff --git a/.github/workflows/test-keystore-clean.yml b/.github/workflows/test-keystore-clean.yml new file mode 100644 index 0000000..3e05f2a --- /dev/null +++ b/.github/workflows/test-keystore-clean.yml @@ -0,0 +1,114 @@ +name: Test Keystore Configuration + +# This workflow tests the production keystore configuration to ensure it works properly +# with the Android build system. It validates that the keystore can be decoded, passwords +# are correct, and jarsigner can successfully sign APKs. + +on: + workflow_dispatch: + +jobs: + test-keystore: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '23' + distribution: 'temurin' + + - name: Create test directory + run: mkdir -p keystore-test + + - name: Decode keystore from base64 + run: | + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore-test/release.keystore + + - name: Verify keystore basic properties + run: | + echo "=== Keystore Basic Information ===" + keytool -list -keystore keystore-test/release.keystore -storepass "${{ secrets.KEYSTORE_PASSWORD }}" | grep -v "Certificate fingerprint" + + - name: Check alias exists + run: | + echo "=== Checking Alias ===" + if keytool -list -keystore keystore-test/release.keystore -storepass "${{ secrets.KEYSTORE_PASSWORD }}" -alias "${{ secrets.KEY_ALIAS }}" > /dev/null 2>&1; then + echo "✅ Alias '${{ secrets.KEY_ALIAS }}' exists in keystore" + else + echo "❌ Alias '${{ secrets.KEY_ALIAS }}' not found in keystore" + exit 1 + fi + + - name: Test jarsigner functionality + run: | + echo "=== Testing jarsigner ===" + + # Create a simple JAR to test signing + echo "Test content" > keystore-test/test.txt + jar cf keystore-test/test.jar keystore-test/test.txt + + # Test signing WITHOUT -keypass (the working approach) + echo "Testing jarsigner without explicit key password..." + if jarsigner -keystore keystore-test/release.keystore \ + -storepass "${{ secrets.KEYSTORE_PASSWORD }}" \ + keystore-test/test.jar \ + "${{ secrets.KEY_ALIAS }}" > keystore-test/jarsigner.txt 2>&1; then + echo "✅ jarsigner succeeded (store password used for both store and key)" + echo "✅ This confirms the keystore is compatible with our build configuration" + else + echo "❌ jarsigner failed even without explicit key password" + cat keystore-test/jarsigner.txt + exit 1 + fi + + # Verify the signed JAR + if jarsigner -verify keystore-test/test.jar > keystore-test/verify.txt 2>&1; then + echo "✅ Signed JAR verification successful" + else + echo "❌ Signed JAR verification failed" + cat keystore-test/verify.txt + exit 1 + fi + + - name: Test Android build configuration + run: | + echo "=== Testing Android Build Configuration ===" + + # Decode keystore to the expected location + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore/trmnl-app-release.keystore + + # Set up Gradle + chmod +x gradlew + + # Test the release build (this will use the new signing configuration) + echo "Building release APK with production keystore..." + if ./gradlew assembleStandardRelease \ + -PKEYSTORE_PASSWORD="${{ secrets.KEYSTORE_PASSWORD }}" \ + -PKEY_ALIAS="${{ secrets.KEY_ALIAS }}" > keystore-test/gradle-build.txt 2>&1; then + echo "✅ Android release build succeeded with production keystore" + echo "✅ APK should be properly signed" + + # Verify the APK exists + if [ -f "app/build/outputs/apk/standard/release/app-standard-release.apk" ]; then + echo "✅ Release APK generated successfully" + else + echo "❌ Release APK not found" + exit 1 + fi + else + echo "❌ Android release build failed" + echo "Build output:" + cat keystore-test/gradle-build.txt + exit 1 + fi + + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: keystore-test-results + path: keystore-test/ + retention-days: 7 diff --git a/KEYSTORE_RESOLUTION.md b/KEYSTORE_RESOLUTION.md new file mode 100644 index 0000000..e69de29 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2ddf48c..1088e79 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -42,16 +42,16 @@ android { } create("release") { - // Uses the same debug keystore for release builds to enable CLI building - // ⚠️ This is temporary solution as app is still under development - // It allows early adopters to test drive the app without us worrying about signing key. - // - 📚 https://github.com/usetrmnl/trmnl-android/blob/main/keystore/README.md - storeFile = file("${rootProject.projectDir}/keystore/debug.keystore") + // Production keystore for release builds + // The keystore file should be decoded from KEYSTORE_BASE64 secret in CI/CD + storeFile = file("${rootProject.projectDir}/keystore/trmnl-app-release.keystore") - // ℹ️ When time comes, these values should come from `secret.properties` file or CI/CD secrets. - storePassword = "android" - keyAlias = "androiddebugkey" - keyPassword = "android" + // Values come from CI/CD secrets or local secret.properties file + // Note: keyPassword is intentionally omitted - this keystore works when + // jarsigner uses the store password for both store and key access + storePassword = System.getenv("KEYSTORE_PASSWORD") ?: project.findProperty("KEYSTORE_PASSWORD") as String? + keyAlias = System.getenv("KEY_ALIAS") ?: project.findProperty("KEY_ALIAS") as String? + // keyPassword = ... // Intentionally omitted - see note above } } @@ -99,7 +99,10 @@ android { // F-Droid specific configuration // No non-free dependencies buildConfigField("Boolean", "FDROID_BUILD", "true") - // ℹ️ No signing config for F-Droid flavor (F-Droid handles signing) + + // ℹ️ Also sign the F-Droid build with the release keystore + // F-Droid will use reproducible builds process to validate authenticity + signingConfig = signingConfigs.getByName("release") } } diff --git a/keystore/README.md b/keystore/README.md index c1170fe..96b26a3 100644 --- a/keystore/README.md +++ b/keystore/README.md @@ -1,18 +1,40 @@ -# Debug Keystore +# App Signing Keystores -The debug keystore file is added to the repository to make it easier for developers to build and run +This directory contains the keystores used for signing the Android app. + +## Debug Keystore + +The debug keystore file (`debug.keystore`) is added to the repository to make it easier for developers to build and run the app without having to generate a new keystore file each time. The debug keystore is used for -signing the app during development and [CI builds](https://github.com/usetrmnl/trmnl-android/actions/workflows/android-release.yml), -which allows early adopters test drive the app. - -This solution is **not intended for production use**, and can't be used to publish the app to the -Google Play Store. If that time ever comes, aside from creating release keystore, there needs to be -a clear strategy for release and maintenance of the keystore file with passcodes. +signing the app during development. > [!NOTE] > The debug keystore is generated automatically by Android Studio > and copied from the `$HOME/.android/debug.keystore` location. +## Production Keystore + +The production keystore (`trmnl-app-release.keystore`) is used for release builds and is stored as a base64-encoded secret in GitHub Actions. The keystore is decoded during CI/CD builds. + +### Important Notes About the Production Keystore + +The production keystore has a specific configuration quirk that's important to understand: + +- **Store Password**: Used to access the keystore file +- **Key Password**: The keystore was created with a key password, but due to PKCS12 format behavior, the private key is only accessible when jarsigner uses the store password for both store and key access +- **Solution**: The `keyPassword` parameter is intentionally omitted from the Android build configuration, allowing the Android build system to use the store password for both purposes + +This is a known characteristic of certain PKCS12 keystores where explicit key passwords can cause "key associated with alias not a private key" errors, even when the keystore is completely valid. + +### Secrets Configuration + +The following GitHub Actions secrets are required: +- `KEYSTORE_BASE64`: Base64-encoded production keystore file +- `KEYSTORE_PASSWORD`: Password for accessing the keystore +- `KEY_ALIAS`: Alias of the signing key within the keystore + +Note that `KEY_PASSWORD` is not used in the build configuration due to the keystore behavior described above. + ## Related Resources - https://developer.android.com/studio/publish/app-signing - https://source.android.com/docs/security/features/apksigning \ No newline at end of file