Merge pull request #132 from usetrmnl/121-enable-release-key-signing

[UPDATE] Enable android app release signing from CI
This commit is contained in:
Hossain Khan
2025-06-28 13:18:18 -04:00
committed by GitHub
8 changed files with 215 additions and 25 deletions
+7
View File
@@ -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
+14 -4
View File
@@ -1,11 +1,14 @@
name: F-Droid Build
# Builds a signed F-Droid APK using the production keystore.
# F-Droid will verify the signature during their reproducible build process.
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# This allows manual triggering of the workflow, which results in making F-Droid APK build
# This allows manual triggering of the workflow, which results in making a signed F-Droid APK build
# Go to the "Actions" tab the repository, select this workflow, and click the "Run workflow" button to run it manually.
workflow_dispatch:
@@ -25,9 +28,16 @@ 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 F-Droid APK
run: ./gradlew buildFDroid
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
- name: Extract version name
id: version
@@ -39,13 +49,13 @@ jobs:
- name: Rename APK
run: |
mkdir -p artifact
cp app/build/outputs/apk/fdroid/release/app-fdroid-release-unsigned.apk artifact/trmnl-mirror-fdroid-v${{ steps.version.outputs.VERSION }}-unsigned.apk
cp app/build/outputs/apk/fdroid/release/app-fdroid-release.apk artifact/trmnl-mirror-fdroid-v${{ steps.version.outputs.VERSION }}.apk
- name: Upload F-Droid APK
uses: actions/upload-artifact@v4
with:
name: trmnl-mirror-fdroid-unsigned
path: artifact/trmnl-mirror-fdroid-v${{ steps.version.outputs.VERSION }}-unsigned.apk
name: trmnl-mirror-fdroid-signed
path: artifact/trmnl-mirror-fdroid-v${{ steps.version.outputs.VERSION }}.apk
# Use maximum allowed retention period
# https://github.com/actions/upload-artifact?tab=readme-ov-file#retention-period
retention-days: 90
+136
View File
@@ -0,0 +1,136 @@
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 standard release build
echo "Building standard release APK with production keystore..."
if ./gradlew assembleStandardRelease \
-PKEYSTORE_PASSWORD="${{ secrets.KEYSTORE_PASSWORD }}" \
-PKEY_ALIAS="${{ secrets.KEY_ALIAS }}" > keystore-test/gradle-build-standard.txt 2>&1; then
echo "✅ Android standard release build succeeded with production keystore"
# Verify the APK exists
if [ -f "app/build/outputs/apk/standard/release/app-standard-release.apk" ]; then
echo "✅ Standard release APK generated successfully"
else
echo "❌ Standard release APK not found"
exit 1
fi
else
echo "❌ Android standard release build failed"
echo "Build output:"
cat keystore-test/gradle-build-standard.txt
exit 1
fi
# Test the F-Droid release build
echo ""
echo "Building F-Droid release APK with production keystore..."
if ./gradlew assembleFdroidRelease \
-PKEYSTORE_PASSWORD="${{ secrets.KEYSTORE_PASSWORD }}" \
-PKEY_ALIAS="${{ secrets.KEY_ALIAS }}" > keystore-test/gradle-build-fdroid.txt 2>&1; then
echo "✅ Android F-Droid release build succeeded with production keystore"
# Verify the APK exists
if [ -f "app/build/outputs/apk/fdroid/release/app-fdroid-release.apk" ]; then
echo "✅ F-Droid release APK generated successfully"
echo "✅ Both standard and F-Droid builds are now properly signed"
else
echo "❌ F-Droid release APK not found"
exit 1
fi
else
echo "❌ Android F-Droid release build failed"
echo "Build output:"
cat keystore-test/gradle-build-fdroid.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
+4 -2
View File
@@ -9,7 +9,7 @@ The app includes specific configurations for F-Droid compatibility:
1. A dedicated `fdroid` product flavor that excludes Google Fonts
2. A specific `fdroidRelease` build type
3. System fonts are used instead of Google Fonts for the F-Droid version
4. The F-Droid build is **not signed** (as per [PR #106](https://github.com/usetrmnl/trmnl-android/pull/106)) - F-Droid handles the signing process
4. The F-Droid build is **signed** with the production keystore for consistency across all distribution channels
## Building the F-Droid Version
@@ -19,7 +19,9 @@ To build the F-Droid version locally:
./gradlew assembleFdroidRelease
```
This will generate an unsigned APK in `app/build/outputs/apk/fdroid/release/` that is suitable for F-Droid submission. Unlike the standard release build, the F-Droid build variant does not have a signing configuration, as F-Droid's build system will handle the signing process.
This will generate a signed APK in `app/build/outputs/apk/fdroid/release/` using the production keystore. The F-Droid build variant now uses the same signing configuration as the standard release build, ensuring consistency across all distribution channels.
> **Note**: F-Droid's reproducible build process will verify that the APK can be rebuilt with the same signature, ensuring the integrity of the build process.
Alternatively, you can use the convenience task:
View File
+13 -10
View File
@@ -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 set to the same value as storePassword because this PKCS12 keystore
// requires the store password to be used 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 = System.getenv("KEYSTORE_PASSWORD") ?: project.findProperty("KEYSTORE_PASSWORD") as String?
}
}
@@ -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")
}
}
+40 -8
View File
@@ -1,18 +1,50 @@
# 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 all release builds (both standard and F-Droid flavors) and is stored as a base64-encoded secret in GitHub Actions. The keystore is decoded during CI/CD builds.
### Build Flavors Using Production Keystore
- **Standard Release**: Signs APKs for general distribution
- **F-Droid Release**: Signs APKs for F-Droid distribution (F-Droid will verify the signature during their reproducible build process)
### 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, both `storePassword` and `keyPassword` are set to the same value in the build configuration
- **Solution**: The Android Gradle Plugin requires both passwords to be explicitly set, so we 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 (used for both store and key access)
- `KEY_ALIAS`: Alias of the signing key within the keystore
### CI/CD Workflows Using Production Keystore
- **`android-release.yml`**: Builds and signs standard release APKs
- **`fdroid-build.yml`**: Builds and signs F-Droid release APKs
Both workflows decode the keystore from the base64 secret and provide the necessary environment variables for signing.
## Related Resources
- https://developer.android.com/studio/publish/app-signing
- https://source.android.com/docs/security/features/apksigning
+1 -1
View File
@@ -41,7 +41,7 @@ Builds:
subdir: app
gradle:
- fdroid
output: build/outputs/apk/fdroid/release/app-fdroid-release-unsigned.apk
output: build/outputs/apk/fdroid/release/app-fdroid-release.apk
MaintainerNotes: This app uses the F-Droid flavor for gradle build.