Skip to content
Latchkey

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_TEMP so it never lands in the workspace.
  • Pass the passwords to Gradle as -P properties 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

Run this faster and cheaper on Latchkey managed runners. Start free →