play-release
Skill0 starsChanged 46 days agoMITView on GitHub
Get a Flutter, React Native or Expo app onto Google Play, and know in advance which steps a human has to do. Covers the order of Play Console declarations, pre-upload checks on the .aab (signing key, target SDK, advertising ID permission, store text and image limits, privacy policy, EEA ad consent), and the upload handoff. Use when releasing, submitting, publishing or shipping an Android app to Google Play or the Play Console; when building an .aab or app bundle, creating a production or internal testing release, or uploading a build; when filling Play declarations such as data safety, content rating (IARC), target audience, advertising ID, ads, health, financial or government apps; when "Send app for review" is greyed out or the setup checklist will not complete; when under-13 age groups are locked or the app is shown as rated "Teen"; when a bundle might be signed with the debug key; or when asked what still blocks a Play release.
- Reads credentials
In this repository7 other files it gives its agents.
- app-similaritySkill
- claude-md-trimSkill
- human-voiceSkill
- listing-accuracySkill
- niche-scoutSkill
- perfSkill
- polishSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 3 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 58
Plan for a manual leg from the start: Do the declarations, build and verify, then hand over a short list of files and clicks.
- Very specific: line 121
If the app already applies child-directed ad treatment to every user (TagForChildDirectedTreatment.yes, MaxAdContentRating.g, …
- Distinctive: line 135
yes: when the above holds and AdMob is the only network.
- Distinctive: line 242
Every Save says changes are *"ready for you to send for review"*.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Releasing to Google Play
Most time lost on a Play release goes on two things this file tells you up front: the order the declarations must be done in, and which steps a human has to do. The console shows neither, and one of its messages misleads.
Assumed layout (run the scripts from the project root):
pubspec.yaml # Flutter, or
package.json # React Native / Expo (names react-native)
android/key.properties # Flutter upload key config, not committed
build/app/outputs/bundle/release/app-release.aab # Flutter default
android/app/build/outputs/bundle/release/app-release.aab # React Native default
fastlane/metadata/android/<locale>/ # fastlane supply layout
title.txt short_description.txt full_description.txt privacy_url.txt
changelogs/<versionCode>.txt | default.txt
images/icon.png images/featureGraphic.png
images/phoneScreenshots/ sevenInchScreenshots/ tenInchScreenshots/The metadata tree is worth keeping even without fastlane: it versions the
listing and lets the preflight check it. An EAS-built bundle has no local
default path: download it and pass it to the scripts. (EAS Metadata's
store.config.json covers the App Store only, so Play copy still lives here.) Deeper material (console failure
modes, browser-automation uploads, EEA consent implementation, rating
examples, tablet capture) is in reference.md.
Quick path
- Declarations, in this order: Content rating → Target audience → Data safety. Everything else (privacy policy, app access, ads, advertising ID, government, financial, health) in any order. Why
- Build with the production configuration stated explicitly:
- Flutter:
flutter build appbundle --releaseplus whatever--flavor/--dart-defineyour production build needs. - React Native:
cd android && ./gradlew bundleRelease, with a releasesigningConfigfor your upload key (the template signs with debug). - Expo:
eas build -p android --profile production; EAS signs with the keystore ineas credentials. Why explicit
- Flutter:
- Preflight:
${CLAUDE_SKILL_DIR}/preflight.sh— fix every FAIL, read every warn. What it checks - Upload the listing assets and the
.aab:- with a Play service account:
fastlane supplydoes it all; - otherwise (macOS):
${CLAUDE_SKILL_DIR}/handoff.shopens the files and the console pages for a human. The manual leg
- with a Play service account:
- Publishing overview → Send app for review. Saving is not submitting. Details
What a human has to do
Plan for a manual leg from the start. Do the declarations, build and verify, then hand over a short list of files and clicks. Do not spend a session trying to automate the last step.
Which steps are manual depends on what you have:
- A service account with Play Console access (Google Cloud project linked
to the developer account):
fastlane supplyuploads the bundle, listing text and images through the Play Developer API. Still manual: creating the app, its first bundle upload (the API cannot create an app, so supply needs one build already in the console — an internal testing track will do), and the declarations. If the project already has a service account, use it. - No service account. Creating one is the developer's decision, not a convenience to reach for mid-release: it is a long-lived credential with publish rights, and it is easy to create in the wrong Google Cloud organisation. Ask; do not assume.
- Driving the console with a browser-automation tool (e.g. Claude in
Chrome): the listing text and images can be done this way, but the
.aabcannot if the tool's file upload is size-capped — Claude in Chrome'sfile_uploadcaps at 10 MB and a Flutter bundle is typically 30-80 MB (a React Native bundle is well over 10 MB as well). That upload is a human's. Technique in reference.md.
Human-only regardless of tooling: accepting third-party terms (the IARC questionnaire, below), and Send app for review unless the developer has said to press it. Say plainly which step is manual and why, so it does not read as something you failed to finish.
The order is not optional
The setup checklist looks like a flat list. It is not:
Content rating → Target audience → Data safetyWith no content rating submitted, Play treats the app as "Teen or higher" and greys out every under-13 age group. The message says the rating "is set as 'teen' or higher", which reads as a wrong rating exists. There is no rating at all. Complete the IARC questionnaire and the age groups unlock.
Target audience then gates Data safety: Data safety refuses to submit with "you must let us know the target age group of your app" and stays draft-only until it has one.
Content rating: answer honestly
Under-declaring is what rating boards penalise, and Play warns it "may reject your app or app update for misrepresentation". Honest answers for cartoon content, described accurately (fantasy setting, no blood, unrealistic reactions), still come out in the lowest band. Worked example in reference.md.
Two questions are narrower than they look and are usually no: Crude humor means bodily functions, not slapstick; Digital purchases means real purchases, cash-convertible rewards and NFTs, not unlocks earned in play.
Completing the questionnaire accepts IARC's Terms of Service in the developer's name. That is a third-party agreement — get an explicit yes from the developer first.
Target audience and ads
If the app already applies child-directed ad treatment to every user
(TagForChildDirectedTreatment.yes, MaxAdContentRating.g, non-personalised
ads; in React Native, tagForChildDirectedTreatment: true and
maxAdContentRating: MaxAdContentRating.G), it has already given up personalised-ad revenue. Declaring an
under-13 audience then costs little and removes the risk of a visibly
child-appealing app claiming 13+.
Verify the flag reaches the SDK. A constant that nothing passes to
MobileAds.instance.updateRequestConfiguration (Flutter) or
mobileAds().setRequestConfiguration (React Native) is a promise the binary
does not keep.
- Ads step: "are all ads suitable for children and from certified networks?" AdMob is on Google's Families self-certified ads SDK list, so yes when the above holds and AdMob is the only network.
- Advertising ID: the Google Mobile Ads SDK declares
com.google.android.gms.permission.AD_IDin its own manifest, and it merges into your app whether or not you list it. The Play declaration must match what is actually in the bundle — preflight reports which. Under the Families policy, apps aimed only at children must not transmit it (remove the permission withtools:node="remove", or in Expo list it inandroid.blockedPermissions); mixed-audience apps must not transmit it from children or users of unknown age. - Teacher Approved is offered inside the Target audience wizard. It is an opt-in programme that submits the app to outside reviewers — the developer's call, not a default.
EEA consent needs both halves
An ad-supported app distributed in the EEA needs a published consent message in AdMob and an in-app way to reopen it. Neither store will tell you if either is missing — Play's policy status reads "No issues found" throughout.
- A published European regulations message — AdMob → Privacy &
messaging → European regulations. Console config, no rebuild, provided the
app already calls the UMP SDK (
ConsentInformation.requestConsentInfoUpdate; in react-native-google-mobile-ads,AdsConsent.requestInfoUpdateorgatherConsent). - An in-app entry point that calls
ConsentForm.showPrivacyOptionsForm(AdsConsent.showPrivacyOptionsForm) — code, and therefore a build.
Ship them together. The published message tells users to look for a link in the app to manage or withdraw consent; ship the message without the link and the consent form describes a control that does not exist.
preflight.sh fails when google_mobile_ads (or
react-native-google-mobile-ads) is a dependency and either call is missing
from the Dart (or TS/JS) sources. Implementation notes, the two settings that
block Publish, and how to test from outside the EEA are in
reference.md.
Preflight
${CLAUDE_SKILL_DIR}/preflight.sh [--offline] [--quiet] [path/to/app.aab]It checks what is silently wrong at upload time and no test catches:
| Check | Why |
| Signer identity | The Flutter template falls back to the debug key without a word when key.properties is missing, the React Native template signs release with the debug keystore outright, and jarsigner -verify still says "jar verified". |
targetSdkVersion (needs bundletool) | Play rejects new apps and updates below the current minimum (API 36 from 2026-08-31; override with PLAY_MIN_TARGET_SDK). |
AD_ID permission | Tells you what the Advertising ID declaration must say. Matches the full name, so ACCESS_ADSERVICES_AD_ID (Privacy Sandbox) is not mistaken for it. |
| Package assets | Lists assets dependencies ship under flutter_assets/packages/ (React Native: node_modules_* resources), so another app's artwork riding in through a shared package gets seen. |
| Store text, every locale | Title 30, short 80, full 4000, release notes 500 — counted in characters, not bytes. |
| Images | Icon 512x512, feature graphic 1024x500, screenshot counts, sizes and aspect (below). |
| Privacy policy | privacy_url.txt must return 200. |
| EEA consent | Both halves, as above. |
| Project (React Native / Expo only) | Package and versionCode from build.gradle or app.json; a release signingConfig pointing at debug (FAIL without eas.json, since EAS injects its own signing); an eas.json production profile that builds an apk; AD_ID removed or not. |
Needs unzip and a JDK's jarsigner (Android Studio's bundled JBR is found
automatically). bundletool and curl are optional. Exit 1 on any failure.
The build configuration cannot be checked afterwards
A staging build and a production build can be indistinguishable by
inspection. If your app picks ad unit ids, API hosts or keys at build time
(--dart-define, a flavor, EXPO_PUBLIC_* or .env values) and both sets
are compiled in as constants, both appear as strings in the
bundle whichever was selected. A staging build on a public track serves test
ads (no revenue) or talks to the wrong backend. The only defence is to control
the invocation: pass the production flags explicitly, and rebuild if unsure.
Store listing images
Play's rules (Play Console Help, "Add preview assets"):
- Icon: 512x512, 32-bit PNG with alpha, ≤ 1 MB.
- Feature graphic: 1024x500, JPEG or 24-bit PNG (no alpha).
- Screenshots: 2-8 per device type; each side 320-3840 px (tablets 1080-7680); the long side at most twice the short.
- Promotion in large-format layouts wants at least 4 screenshots at 16:9 or 9:16 and ≥ 1080 px. Other ratios are accepted but earn no promotion credit, so preflight warns rather than fails.
- Preview video: a YouTube URL, not a file. It must be public, embeddable,
not age-restricted, not monetized and not a Short. YouTube makes every
vertical upload a Short, and the console then refuses the URL with "Enter a
valid YouTube video URL". So lay a portrait game over a blurred copy of itself
at 1920x1080. Use 15-30 s, start on real use in the first second, and use the
full
watch?v=form of the URL. The video shows only if a feature graphic is set. Check at each release that the listing still has one.
Fill the tablet slots if the app runs on tablets. Phone, 7-inch and 10-inch are separate slots, further down the listing page than you expect. Leaving tablets empty loses tablet placement. Capture them on a real tablet emulator, not by upscaling phone shots — see reference.md.
Privacy policy
Play requires a policy URL and follows it. Two failures are common when one policy is shared across apps:
- The URL 404s. Preflight checks it. Never invent a per-app path.
- The policy describes a different app, or claims data this app does not collect, and so contradicts the Data safety form. Read it.
Saving is not submitting
Every Save says changes are "ready for you to send for review". Nothing reaches Google until Publishing overview → Send app for review, which stays locked until the dashboard checklist is complete. Verify state in the console, not from the absence of an error.
The console is flaky in recognisable ways (form state desyncing from what is shown, deep links bouncing, native dropdowns mis-selecting). Read the section counters and summaries, not the checkboxes. See reference.md.
The handoff (macOS)
${CLAUDE_SKILL_DIR}/handoff.sh [--dry-run] [--force] [--aab PATH]Runs the preflight and, only if it passes (pass --aab for an EAS-built
bundle), opens Finder at the images
folder, reveals the .aab, and opens three console tabs — store listing,
production track, publishing overview — then prints the steps in order.
--dry-run prints instead of opening and works on any OS. --force opens
despite failures.
Tabs open in the normal browser profile (Chrome, or PLAY_BROWSER, else the
default browser) so the signed-in session is used. Deep links need the
console ids; save them once in .play-release at the project root:
PLAY_DEV_ID=<developer id> # from /developers/<id>/app/...
PLAY_APP_ID=<app id> # from .../app/<id>/...Without them it opens the Play Console home page. Keep .play-release out
of a public repo if you consider the ids private.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

