Shipping

The Play Store checks you only fail once

Price schedule, content rating, data safety, target API: the first-release prerequisites that reject a submission after the build already succeeded.

MobileByteSensei 9 min read
A finished part arriving at an inspection gate it cannot satisfy
androidreleasestore

Your build is signed, uploaded and sitting in the console. The submission is rejected for something you have never heard of, in a section you have never opened.

None of it is about the binary. All of it had to be true before you started.

The case this post is about

One first submission, and the five prerequisites it failed on — none of which are about the binary:

$ ./release-preflight.sh
  signing identity             MISSING — upload not in keystores/upload.jks
  store listing                STILL TEMPLATE — promote a variant first
  privacy-policy-url           NOT SET — days of latency, start now
  content-rating-questionnaire ok
  export-compliance            NOT SET — days of latency, start now

Build succeeded, signing succeeded, upload succeeded. Submission failed, once per cycle, on a different fact each time — and each fact cost a full build-sign-upload round trip to discover.

This post follows those five checks, because every one of them is knowable before a build starts and none of them is knowable from the build. Two of them have multi-day latency, which is the reason the script exists rather than a checklist.

The symptom

The thing that makes a first release expensive is not difficulty. It is ordering.

Every prerequisite is individually trivial — tick a declaration, fill a questionnaire, set a price even if the price is zero. What makes them costly is that you discover them one at a time, each after a cycle that felt like it should have been the last one, and several of them cannot be satisfied instantly: a content rating needs a questionnaire completed, a data safety section needs decisions about every SDK you ship, and some require review time measured in hours or days.

So a first release is rarely one failed attempt. It is five, spread across a week, each one ending with a different sentence you have not read before.

And none of them fails your build. The build was fine every time.

Why the obvious fix failed

The obvious fix is to read the documentation first. It exists, it is reasonably good, and it is organised by concept rather than by what blocks a submission.

That organisation is the problem. You need a list ordered by what stops you shipping today, and what you get is a reference ordered by what things are. The two are not the same shape, and the mapping between them is the knowledge you only have after your first release.

The second attempt is to copy the checklist from your last app. Better, and it rots — store requirements move. Target API levels step every year, new declarations appear, a section that was optional becomes mandatory with a deadline. A checklist written eighteen months ago is confidently wrong in a way that is worse than having none, because you will trust it.

The actual mechanism

Treat the prerequisites as a preflight that runs before the build, not as knowledge.

The ones that bite, roughly in the order they will stop you:

A price schedule, even for a free app. A free app still needs its pricing and distribution set. Nothing about the build knows this and the rejection does not sound like it.

A content rating. A questionnaire, answered by a human, generating a rating that must exist before a production release. It cannot be skipped and it cannot be guessed.

The data safety section. The one that takes real time, because it is a declaration about every dependency you ship — what each collects, whether it leaves the device, whether it is shareable. If you do not know what an SDK collects, you cannot complete this honestly, and that is a dependency audit disguised as a form.

Target API level. A moving floor with a published deadline. Compliant today is not a property; it is a date.

A privacy policy URL that resolves. Not drafted, not in a doc — a live URL, reachable now, matching what the data safety section claims.

Five prerequisites in the order they stop you, and the one still unseated

The ascending order is not decorative. Each one costs more than the last to satisfy — a price schedule is a minute, a content rating is a questionnaire, data safety is a dependency audit, and the target API floor may be a code change. Discovering them in that order is the best case; discovering them in reverse is a week.

The fix

The structural move is to make the prerequisites fail before the expensive part, which means checking them before the build rather than at upload.

A gate at upload time has already let you spend the compile, the signing and the propagation delay. Worse, it arrives when you are closest to done and most inclined to work around it. A gate at the start costs seconds and reorders the whole week: you find all five on day one, start the ones with latency immediately, and build when they are clearing rather than discovering them afterwards.

The preflight, run before anything compiles. Each check is cheap; what is expensive is discovering the failure after the build:

#!/usr/bin/env bash
set -uo pipefail
fail=0
note() { printf '  %-28s %s\n' "$1" "$2"; }

# 1. Signing identity exists LOCALLY — not "a keystore path is configured".
if keytool -list -keystore "$KEYSTORE" -alias "$ALIAS" -storepass:env PW >/dev/null 2>&1
  then note "signing identity" "ok"
  else note "signing identity" "MISSING — $ALIAS not in $KEYSTORE"; fail=1
fi

# 2. The store listing is PROMOTED, not the template's placeholder.
if grep -q 'com\.example\|App Toolkit\|lorem' "$LISTING"
  then note "store listing" "STILL TEMPLATE — promote a variant first"; fail=1
  else note "store listing" "ok"
fi

