Offline preflight
gplay preflight inspects a local .aab or .apk and reports what would get
your build rejected, or what would break on a user’s device. It makes no API
calls and needs no credentials, so it runs on any machine and in any CI job —
including the ones that never see a Play service account.
gplay preflight --file app-release.aabNine scanners run by default. Every finding is an error, a warning, or
info, and --fail-on decides which of those fails the build.
Why offline scanning is different
Section titled “Why offline scanning is different”Most tooling that claims to check an Android build matches strings against the
compressed archive. That cannot tell android:debuggable="true" apart from
android:debuggable="false", and it cannot see an attribute that points at a
resource.
preflight decodes AndroidManifest.xml properly — binary AXML for APKs, the
aapt2 protobuf encoding for App Bundles — and reads typed attribute values.
When a value genuinely cannot be resolved statically (say
android:debuggable="@bool/isDebug"), it reports that rather than guessing.
Install-blocking issues it catches
Section titled “Install-blocking issues it catches”These are the ones worth knowing about, because they fail after upload:
Exported components without android:exported. On targetSdk 31 and
above, a component with an <intent-filter> and no explicit android:exported
does not install. The app builds fine and Play accepts the upload; the install
fails on device.
Foreground service types without their permission. Android 14 requires each
android:foregroundServiceType to have a matching permission. A dataSync
service without FOREGROUND_SERVICE_DATA_SYNC throws SecurityException when
it starts — a crash your CI never sees.
16 KB memory page alignment. Android 15 devices can use 16 KB memory pages,
and Play requires native libraries to be aligned for it. preflight reads the
real ELF program headers of every .so in the bundle and checks p_align.
Misaligned libraries simply fail to load.
Exported providers granting URI permissions. An exported <provider> with
grantUriPermissions hands any installed app a path into your data.
The nine scanners
Section titled “The nine scanners”| Scanner | What it checks |
|---|---|
manifest |
debuggable, testOnly, exported components, exported providers, foreground service types, cleartext traffic, allowBackup, package and version sanity |
permissions |
Restricted permissions needing a Play declaration form, sensitive permissions needing a Data safety disclosure, legacy storage on modern targets, duplicates, deprecated permissions |
native_libs |
Missing arm64-v8a, 16 KB page alignment, unstripped debug symbols, extractNativeLibs |
metadata |
Listing text limits and real screenshot pixel dimensions, aspect ratios, counts, icon and feature-graphic sizes. Needs --listings-dir |
secrets |
Private keys, cloud and vendor tokens, service-account JSON, shipped keystores, .git and .env leakage. Scans dex bytecode too |
billing |
Play Billing vs third-party payment processors, BILLING permission with no implementation |
privacy |
Analytics, attribution, and ads SDK inventory, plus AD_ID permission consistency |
policy |
Target API level floor, restricted services, APK-vs-AAB upload format |
size |
Download size budget, dex fragmentation, payload breakdown |
List them at any time:
gplay preflight --list-scannersChecking the store listing too
Section titled “Checking the store listing too”Point --listings-dir at a Fastlane-style metadata directory and the
metadata scanner joins the run. Without it, that scanner is skipped and the
report says so.
gplay preflight --file app-release.aab --listings-dir ./fastlane/metadata/androidIt validates text limits in runes, not bytes — so an emoji or a CJK character counts once, the way Play counts it — and it decodes each image to check real pixel dimensions rather than trusting the filename.
Selecting scanners
Section titled “Selecting scanners”# Only the ones you care aboutgplay preflight --file app.aab --only manifest,permissions,native_libs
# Everything except onegplay preflight --file app.aab --skip size
# Skip the secret scan on very large buildsgplay preflight --file app.aab --skip-secretsAs a CI gate
Section titled “As a CI gate”# Block only on hard blockersgplay preflight --file app-release.aab --fail-on error
# Strictergplay preflight --file app-release.aab --fail-on warning| Exit code | Meaning |
|---|---|
0 |
No findings at or above --fail-on |
1 |
Findings at or above --fail-on |
GitHub Actions:
- name: Offline preflight run: | gplay preflight \ --file app/build/outputs/bundle/release/app-release.aab \ --listings-dir fastlane/metadata/android \ --fail-on errorBecause it needs no credentials, this step can run in a fork PR build, before any secret is available.
JSON output
Section titled “JSON output”gplay preflight --file app.aab --output json --prettyThe report carries the detected format, package name, version code and name,
min and target SDK, a per-scanner run status, and every finding with its
check, severity, message, entry, hint, and — where a Play policy page
applies — a ref link.
# Just the blockersgplay preflight --file app.aab --output json \ | jq '[.findings[] | select(.severity=="error")]'
# Which scanners were skipped, and whygplay preflight --file app.aab --output json | jq '.scanners[] | select(.skipped)'A note on API keys
Section titled “A note on API keys”If the secrets scanner finds a Google API key (AIza…), it reports a
warning, not an error. Android apps embed Maps and Firebase keys by design;
they cannot be kept out of the binary. The fix is restricting the key to your
package name and signing certificate in Cloud Console.
Credentials that genuinely must never ship — private keys, service-account
JSON, sk_live_ secrets, GitHub and Slack tokens, keystores — are errors.
Where preflight fits
Section titled “Where preflight fits”| Command | Needs auth | Scope |
|---|---|---|
gplay preflight |
No | The artifact and listing files, offline |
gplay validate |
Yes | Release readiness: local checks plus live track and listing state |
gplay checks analyze |
Yes | Google Checks privacy and policy analysis, server-side |
gplay release --dry-run |
Yes | The full pipeline against a real edit, discarded before commit |
Run them in that order. preflight is the cheapest and catches the most.
Limitations
Section titled “Limitations”Aggressive code shrinking can rename or remove classes, so a missing SDK detection is not proof the SDK is absent — matches are high confidence, misses are not.
The manifest decoder relies on attribute names, which aapt2 emits for normal
builds. Deliberately obfuscated APKs that strip names and keep only resource
IDs are not fully decoded; preflight falls back to heuristics and says so.
The policy scanner compares targetSdkVersion against a constant that Google
raises roughly every August. Pass --min-target-sdk to override it without
waiting for a new gplay release.
- Command reference:
gplay preflight - Use with AI agents — the
gplay-preflightskill teaches an agent to run this before every upload