Automated Android CI/CD pipeline to Google Play — supports TWA, React Native, Flutter, and native Android. Run npx android-cicd to set up keystore generation, GitHub Secrets, and a multi-stage workflow (internal/alpha/beta/production) with auto-bump versionCode.
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.
main and on version tagsversionCode bumpingRun the interactive setup wizard from the root of the target project:
npx android-cicd
The wizard handles: framework detection → keystore generation → GitHub Secrets → workflow scaffold.
Before running the wizard, ensure:
keytool accessible (JAVA_HOME set, or installed via Eclipse Adoptium / Android Studio)gh CLI installed and authenticated (gh auth login)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.
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.0
On every push to main, CI automatically:
versionCode from the version file for the detected framework[skip ci] (prevents re-triggering the workflow)mainVersion 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.
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 |
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.
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
}
}
}
github-play-publisher → Done (no roles needed)Google Cloud Console → APIs & Services → search Google Play Android Developer API → Enable
github-play-publisher@YOUR-PROJECT.iam.gserviceaccount.comGoogle 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.
| 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 |
If the app uses Play App Signing (recommended):
npx android-cicd and choose "I already have a keystore: No"keytool -export -rfc -keystore upload.jks -alias ALIAS -storepass PASSWORD -file cert.pem
cert.pemKEYSTORE_FILE secret with the new keystore base64Before 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+8
Then 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
Copy a source-pinned command for your client. You run it yourself.
Destination: .claude/skills/android-cicd · pinned to the source commit
# Run from your project root
git clone https://github.com/davila7/claude-code-templates.git .skillboard-tmp
git -C .skillboard-tmp checkout 5cc17cf267132cc690dee82239b80a99ec82e4f8
mkdir -p ".claude/skills"
cp -r ".skillboard-tmp/cli-tool/components/skills/development/android-cicd" ".claude/skills/"
rm -rf .skillboard-tmpReview the source before running. This copies files into your project; it is not a one-click install and does not verify runtime safety.
sudo apt update && sudo apt install -y gitnpm install -g @anthropic-ai/claude-code# Run from your project root
git clone https://github.com/davila7/claude-code-templates.git .skillboard-tmp
git -C .skillboard-tmp checkout 5cc17cf267132cc690dee82239b80a99ec82e4f8
mkdir -p ".claude/skills"
cp -r ".skillboard-tmp/cli-tool/components/skills/development/android-cicd" ".claude/skills/"
rm -rf .skillboard-tmpDestination: .claude/skills/android-cicd
Scanner static-checks@0.1.0 · commit 5cc17cf26713. Static checks cannot prove runtime safety – review the source and the exact diff before installing. How checks work.
References credentials, tokens or secret files that a skill should not need.
Evidence: [redacted]· fingerprint 0be64ae89ddd24e2