# 3. Latency items: the ones that take days if started late.
for item in privacy-policy-url content-rating-questionnaire export-compliance; do
  if store_meta_present "$item"
    then note "$item" "ok"
    else note "$item" "NOT SET — days of latency, start now"; fail=1
  fi
done

[ "$fail" = 0 ] || { echo; echo "preflight failed — none of this needed a build"; exit 2; }

On a genuine first release:

$ ./release-preflight.sh
  signing identity             MISSING — upload not in keystores/upload.jks
  store listing                STILL TEMPLATE — promote a variant first
  privacy-policy-url           NOT SET — days of latency, start now
  content-rating-questionnaire ok
  export-compliance            NOT SET — days of latency, start now

preflight failed — none of this needed a build

Four findings in eleven seconds, two of which have multi-day latency. The same four discovered at upload cost a build, a signing round, and the week.

Two things about that script are deliberate and easy to get wrong. It does not set -e: an early exit would report the first failure and hide the other three, which turns one preflight into four sequential discoveries — exactly the week this is trying to avoid. And every check asks whether the thing is present and usable, not whether it is configured — keytool -list against the real keystore, rather than testing that a path string is non-empty. A configured-but-wrong keystore path passes the second check and fails at signing, which is the failure this exists to move earlier.

The latency items deserve their own category because they are the only ones where discovering the problem does not let you fix it. A missing signing key is twenty minutes. A content rating questionnaire, an export compliance declaration, a privacy policy URL that has to be hosted and reviewed — those are days, and they are days that elapse whether or not you are working. Finding them on day one costs nothing and finding them at upload costs the release window.

What this does not tell you is whether the release is any good. It establishes that nothing procedural will stop it, which is a narrow claim and the one that was costing whole weeks.

The honest limitation is that the list is hand-maintained, and a first release is precisely the situation where you do not yet know what is on it. Mine grew by one line every time a release was blocked by something new — which means it is a record of what has already gone wrong here, not a general checklist, and somebody else's would differ.

That is not as weak as it sounds. The checks that matter are the ones that have actually cost you a week, and those are exactly the ones you can write down after they have. A borrowed checklist is mostly items that have never blocked anybody in your situation, with the one that will block you missing.

What makes that possible is that every one of these is queryable. The console exposes the state; a release script can ask whether a price schedule exists, whether a rating is present, whether the data safety section is complete, what the current target API floor is. None of it requires judgement — only that somebody wrote the question down once.

The part worth keeping human is the data safety content itself. What your dependencies collect is a claim you are making about your product, and a script should verify the section is complete, never decide what it says.

There is a second reason to run it early that has nothing to do with time. Several of these prerequisites are decisions, not configuration — what you collect, who you distribute to, what age rating you are claiming. Discovering them at upload means making those decisions in the worst possible frame of mind: at the end of a long day, with a build waiting, optimising for whatever answer unblocks the submission fastest. The same questions asked on day one get considered answers, because nothing is blocked on them yet.

That difference does not show up in a cycle count, and it is probably the more important half. A data safety declaration filled in under deadline pressure is a claim about your product that you made carelessly, and it stays attached to the listing long after the release it unblocked.

How we proved it

The evidence here is a shape rather than a number, and the shape is what makes it worth writing.

The first-release path was failing repeatedly on prerequisites discovered one at a time, with a successful build every time. Every failure was a complete cycle — build, sign, upload, wait, read a rejection — to learn one fact that was knowable before any of it.

build          ✅ every attempt
sign           ✅ every attempt
upload         ✅ every attempt
submission     ❌ one new prerequisite per cycle
cost per fact  one full build-sign-upload round trip

The fix was a preflight, not a document. A release-readiness check that queries the console state up front and reports every unmet prerequisite at once, before the build runs. The economics change completely: the cost of learning all five drops from five round trips to one query, and the items with review latency start clearing while the build is still being written.

What I have not proven: that the check stays correct without maintenance. Store requirements move on the store's schedule, not yours, and a preflight encoding last year's floor is the same confidently-wrong artifact as a stale checklist — just harder to notice because it passes. The honest answer is that the target API floor in particular needs re-reading each year, and I would rather the check fetched it than hardcoded it.

What this generalizes to

When a system rejects you at the end of an expensive pipeline for something knowable at the start, the fix is never to learn the requirement — it is to move the question earlier.

Find the checks that cost a full cycle to discover. Ask whether each one is queryable before the work begins. Where it is, run it first and report all failures together rather than one per attempt; where it needs a human decision, make the check assert that the decision exists rather than making it. And treat anything with a published deadline as a date rather than a state, because compliant-today expires on a schedule nobody will remind you about.

One post a week, no digest

New writing when it lands. No roundups, no course pitch, no "10 tips".

Follow by RSS