How to Sign an Android App in CI With a Keystore
Store the keystore as a base64 secret, decode it to a temp file at build time, and pass passwords via secret-backed env vars, never committed.
Base64-encode the .jks locally (base64 -w0 release.jks), save it plus the store/key passwords as repo secrets, then decode into a runner-only file that you remove after the build.
Steps
- Add secrets:
ANDROID_KEYSTORE_B64,KEYSTORE_PASSWORD,KEY_ALIAS,KEY_PASSWORD. - Decode the keystore into
$RUNNER_TEMPso it never lands in the workspace. - Pass the passwords to Gradle as
-Pproperties read from env secrets.
Workflow
.github/workflows/ci.yml
- name: Decode keystore
run: echo "${{ secrets.ANDROID_KEYSTORE_B64 }}" | base64 -d > "$RUNNER_TEMP/release.jks"
- name: Build signed bundle
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
./gradlew bundleRelease \
-Pandroid.injected.signing.store.file="$RUNNER_TEMP/release.jks" \
-Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"
- name: Clean up keystore
if: always()
run: rm -f "$RUNNER_TEMP/release.jks"Gotchas
- Never echo the decoded keystore or passwords; GitHub masks registered secrets in logs but not files you print.
- Run the cleanup step with
if: always()so the keystore is removed even when the build fails.
Related guides
How to Build an Android APK or AAB in CIBuild a release Android APK or app bundle in CI by running gradlew assembleRelease or bundleRelease on a JDK-…
How to Manage Mobile Signing Secrets Safely in CIManage mobile signing secrets safely in CI by base64-encoding keys into repo or environment secrets, decoding…