Gplay release flow
Skill unliftedq/agents/android-dev/skills/gplay-release-flow
Ship Android releases through Google Play with the gplay CLI — upload an AAB (or a legacy APK) to a track (large artifacts upload resumably), promote a build up the track ladder without re-uploading, run and steer a staged production rollout (halt / resume / complete), inspect what is live on a track, list or download the signed APKs Play generates from an uploaded AAB, attach ProGuard/R8 deobfuscation mappings for vitals symbolication, push a private Internal App Sharing build, and manage legacy OBB expansion files. Use when uploading a build, cutting or shipping a release, promoting internal → alpha → beta → production, starting/ramping/pausing/finishing a staged rollout, checking which releases sit on a track, fetching the exact split/standalone/universal artifacts Play serves, symbolicating vitals crash stacks, sharing a QA build link, or working with OBB files.From its SKILL.md
npx -y skills add unliftedq/agents --skill gplay-release-flowAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
15.9 KB, ~3.9k tokens by cl100k_base, as published. Nobody here has run it
gplay release flow
Drive the Google Play release lifecycle from the command line with gplay:
upload a build to a track, promote it up the ladder, run a staged
rollout on production (and halt / resume / complete it), and list
what is currently on a track. gplay hides Google's three-step Edit transaction
(edits.insert → change → edits.commit) behind a single call per command.
The --help is the source of truth
Flags and defaults evolve with the CLI. This skill pins the command shapes, the mental model, and the gotchas — not an exhaustive flag list. Before you build an invocation, confirm the current flags with:
gplay releases --help # the whole release cluster
gplay releases <command> --help # one command (upload, promote, rollout, …)
If a flag you expect is absent, trust --help, not this document.
Before you run anything
- Auth. Every command needs a working service-account credential. If you
are not sure it is wired up, run
gplay auth doctor(and see thegplay-setupskill for onboarding). Auth failures exit10(bad credential) or11(the service account is not invited on the app). - Which app. Pin the package once with
gplay init(writes.gplay/config.json), or pass--package com.example.appon each call. - Output for machines. In CI or when parsing, add
--output json. On a TTY the default is a human table; piped/non-TTY defaults to JSON already. - Branch on exit codes, not text. gplay returns semantic exit codes
(
gplay exit-codesprints the table). The ones that matter here:3a required--confirmis missing,30an API 4xx such as a missing track,60an ambiguous target (two releases coexist).40/50are retry-safe (5xx / network).
Mental model: the track ladder + the rollout state machine
A build is uploaded to one track (internal, alpha, beta,
production, or any custom closed-track name) and then promoted up the
ladder — the same versionCode, no AAB re-upload. On a track, the latest
release moves through a small state machine:
draft ──► inProgress (userFraction f) ──► completed (f = 1.0)
│ ▲
halt│ │resume
▼ │
halted (fraction preserved)
rollout sets the fraction, halt freezes it, resume un-freezes it, and
complete ramps to 100%.
Upload a build to a track
gplay releases upload ./app.aab --track internal
gplay releases upload ./app.aab --track production --staged 0.1 --confirm
gplay releases upload ./app.aab --track production --complete --confirm
One call runs the full Edit lifecycle (edits.insert → bundles.upload → tracks.update → edits.commit). Any string is a valid --track, so custom
closed tracks "just work" — as long as the track already exists (see
Track must exist first below). Attach notes with --release-notes or a
--release-notes-dir of <locale>.txt files, and a ProGuard/R8
--mapping mapping.txt so vitals can symbolicate this build's crash stacks
(see Deobfuscation mappings below). Run gplay releases upload --help for
the full set.
Large-artifact uploads are resumable (v0.16.0): gplay transfers the
AAB/APK over Google's resumable upload protocol, so a transient interruption
during a big upload resumes instead of restarting from zero. It is automatic —
there is no flag to set (and no --timeout cap applies to the upload leg).
upload also accepts a legacy .apk ([experimental], via
edits.apks.upload): AAB vs APK is auto-detected by extension, and
--format apk|bundle overrides when the extension is ambiguous. The rest of
the pipeline (track, notes, --mapping, draft-by-default on production,
--dry-run/--confirm) is identical. Google has required the AAB for new
apps since August 2021, so APK uploads only serve existing apps still
distributed as APKs — if the app requires an App Bundle, Google's rejection
passes through verbatim.
Promote a build up the ladder (no re-upload)
gplay releases promote --from internal --to alpha
gplay releases promote --from beta --to production --staged 0.1 --confirm
promote copies the latest release on --from to --to, keeping the same
versionCode. Release notes carry over from the source unless you override
with --release-notes / --release-notes-dir. If the source track holds more
than one release (e.g. an inProgress plus a halted one), disambiguate with
--version-code N or --release-name <name> — otherwise the command refuses
rather than guess (exit 60).
Staged rollout: rollout / halt / resume / complete
These four act on the latest release of --track. On production each one
reaches real users, so each requires --confirm.
gplay releases rollout --track production --to 0.25 --confirm # set fraction → inProgress
gplay releases halt --track production --confirm # freeze at current fraction
gplay releases resume --track production --confirm # un-freeze, continue
gplay releases complete --track production --confirm # ramp to 1.0 → completed
rollout --to <f>sets the staged fraction (0 < f ≤ 1.0) and flips status toinProgress.haltsetsstatus=haltedwhile preserving the currentuserFraction, so a laterresumepicks up exactly where it left off.resumereturns the release toinProgressat the halted fraction.completeramps touserFraction=1.0,status=completed, ending the rollout.
When two releases coexist on the track, pin one with --version-code or
--release-name (same rule as promote).
Inspect what is on a track
gplay releases list --track production
gplay releases list --track production --output json
gplay releases list --track production --columns name,status,userFraction
releases list reads the track inside a read-only Edit (nothing is committed)
and shows every release on it — draft, inProgress, halted, completed. For a
cross-track or whole-track view use the gplay-tracks skill (gplay tracks list / gplay tracks view).
Generated APKs: list + download what Play signs from your AAB
After an upload, Play generates and signs the APKs it actually serves to
devices from your AAB — split, standalone, and universal APKs, plus asset-pack
and recovery-module slices. The generated sub-surface lists their download
metadata and fetches the raw signed bytes — handy to verify the signing
identity, sideload, or archive the exact artifacts Play serves after a
releases upload. Both commands are [experimental]; confirm flags with
gplay releases generated --help.
gplay releases generated list --version-code 42
gplay releases generated list --version-code 42 --output json
gplay releases generated download <downloadId> --version-code 42 --dest ./universal.apk
gplay releases generated download <downloadId> --version-code 42 --dest - # stream to stdout
These reads are Edit-free: unlike releases list, the generatedapks
endpoints are application-scoped (not under an Edit), so gplay issues a
direct GET. Don't pattern-match releases list and wrap an Edit around them —
the endpoints don't accept one. They are pure reads needing only that the
service account is invited on the app: no Edit, no financial capability.
generated list — enumerate the artifacts (keyed by --version-code)
--version-code N is required — it addresses the uploaded bundle. The API
groups the artifacts by signing key; gplay flattens that envelope into one
row per artifact — type · module · split/variant/slice id · downloadId · short
cert hash — so you can scan every generated artifact at once; narrow it with
--columns (e.g. --columns type,downloadId,cert). --output json stays the
verbatim GeneratedApksListResponse (ADR-0003), the signing-key groups intact,
for machines.
Read each artifact's Download ID (the downloadId field) from this list —
it is the opaque handle you hand to download. It is not a URL and not
stable across re-generation, so always read a fresh one from list rather
than caching it.
generated download — fetch one artifact's bytes to a file
This is gplay's first binary download-to-file gesture (ADR-0034), so it has its own conventions, distinct from the structured reads above:
- The Download ID is the positional argument (the addressed artifact);
--version-code Nis still required to locate the bundle. - Destination is
--dest PATH(required), not--output— the payload is opaque bytes, not a renderable table, so this command does not expose the global--output json|table|markdownflag. Use--dest -to stream the bytes to stdout for piping. - Bytes are streamed to the file, never buffered whole (a universal APK can
be large). On success a
✓line on stderr names the byte count and destination; stdout stays the data path. - A failed transfer leaves no file behind: gplay removes the partial APK on any transport or close error, so a non-zero exit never strands a truncated artifact you might mistake for a good one.
Exit codes: both commands return 11 (403 — the service account is not invited
on the app), 30 (404 — unknown package/version code, none generated, or
unknown Download ID), and 40/50 (retry-safe 5xx / network). download adds
20 when the --dest path can't be written (local IO). A missing
--version-code / --dest / <downloadId> is usage (exit 2), caught before
any network call.
Deobfuscation mappings (symbolicate vitals crash stacks)
A ProGuard/R8 mapping.txt lets Play vitals de-obfuscate a release's crash
stacks. There are two ways to attach one:
# The common case — with the artifact, in the same Edit:
gplay releases upload ./app.aab --track production --mapping ./mapping.txt --confirm
# After the fact — attach to an already-published versionCode:
gplay releases mappings upload ./mapping.txt --version-code 42
gplay releases mappings upload ./native.txt --version-code 42 --type nativeCode
Prefer --mapping on upload when the mapping exists at build time.
releases mappings upload covers the case where the version is already live and
you only later need symbolication — it runs its own Edit lifecycle
(edits.insert → deobfuscationfiles.upload → edits.commit). --version-code is
required; --type is proguard (default) or nativeCode; --dry-run previews
without a call. See the gplay-vitals skill for reading the symbolicated stacks.
Internal App Sharing (private shareable build links)
gplay releases sharing upload ./app.aab # prints a private downloadUrl
gplay releases sharing upload ./app.apk --output json
gplay releases sharing upload ./app.aab --dry-run
releases sharing upload ([experimental]) pushes an APK or AAB to Google Play
Internal App Sharing and prints the private, shareable downloadUrl an
authorized tester follows to install it. It bypasses tracks and the Edit
lifecycle entirely — a QA/preview gesture, not a release: no track, no
rollout, no versionCode promotion. APK vs AAB is auto-detected by extension
(--format apk|bundle overrides). No --confirm is needed (the link is private
and creates no release), but GPLAY_READONLY=1 still refuses it (exit 4).
--output json passes the InternalAppSharingArtifact through verbatim
(downloadUrl, certificateFingerprint, sha256).
Legacy OBB expansion files
Legacy surface. Expansion files are the pre-AAB mechanism for >150 MB
out-of-APK assets, and only APK-based apps use them — most apps use Play Asset
Delivery instead. The whole expansion-files namespace is [experimental].
gplay releases expansion-files upload ./main.obb --version-code 42 --type main
gplay releases expansion-files set --version-code 43 --references-version 42 --type main
gplay releases expansion-files view --version-code 42 --type main
uploadattaches an.obbto an already-published APK--version-code(--type main|patch, the two files an APK can have), via the full Edit lifecycle.setpoints one APK's expansion config at another APK's already-uploaded file (--references-version N) — no new binary uploaded.viewreads an APK's expansion config (its ownfileSize, or thereferencesVersionit points at) inside a read-only Edit.
All three take --dry-run and are refused under GPLAY_READONLY.
Production safety is built in
gplay defaults to the cautious choice on production (ADR-0002): an upload or
promote that targets production becomes a draft release unless you ask for
a live one with --complete or --staged, and those — plus every
rollout/halt/resume/complete on production — require an explicit
--confirm. If you omit it, the command fails with exit 3 and names the
flag it wants; re-run with that flag added. Treat exit 3 as "safe to retry
verbatim once --confirm is appended", never as a hard failure.
Every write command also takes --dry-run: it validates inputs and prints
the payload it would send without making any HTTP call. Use it to preview a
production change before committing to it.
Track must exist first (the trackhint behavior)
gplay never auto-creates a track as a side effect of an upload or promote —
a typo'd --track must fail loudly, not silently spawn a phantom track. When
upload or promote targets a custom closed track that has not been created
yet, the command fails with exit 30 and a hint naming the fix:
track "qa-team" does not exist — create it first with
`gplay tracks create qa-team`, then re-run …
Recovery: create the track once (gplay tracks create <name> — see the
gplay-tracks skill), then re-run the upload/promote. The standard tracks
(internal, alpha, beta, production) always exist and never need this.
Quick recipes
# CI: upload to internal, fail the job on any non-zero exit
gplay releases upload ./app.aab --track internal --output json || exit $?
# Cut a cautious production release: 10% staged, then ramp once metrics look OK
gplay releases upload ./app.aab --track production --staged 0.1 --confirm
gplay releases rollout --track production --to 0.5 --confirm
gplay releases complete --track production --confirm
# Something looks wrong mid-rollout — freeze, investigate, then resume
gplay releases halt --track production --confirm
gplay releases resume --track production --confirm
# Preview a production promote without sending anything
gplay releases promote --from beta --to production --staged 0.1 --confirm --dry-run
# After an upload: see what Play generated, then archive one artifact
gplay releases generated list --version-code 42
gplay releases generated download <downloadId> --version-code 42 --dest ./universal.apk
# Symbolicate a version already live — attach its mapping after the fact
gplay releases mappings upload ./mapping.txt --version-code 42
# Hand a QA reviewer a private install link (no track, no release)
gplay releases sharing upload ./app.aab
What ships with it: 1 file
221 B alongside SKILL.md
- .kman-skill.json221 B