Android Cicd
npx claude-code-templates@latest --skill development/android-cicd Content
Skill: android-cicd
Purpose
Set up a complete, multi-stage Android CI/CD pipeline that automatically builds and publishes to Google Play via GitHub Actions. Supports TWA (Trusted Web Activity / Bubblewrap), React Native, Flutter, and native Android (Gradle) projects.
When to Use
- The project has an Android app tracked in a GitHub repository
- No CI/CD pipeline exists yet for the Android build
- Goal: automate publishing to Google Play on every push to
mainand on version tags - User wants to avoid manual
versionCodebumping
Quick Start
Run the interactive setup wizard from the root of the target project:
npx android-cicdThe wizard handles: framework detection → keystore generation → GitHub Secrets → workflow scaffold.
Prerequisites
Before running the wizard, ensure:
- Node.js ≥ 18
- JDK 17 installed with
keytoolaccessible (JAVA_HOMEset, or installed via Eclipse Adoptium / Android Studio) ghCLI installed and authenticated (gh auth login)- App already created in Google Play Console — at least one manual AAB/APK upload done (required before the API can publish)
- App enrolled in Play App Signing (Google manages the signing key; you manage the upload key)
- Google Play Android Developer API enabled in Google Cloud Console
- Service account JSON key downloaded (see Manual Steps below)
Framework Detection
The wizard auto-detects the framework from the project directory structure:
| Condition | Detected framework |
|---|---|
pubspec.yaml contains flutter: |
flutter |
android/app/build.gradle exists + package.json has react-native dep |
react-native |
android-root-app/build.gradle or twa-manifest.json or .bubblewrap/config.json exists |
twa |
app/build.gradle exists |
native |
android/app/build.gradle exists (fallback) |
native |
The user can override the detected framework during the wizard.
Multi-Stage Pipeline
The scaffolded workflow publishes to different tracks based on the git ref:
| Git event | Google Play track |
|---|---|
Push to main |
internal |
Tag matching v*-alpha (e.g. v1.2-alpha) |
alpha |
Tag matching v*-beta (e.g. v1.2-beta) |
beta |
Tag matching v* (e.g. v1.2.0) |
production |
Manual workflow_dispatch |
User-selectable (internal / alpha / beta / production) |
To release to production:
git tag v1.2.0
git push origin v1.2.0Auto-Bump versionCode
On every push to main, CI automatically:
- Reads the current
versionCodefrom the version file for the detected framework - Increments it by 1
- Commits the change with
[skip ci](prevents re-triggering the workflow) - Pushes the commit back to
main
Version file by framework:
| Framework | Version file | Field |
|---|---|---|
| TWA | android-root-app/build.gradle |
versionCode |
| React Native | android/app/build.gradle |
versionCode |
| Flutter | pubspec.yaml |
version: x.y.z+N (the +N build number) |
| Native | app/build.gradle |
versionCode |
For tag-based builds (alpha / beta / production), auto-bump does not run — the tag represents a pinned commit. Increment the version manually before tagging.
Required GitHub Secrets
The wizard sets these automatically via gh secret set:
| Secret | Description |
|---|---|
KEYSTORE_FILE |
Base64-encoded upload keystore (.jks) |
KEYSTORE_PASSWORD |
Keystore password |
KEY_ALIAS |
Key alias (e.g. upload) |
KEY_PASSWORD |
Key password (usually same as KEYSTORE_PASSWORD) |
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON |
Full JSON content of the service account key |
Signing Configuration
TWA / Native Android
Add to your build.gradle (see templates/gradle/signing.gradle):
android {
signingConfigs {
release {
storeFile file("keystore.jks")
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
}
}
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
}
}
}Never set
org.gradle.java.homeingradle.properties— it breaks Linux CI runners.
Flutter
The CI workflow creates android/key.properties at build time (from secrets) and cleans it up after. Your android/app/build.gradle should read from it:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}Manual Steps (Cannot Be Automated)
1. Create the service account
- Google Cloud Console → your project → IAM & Admin → Service Accounts
- Create service account → name:
github-play-publisher→ Done (no roles needed) - Click the service account → Keys tab → Add key → Create new key → JSON → download
2. Enable the Play API
Google Cloud Console → APIs & Services → search Google Play Android Developer API → Enable
3. Invite the service account in Play Console
- Play Console → Users and permissions → Invite new user
- Email:
github-play-publisher@YOUR-PROJECT.iam.gserviceaccount.com - Account-level permissions:
- ✅ Release apps to testing tracks
- ✅ Manage testing tracks and edit testers
- Apply
4. First manual upload
Google Play requires at least one manually uploaded AAB before the API can publish. If this is a brand-new app, upload the first build from your local machine before running the CI pipeline.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
Java home supplied is invalid |
org.gradle.java.home hardcoded in gradle.properties |
Remove that line |
signed with the wrong key |
Keystore in secret doesn't match Play's registered upload key | Update KEYSTORE_FILE secret |
The caller does not have permission |
Service account missing permissions or API not enabled | Re-check Manual Steps 2 and 3 |
Upload failed — wrong versionCode |
versionCode not incremented (tag-based build) | Increment versionCode manually before tagging |
shallow update not allowed |
Shallow git checkout when pushing version bump | Workflow uses fetch-depth: 0 — verify the checkout step |
| Workflow not triggering on tag | Tag not pushed to remote | Run git push origin TAG_NAME |
gh: command not found |
gh CLI not installed | Install from https://cli.github.com |
keytool not found |
JDK not installed or not on PATH | Set JAVA_HOME or install JDK 17 |
Recovering a Lost Upload Keystore
If the app uses Play App Signing (recommended):
- Generate a new keystore:
npx android-cicdand choose "I already have a keystore: No" - Export the PEM certificate:sh
keytool -export -rfc -keystore upload.jks -alias ALIAS -storepass PASSWORD -file cert.pem - Play Console → app → App integrity → App signing → Request upload key reset
- Select "I forgot my password" → upload
cert.pem - Wait 1–2 business days for Google approval
- Update the
KEYSTORE_FILEsecret with the new keystore base64
Manually Bumping the Version (Tag Releases)
Before pushing a tag for alpha / beta / production:
TWA / Native / React Native — edit build.gradle:
versionCode 8 // increment
versionName "1.2.0"Flutter — edit pubspec.yaml:
version: 1.2.0+8Then tag and push:
git add .
git commit -m "chore: bump version to 1.2.0"
git tag v1.2.0
git push origin main --tags