android-cicd — quality + safety report

In the Skillier index (davila7__android-cicd) · scanned 2026-06-03 · engine: builtin+triage

B
Quality
88/100
Safety

3 heuristic flags to review

Heuristic flags from the builtin scanner, which is known to over-flag (it trips on legitimate env-reading integrations, security skills, and library .eval calls). This is NOT an authoritative malicious verdict — re-scan with SkillSpector for the authoritative result. Run the authoritative scan →

Skillproof quality grade B

📇 This skill is in the Skillier index (curated · deduped · quality-filtered). Install Skillier to route & load it into your AI client.

Quality notes

Skill is large (~2066 tokens)
medium · quality · body
→ Tighten to the essential procedure; move long reference material to linked files.
No explicit trigger / 'when to use'
low · quality · body
→ Add a 'When to use' section or 'Use this when …' line listing trigger conditions.
No explicit output format / contract
low · quality · body
→ State the expected output format (structure, sections, or schema).

About this skill

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.

📄 Read the SKILL.md
---
name: android-cicd
description: "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."
risk: safe
source: community
date_added: "2026-05-12"
license: MIT
metadata:
  author: ezescholz
  version: "1.0.0"
  npm: android-cicd
---

# 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 `main` and on version tags
- User wants to avoid manual `versionCode` bumping

---

## Quick Start

Run the interactive setup wizard from the root of the target project:

```sh
npx android-cicd
```

The wizard handles: framework detection → keystore generation → GitHub Secrets → workflow scaffold.

---

## Prerequisites

Before running the wizard, ensure:

- Node.js ≥ 18
- JDK 17 installed with `keytool` accessible (`JAVA_HOME` set, or installed via Eclipse Adoptium / Android Studio)
- [`gh` CLI](https://cli.github.com) installed and authenticated (`gh auth login`)
- App already created in [Google Play Console](https://play.google.com/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:
```sh
git tag v1.2.0
git push origin v1.2.0
```

---

## Auto-Bump versionCode

On every push to `main`, CI automatically:

1. Reads the current `versionCode` from the version file for the detected framework
2. Increments it by 1
3. Commits the change with `[skip ci]` (prevents re-triggering the workflow)
4. 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`):

```groovy
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.home` in `gradle.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:

```groovy
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

1. [Google Cloud Console](https://console.cloud.google.com) → your project → **IAM & Admin** → **Service Accounts**
2. **Create service account** → name: `github-play-publisher` → **Done** (no roles needed)
3. 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

1. Play Console → **Users and permissions** → **Invite new user**
2. Email: `github-play-publisher@YOUR-PROJECT.iam.gserviceaccount.com`
3. Account-level permissions:
   - ✅ Release apps to testing tracks
   - ✅ Manage testing tracks and edit testers
4. **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):

1. Generate a new keystore: `npx android-cicd` and choose "I already have a keystore: No"
2. Export the PEM certificate:
   ```sh
   keytool -export -rfc -keystore upload.jks -alias ALIAS -storepass PASSWORD -file cert.pem
   ```
3. Play Console → app → **App integrity** → **App signing** → **Request upload key reset**
4. Select "I forgot my password" → upload `cert.pem`
5. Wait 1–2 business days for Google approval
6. Update the `KEYSTORE_FILE` secret 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`:
```groovy
versionCode 8   // increment
versionName "1.2.0"
```

**Flutter** — edit `pubspec.yaml`:
```yaml
version: 1.2.0+8
```

Then tag and push:
```sh
git add .
git commit -m "chore: bump version to 1.2.0"
git tag v1.2.0
git push origin main --tags
```
Scan or optimize your own skill →

Want a live grade + an embeddable README badge? Run your skill through the free scanner.

Graded independently by Skillproof — nothing to sell the author. Quality is mechanical + corpus-grounded; safety flags are heuristic (builtin+triage), not a malicious verdict.