GitLab CI/CD Android Signed APK/AAB Build Pipeline Prompt
Build, test, and sign Android APK/AAB artifacts in GitLab CI — keystore secrecy, Gradle caching, SDK licenses, and Play Store upload without leaking signing keys.
- Target user
- Mobile and DevOps engineers building Android apps in GitLab CI
- Difficulty
- Advanced
- Tools
- Claude, ChatGPT, Cursor
The prompt
You are a senior mobile CI engineer who builds and signs Android apps in GitLab CI at production scale. You know Gradle caching, the Android SDK/build-tools/NDK license dance, keystore secrecy, APK vs. AAB, `apksigner`/`jarsigner`, and Play Store upload via the Publisher API — and how each of these leaks time or secrets when done naively. I will provide: - App details (Gradle version, `compileSdk`/`targetSdk`, NDK use, flavors/build types) - Current `.gitlab-ci.yml` (or none) and the base image - Signing approach today (unsigned, debug-signed, manual) - The goal (add signing, speed up, produce AAB, upload to Play) - Runner type (shared / self-hosted / Kubernetes) Your job: 1. **Pick a correct base image**: a pinned Android SDK image with the right build-tools; accept licenses non-interactively but only for pinned versions. 2. **Cache Gradle aggressively**: cache `~/.gradle/caches` and `~/.gradle/wrapper` keyed on the Gradle lockfiles; use `--build-cache` and `org.gradle.caching=true`. Separate the config cache carefully. 3. **Handle signing securely**: - Store keystore as base64 (`KEYSTORE_BASE64` masked var) or a GitLab Secure File. - Decode to a temp path at job time, pass passwords via env (`KEYSTORE_PASSWORD`, `KEY_ALIAS`, `KEY_PASSWORD` — all masked/protected). - Sign the release AAB/APK with Gradle signingConfig OR post-build `apksigner`. - `after_script` ALWAYS deletes the decoded keystore. 4. **Build the right artifact**: AAB (`bundleRelease`) for Play Store, APK (`assembleRelease`) for direct distribution; verify signature with `apksigner verify --verbose`. 5. **Gate on protected branches/tags**: signing + Play upload only on protected refs so MRs from forks can never touch the keystore. 6. **Play Store upload** (optional): use the Publisher API service account JSON (also a Secure File), upload to internal/alpha track, and make promotion manual. 7. **Speed**: parallelize lint/unit/instrumented tests via `needs`; use a warm Gradle daemon setting suited to ephemeral runners (`--no-daemon` on throwaway runners). Deliverables: - A staged `.gitlab-ci.yml`: build → test → sign (protected only) → upload (manual) - The exact secret-handling block (decode, sign, verify, cleanup) - Gradle cache config keyed correctly - The signature-verification step proving the artifact is signed with the right key Mark DESTRUCTIVE or RISKY: any path where the keystore could reach logs/artifacts, running signing on unprotected refs, and Play Store promotion straight to production. --- App: [Gradle ver, compileSdk, NDK?, flavors] Current CI (YAML): [PASTE or none] Signing today: [unsigned / debug / manual] Goal: [signing / speed / AAB / Play upload] Runner: [shared / self-hosted / kubernetes]
Run this prompt with AI
Test it, get an AI-improved version, or compare models — live in the Prompt Workspace. No copy-paste.
Why this prompt works
Android CI has two hard parts that naive pipelines get wrong: Gradle caching (the difference between a 3-minute and 25-minute build) and keystore secrecy (the difference between a normal release and an unrecoverable key leak). This prompt treats the keystore as a decode-use-destroy secret bound to protected refs, and keys the Gradle cache on lockfiles rather than hoping.
How to use it
- Decode the keystore from a masked base64 var at job time.
- Sign only on protected branches/tags.
- Verify the signature before uploading.
- Delete the keystore in
after_script, always.
Useful commands
# Encode keystore for a masked CI variable (run locally)
base64 -w0 release.keystore # paste into KEYSTORE_BASE64 (masked, protected)
# Verify a built artifact is signed with the expected key
apksigner verify --verbose --print-certs app-release.apk
jarsigner -verify -verbose -certs app-release.aab
# Non-interactive SDK license accept (pinned versions)
yes | sdkmanager --licenses > /dev/null
GitLab CI patterns
Staged build → sign (protected only) → upload
stages: [build, test, sign, publish]
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
.gradle-cache:
cache:
key:
files:
- gradle/wrapper/gradle-wrapper.properties
- gradle.lockfile
paths:
- .gradle/caches
- .gradle/wrapper
assemble:
extends: .gradle-cache
stage: build
image: mobiledevops/android-sdk-image:34.0.0 # pin SDK/build-tools
script:
- ./gradlew --build-cache assembleRelease -x lint
artifacts:
paths: [app/build/outputs/apk/release/*-unsigned.apk]
unit-test:
extends: .gradle-cache
stage: test
needs: [assemble]
image: mobiledevops/android-sdk-image:34.0.0
script:
- ./gradlew --build-cache testReleaseUnitTest
sign:
stage: sign
image: mobiledevops/android-sdk-image:34.0.0
rules:
- if: '$CI_COMMIT_TAG' # protected tags only
script:
- echo "$KEYSTORE_BASE64" | base64 -d > "$CI_PROJECT_DIR/release.keystore"
- >
$ANDROID_HOME/build-tools/34.0.0/apksigner sign
--ks "$CI_PROJECT_DIR/release.keystore"
--ks-key-alias "$KEY_ALIAS"
--ks-pass "env:KEYSTORE_PASSWORD"
--key-pass "env:KEY_PASSWORD"
--out app-release.apk
app/build/outputs/apk/release/*-unsigned.apk
- $ANDROID_HOME/build-tools/34.0.0/apksigner verify --verbose app-release.apk
after_script:
- rm -f "$CI_PROJECT_DIR/release.keystore" # ALWAYS delete keystore
artifacts:
paths: [app-release.apk]
play-upload:
stage: publish
image: ruby:3.3
rules:
- if: '$CI_COMMIT_TAG'
when: manual # human promotes to a track
script:
- bundle exec fastlane supply --aab app-release.aab --track internal
AAB for Play Store via Gradle signingConfig
// build.gradle (release signingConfig reads env — no plaintext in repo)
android {
signingConfigs {
release {
storeFile file(System.getenv("KEYSTORE_PATH") ?: "/dev/null")
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
}
}
buildTypes { release { signingConfig signingConfigs.release } }
}
Common findings this catches
- Keystore or passwords committed, baked into the image, or echoed to logs.
- Signing jobs runnable from fork MRs because they are not gated to protected refs.
- No signature verification step — a mis-signed build reaches the store.
- Cold Gradle cache every run because the cache key is not tied to lockfiles.
after_scriptmissing, leaving the decoded keystore in the build dir.
When to escalate
- Suspected keystore leak — treat as P1; you generally cannot rotate an app signing key already trusted by installed users.
- Play App Signing enrollment decisions — coordinate with the release owner.
- NDK/native build times — may need a self-hosted runner with more cores and a warm cache volume.
Related prompts
-
GitLab CI/CD Container Image Size Budget Gate Prompt
Add an image-size budget gate to GitLab CI — measure built image size, diff against a baseline, and fail the pipeline when a layer bloats the image past budget.
-
GitLab CI/CD Playwright Sharded E2E Test Pipeline Prompt
Run Playwright end-to-end tests sharded across GitLab CI jobs — browser caching, blob-report merge, flaky retries, traces on failure, and a single merged HTML report.
-
GitLab CI/CD FIPS-Compliant Runner Hardening Prompt
Stand up and validate FIPS 140-compliant GitLab Runners and pipelines — FIPS images, crypto policy, verification, and avoiding silent non-compliant fallbacks.
-
GitLab CI/CD Git LFS Large-File Pipeline Prompt
Run GitLab pipelines against repos with Git LFS objects — correct fetch, cache, bandwidth, and locking without smudge failures or blown quotas.
More GitLab CI/CD prompts & error guides
Browse every GitLab CI/CD prompt and troubleshooting guide in one place.
Reading prompts? Get all 500 in one free PDF
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.