DEV Community

Cover image for I stopped submitting to Google Play without running this first
Yasser's studio
Yasser's studio

Posted on

I stopped submitting to Google Play without running this first

Last year I uploaded an AAB to the Play Console, waited four days for review, and got rejected.

The reason: targetSdkVersion was one level below the new minimum. Google had bumped the requirement that month. I hadn't noticed.

I fixed it, re-uploaded, waited another three days. That's a week lost for a one-line fix in my build config.

The frustrating part: I could have caught that before uploading. The information was right there in the manifest. I just didn't check.

The pattern

Most Play Store rejections follow the same pattern:

  1. Upload your AAB
  2. Wait 1-7 days for review
  3. Get a short rejection email
  4. Fix something you could have checked locally
  5. Re-upload, wait again

And the rejection reasons are almost always predictable. targetSdk below the minimum. A restricted permission you didn't declare properly. A third-party billing SDK that violates Play's billing
policy. A hardcoded API key in the bundle.

None of these require Google's review to catch. They're all checkable from the file itself.

gpc preflight

$ gpc preflight app.aab

9 scanners run against your AAB file. Entirely offline. No API calls, no credentials, no network.

Here's what each scanner checks:

  1. Manifest validation

The big one. Parses your AndroidManifest.xml from the bundle and checks:

  • targetSdkVersion below Google's current minimum (critical)
  • debuggable flag left on in a release build
  • testOnly flag set
  • Missing android:exported on components (required since API 31)
  • Missing foregroundServiceType (required since API 34)
  • Cleartext HTTP traffic enabled

✗ CRITICAL targetSdkVersion 33 is below minimum 35
Google Play requires targetSdkVersion >= 35 for new app updates.
→ Update targetSdkVersion in your build.gradle to 35 or higher.

This single check would have saved me that week.

  1. Permissions audit

Flags 16+ restricted Google Play permissions:

  • SMS and call log access (heavily restricted since 2019)
  • QUERY_ALL_PACKAGES (requires declaration)
  • Background location
  • Accessibility service
  • VPN service
  • INSTALL_PACKAGES
  • REQUEST_DELETE_PACKAGES

Each finding includes a note about what data safety declarations you'll need.

  1. Native library architecture

Checks your native libraries for 64-bit compliance:

  • arm64-v8a is required
  • x86_64 recommended
  • Warns if native libs total exceeds 150MB uncompressed
  1. Metadata validation

If you pass --metadata

, it checks your Fastlane-format store listings:
  • Title under 30 characters
  • Short description under 80 characters
  • Full description under 4,000 characters
  • At least 2 phone screenshots
  • Privacy policy URL present
  1. Secrets detection

Scans your source directory (with --source

) for hardcoded credentials:
  • AWS access keys (AKIA...)
  • Google API keys (AIza...)
  • Stripe live keys (sk_live_...)
  • RSA/EC/DSA private keys
  • Firebase config keys
  • Generic bearer tokens

You'd be surprised how often these end up in a bundle.

  1. Billing compliance

Detects non-Play billing SDKs that violate Google's billing policy:

  • Stripe SDK
  • Braintree
  • PayPal
  • Razorpay
  • Adyen
  • Square

These are flagged as warnings. Some apps have exemptions, but most don't.

  1. Privacy and tracking

Identifies tracking SDKs and cross-references with your permissions:

  • Facebook SDK, Adjust, AppsFlyer, Amplitude, Mixpanel, Branch, CleverTap
  • Advertising ID usage
  • Data collection indicators vs declared permissions
  1. Policy heuristics

Pattern-matches for common policy traps:

  • COPPA/Families indicators (child-targeted features + data collection)
  • Financial app indicators (SMS access + autofill)
  • Health app indicators (body sensor permissions)
  • User-generated content indicators
  • System alert window (overlay) permission
  1. Size analysis

Breaks down your bundle size by category:

ℹ INFO Total size: 64.8 MB compressed, 212.6 MB uncompressed
909 files. Breakdown: native-libs: 31.1 MB, dex: 7.2 MB,
assets: 1.1 MB, resources: 0.8 MB, signing: 0.1 MB

Warns if total download size exceeds the configurable limit (default: 150 MB). Flags individual native libs over 50 MB and assets over 30 MB.

Severity levels

Every finding has a severity:

┌──────────┬───────────────┬───────────────────────────────────────┐
│ Level │ Icon │ Meaning │
├──────────┼───────────────┼───────────────────────────────────────┤
│ critical │ ✗ (red, bold) │ Will almost certainly cause rejection │
├──────────┼───────────────┼───────────────────────────────────────┤
│ error │ ✗ (red) │ Likely to cause rejection │
├──────────┼───────────────┼───────────────────────────────────────┤
│ warning │ ⚠ (yellow) │ Worth reviewing, may cause issues │
├──────────┼───────────────┼───────────────────────────────────────┤
│ info │ ℹ (gray) │ Informational, no action needed │
└──────────┴───────────────┴───────────────────────────────────────┘

By default, the command exits code 6 if any finding is error or higher. Override with --fail-on:

gpc preflight app.aab --fail-on critical # only fail on critical
gpc preflight app.aab --fail-on warning # strict mode

In CI

Add one step before your upload:

  • name: Pre-submission check
    run: gpc preflight app.aab

  • name: Upload to Play Store
    run: gpc releases upload app.aab --track internal

If preflight fails (exit code 6), the upload never happens. You find out in your CI logs instead of in a rejection email three days later.

Configuration

For teams, drop a .preflightrc.json in your project root:

{
"failOn": "error",
"targetSdkMinimum": 35,
"maxDownloadSizeMb": 150,
"allowedPermissions": ["android.permission.CAMERA"],
"disabledRules": ["billing-third-party"],
"severityOverrides": {
"size-total-warning": "info"
}
}

Whitelist permissions your app legitimately needs. Disable rules that don't apply. Override severities for your specific situation.

What it doesn't catch

To be clear about the limits:

  • It can't check your app's runtime behavior
  • It can't verify your data safety form matches reality
  • It can't predict policy changes Google hasn't announced
  • It can't check things that require running the app (ANR patterns, performance)

It checks what's checkable from the file and your source. The static stuff. The things that are embarrassing to get rejected for because they were right there the whole time.

Run specific scanners

Don't need all 9? Run individual scanners:

gpc preflight manifest app.aab # manifest only
gpc preflight permissions app.aab # permissions only
gpc preflight metadata ./fastlane # metadata only
gpc preflight codescan ./src # secrets + billing + privacy

Quick context on GPC

If you're new here: GPC is a CLI that covers the entire Google Play Developer API. 208 endpoints. 7 TypeScript packages. 1,863 tests.

Previous articles in this series:

  • Part 1: I built a CLI that covers the entire Google Play Developer API
  • Part 2: I replaced 4 Play Console tabs with one terminal command

Try it

# npm
npm install -g @gpc-cli/cli

# Homebrew
brew install yasserstudio/tap/gpc

# Standalone binary (no Node.js required)
curl -fsSL https://raw.githubusercontent.com/yasserstudio/gpc/main/scripts/install.sh | sh

Then:

gpc preflight your-app.aab
gpc preflight your-app.aab --source ./src --metadata ./fastlane

Docs | GitHub | Preflight docs

Free to use. Code is on GitHub.

What's your pre-submission process look like? Manual checklist, automated, or just upload and hope for the best?

Top comments (0)