Generic CI: github-actions. Here: signing, AAB, and the store. App: android-development. JVM tests: android-unit-testing. IAP (billing) ≠ upload: play-billing.
Detect native (gradlew) vs Expo/RN (eas.json). Do not mix eas build and bundleRelease in the same job unnecessarily.
Non-negotiable rules
- Play receives AAB (
bundleRelease), not APK — APK only for sideload/Firebase App Distribution. - Keystore / passwords only in GitHub Secrets. Never
.jks/.keystorein git. - Play App Signing enabled: CI uses the upload key.
- Play service account (JSON) in a secret, least privilege (Release to testing / production).
versionCodedeterministic (versioned file orgithub.run_numberdocumented — one source).- Track
internal(oralpha) in automated CI;productionwithenvironment: production+ reviewer.
CI (native)
jobs:
test:
runs-on: ubuntu-latest
permissions: { contents: read }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17" # o JDK do projeto
cache: gradle
- run: chmod +x gradlew
- run: ./gradlew --no-daemon lintDebug testDebugUnitTest
- Wrapper
gradlewrequired. Same JDK ascompileOptions. - Instrumented (
connectedCheck/ emulator): not in the default PR. Use Firebase Test Lab or jobworkflow_dispatch—android-instrumented-testing. - Cache Gradle via
setup-javacache: gradle.
Signing
Typical secrets: ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD.
- Decode the keystore in the release job (
base64 -d). Delete it at the end if writing to disk. signingConfigsrelease reads env (ORG_GRADLE_PROJECT_…orgradle.propertiesgenerated on the runner).- Debug signing only in
debug. NeversigningConfig signingConfigs.debuginrelease.
Release and Play
deploy-play:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: internal # ou production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: "17", cache: gradle }
- run: ./gradlew --no-daemon bundleRelease
# Fastlane supply ou action de Play Publisher
- Fastlane
supply/upload_to_play_storeor maintained action (e.g. Play Publisher) — whichever the repo already uses. - Service account JSON:
PLAY_SERVICE_ACCOUNT_JSON. - Tracks:
internal→alpha/beta→production. Promote in the Console or an explicit lane; do not jump to prod in the first workflow. - Changelog
en-US/pt-BRinfastlane/metadataif the team versions notes. - Artifact: upload the
.aab(upload-artifact) in addition to the store, short retention.
Versioning
versionNamevisible (product semver / calver).versionCodealways goes up. Conflict on Play = red CI, not--force.- Flavors (
prod/staging): correct bundle and package name; do not publishdebugto Play.
Expo / React Native
EXPO_TOKEN(or Expo OIDC if the project has it).eas build --platform android --profile productionandeas submit --platform androidor download the AAB and use the samesupply.- Profile
eas.jsonfor production points to the same internal track first. - Do not run
gradlewat the Expo root if the binary is EAS's responsibility.
Anti-patterns
- Committing the keystore "just for CI"
- APK on the Play Store
assembleReleasewithoutsigningConfig- Production track on every push
- Emulator in the PR for every change
- Recycle the service account JSON with the owner of the entire org
Integration with other skills
- Pipeline:
github-actions - App:
android-development - Tests:
android-unit-testing/android-instrumented-testing - iOS:
github-actions-ios - Go-live:
production-readiness
Completion criteria
lint+ unit in the PR- AAB signed with upload key via secrets
- Upload to a non-prod track (or protected prod)
versionCodeunique and increasing- No keystore in git