This is the full developer documentation for gplay
# What is gplay?
> gplay is a single-binary Go CLI for the Google Play Developer API — upload releases, manage tracks, sync store listings, reply to reviews, and administer your team from CI pipelines or AI agents.
**gplay** is a fast, dependency-free command-line interface for the [Google Play Developer API](https://developers.google.com/android-publisher). It drives the publishing surface of Google Play — releases, tracks, reviews, store listings, compliance declarations, and team permissions — from your terminal, your CI pipeline, or an AI agent.
Every team shipping an Android app eventually hits the same wall: Fastlane `supply` drags a Ruby runtime into the CI image, prints output meant for humans, and exits with generic codes that make retry logic guesswork. gplay is what you'd build today if you started fresh.
## Why gplay
[Section titled “Why gplay”](#why-gplay)
* **One static binary.** Written in Go, zero runtime dependencies — no Ruby, no Node, no Python. One file in your CI image, fast cold start.
* **Built for CI and agents.** TTY-aware output (`table` for humans, `json` in pipes and CI), no interactive prompts, explicit flags, and [semantic exit codes](/docs/concepts/exit-codes/) that tell scripts whether a retry is safe.
* **API-faithful JSON.** `--output json` returns the raw Google Play Developer API response shape — no custom envelope to learn. The Google docs *are* the schema docs. See [output formats](/docs/concepts/output-formats/).
* **Safe by default on production.** Uploading or promoting to the `production` track creates a `draft` release unless you explicitly pass `--complete` or `--staged `. A typo can't ship to 100% of users.
## What it covers today
[Section titled “What it covers today”](#what-it-covers-today)
| Namespace | What it does |
| ------------------ | --------------------------------------------------------------------------- |
| `gplay auth` | Service-account credentials: login, status, doctor, multiple accounts |
| `gplay apps` | Local app registry, app details (contact info, default language) |
| `gplay releases` | Upload AABs, promote between tracks, staged rollouts (halt/resume/complete) |
| `gplay tracks` | List, inspect, and create release tracks; country availability |
| `gplay testers` | Declare the Google Groups authorized to test a track |
| `gplay reviews` | Read recent user reviews and reply to them |
| `gplay metadata` | Sync store listings (per-locale text) and store images with your repo |
| `gplay compliance` | Push and validate the Data Safety declaration |
| `gplay team` | Manage Play Console users and their per-app permission grants |
| `gplay schema` | Offline introspection of the Android Publisher API surface |
The full, generated command reference lives at [CLI reference](/docs/reference/) — one page per command, produced from the binary's own `--help` output.
## Status: public preview
[Section titled “Status: public preview”](#status-public-preview)
gplay is **pre-1.0**. The surface above is implemented and usable today, and this is an invitation to test and give feedback — breaking changes are still possible before `v1.0`, where per-command stability labels will mark what's frozen. Development is documentation-first: design conventions and architecture decision records are public in the [GitHub repository](https://github.com/PollyGlot/google-play-cli).
## Next steps
[Section titled “Next steps”](#next-steps)
1. [Install gplay](/docs/getting-started/installation/)
2. [Set up a service account](/docs/getting-started/service-account/)
3. [Run your first commands](/docs/getting-started/quickstart/)
## Not affiliated with Google
[Section titled “Not affiliated with Google”](#not-affiliated-with-google)
gplay is an independent open-source project (MIT license). It uses the public Google Play Developer API and is not endorsed by, affiliated with, or sponsored by Google LLC. "Google Play" is a trademark of Google LLC.
# gplay for AI agents
> Why gplay is agent-first — machine-stable JSON, semantic exit codes with resolvable refusals, dry-runs that announce required safety flags, offline API introspection, and llms.txt.
gplay is designed to be **driven by AI agents** — coding assistants, CI bots, autonomous release managers — as a first-class use case, not an afterthought. This page is the contract an agent can rely on.
## Machine-stable output
[Section titled “Machine-stable output”](#machine-stable-output)
* Piped output (and `CI=true`) defaults to **JSON** — an agent running gplay through a shell tool gets JSON without asking.
* For commands that wrap a Play API call, JSON is the **API's own response shape** ([pass-through](/docs/concepts/output-formats/)) — schemas are documented by Google, not invented by gplay.
* stdout carries data only; logs and warnings go to stderr. Parsing stdout never breaks on a log line.
* `--output markdown` renders tables and checklists ready to paste into a PR comment or chat reply.
## Semantic exit codes
[Section titled “Semantic exit codes”](#semantic-exit-codes)
The [exit code taxonomy](/docs/concepts/exit-codes/) makes failure handling decidable without parsing prose: `40`/`50` are retry-safe, `2` means the command was malformed, `10`/`11` mean credentials/permissions, `60` means state conflict.
**Exit `3` is the agent-resolvable refusal**: the command was well-formed, but a named safety acknowledgment (`--confirm`, `--grant-admin`) is missing — and the error names it. An agent can surface the decision to a human, or re-run with the flag if its policy allows.
Under `--output json`, a failure also writes a structured envelope to stdout (`{"error":{"exitCode","message","reasons","requires"}}`), so the failure is parseable without scraping stderr — and `requires` names the missing flag at failure time, not just in a dry-run. See [output formats](/docs/concepts/output-formats/).
## The read-only authority boundary — `GPLAY_READONLY`
[Section titled “The read-only authority boundary — GPLAY\_READONLY”](#the-read-only-authority-boundary--gplay_readonly)
`--confirm` and `--grant-admin` are *advisory*: an agent that holds the credential can pass them itself. `GPLAY_READONLY` is the boundary a **harness** imposes regardless of what the model chooses. When it is truthy (`1`/`true`/`yes`/`on`), every command that mutates Google Play state is refused with **exit 4** — *before* credential resolution and any network call — while read commands and `--dry-run` of mutating commands still run. So an agent can observe and plan against a production credential, but cannot publish unless the environment lets it.
Exit 4 is deliberately distinct from exit 3: it is **not** resolvable by adding a flag (the message says so). Pair it with a read-only service account for defence in depth — see the [CI/CD guide](/docs/guides/ci-cd/).
## Bounded runtime — `--timeout` and `--retry`
[Section titled “Bounded runtime — --timeout and --retry”](#bounded-runtime----timeout-and---retry)
Two global flags make unattended runs predictable. `--timeout ` bounds each API request (control-plane calls default to 60s; uploads are exempt unless you set it). `--retry N` retries the transient classes (transport errors, 5xx, 429 honouring `Retry-After`) with exponential backoff, and never retries non-transient 4xx or `edits.commit`. Both are safe defaults for an agent to set on every call.
## Rehearse before writing
[Section titled “Rehearse before writing”](#rehearse-before-writing)
Every write command accepts `--dry-run`: validate inputs and preview the payload with **no HTTP call**. The team write commands go further — their `--dry-run --output json` includes a `requires` array naming the safety flags the live write would need:
```sh
gplay team users add dev@example.com --role admin --dry-run --output json
# → { ..., "requires": ["grant-admin"] }
```
So an agent can discover the gate *before* tripping it.
## No prompts, ever
[Section titled “No prompts, ever”](#no-prompts-ever)
gplay never asks an interactive question. Required acknowledgments are expressed as flags (exit `3` when missing), credentials come from flags, env vars, or the stored Account — nothing blocks on a TTY.
## Offline API introspection
[Section titled “Offline API introspection”](#offline-api-introspection)
`gplay schema` queries an embedded index of the Android Publisher API — methods, request/response types, enums — with **no network and no credentials**:
```sh
gplay schema edits.tracks.update
gplay schema Track
gplay schema --list --method POST
```
An agent can answer "what fields does a Track carry?" without leaving the shell. (`[experimental]` — the JSON shape may still evolve.)
## llms.txt
[Section titled “llms.txt”](#llmstxt)
This site publishes [/llms.txt](/llms.txt) and a concatenated [/llms-full.txt](/llms-full.txt) so agents can ingest the documentation — including the full generated command reference — in one fetch.
## Ready-made skills
[Section titled “Ready-made skills”](#ready-made-skills)
If your agent framework supports skills (Claude Code and compatible tools), the [companion skills](/docs/agents/skills/) package these conventions per workflow — install with `npx skills add PollyGlot/google-play-cli-skills`.
# Agent skills
> Pre-built agent skills for gplay — one per command namespace, installable with npx skills add, for Claude Code and compatible AI coding tools.
**Agent skills** drive gplay from natural-language prompts. Each skill is a folder with a `SKILL.md` that documents the intent, the gplay commands it runs, and the safety rails it enforces. They live in a companion repository: [PollyGlot/google-play-cli-skills](https://github.com/PollyGlot/google-play-cli-skills).
## Install
[Section titled “Install”](#install)
```sh
npx skills add PollyGlot/google-play-cli-skills
```
Works with Claude Code and other agent frameworks that read the `skills` format.
## The roster
[Section titled “The roster”](#the-roster)
One skill per shipped namespace, plus a foundation skill for cross-cutting conventions:
| Skill | Drives |
| --------------------- | --------------------------------------------------------------------------- |
| `gplay-cli-usage` | Cross-cutting conventions: output, exit codes, auth resolution (foundation) |
| `gplay-setup` | Auth onboarding — service account, login, doctor |
| `gplay-apps` | Apps registry + app details |
| `gplay-release-flow` | upload / promote / staged rollouts |
| `gplay-tracks` | Tracks + closed-track testers |
| `gplay-reviews` | Review triage and replies |
| `gplay-metadata-sync` | Store listings + images sync |
| `gplay-compliance` | Data Safety declarations |
| `gplay-team` | Users, grants, permission vocabulary |
Two more are gated until their CLI surfaces ship: `gplay-vitals` ([#49](https://github.com/PollyGlot/google-play-cli/issues/49)) and `gplay-subscription-management` ([#51](https://github.com/PollyGlot/google-play-cli/issues/51)).
## Why skills instead of raw prompting?
[Section titled “Why skills instead of raw prompting?”](#why-skills-instead-of-raw-prompting)
A skill encodes the **safe path** for a workflow: which command sequence, which rehearsal steps (`--dry-run` first), which acknowledgment flags exist and when a human should approve them. The [agent contract](/docs/agents/agent-guide/) explains the CLI-level primitives the skills build on.
# Authentication & accounts
> How gplay resolves credentials — service accounts, named Accounts, the five-step precedence order, and the auth doctor diagnostic.
gplay authenticates with a Google Cloud **service account**: it reads the service-account JSON, signs a JWT, exchanges it for a short-lived OAuth2 access token, and uses that token for all Google Play Developer API calls.
## Accounts
[Section titled “Accounts”](#accounts)
An **Account** is a named credential profile registered in gplay — the local registration of one service-account JSON, with a human-friendly name. Multiple Accounts can coexist (different apps, different orgs, dev vs CI), and exactly one is **active** at a time.
```sh
gplay auth login --service-account ./sa.json # register + set active
gplay auth list # all Accounts, active marked
gplay auth status # the active Account
gplay auth logout # remove an Account
```
The credential itself lives in your **OS keystore** (Keychain on macOS, Secret Service on Linux, Credential Manager on Windows), with a `0600` fallback file where no keystore is available. The config file only records which Accounts exist and which is active — never the private key.
## Credential resolution precedence
[Section titled “Credential resolution precedence”](#credential-resolution-precedence)
When a command needs a credential, the first match wins:
1. `--service-account ` flag
2. `--account ` flag (selects a stored Account)
3. `GPLAY_SERVICE_ACCOUNT` env var (path **or inline JSON**)
4. `GPLAY_ACCOUNT` env var (name of a stored Account)
5. The Account marked active in the gplay config
If nothing resolves, the command exits with code `10` and points at `gplay auth login`.
CI
In CI, skip `auth login` entirely: store the JSON content as a secret and expose it as `GPLAY_SERVICE_ACCOUNT` — inline JSON means no temp file and no private key written to disk. See the [CI/CD guide](/docs/guides/ci-cd/).
## Absent vs. invalid credentials
[Section titled “Absent vs. invalid credentials”](#absent-vs-invalid-credentials)
The two failure modes are deliberately kept apart:
* **Absent** — no source is configured. A benign state: commands that need a credential exit `10` with a pointer to `auth login`, but `gplay auth status` reports "No active account" and exits `0`, and `apps list` (local registry) still works.
* **Invalid** — a credential *was* provided but its bytes are unusable (malformed JSON, missing required field, unreadable file). Always an error: exit `10` with the underlying cause, on every command — including `auth status`, which never masks a corrupt credential as "no account".
## `gplay auth doctor`
[Section titled “gplay auth doctor”](#gplay-auth-doctor)
The diagnostic command runs four checks in order, stopping at the first hard failure: JSON well-formed → token mints → `androidpublisher` scope present → a real `edits.insert`/`edits.delete` round-trip against your package. It exists because the most common failure is *"service account created but never invited on the app in Play Console"* — and that only surfaces on a real API call.
## Related
[Section titled “Related”](#related)
* [Service account setup](/docs/getting-started/service-account/)
* [Configuration cascade](/docs/concepts/configuration/)
* [`gplay auth` reference](/docs/reference/auth/)
# Configuration
> gplay's cascading configuration — global config, committed project pins, gitignored local overrides, environment variables, and CLI flags.
gplay reads the same `config.json` schema at several levels. Later layers win:
```txt
$XDG_CONFIG_HOME/gplay/config.json global, machine-local — Accounts live here
/.gplay/config.json project, committed — the package pin
/.gplay/config.local.json project, gitignored — per-developer overrides
GPLAY_* env vars e.g. GPLAY_ACCOUNT, GPLAY_SERVICE_ACCOUNT
CLI flags e.g. --account, --package, --service-account
```
On Linux the global config lives under `$XDG_CONFIG_HOME/gplay` (default `~/.config/gplay`); on macOS and Windows it is `~/.gplay`, next to other dotfile-style developer tooling.
## Project pinning
[Section titled “Project pinning”](#project-pinning)
```sh
gplay init --package com.example.myapp
```
writes `.gplay/config.json` at the repo root. Any gplay command run inside that tree (gplay walks up from the current directory looking for `.gplay/`) targets that package by default, so `--package` becomes optional. An explicit `--package` always overrides the pin.
The walk-up refuses to traverse into `$HOME` or any of its ancestors, so a stray `~/.gplay/config.json` can never masquerade as a project pin — and `gplay init` refuses to run when the current directory *is* `$HOME`.
## What goes in `.gplay/`
[Section titled “What goes in .gplay/”](#what-goes-in-gplay)
| File | Commit? | Purpose |
| --------------------- | --------------- | --------------------------------------- |
| `config.json` | **Yes** | The package pin shared with the team |
| `config.local.json` | No (gitignored) | Per-developer overrides, e.g. `account` |
| `edit-.json` | No (gitignored) | Transient open-Edit state |
`gplay init` writes a `.gplay/.gitignore` for you so the local and transient files stay out of git.
## One hard rule: `account` is never committed
[Section titled “One hard rule: account is never committed”](#one-hard-rule-account-is-never-committed)
Account names are machine-local. Pinning one in the shared `config.json` would break every teammate, so the loader **rejects** it with an error naming the offending file. Put it in `config.local.json`, the `GPLAY_ACCOUNT` env var, or the `--account` flag instead.
## Environment variables
[Section titled “Environment variables”](#environment-variables)
| Variable | Purpose |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GPLAY_SERVICE_ACCOUNT` | Service-account credential — a file path or inline JSON. The right choice in CI. |
| `GPLAY_ACCOUNT` | Name of a stored Account to use. |
| `GPLAY_READONLY` | When truthy (`1`/`true`/`yes`/`on`), refuse every command that mutates Google Play state (exit `4`) before any network call. Read commands and `--dry-run` still run. See [gplay for AI agents](/docs/agents/agent-guide/). |
| `GPLAY_INSTALL_NO_VERIFY` | Read by the install script only: bypass the SHA-256 checksum verification (air-gapped / mirrored installs). Prints a warning. See [installation](/docs/getting-started/installation/). |
| `CI` | When `true`, output defaults to JSON. See [output formats](/docs/concepts/output-formats/). |
| `NO_COLOR` | Disable colour in output. |
## Related
[Section titled “Related”](#related)
* [Authentication & accounts](/docs/concepts/authentication/)
* [`gplay init` reference](/docs/reference/init/)
# The Edits model
> How gplay wraps the Google Play Developer API's transactional Edits — implicit one-shot transactions today, explicit batching planned.
Most write operations on the Google Play Developer API are **transactional**: you open an *Edit* on a package, accumulate changes (releases, listings, tracks, testers), and commit it atomically. Edits expire after roughly 24 hours and are exclusive per app — only one can be open at a time.
gplay keeps Google's term **Edit** (rather than "transaction" or "changeset") so what you read here matches the official API docs.
## Implicit Edits — the default
[Section titled “Implicit Edits — the default”](#implicit-edits--the-default)
Every gplay write command wraps the full lifecycle in one invocation:
```txt
edits.insert → change (upload/patch) → edits.commit
```
For example, `gplay releases upload` performs `edits.insert → bundles.upload → tracks.update → edits.commit` in a single call. You never see the Edit ID unless you ask for it with `--verbose`.
**On any failure after the Edit opens, gplay auto-discards it** before the error propagates — no half-open transactions left to block the next run. Pass `--keep-edit-on-failure` to skip that cleanup when debugging.
## Dry runs
[Section titled “Dry runs”](#dry-runs)
Write commands accept `--dry-run`: validate inputs and preview the exact payload that would be sent, **without any HTTP call**. Combined with `--output json`, this is the safest way for scripts and agents to check a mutation before performing it.
## Explicit Edits — planned
[Section titled “Explicit Edits — planned”](#explicit-edits--planned)
An explicit mode — `gplay edits begin / commit / discard`, batching several changes into one transaction with the open Edit ID persisted to `.gplay/edit-.json` — is designed but **not shipped yet**. It is tracked in [issue #48](https://github.com/PollyGlot/google-play-cli/issues/48). Today, every write is its own implicit transaction.
## Related
[Section titled “Related”](#related)
* [Tracks & releases](/docs/concepts/tracks-and-releases/)
* [`gplay releases` reference](/docs/reference/releases/)
# Exit codes
> gplay's semantic exit code taxonomy — which failures are retry-safe (API 5xx, network) and which are terminal, so CI scripts and agents can decide without parsing error text.
gplay's exit codes are **semantic**: the code alone tells a script or an agent whether retrying can help, without parsing error messages.
| Code | Meaning | Retry-safe? |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| `0` | Success | — |
| `1` | Generic error (fallback when nothing more specific fits) | No |
| `2` | CLI misuse — unknown flag, bad value, missing required argument | No |
| `3` | Safety flag required — the command is well-formed but a named acknowledgment flag (`--confirm` / `--grant-admin`) is missing; the error names it | Deterministic: re-run with the named flag |
| `4` | Denied by environment policy — a mutating command was refused because `GPLAY_READONLY` is set; the message names the env var | No — **not** fixable by a flag; change the environment |
| `10` | Authentication failure — service account invalid, token refused, scope missing | No |
| `11` | Authorization — HTTP 403, e.g. the service account was never invited on the app | No |
| `20` | Client-side validation — malformed AAB, unknown locale, oversized listing text | No |
| `30` | API 4xx other than auth/permissions — not found, conflict, gone | No |
| `40` | API 5xx — upstream temporarily unhealthy | **Yes** |
| `50` | Network — timeout, DNS, connection refused | **Yes** |
| `60` | State conflict — another Edit open, rate-limited, ambiguous release target | Sometimes |
The same table ships inside the binary: `gplay help exit-codes` is generated from the same source the binary actually returns, so it can never drift.
## Built-in retry — `--retry`
[Section titled “Built-in retry — --retry”](#built-in-retry----retry)
The transient classes (`40`, `50`, and a rate-limited `60`) are exactly what the global `--retry N` flag handles for you, so you rarely need a hand-rolled loop:
```bash
# Retry transient failures up to 3 times with exponential backoff + jitter
# (429 honors Retry-After). With --retry, --timeout bounds each attempt.
gplay releases upload app.aab --track internal --retry 3 --timeout 2m
```
`--retry` defaults to `0` (no retry). It **never** retries non-transient 4xx (auth, validation) or `edits.commit` — a duplicate commit could double-publish — so it is safe to leave on. `--timeout` defaults to 60s for control-plane calls and is unbounded for uploads; with `--retry` it becomes a per-attempt bound. A deadline-exceeded failure maps to exit `50`, so the same wrapper that retries a network blip retries a timeout.
## Hand-rolled retry across commands
[Section titled “Hand-rolled retry across commands”](#hand-rolled-retry-across-commands)
When you need shell-level control — retrying across *separate* commands, or adding alerting — branch on the exit code yourself:
```bash
for attempt in 1 2 3; do
gplay releases upload app.aab --track internal
code=$?
case $code in
0) exit 0 ;;
40|50) echo "transient (exit $code), retrying..."; sleep $((attempt * 10)) ;;
*) exit $code ;;
esac
done
exit 1
```
## Exit 3 — machine-resolvable refusals
[Section titled “Exit 3 — machine-resolvable refusals”](#exit-3--machine-resolvable-refusals)
Code `3` is designed for agents: the command was valid, but a deliberate safety acknowledgment is missing (for example `--confirm` on a production publish, or `--grant-admin` when granting admin). The error message names the exact flag, so an automated caller can decide to re-run with it — a *resolvable* refusal rather than a dead end.
## Exit 4 — environment-enforced refusals
[Section titled “Exit 4 — environment-enforced refusals”](#exit-4--environment-enforced-refusals)
Code `4` is the opposite of `3`: it is **not** resolvable by adding a flag. A mutating command was refused because `GPLAY_READONLY` is set in the environment — an authority boundary a harness can impose on an agent independent of the flags the agent chooses. The fix is to change the environment, not the command. See [gplay for AI agents](/docs/agents/agent-guide/).
## Related
[Section titled “Related”](#related)
* [CI/CD guide](/docs/guides/ci-cd/)
* [gplay for AI agents](/docs/agents/agent-guide/)
# The metadata model
> The on-disk metadata tree, additive sync semantics, and content-hash image reconciliation that keep Google Play store listings in git.
`gplay metadata` keeps your store front in git: per-locale **listings** (title, descriptions, promo video) and **store images** (icon, feature graphic, screenshots) live as plain files, pulled from and applied to Google Play.
## The metadata tree
[Section titled “The metadata tree”](#the-metadata-tree)
One directory per locale; one `.txt` file per listing field; an `images/` subdirectory for store assets:
```txt
metadata/
en-US/
title.txt
short_description.txt
full_description.txt
video.txt
images/
icon.png
featureGraphic.png
phoneScreenshots/
01-home.png
02-detail.png
fr-FR/
title.txt
...
```
The file names match `fastlane supply`, so an existing fastlane metadata tree is a drop-in — minus fastlane's redundant `android/` segment and minus `changelogs/` (release notes belong to `releases upload`, not metadata). Plain text keeps a 4000-character description human-editable with line-by-line git diffs.
Google's limits — title 30, short description 80, full description 4000 characters — are enforced **offline** by `gplay metadata validate`, before any API call.
## Missing vs. empty
[Section titled “Missing vs. empty”](#missing-vs-empty)
Within a locale, a **missing** field file and an **empty** one mean different things to `apply`:
* missing → "I don't manage this field — leave the online value alone"
* empty → "clear this field online"
`pull` writes a file only for non-empty online fields, so a `pull` followed by an unmodified `apply` is a no-op.
## Additive sync
[Section titled “Additive sync”](#additive-sync)
`metadata apply` only ever **upserts** the locales and fields it finds on disk. Anything live on Play but absent locally is left untouched — never deleted by omission. Deletion is opt-in with `--prune`, which still refuses to remove the app's default-language listing.
This stance exists because the mirror alternative ("disk is the sole truth") turns a partial `pull` + `apply` into a data-loss event.
## Images: reconciliation by content hash
[Section titled “Images: reconciliation by content hash”](#images-reconciliation-by-content-hash)
The images API has no caller-assigned keys — Google identifies each image by a server-assigned ID and a SHA-256 of its content. So `metadata images apply` reconciles each **slot** (a `(locale, type)` pair) by content:
* **Singular slots** (`icon`, `featureGraphic`, `tvBanner`, `promoGraphic`) hold one file, named by type.
* **Gallery slots** (`phoneScreenshots`, `sevenInchScreenshots`, `tenInchScreenshots`, `tvScreenshots`, `wearScreenshots`) are directories whose files, **sorted by filename, define the display order** (the API carries no order field — display order is upload order).
If a gallery's local sequence matches the live one by content hash, nothing happens; if it differs in content *or* order, gplay clears the slot and re-uploads in order. Accepted extensions: `.png`, `.jpg`, `.jpeg`.
## Typical workflow
[Section titled “Typical workflow”](#typical-workflow)
```sh
gplay metadata pull # snapshot live listings into ./metadata
git add metadata && git commit # review like any other change
gplay metadata validate # offline checks (limits, locales, files)
gplay metadata apply --dry-run # preview exactly what would change
gplay metadata apply # upsert to Google Play
```
See the [metadata sync guide](/docs/guides/metadata-sync/) for the complete walkthrough including images.
## Related
[Section titled “Related”](#related)
* [`gplay metadata` reference](/docs/reference/metadata/)
* [Fastlane migration](/docs/guides/fastlane-migration/)
# Output formats
> TTY-aware output in gplay — table for humans, JSON as raw API pass-through for machines, Markdown for docs and agents, and a strict stdout/stderr split.
Every read command renders its result in one of three **formats**, selected with `--output table|json|markdown`. The default is `auto`:
* `CI=true` (non-empty) → `json`
* stdout is not a TTY (piped) → `json`
* otherwise → `table`
So the same command prints a human table in your terminal and clean JSON in a pipe or CI — with `--output` as the explicit override when the auto-detection isn't what you want (e.g. behind `tee`).
## `json` is API pass-through
[Section titled “json is API pass-through”](#json-is-api-pass-through)
For commands that wrap a Google Play Developer API call, `--output json` returns the **API's native response shape**, including its per-endpoint envelope (`{"reviews": [...]}`, `{"tracks": [...]}`). gplay adds no custom envelope — the official Google API documentation *is* the schema documentation for gplay's JSON output.
Two deliberate exceptions synthesise their own JSON, because they wrap no API call: `gplay apps list` (a local registry; there is no `apps.list` endpoint) and the offline reference commands (`team permissions`, `schema`).
## Control-sequence sanitization (human formats only)
[Section titled “Control-sequence sanitization (human formats only)”](#control-sequence-sanitization-human-formats-only)
API strings are often user-generated (review text, store-listing copy). The `table` and `markdown` renderers strip ANSI escape sequences and control characters from every cell, so a hostile value can't inject colour, cursor, or terminal-title sequences into your terminal or CI log. The stripping is rune-based — accents, CJK, and emoji pass through untouched.
`--output json` is **never** sanitized: machine consumers get the bytes verbatim (that's the pass-through promise). Fidelity lives on the JSON path, safety on the human path.
## `table`
[Section titled “table”](#table)
Columns are chosen for readability, not pass-through. Each command's default columns are documented in its `--help`, and `--columns col1,col2,...` lets you override them.
## `markdown`
[Section titled “markdown”](#markdown)
A first-class format, not "a table with pipes": tabular data renders as a Markdown table, status output as `- **Field**: value` lines, and checklists (`auth doctor`) as GitHub-style task lists. Useful for PR comments, docs, and chat agents.
## stdout vs. stderr
[Section titled “stdout vs. stderr”](#stdout-vs-stderr)
The split is strict and scriptable:
* **stdout** carries data only — the requested output, nothing else.
* **stderr** carries logs, warnings, and errors. `--verbose` (short form `-v`) adds info-level flow steps (the Edit ID, the deduced versionCode, each API call) and works in any position: `gplay --verbose auth status` or `gplay auth status --verbose`.
Errors are **never** pass-through. A human-readable line always goes to stderr. Under `--output json`, a failing command *additionally* writes one structured envelope to **stdout**, so an agent or CI consumer can branch on the failure without scraping stderr:
```json
{
"error": {
"exitCode": 60,
"message": "edits.commit on com.example.app: edit already exists (HTTP 409) [reason: editAlreadyExists]",
"reasons": ["editAlreadyExists"],
"requires": ["confirm"]
}
}
```
* `exitCode` and `message` are always present; `exitCode` mirrors the process [exit code](/docs/concepts/exit-codes/).
* `reasons` carries the upstream `error.errors[].reason` values when an API envelope was parsed; omitted otherwise.
* `requires` names the missing safety flag on an exit-3 refusal; omitted otherwise.
Under `table` / `markdown` a failure leaves stdout empty — the error goes to stderr only. The envelope shape is part of gplay's public contract.
## Related
[Section titled “Related”](#related)
* [Exit codes](/docs/concepts/exit-codes/)
* [gplay for AI agents](/docs/agents/agent-guide/)
# Tracks & releases
> Standard and closed tracks, safe production defaults, the staged rollout state machine, and how gplay targets releases.
## Tracks
[Section titled “Tracks”](#tracks)
Google Play provisions four **standard tracks** for every app, in promotion order: `internal`, `alpha`, `beta`, `production`. Beyond those, an app can have **closed tracks** with custom names (`qa-team`, `external-beta`, …) — the only kind gplay can create, because closed testing is the only type the API's create endpoint supports:
```sh
gplay tracks create qa-team
```
There is no `tracks delete` — the API exposes none; removing a track is a Play Console-only gesture.
The `--track` flag on release commands is **passthrough**: any string is accepted, so closed tracks work everywhere the four standard names do. A typo'd track name fails at the API — gplay never auto-creates a track as a side effect.
## Safe production defaults
[Section titled “Safe production defaults”](#safe-production-defaults)
What status a new release gets depends on the target track:
| Target track | Default status | Default userFraction |
| -------------------------------------- | -------------- | -------------------- |
| `production` | **`draft`** | — |
| `internal` / `alpha` / `beta` / closed | `completed` | `1.0` |
Shipping to real users is always explicit: `--complete` (100%) or `--staged `, and on production those additionally require `--confirm`. Everything else defaults to the behavior you'd expect from a testing track.
## The rollout state machine
[Section titled “The rollout state machine”](#the-rollout-state-machine)
Each transition on a staged production rollout is its own verb:
```sh
gplay releases rollout --track production --to 0.05 # set userFraction
gplay releases halt --track production # pause on bad metrics
gplay releases resume --track production # continue
gplay releases complete --track production # → 100%, completed
```
## Targeting a release
[Section titled “Targeting a release”](#targeting-a-release)
* `releases upload ` reads the versionCode **from the AAB** — never a flag, so it can't disagree with the artifact.
* `promote` / `rollout` / `halt` / `resume` / `complete` target the **latest** release on the track. Override with `--version-code N` or `--release-name `. If two releases coexist on the track (e.g. one `inProgress` and one `halted`) and no selector is passed, gplay **refuses with exit `60`** rather than guess.
## Release notes
[Section titled “Release notes”](#release-notes)
Two mutually exclusive flags on `releases upload`:
* `--release-notes ""` — one text, applied to the app's default language.
* `--release-notes-dir ` — one file per locale (`en-US.txt`, `fr-FR.txt`, …), with an optional `default.txt` fallback for locales without a dedicated file.
## Country availability
[Section titled “Country availability”](#country-availability)
Per-track country availability (`syncWithProduction`, `restOfWorld`, targeted countries) is **read-only on the API**, so gplay surfaces it as a read: `gplay tracks availability view --track production`. Changing it is a Play Console gesture.
## Related
[Section titled “Related”](#related)
* [Release flow guide](/docs/guides/release-flow/)
* [Testers guide](/docs/guides/testers/)
* [`gplay releases` reference](/docs/reference/releases/)
# Installation
> Install the gplay CLI with Homebrew, the install script, go install, or pre-built binaries for Linux, macOS, and Windows.
gplay ships as a single static binary. Pick whichever method fits your machine or CI image — they all install the same thing.
## Homebrew (macOS / Linux)
[Section titled “Homebrew (macOS / Linux)”](#homebrew-macos--linux)
```sh
brew install PollyGlot/tap/gplay
```
## Install script
[Section titled “Install script”](#install-script)
Downloads the right pre-built binary for your OS and architecture:
```sh
curl -fsSL https://gplay.sh/install | sh
```
The script **verifies the downloaded archive's SHA-256 against the release `checksums.txt` and fails closed** — a missing checksum file, no entry for your platform, or a mismatch all abort the install. For air-gapped or mirrored installs where the checksum file is unreachable, set `GPLAY_INSTALL_NO_VERIFY=1` to bypass (it prints a warning and stays greppable in your CI config).
## go install
[Section titled “go install”](#go-install)
With a Go toolchain installed:
```sh
go install github.com/PollyGlot/google-play-cli/cmd/gplay@latest
```
## Pre-built binaries
[Section titled “Pre-built binaries”](#pre-built-binaries)
Archives for Linux, macOS, and Windows (amd64 and arm64), with checksums and signatures, are on the [GitHub releases page](https://github.com/PollyGlot/google-play-cli/releases).
## Verify a release
[Section titled “Verify a release”](#verify-a-release)
Every release ships two origin-independent proofs you can gate on before trusting `gplay` in a pipeline: a **GitHub build-provenance attestation** over each archive, and a **keyless cosign signature** over `checksums.txt`.
```sh
# Provenance — proves the archive was built by this repo's release workflow.
gh attestation verify gplay___.tar.gz \
-R PollyGlot/google-play-cli
# cosign signature over checksums.txt, then the archive against it.
cosign verify-blob checksums.txt \
--bundle checksums.txt.sigstore.json \
--certificate-identity-regexp '^https://github.com/PollyGlot/google-play-cli/\.github/workflows/release\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
shasum -a 256 -c <(grep " gplay___.tar.gz$" checksums.txt)
```
A ready-to-paste CI step that installs and verifies in one shot is in the [CI/CD guide](/docs/guides/ci-cd/).
## Verify the install
[Section titled “Verify the install”](#verify-the-install)
```sh
gplay version
gplay --help
```
`gplay --help` prints the live command tree — it is always the source of truth for what your installed version supports.
## In CI
[Section titled “In CI”](#in-ci)
In a CI pipeline, the install script is usually the fastest option:
```yaml
- run: curl -fsSL https://gplay.sh/install | sh
```
See the [CI/CD guide](/docs/guides/ci-cd/) for a complete GitHub Actions workflow, including credential injection and retry handling.
## Next step
[Section titled “Next step”](#next-step)
[Set up a Google Cloud service account](/docs/getting-started/service-account/) so gplay can authenticate against your Play Console account.
# Quickstart
> From zero to a release upload in five commands — login, doctor, project pinning, and the core gplay release workflow.
This page assumes gplay is [installed](/docs/getting-started/installation/) and you have a [service-account JSON](/docs/getting-started/service-account/) with access to your app.
## 1. Authenticate
[Section titled “1. Authenticate”](#1-authenticate)
```sh
# Register the credential (stored in your OS keychain) and make it active.
gplay auth login --service-account ./service_account.json
# See which accounts exist and which one is active.
gplay auth list
gplay auth status
```
## 2. Verify access to your app
[Section titled “2. Verify access to your app”](#2-verify-access-to-your-app)
```sh
gplay auth doctor --package com.example.myapp
```
This round-trips a real API call and tells you exactly what's missing if the service account isn't fully wired up.
## 3. Pin your project (optional, recommended)
[Section titled “3. Pin your project (optional, recommended)”](#3-pin-your-project-optional-recommended)
```sh
cd ~/code/my-android-app
gplay init --package com.example.myapp
```
`gplay init` writes `.gplay/config.json` at the repo root. Every gplay command run inside that tree now targets `com.example.myapp` by default — no more `--package` on each call. The pin is meant to be committed; see [Configuration](/docs/concepts/configuration/).
## 4. Look around
[Section titled “4. Look around”](#4-look-around)
```sh
# The release tracks and what's on them.
gplay tracks list
gplay tracks view --track production
# Recent user reviews (the API exposes the last 7 days).
gplay reviews list --stars 1-2
```
## 5. Ship something
[Section titled “5. Ship something”](#5-ship-something)
```sh
# Upload an AAB to the internal track with localized release notes.
gplay releases upload app.aab --track internal --release-notes-dir ./whatsnew
# Promote the latest internal build to beta — same versionCode, no re-upload.
gplay releases promote --from internal --to beta
# Stage a production rollout at 10%, then advance it.
gplay releases rollout --track production --to 0.10
```
Safe by default
Targeting `production` creates a **draft** release unless you explicitly pass `--complete` or `--staged ` — and completing or staging a production release additionally requires `--confirm`. Nothing reaches users by accident. See [Tracks & releases](/docs/concepts/tracks-and-releases/).
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Release flow guide](/docs/guides/release-flow/) — the full upload → promote → rollout lifecycle.
* [CI/CD guide](/docs/guides/ci-cd/) — the same flow in GitHub Actions.
* [Metadata sync](/docs/guides/metadata-sync/) — keep store listings in git.
* [CLI reference](/docs/reference/) — every command and flag.
# Service account setup
> Create a Google Cloud service account, link it to your Play Console, and grant it the permissions gplay needs — with gplay auth doctor to verify the result.
gplay authenticates to Google with a **Google Cloud service account** that has been granted access to your Play Console app. This is a one-time setup.
## 1. Create the service account
[Section titled “1. Create the service account”](#1-create-the-service-account)
1. In the **Google Cloud Console**, create or pick a project, then go to **IAM & Admin → Service accounts → Create service account**.
2. On the **Keys** tab, choose **Add key → JSON** and download the `*.json` file.
Caution
Treat the JSON key as a secret — it can publish to your store listings. Never commit it to a repository.
## 2. Link it in the Play Console
[Section titled “2. Link it in the Play Console”](#2-link-it-in-the-play-console)
1. In the **Play Console**, go to **Setup → API access**.
2. Link the Google Cloud project that owns the service account.
3. Grant the service account the permissions your workflow needs:
* *"Release apps to production, exclude devices, and use Play App Signing"* — required for `releases upload`, `promote`, and the rollout verbs.
* *"Reply to reviews"* — required for `reviews reply`.
* Whatever else maps to the commands you'll run.
## 3. Verify with `gplay auth doctor`
[Section titled “3. Verify with gplay auth doctor”](#3-verify-with-gplay-auth-doctor)
```sh
gplay auth login --service-account ./service_account.json
gplay auth doctor --package com.example.myapp
```
`auth doctor` runs four checks in order, stopping at the first hard failure:
1. The service account JSON is present, readable, and well-formed.
2. An OAuth2 access token can be minted (the signed JWT exchange succeeds).
3. The token bears the `androidpublisher` scope.
4. For the targeted package, a real `edits.insert` + `edits.delete` round-trip succeeds against the Play API.
That last check catches the single most common setup error: **the service account exists but was never invited on the app in the Play Console**. The doctor output names the failing step and what to do about it.
## How gplay talks to Google
[Section titled “How gplay talks to Google”](#how-gplay-talks-to-google)
gplay reads the service-account JSON, signs a JWT with its private key, exchanges it for a short-lived OAuth2 access token, and uses that token for all Google Play Developer API calls. Tokens are minted on demand — nothing long-lived is written to disk, and the credential itself is stored in your OS keychain when you use `gplay auth login` (see [Authentication & accounts](/docs/concepts/authentication/)).
## Next step
[Section titled “Next step”](#next-step)
[Run your first commands](/docs/getting-started/quickstart/).
# CI/CD integration
> Wire gplay into GitHub Actions or any CI — inject the service account as a secret, verify with auth doctor, upload releases, and retry on semantic exit codes.
gplay is built for CI: one static binary, no runtime, JSON output by default when piped, and [exit codes](/docs/concepts/exit-codes/) that make retry decisions trivial. The example below is GitHub Actions; the same pattern applies to GitLab CI, Bitrise, CircleCI, or Jenkins — only the secret injection changes.
## Inject the credential
[Section titled “Inject the credential”](#inject-the-credential)
In CI, **never** use `gplay auth login`. Pass the credential through the environment: `GPLAY_SERVICE_ACCOUNT` accepts a file path or the **JSON content inline**. Inline is the right choice in CI — no temp file, no private key written to disk.
Store the entire service-account JSON as a repository secret named `GPLAY_SERVICE_ACCOUNT`, then pass it through the **environment**, never the `--service-account` flag: an inline-JSON flag value lands in shell history and in the process listing (`ps`, `/proc//cmdline`), exposing your private key. An env var is in neither. (`--service-account` is for a *path* in local use.)
Least privilege
The real authority boundary is the service account's **Play Console permission set**, not the flags a job passes. Mint a separate, narrowly-scoped account per job archetype — a leaked metadata-job key then can't publish a release. Give dashboards and AI agents a **read-only** account, and add `GPLAY_READONLY=1` to their environment as defence in depth: every mutating command is refused with exit `4` before any network call, whatever flags the agent chooses. See [gplay for AI agents](/docs/agents/agent-guide/).
## GitHub Actions workflow
[Section titled “GitHub Actions workflow”](#github-actions-workflow)
.github/workflows/play-release.yml
```yaml
name: Release to Play Store
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
env:
GPLAY_SERVICE_ACCOUNT: ${{ secrets.GPLAY_SERVICE_ACCOUNT }}
steps:
- uses: actions/checkout@v6
# Build your AAB however you do today (Gradle, Bazel, ...).
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'
- run: ./gradlew bundleRelease
# Install gplay (the script verifies the archive checksum and fails
# closed). To also gate on provenance/signature, see "Verify the install"
# below.
- run: curl -fsSL https://gplay.sh/install | sh
# Verify auth before any mutating call.
- run: gplay auth doctor --package com.example.myapp
# Upload to the internal track.
- run: |
gplay releases upload app/build/outputs/bundle/release/app-release.aab \
--package com.example.myapp \
--track internal \
--release-notes-dir ./whatsnew
```
JSON output needs no flag in CI: gplay emits JSON when stdout is not a TTY (piped/captured output) **or** when `CI=true` is set — each condition triggers it on its own, and CI runners satisfy both. An explicit `--output table` remains the override if you ever want human-shaped logs. See [output formats](/docs/concepts/output-formats/).
## Bound and retry transient failures
[Section titled “Bound and retry transient failures”](#bound-and-retry-transient-failures)
Prefer the built-in `--retry` over a hand-rolled loop. It retries the transient classes (transport errors, 5xx, 429 honouring `Retry-After`) with exponential backoff, and never retries non-transient 4xx or `edits.commit`:
```bash
gplay releases upload app.aab --package com.example.myapp --track internal \
--retry 3 --timeout 2m
```
`--timeout` bounds each request (60s default for control-plane calls; uploads exempt unless set), so a hung connection fails in seconds instead of stalling the job. With `--retry`, it's a per-attempt bound.
When you need shell-level control — retrying across *separate* commands, or adding alerting — branch on the [exit code](/docs/concepts/exit-codes/) yourself (`40`/`50` are retry-safe; `4`, from `GPLAY_READONLY`, is not, and isn't fixable by a flag):
```bash
for attempt in 1 2 3; do
gplay releases upload app.aab --package com.example.myapp --track internal
code=$?
case $code in
0) exit 0 ;;
40|50) echo "transient (exit $code), retrying..."; sleep $((attempt * 10)) ;;
*) exit $code ;;
esac
done
exit 1
```
## Splitting the pipeline
[Section titled “Splitting the pipeline”](#splitting-the-pipeline)
A common shape across workflows:
1. **Every merge to main** → `releases upload --track internal`
2. **Manual or scheduled promotion** → `releases promote --from internal --to beta`
3. **Release tag** → `releases promote --from beta --to production` (lands as a draft), then a human or a final job runs `releases rollout --track production --to 0.05 --confirm`
Google rate-limits publishing; as a rule of thumb, don't publish to alpha/beta more than once a day, and less often to production.
## Verify the install in CI
[Section titled “Verify the install in CI”](#verify-the-install-in-ci)
To gate the pipeline on artifact provenance, install from the release archive and verify it before running anything:
```yaml
- name: Install and verify gplay
env:
GH_TOKEN: ${{ github.token }}
VERSION: v0.5.0
run: |
set -euo pipefail
base="https://github.com/PollyGlot/google-play-cli/releases/download/$VERSION"
archive="gplay_${VERSION#v}_linux_amd64.tar.gz"
curl -fsSLO "$base/$archive"
# Provenance: built by this repo's release workflow.
gh attestation verify "$archive" -R PollyGlot/google-play-cli
# Signature over the checksum file, then the archive against it.
curl -fsSLO "$base/checksums.txt"
curl -fsSLO "$base/checksums.txt.sigstore.json"
cosign verify-blob checksums.txt \
--bundle checksums.txt.sigstore.json \
--certificate-identity-regexp '^https://github.com/PollyGlot/google-play-cli/\.github/workflows/release\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
shasum -a 256 -c <(grep " $archive$" checksums.txt)
tar -xzf "$archive" && sudo install gplay /usr/local/bin/
```
See [Verify a release](/docs/getting-started/installation/) for the standalone commands.
## Related
[Section titled “Related”](#related)
* [Release flow](/docs/guides/release-flow/)
* [Authentication & accounts](/docs/concepts/authentication/)
* [gplay for AI agents](/docs/agents/agent-guide/)
# Data Safety declarations
> Version your Google Play Data Safety declaration as a CSV in git and push it with gplay compliance datasafety — validated offline, gated by --confirm.
The **Data Safety declaration** states what user data your app collects, how it's shared, and your security practices. It is a **publication gate**: Google blocks releases when it's stale, so it bears on every app — not just monetised ones.
## The model: one CSV, versioned in git
[Section titled “The model: one CSV, versioned in git”](#the-model-one-csv-versioned-in-git)
The declaration travels as the same import/export **CSV** the Play Console uses — gplay adopts it verbatim rather than re-modelling Google's evolving schema. Export it once from the Play Console (App content → Data safety), commit it, and from then on the repo is your source of truth:
```txt
compliance/
data-safety.csv
```
## Validate and push
[Section titled “Validate and push”](#validate-and-push)
```sh
# Offline structural validation — no network, no credentials.
gplay compliance datasafety validate
# Rehearse: validates, resolves the target, reports what would be sent.
gplay compliance datasafety set --dry-run
# The real write — replaces the live declaration, hence the gate.
gplay compliance datasafety set --confirm
```
`set` runs `validate` implicitly first, so a structurally invalid CSV never reaches the network. The real write **requires `--confirm`** — a wrong declaration can block your releases or misstate your data practices — and `CI=true` does *not* auto-confirm.
The default file is `./compliance/data-safety.csv`; point elsewhere with `--file`.
## Why there is no `pull`
[Section titled “Why there is no pull”](#why-there-is-no-pull)
The Developer API exposes **only a write** for Data Safety — there is no read endpoint. gplay can set the declaration but never show you the live one; only the POST itself validates contents end-to-end. That single API fact shapes the whole command: no `pull`, no diff, and an offline-only dry-run. (This differs from [metadata sync](/docs/guides/metadata-sync/), where pull/diff/apply is possible.)
## When to re-push
[Section titled “When to re-push”](#when-to-re-push)
Re-run `set --confirm` whenever the CSV changes — typically when a new SDK changes your data collection, or when Google evolves the form. Keeping the CSV in the repo means the change is reviewed like any other code change.
## Related
[Section titled “Related”](#related)
* [`gplay compliance` reference](/docs/reference/compliance/)
# Migrating from Fastlane
> Map fastlane supply to gplay commands — drop the Ruby runtime from your Android CI, keep your metadata tree, and learn the one behavioral difference on production uploads.
gplay covers the `fastlane supply` surface (and more) as a single static binary — no Ruby in the CI image. Migration is mostly mechanical.
## The one behavioral surprise
[Section titled “The one behavioral surprise”](#the-one-behavioral-surprise)
Coming from Fastlane, the single most common surprise: `gplay releases upload --track production` does **not** publish to 100% by default. It creates a `draft` release that a follow-up step must explicitly publish with `--complete` or `--staged ` (plus `--confirm`). This is [deliberate](/docs/concepts/tracks-and-releases/). On every other track the behavior matches Fastlane: `completed` at 100%.
## Command mapping
[Section titled “Command mapping”](#command-mapping)
| fastlane | gplay |
| ------------------------------------------------- | -------------------------------------------------------------- |
| `supply --aab app.aab --track internal` | `gplay releases upload app.aab --track internal` |
| `supply --track internal --track_promote_to beta` | `gplay releases promote --from internal --to beta` |
| `supply --rollout 0.1` | `gplay releases rollout --track production --to 0.1 --confirm` |
| `supply --skip_upload_apk ... metadata sync` | `gplay metadata apply` / `gplay metadata images apply` |
| `supply --metadata_path ./metadata` | `gplay metadata apply --dir ./metadata` |
## Your metadata tree drops in
[Section titled “Your metadata tree drops in”](#your-metadata-tree-drops-in)
The on-disk layout is fastlane's, minus the redundant `android/` segment and minus `changelogs/`:
* `title.txt`, `short_description.txt`, `full_description.txt`, `video.txt` per locale — identical names.
* Release notes move out of the metadata tree: gplay owns them at upload time via `--release-notes` or `--release-notes-dir ./whatsnew` (one `.txt` per file, optional `default.txt` fallback).
Start with `gplay metadata pull` into a fresh directory and diff it against your fastlane tree — differences are usually un-pushed edits.
## Authentication is the same JSON
[Section titled “Authentication is the same JSON”](#authentication-is-the-same-json)
The Google Cloud service-account JSON you already use with Fastlane works as-is. In CI, export it inline as `GPLAY_SERVICE_ACCOUNT`; locally, `gplay auth login --service-account ./sa.json` stores it in the OS keychain. `gplay auth doctor` verifies the wiring with a real API round-trip.
## What you gain
[Section titled “What you gain”](#what-you-gain)
* No Ruby runtime or `bundle install` in CI — one binary, fast cold start.
* [Semantic exit codes](/docs/concepts/exit-codes/) — retry on `40`/`50`, fail fast on everything else, no log parsing.
* JSON output that mirrors the [Play API responses](/docs/concepts/output-formats/) for scripting.
* `--dry-run` on every write.
## Honest gaps
[Section titled “Honest gaps”](#honest-gaps)
gplay is pre-1.0; a detailed pitfall-by-pitfall migration table is planned once more real migrations surface them. If you hit one, please [open an issue](https://github.com/PollyGlot/google-play-cli/issues).
## Related
[Section titled “Related”](#related)
* [Quickstart](/docs/getting-started/quickstart/)
* [CI/CD integration](/docs/guides/ci-cd/)
# Metadata sync
> Keep Google Play store listings and images in git with gplay — pull the live store front, validate offline, preview with dry-run, and apply additively.
`gplay metadata` treats your store front as code: per-locale listing text and store images live in a directory you commit, review, and apply. The [metadata model](/docs/concepts/metadata-model/) page covers the semantics; this is the working loop.
## First sync: pull what's live
[Section titled “First sync: pull what's live”](#first-sync-pull-whats-live)
```sh
gplay metadata pull # text listings → ./metadata
gplay metadata images pull # store images → ./metadata//images
git add metadata && git commit -m "snapshot store front"
```
`pull` writes one directory per locale with `title.txt`, `short_description.txt`, `full_description.txt`, optional `video.txt` — the same names `fastlane supply` uses, so an existing fastlane tree drops in.
## Edit, validate, preview
[Section titled “Edit, validate, preview”](#edit-validate-preview)
```sh
$EDITOR metadata/en-US/full_description.txt
# Offline checks: field limits (30/80/4000 chars), locales, file shape.
gplay metadata validate
# Exactly what would change, with no HTTP call.
gplay metadata apply --dry-run
```
## Apply
[Section titled “Apply”](#apply)
```sh
gplay metadata apply
```
`apply` is **additive**: it upserts the locales and fields present on disk and never deletes anything by omission. A locale live on Play but absent locally is reported, not touched. To actually delete online-only locales, opt in with `--prune` (it still refuses to remove the app's default-language listing).
Remember the missing-vs-empty rule: deleting a *file* means "stop managing this field"; an *empty file* means "clear this field online".
## Images
[Section titled “Images”](#images)
```sh
gplay metadata images list # live slots and hashes
gplay metadata images validate # offline: types, extensions
gplay metadata images apply --dry-run # diff by content hash
gplay metadata images apply
```
Images reconcile per slot (`icon`, `featureGraphic`, screenshot galleries…) by SHA-256: identical content and order → no-op; any difference → the slot is cleared and re-uploaded in filename order. Removing online-only images from a managed slot is the separate destructive mode `images apply --prune`, which requires `--confirm`.
## In CI
[Section titled “In CI”](#in-ci)
A metadata job fits naturally next to your release job:
```yaml
- run: gplay metadata validate
- run: gplay metadata apply --dry-run --output json
- run: gplay metadata apply
```
## Related
[Section titled “Related”](#related)
* [The metadata model](/docs/concepts/metadata-model/)
* [`gplay metadata` reference](/docs/reference/metadata/)
* [Fastlane migration](/docs/guides/fastlane-migration/)
# Release flow
> The complete gplay release lifecycle — upload an AAB to internal, promote through beta, stage a production rollout, ramp, halt, or complete it.
This is the canonical path an Android release takes through gplay, from CI build to 100% of production users. All commands assume a [pinned package](/docs/concepts/configuration/) — add `--package com.example.myapp` otherwise.
## 1. Upload to internal
[Section titled “1. Upload to internal”](#1-upload-to-internal)
```sh
gplay releases upload app/build/outputs/bundle/release/app-release.aab \
--track internal \
--release-notes-dir ./whatsnew
```
The versionCode is read from the AAB itself. On a testing track the release lands as `completed` at 100% of that track's audience. Behind the scenes this is one [implicit Edit](/docs/concepts/edits/): `edits.insert → bundles.upload → tracks.update → edits.commit`, auto-discarded if anything fails.
## 2. Promote a green build
[Section titled “2. Promote a green build”](#2-promote-a-green-build)
```sh
gplay releases promote --from internal --to beta
```
Promotion copies the **latest release** on the source track to the target — same versionCode, no re-upload. Add `--version-code N` (or `--release-name `) when the source track holds more than one release; if it's ambiguous and you pass nothing, gplay refuses with exit `60` rather than guess.
## 3. Enter production — as a draft
[Section titled “3. Enter production — as a draft”](#3-enter-production--as-a-draft)
```sh
gplay releases promote --from beta --to production
```
By default this creates a **draft** release on production: visible in the Play Console, shipped to nobody. This is [deliberate](/docs/concepts/tracks-and-releases/) — reaching real users is always an explicit step.
## 4. Stage, ramp, complete
[Section titled “4. Stage, ramp, complete”](#4-stage-ramp-complete)
```sh
# Start at 5% of users (requires --confirm on production).
gplay releases rollout --track production --to 0.05 --confirm
# Ramp up over the following days.
gplay releases rollout --track production --to 0.20 --confirm
gplay releases rollout --track production --to 0.50 --confirm
# Finish: userFraction → 1.0, status → completed.
gplay releases complete --track production --confirm
```
## If metrics go bad
[Section titled “If metrics go bad”](#if-metrics-go-bad)
```sh
gplay releases halt --track production # pause the staged rollout
gplay releases resume --track production --confirm # continue after the fix
```
A halted release stays on the track; `resume` picks it up where it stopped. Halting never needs `--confirm` — reducing exposure is always safe; resuming reaches real users again, so it does.
## Inspecting state
[Section titled “Inspecting state”](#inspecting-state)
```sh
gplay releases list --track production
gplay tracks view --track production
```
Both support `--output json` (the raw API shape) for scripting — see [output formats](/docs/concepts/output-formats/).
## Rehearsing a write
[Section titled “Rehearsing a write”](#rehearsing-a-write)
Every write verb accepts `--dry-run`: inputs are validated and the exact payload is printed, with **no HTTP call**. Use it in CI to lint a release step before the real run.
## Related
[Section titled “Related”](#related)
* [CI/CD guide](/docs/guides/ci-cd/) — this flow in GitHub Actions
* [`gplay releases` reference](/docs/reference/releases/)
# Reviews
> Read recent Google Play user reviews and reply to them with gplay — star filtering, JSON pass-through, and batch replies from a TSV file or stdin.
## Listing reviews
[Section titled “Listing reviews”](#listing-reviews)
```sh
gplay reviews list # most recent reviews
gplay reviews list --stars 1-2 # only 1- and 2-star
gplay reviews list --stars 1,3,5 # a set
gplay reviews list --limit 20 # cap the count
```
The 7-day window
The Google Play Developer API only exposes reviews from the **last 7 days**. gplay prints a `WARN` line to stderr on every run — including an empty result, so "no output" never silently reads as "this app has no reviews". Long-history retrieval (Google's CSV reports in Cloud Storage) is on the roadmap.
`--stars` filters **client-side** (the API has no server-side rating filter), results auto-paginate until exhausted, and `--output json` is the `{"reviews": [...]}` pass-through reflecting the filtered set.
## Replying
[Section titled “Replying”](#replying)
```sh
gplay reviews reply --review-id REVIEW_ID --reply "Thanks for the feedback!"
```
## Batch replies
[Section titled “Batch replies”](#batch-replies)
For support workflows (or an agent drafting replies for approval), `reply` takes a TSV stream — one `\t` line per reply, `-` for stdin:
```sh
gplay reviews reply --batch replies.tsv
gplay reviews list --stars 1-2 --output json | my-draft-tool | gplay reviews reply --batch -
```
Blank lines and `#` comments are skipped; replies containing tabs or newlines use RFC 4180 double-quoting. Each line posts sequentially — a failing line is reported on stderr and does **not** abort the rest, and the process exits with the highest exit code seen.
Use `--dry-run` to parse and print the planned replies without calling the API.
## Permissions
[Section titled “Permissions”](#permissions)
Replying requires the *"Reply to reviews"* permission on the service account in the Play Console — `gplay auth doctor` and the [service account setup](/docs/getting-started/service-account/) page cover granting it.
## Related
[Section titled “Related”](#related)
* [`gplay reviews` reference](/docs/reference/reviews/)
* [Output formats](/docs/concepts/output-formats/)
# Team management
> Manage Play Console users and per-app permission grants with gplay — friendly permission aliases, frozen role bundles, and an explicit gate for admin access.
`gplay team` manages your **Developer account** (the Play Console organisation): its members (*users*) and their per-app access (*grants*). It's the one gplay surface keyed by the organisation rather than a package.
## Users — account-wide membership
[Section titled “Users — account-wide membership”](#users--account-wide-membership)
```sh
gplay team users list
gplay team users add dev@example.com --role release-manager
gplay team users set dev@example.com --permissions reply-reviews
gplay team users remove dev@example.com
```
## Grants — per-app access
[Section titled “Grants — per-app access”](#grants--per-app-access)
```sh
gplay team grants list --package com.example.myapp
gplay team grants set dev@example.com --package com.example.myapp --role reviewer
gplay team grants remove dev@example.com --package com.example.myapp
```
`grants set` is an **upsert**: gplay reads the member's current grants, then creates or updates as needed. With `--dry-run --output json` it reports the resolved verb (create/update) and the permission diff (current → desired) before you commit to anything.
## Permissions without memorising Google enums
[Section titled “Permissions without memorising Google enums”](#permissions-without-memorising-google-enums)
Google expresses permissions as `CAN_*` enums in two near-parallel families (account-wide `_GLOBAL` and per-app). gplay layers two friendlier forms on top:
* **Aliases** — scope-independent names like `release-production` or `reply-reviews`, resolved to the right enum family for the command's scope. Raw `CAN_*` enums are always accepted too, so nothing is ever un-grantable.
* **Role bundles** — frozen presets: `viewer`, `reviewer`, `tester-manager`, `release-manager`, `admin`. *Frozen* means a bundle's membership only changes by an explicit, versioned gplay release — never silently when Google adds enums. Money-related capabilities (financial data, orders) are deliberately excluded from every bundle and must be granted as explicit permissions.
The whole vocabulary is introspectable offline — no credentials needed:
```sh
gplay team permissions # aliases + bundles, account scope
gplay team permissions --scope app # the per-app enum family
```
## The admin gate
[Section titled “The admin gate”](#the-admin-gate)
Granting admin is never silent. An admin-conferring `add` or `set` (via `--role admin` or a permission set including the all-permissions enum) requires the named acknowledgment flag `--grant-admin`, otherwise gplay refuses with exit `3` and names the flag. Agents can discover the gate ahead of time: `--dry-run --output json` emits a `requires` array listing the safety flags the live write needs.
## Related
[Section titled “Related”](#related)
* [Exit codes](/docs/concepts/exit-codes/) — exit `3`, the resolvable refusal
* [`gplay team` reference](/docs/reference/team/)
# Closed tracks & testers
> Create closed testing tracks and declare their authorized Google Groups with gplay — declarative tester management for QA teams and external betas.
A **closed track** gates a build to an explicit audience — a QA team, an external beta cohort. gplay creates the track and declares who can test it.
## Create a closed track
[Section titled “Create a closed track”](#create-a-closed-track)
```sh
gplay tracks create qa-team
```
Closed testing is the only track type the API can create, so there is no `--type` flag. The new track is a phone (default form factor) track. Creating a track that already exists surfaces the API error (exit `30`) — gplay doesn't fake idempotency.
## Declare the testers
[Section titled “Declare the testers”](#declare-the-testers)
The Play API expresses a track's audience **only as Google Groups** — individual emails are a Play Console-only gesture:
```sh
gplay testers set --track qa-team --group qa@googlegroups.com
gplay testers list --track qa-team
```
`testers set` is **declarative**: it replaces the whole group list (it maps 1:1 to the API's `testers.update`). There is no add/remove — re-run `set` with the full desired list. Idempotent by design, which is exactly what CI and agents want.
Two safety rails:
* A bare `set` with neither `--group` nor `--clear` is refused (exit `2`) — a forgotten `--group` can never silently wipe the audience.
* Emptying the list on purpose is the explicit `--clear`.
There is no `--confirm` here: a closed test track is low-stakes and reversible, unlike a production rollout.
## Ship a build to the track
[Section titled “Ship a build to the track”](#ship-a-build-to-the-track)
```sh
gplay releases upload app.aab --track qa-team
gplay releases promote --from internal --to qa-team
```
`--track` accepts any name. If the track doesn't exist yet, the upload fails with a hint pointing at `gplay tracks create` — gplay never auto-creates a track from a typo.
## Rehearse first
[Section titled “Rehearse first”](#rehearse-first)
Both writes accept `--dry-run` (validate + preview the payload, no HTTP call) and `--keep-edit-on-failure` for debugging the underlying [Edit](/docs/concepts/edits/).
## Related
[Section titled “Related”](#related)
* [Tracks & releases](/docs/concepts/tracks-and-releases/)
* [`gplay testers` reference](/docs/reference/testers/)
* [`gplay tracks` reference](/docs/reference/tracks/)
# gplay
> gplay — gplay — fast, lightweight CLI for the Google Play Developer API.
gplay — fast, lightweight CLI for the Google Play Developer API.
Reads service-account credentials, mints OAuth2 tokens, and drives the publishing surface (releases, tracks, reviews, metadata, compliance, team). Designed to replace Fastlane on Android CI pipelines.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay [flags]
gplay [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| ------------------------------------------------- | ---------------------------------------------------------------------- |
| [`gplay apps`](/docs/reference/apps/) | Manage Android packages registered with gplay |
| [`gplay auth`](/docs/reference/auth/) | Manage gplay credentials |
| [`gplay compliance`](/docs/reference/compliance/) | Manage an app's regulatory declarations (Data Safety, ...) |
| [`gplay init`](/docs/reference/init/) | Pin a Google Play package to the current repo |
| [`gplay metadata`](/docs/reference/metadata/) | Manage Store front Listings (per-locale title/description/video) |
| [`gplay releases`](/docs/reference/releases/) | Manage app releases (upload, promote, rollout) |
| [`gplay reviews`](/docs/reference/reviews/) | Read and reply to user reviews |
| [`gplay schema`](/docs/reference/schema/) | \[experimental] Introspect the Android Publisher API surface offline |
| [`gplay team`](/docs/reference/team/) | Manage the Developer account's members and permissions (users, grants) |
| [`gplay testers`](/docs/reference/testers/) | Manage the Google Groups authorized to test a track |
| [`gplay tracks`](/docs/reference/tracks/) | Inspect and create release tracks (standard and custom closed) |
| [`gplay version`](/docs/reference/version/) | Print gplay version |
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay apps
> gplay apps — Manage Android packages registered with gplay
Manage Android packages registered with gplay
## Usage
[Section titled “Usage”](#usage)
```txt
gplay apps [flags]
gplay apps [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| ----------------------------------------------------- | --------------------------------------------------------------------------------- |
| [`gplay apps add`](/docs/reference/apps/add/) | Register an Android package under the active Account |
| [`gplay apps details`](/docs/reference/apps/details/) | Read and set an app's App details (default language, contact email/phone/website) |
| [`gplay apps init`](/docs/reference/apps/init/) | Pin a Google Play package to the current repo |
| [`gplay apps list`](/docs/reference/apps/list/) | List packages registered under the active Account |
| [`gplay apps remove`](/docs/reference/apps/remove/) | Remove an Android package from the active Account's registry |
| [`gplay apps view`](/docs/reference/apps/view/) | Show default language, title, and contact email for an app |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay apps add
> gplay apps add — Register an Android package name (e.g.
Register an Android package name (e.g. com.example.myapp) under the active Account in gplay's local registry. By default `apps add` validates access by opening and immediately discarding a Google Play Edit on the package — a cheap probe that catches typos and missing per-app permission grants at registration time rather than weeks later in CI.
Pass --no-verify to skip the API round-trip (useful for offline or preparatory registration).
## Usage
[Section titled “Usage”](#usage)
```txt
gplay apps add [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------- | ------------------------------------------------------------------------------ |
| `--no-verify` | skip the edits.insert+delete access probe (record the package unconditionally) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay apps details
> gplay apps details — Group for an app's App details — the app-global edits.details record holding defaultLanguage and the user-visible contactEmail, contac…
Group for an app's App details — the app-global edits.details record holding defaultLanguage and the user-visible contactEmail, contactPhone, and contactWebsite.
`apps details view` reads the record; `apps details set` writes it field-by-field. The bare `apps details` command prints this help.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay apps details [flags]
gplay apps details [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| [`gplay apps details set`](/docs/reference/apps/details/set/) | Set an app's App details fields (default language, contact email/phone/website) |
| [`gplay apps details view`](/docs/reference/apps/details/view/) | Show an app's App details (default language, contact email/phone/website) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay apps details set
> gplay apps details set — Set the App details fields one by one.
Set the App details fields one by one. set is a partial patch at flag granularity: a field you pass is written, a field you omit is left intact, and an explicit empty value clears a field (e.g. --contact-phone "" removes the number). It maps to edits.details.patch.
There is no --confirm: contact info is low-stakes and reversible (same as testers set). There is no offline format validation: the API arbitrates email / URL / phone shape, and its error surfaces verbatim.
A bare `set` with no field flag is refused (exit 2) so a forgotten flag can never emit an empty patch. Writes inside an implicit Edit (open → details.patch → commit). Use --dry-run to preview the patch without any HTTP call (no auth needed), and --keep-edit-on-failure to skip the auto-discard cleanup for debugging.
\--output json returns the edits.details.patch body verbatim.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay apps details set [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| --------------------------- | ---------------------------------------------------------------------------------------- |
| `--contact-email string` | set the user-visible contact email (empty value clears it) |
| `--contact-phone string` | set the user-visible contact phone (empty value clears it) |
| `--contact-website string` | set the user-visible contact website (empty value clears it) |
| `--default-language string` | set the app's default language (BCP-47 locale, e.g. en-US) |
| `--dry-run` | preview the patch without any HTTP call (no auth needed) |
| `--keep-edit-on-failure` | skip the auto-discard cleanup on failure (debug) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay apps details view
> gplay apps details view — Show the full edits.details record for an app: defaultLanguage and the user-visible contactEmail, contactPhone, and contactWebsite.
Show the full edits.details record for an app: defaultLanguage and the user-visible contactEmail, contactPhone, and contactWebsite.
Reads from the Google Play Developer API inside a read-only Edit (open → details.get → discard); nothing is committed. The package defaults to the repo's .gplay/config.json pin when --package is omitted.
\--output json returns the edits.details.get body verbatim (a clean ADR-0003 pass-through — a single endpoint is read, so there is no gplay envelope). This is distinct from `gplay apps view`, the terse cross-resource identity card (package + title + default language).
## Usage
[Section titled “Usage”](#usage)
```txt
gplay apps details view [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay apps init
> gplay apps init — Write .gplay/config.json with the given Android package name so every subsequent gplay command in this directory tree picks it up automat…
Write .gplay/config.json with the given Android package name so every subsequent gplay command in this directory tree picks it up automatically via walk-up. Also creates .gplay/.gitignore so per-developer overrides (config.local.json) and transient edit-ID files stay out of git.
Run from the repo root.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay apps init [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | -------------------------------------------------------- |
| `--package string` | Android package name (e.g. com.example.myapp). Required. |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay apps list
> gplay apps list — List every Android package registered under the active Account in gplay's local registry.
List every Android package registered under the active Account in gplay's local registry. In table and markdown output the row matching the current repo's .gplay/config.json pin (if any) is marked with a ✓ in the Pinned column; in JSON output the same row carries "pinned": true. Pass no positional arguments — listing scope is always the active Account.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay apps list [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay apps remove
> gplay apps remove — Remove an Android package name (e.g.
Remove an Android package name (e.g. com.example.myapp) from the active Account's local registry. This is a pure local-state operation: the Google Play Console is never touched, no API call is made.
Idempotent: removing a package that isn't in the registry exits 0 with a stderr note. If the removed package is currently pinned by the repo's .gplay/config.json, a stderr warning is printed but the project config is left untouched (repinning is the caller's decision).
## Usage
[Section titled “Usage”](#usage)
```txt
gplay apps remove [flags]
```
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay apps view
> gplay apps view — Show the app's defaultLanguage, title (on the default language), and contactEmail — the minimum needed to confirm "yes, I'm looking at th…
Show the app's defaultLanguage, title (on the default language), and contactEmail — the minimum needed to confirm "yes, I'm looking at the right app".
Reads from the Google Play Developer API inside a read-only Edit (open → details.get + listings.get → discard); nothing is committed. The package defaults to the repo's .gplay/config.json pin when --package is omitted.
\--output json returns the gplay envelope {"details":..,"listing":..} — each sub-object is the upstream API body verbatim. (Explicit exception to ADR-0003: two endpoints are merged here, so the JSON shape is gplay- defined rather than a single API pass-through.)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay apps view [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay auth
> gplay auth — Manage gplay credentials
Manage gplay credentials
## Usage
[Section titled “Usage”](#usage)
```txt
gplay auth [flags]
gplay auth [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| --------------------------------------------------- | ------------------------------------------------------------------------------ |
| [`gplay auth doctor`](/docs/reference/auth/doctor/) | Run ordered diagnostic checks on the active credential |
| [`gplay auth list`](/docs/reference/auth/list/) | List every registered Account |
| [`gplay auth login`](/docs/reference/auth/login/) | Register a service account as the active Account |
| [`gplay auth logout`](/docs/reference/auth/logout/) | Remove a registered Account from the config and the keystore |
| [`gplay auth status`](/docs/reference/auth/status/) | Print the active Account, the keystore backend, and where the credential lives |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay auth doctor
> gplay auth doctor — Run the doctor sequence from docs/DESIGN.md §1: 1.
Run the doctor sequence from docs/DESIGN.md §1:
1. Service account JSON is valid
2. OAuth2 access token can be minted
3. Token carries the androidpublisher scope
4. (Per --package) edits.insert + edits.delete round-trip on the package
Checks 1–3 run once. Check 4 runs once per --package value passed (in order). Checks run in order and the chain stops on the first failure; subsequent checks are reported as skipped. Use --output json to get a structured \[]CheckResult for scripting.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay auth doctor [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------- | ----------------------------------------------------------------------------------------------- |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package strings` | Android package name to round-trip an edits.insert+delete against; repeat for multiple packages |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay auth list
> gplay auth list — List every registered Account
List every registered Account
## Usage
[Section titled “Usage”](#usage)
```txt
gplay auth list [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay auth login
> gplay auth login — Register a Google Cloud service account JSON as a named Account in the local gplay registry and mark it active so subsequent commands us…
Register a Google Cloud service account JSON as a named Account in the local gplay registry and mark it active so subsequent commands use it without an explicit --account flag.
The credential is stored in the OS keystore (macOS Keychain, Windows Credential Manager, or Linux Secret Service). On systems without a keystore daemon (headless Linux, CI containers), gplay transparently falls back to a 0600 file under the config directory. The active backend is reported by `gplay auth status` and logged once per process at -v.
Pass --activate=false to add a second Account without changing which one is active. (The very first registered Account becomes active regardless, so the registry is never left without one when --activate=false is set.)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay auth login [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| --------------------------- | ---------------------------------------------------------------------------- |
| `--activate` | mark the new Account active (default true) (default true) |
| `--developer-id gplay team` | Play Console Developer account id to record on this Account (for gplay team) |
| `--name string` | friendly Account name (default: derived from client\_email) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay auth logout
> gplay auth logout — Remove a registered Account from the config and the keystore
Remove a registered Account from the config and the keystore
## Usage
[Section titled “Usage”](#usage)
```txt
gplay auth logout [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------- | ------------------------------------------------------------ |
| `--confirm` | confirm credential removal (required; see docs/DESIGN.md §9) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay auth status
> gplay auth status — Print the active Account, the keystore backend, and where the credential lives
Print the active Account, the keystore backend, and where the credential lives
## Usage
[Section titled “Usage”](#usage)
```txt
gplay auth status [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay compliance
> gplay compliance — Manage an app's regulatory declarations (Data Safety, ...)
Manage an app's regulatory declarations (Data Safety, ...)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay compliance [flags]
gplay compliance [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| ----------------------------------------------------------------------- | ---------------------------------------------------------------- |
| [`gplay compliance datasafety`](/docs/reference/compliance/datasafety/) | Push and validate the app's Data Safety declaration (write-only) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay compliance datasafety
> gplay compliance datasafety — Push and validate the app's Data Safety declaration (write-only)
Push and validate the app's Data Safety declaration (write-only)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay compliance datasafety [flags]
gplay compliance datasafety [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| [`gplay compliance datasafety set`](/docs/reference/compliance/datasafety/set/) | Push the app's Data Safety declaration from the canonical CSV |
| [`gplay compliance datasafety validate`](/docs/reference/compliance/datasafety/validate/) | Structurally check the Data Safety CSV offline (no network, no auth) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay compliance datasafety set
> gplay compliance datasafety set — Push the canonical Data Safety CSV (--file, default ./compliance/data-safety.csv) to Google as the app's Data Safety decl…
Push the canonical Data Safety CSV (--file, default ./compliance/data-safety.csv) to Google as the app's Data Safety declaration. The declaration is write-only and replaces the whole document — a direct POST outside the Edits model (ADR-0014). gplay cannot read it back; only this POST validates the contents.
set runs `validate` implicitly first, so a structurally invalid CSV never reaches the network. Use --dry-run to rehearse the write — it validates the CSV, resolves the target package and Account, and reports "would POST N bytes / N rows to \" without any network call (and without needing --confirm).
The real write requires --confirm (a stale or wrong declaration can block releases or misstate your data practices); without it set refuses, exits 2, and points here. CI=true does NOT auto-confirm. --output json passes the API response through verbatim (or, when the API returns an empty body, a gplay-shaped success object).
## Usage
[Section titled “Usage”](#usage)
```txt
gplay compliance datasafety set [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `--confirm` | authorize the real write (replaces the live Data Safety declaration) |
| `--dry-run` | rehearse the write (validate + resolve target + report size) without any HTTP call |
| `--file string` | path to the canonical Data Safety CSV to push (default "./compliance/data-safety.csv") |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay compliance datasafety validate
> gplay compliance datasafety validate — Structurally check the canonical Data Safety CSV (--file, default ./compliance/data-safety.csv) without contacting G…
Structurally check the canonical Data Safety CSV (--file, default ./compliance/data-safety.csv) without contacting Google Play: it verifies the file is non-empty, valid UTF-8 (a leading byte-order mark is stripped), parses as a well-formed, rectangular CSV, and has a header row. A header that diverges from gplay's bundled reference template is reported as a NON-FATAL hint (Google's template evolves), never a failure.
This command is OFFLINE — it needs no credentials and makes no network call, so it is safe in a pre-commit hook or a CI gate. It is also STRUCTURAL ONLY: gplay cannot read your live Data Safety declaration back (the API is write-only), so a CSV that passes here can still be rejected by Google. "Valid" means "well-formed", NOT "Google will accept it" — only the live `gplay compliance datasafety set` POST arbitrates that.
Any structural failure exits 20.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay compliance datasafety validate [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------- | ------------------------------------------------------------------------------------------ |
| `--file string` | path to the canonical Data Safety CSV to validate (default "./compliance/data-safety.csv") |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay init
> gplay init — Write .gplay/config.json with the given Android package name so every subsequent gplay command in this directory tree picks it up automaticall…
Write .gplay/config.json with the given Android package name so every subsequent gplay command in this directory tree picks it up automatically via walk-up. Also creates .gplay/.gitignore so per-developer overrides (config.local.json) and transient edit-ID files stay out of git.
Run from the repo root.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay init [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | -------------------------------------------------------- |
| `--package string` | Android package name (e.g. com.example.myapp). Required. |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay metadata
> gplay metadata — Manage Store front Listings (per-locale title/description/video)
Manage Store front Listings (per-locale title/description/video)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay metadata [flags]
gplay metadata [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [`gplay metadata apply`](/docs/reference/metadata/apply/) | Reconcile the local metadata tree with the Listings live on Play |
| [`gplay metadata images`](/docs/reference/metadata/images/) | Manage Store images (per-locale icon, feature graphic, screenshots) |
| [`gplay metadata list`](/docs/reference/metadata/list/) | Summarize the Store front Listings live on Play, per locale |
| [`gplay metadata pull`](/docs/reference/metadata/pull/) | Rapatriate the Store front Listings live on Play into the local Metadata tree |
| [`gplay metadata validate`](/docs/reference/metadata/validate/) | Lint the on-disk metadata tree offline (no network, no auth) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay metadata apply
> gplay metadata apply — Reconcile the local Metadata tree (--dir, default ./metadata) with the Store front Listings live on Google Play for --package.
Reconcile the local Metadata tree (--dir, default ./metadata) with the Store front Listings live on Google Play for --package.
By default the sync is ADDITIVE: it upserts only the locales and fields present on disk; a locale live on Play but absent locally is left intact and reported. Use --prune to also delete online-only locales (it refuses to remove the app's defaultLanguage). Note: a locale is "present on disk" only if its directory holds at least one recognized field file (title.txt, …) — a directory with only a README or unrecognized files is NOT seen as managed and, under --prune, would be deleted online. Preview with --dry-run first.
\--dry-run reads the live Listings and prints the per-locale delta without committing (it is ONLINE — it diffs disk against Play). --output json is the gplay diff schema {package, changes\[], summary}, so a CI gate is one jq line: jq -e '.summary.create + .summary.update > 0'.
A real apply requires --confirm (every committed Listing is live on the store immediately); without it apply refuses and points here. CI=true does NOT auto-confirm. The publish is atomic: all locales patch inside one Edit committed once, and any per-locale failure discards the Edit (0 published).
## Usage
[Section titled “Usage”](#usage)
```txt
gplay metadata apply [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ---------------------------- | ---------------------------------------------------------------------------------------- |
| `--allow-locale stringArray` | whitelist a locale code outside the embedded registry (repeatable) |
| `--confirm` | authorize the real publish (Listings go live immediately) |
| `--dir string` | metadata tree root directory (default "./metadata") |
| `--dry-run` | read live Play and print the delta without committing (online) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--prune` | also delete locales live on Play but absent on disk (refuses defaultLanguage) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay metadata images
> gplay metadata images — Manage Store images (per-locale icon, feature graphic, screenshots)
Manage Store images (per-locale icon, feature graphic, screenshots)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay metadata images [flags]
gplay metadata images [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| [`gplay metadata images apply`](/docs/reference/metadata/images/apply/) | Reconcile the local Store-image tree with the images live on Play |
| [`gplay metadata images list`](/docs/reference/metadata/images/list/) | Summarize the Store images live on Play, per locale and slot |
| [`gplay metadata images pull`](/docs/reference/metadata/images/pull/) | Rapatriate the Store images live on Play into the local Metadata tree |
| [`gplay metadata images validate`](/docs/reference/metadata/images/validate/) | Lint the on-disk Store-image tree offline (no network, no auth) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay metadata images apply
> gplay metadata images apply — Reconcile the local Store-image tree (--dir, default ./metadata) with the images live on Google Play for --package.
Reconcile the local Store-image tree (--dir, default ./metadata) with the images live on Google Play for --package.
The sync is ADDITIVE: it uploads images present on disk and reorders a gallery whose order changed, but an image live on Play yet absent from a managed slot is left intact (removing online-only images is `--prune`, a separate destructive mode). A slot absent or empty on disk is unmanaged and never touched.
\--dry-run reads the live images and prints the per-slot delta without committing (it is ONLINE — it diffs disk against Play). --output json is the diff schema {package, slots\[], summary}, so a CI gate is one jq line: jq -e '.summary.upload + .summary.delete + .summary.reorder > 0'.
A real apply requires --confirm (every committed image is live on the store immediately; there is no draft). Without it apply refuses and points here. CI=true does NOT auto-confirm. The publish is atomic: all slots reconcile inside one Edit committed once, and any per-slot failure discards the Edit (0 published).
`metadata images validate` runs as a fail-fast pre-check; --no-validate bypasses it (Play's commit stays the ultimate authority). --locale and --type restrict the reconciliation to a subset of slots.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay metadata images apply [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ---------------------- | ---------------------------------------------------------------------------------------- |
| `--confirm` | authorize the real publish (images go live immediately) |
| `--dir string` | metadata tree root directory (default "./metadata") |
| `--dry-run` | read live Play and print the delta without committing (online) |
| `--locale stringArray` | restrict to these locale codes (repeatable) |
| `--no-validate` | skip the offline image validation pre-check (Play's commit stays the authority) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--prune` | also delete a managed slot's online-only images (destructive; requires --confirm) |
| `--type stringArray` | restrict to these image types (repeatable) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay metadata images list
> gplay metadata images list — Summarize the Store images currently live on Google Play for --package: one row per non-empty slot (a locale × image-type pair…
Summarize the Store images currently live on Google Play for --package: one row per non-empty slot (a locale × image-type pair), showing the image count and each image's content sha256 in display order.
Reads inside a read-only Edit (open → listings.list to enumerate the app's locales → images.list for each of the 9 image types per locale → discard); nothing is committed and nothing on disk is read. The API exposes no list-by-locale endpoint, so list walks the 9 types across every locale that has a Listing.
(--output json carries each slot's images verbatim — the edits.images.list objects, id/url/sha1/sha256; --output markdown renders a Markdown table.)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay metadata images list [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay metadata images pull
> gplay metadata images pull — Rapatriate the Store images currently live on Google Play for --package into the local Metadata tree under --dir (default ./me…
Rapatriate the Store images currently live on Google Play for --package into the local Metadata tree under --dir (default ./metadata): singular slots as `/images/.` and gallery slots as `/images//1.…N.` in display order.
Reads inside a read-only Edit (open → list slots → download bytes → discard); nothing is committed. The write is additive — a slot with no images online writes nothing, so pull never emits an empty slot and a `metadata images apply` immediately after a pull is a no-op (ADR-0013). Filenames are synthesized (the API carries none) and the extension is sniffed from the image bytes (PNG/JPEG), not the response header.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay metadata images pull [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `--dir string` | directory to write the Metadata tree into (default "./metadata") |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay metadata images validate
> gplay metadata images validate — Lint the Store images under --dir without contacting Google Play: checks exact dimensions (icon 512×512, feature graphic 1…
Lint the Store images under --dir without contacting Google Play: checks exact dimensions (icon 512×512, feature graphic 1024×500, TV banner 1280×720), screenshot side range (320–3840 px) and 2:1 aspect ratio, format (PNG/JPEG only, read from the bytes), per-image byte size, and per-slot count (≤8).
This command is OFFLINE — no credentials, no network — so it is safe in a pre-commit hook or a CI gate. Diffing the tree against what is live on Play is the job of `gplay metadata images apply --dry-run`.
The rules are a versioned in-code table (Play's commit is the ultimate authority); `images apply --no-validate` bypasses this check. Any violation exits 20.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay metadata images validate [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `--dir string` | path to the metadata tree to lint (default "./metadata") |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay metadata list
> gplay metadata list — Summarize the Store front Listings currently live on Google Play for --package: one row per locale, showing which managed fields the…
Summarize the Store front Listings currently live on Google Play for --package: one row per locale, showing which managed fields the locale has and the character length of each (TITLE, SHORT\_DESC, FULL\_DESC, VIDEO). A field Play holds empty leaves a blank cell.
Reads the Listings inside a read-only Edit (open → listings.list → discard); nothing is committed and nothing on disk is read. Comparing the live Listings against an on-disk metadata tree is the job of `gplay metadata apply --dry-run`.
(--output json is the raw edits.listings.list payload; --output markdown renders a Markdown table.)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay metadata list [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay metadata pull
> gplay metadata pull — Rapatriate the Store front Listings currently live on Google Play for --package into the local Metadata tree under --dir (default ./m…
Rapatriate the Store front Listings currently live on Google Play for --package into the local Metadata tree under --dir (default ./metadata): one `/.txt` file per managed field a locale holds non-empty online (title, short\_description, full\_description, video).
Reads the Listings inside a read-only Edit (open → listings.list → discard); nothing is committed. The write is additive — a field Play holds empty writes no file, and a local locale absent online is left intact. Removing locales/fields no longer online is the opt-in job of `gplay metadata apply --prune`.
Because pull writes only non-empty online values and the tree codec is a value-level inverse, a `metadata apply` immediately after a pull is a guaranteed no-op (ADR-0011).
## Usage
[Section titled “Usage”](#usage)
```txt
gplay metadata pull [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `--dir string` | directory to write the Metadata tree into (default "./metadata") |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay metadata validate
> gplay metadata validate — Lint the Metadata tree under --dir without contacting Google Play: checks character limits (title 30, short description 80, full…
Lint the Metadata tree under --dir without contacting Google Play: checks character limits (title 30, short description 80, full description 4000), required non-empty fields (title and full description — an empty file is a validation error, not a clear), and that every locale directory names a known Play store locale.
This command is OFFLINE — it needs no credentials and makes no network call, so it is safe in a pre-commit hook or a CI gate. Diffing the tree against what is live on Play is the job of `gplay metadata apply --dry-run`.
A locale Google added after this gplay release can be whitelisted with --allow-locale xx-YY (repeatable). Any violation exits 20.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay metadata validate [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ---------------------------- | ---------------------------------------------------------------------------------------- |
| `--allow-locale stringArray` | whitelist a locale code not in gplay's embedded Play-locale list (repeatable) |
| `--dir string` | path to the metadata tree to lint (default "./metadata") |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay releases
> gplay releases — Manage app releases (upload, promote, rollout)
Manage app releases (upload, promote, rollout)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay releases [flags]
gplay releases [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| --------------------------------------------------------------- | -------------------------------------------------------------------- |
| [`gplay releases complete`](/docs/reference/releases/complete/) | Complete the rollout on the latest release of a track (ramp to 100%) |
| [`gplay releases halt`](/docs/reference/releases/halt/) | Halt the staged rollout on the latest release of a track |
| [`gplay releases list`](/docs/reference/releases/list/) | List every release currently on a track |
| [`gplay releases promote`](/docs/reference/releases/promote/) | Promote a release from one track to another (no AAB re-upload) |
| [`gplay releases resume`](/docs/reference/releases/resume/) | Resume a halted rollout on the latest release of a track |
| [`gplay releases rollout`](/docs/reference/releases/rollout/) | Set the staged-rollout fraction on the latest release of a track |
| [`gplay releases upload`](/docs/reference/releases/upload/) | Upload an AAB to a track on Google Play |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay releases complete
> gplay releases complete — Ramp the latest release on --track to userFraction=1.0 and status=completed, ending the staged rollout.
Ramp the latest release on --track to userFraction=1.0 and status=completed, ending the staged rollout.
Targets the latest release on the track; when two releases coexist pass --version-code N or --release-name \ to pick one.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay releases complete [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| `--confirm` | required to roll out / resume / complete a release on production (reaches real users) |
| `--dry-run` | validate inputs and preview the transition without any HTTP call |
| `--keep-edit-on-failure` | skip the auto-discard cleanup on failure (debug) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--release-name string` | pick the release with this name (disambiguator) |
| `--track string` | target track (internal, alpha, beta, production, or any closed-track name) |
| `--version-code int` | pick the release with this versionCode (disambiguator when the track holds more than one) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay releases halt
> gplay releases halt — Set the latest release on --track to status=halted, preserving its current userFraction so a later `gplay releases resume` picks up w…
Set the latest release on --track to status=halted, preserving its current userFraction so a later `gplay releases resume` picks up where it left off.
Targets the latest release on the track; when two releases coexist pass --version-code N or --release-name \ to pick one.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay releases halt [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| `--confirm` | required to roll out / resume / complete a release on production (reaches real users) |
| `--dry-run` | validate inputs and preview the transition without any HTTP call |
| `--keep-edit-on-failure` | skip the auto-discard cleanup on failure (debug) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--release-name string` | pick the release with this name (disambiguator) |
| `--track string` | target track (internal, alpha, beta, production, or any closed-track name) |
| `--version-code int` | pick the release with this versionCode (disambiguator when the track holds more than one) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay releases list
> gplay releases list — List every release currently attached to --track, including any draft, inProgress, halted, or completed entries that coexist on it.
List every release currently attached to --track, including any draft, inProgress, halted, or completed entries that coexist on it.
Reads the track inside a read-only Edit (open → tracks.get → discard); nothing is committed. Cross-track listing is the job of `gplay tracks list`.
Default table columns: name, status, userFraction, versionCodes, notes. Override with --columns name,status,... (--output json is the raw tracks.get payload; --output markdown renders a Markdown table.)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay releases list [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | -------------------------------------------------------------------------------------------- |
| `--columns string` | comma-separated table columns to show (default: name,status,userFraction,versionCodes,notes) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--track string` | track to list releases from (internal, alpha, beta, production, or any closed-track name) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay releases promote
> gplay releases promote — Copy the latest release on --from to --to, keeping the same versionCode.
Copy the latest release on --from to --to, keeping the same versionCode.
Targeting production defaults to a draft release (ADR-0002) unless --complete or --staged is supplied. Release notes carry over from the source unless --release-notes / --release-notes-dir is passed.
When the source track has multiple coexisting releases (e.g. inProgress + halted), pass --version-code N or --release-name \ to pick one.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay releases promote [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ---------------------------- | ---------------------------------------------------------------------------------------- |
| `--complete` | force the release status to completed (1.0 user fraction) |
| `--confirm` | explicit confirmation required when promoting to production with --complete / --staged |
| `--draft` | force the release status to draft on the destination |
| `--dry-run` | validate inputs and preview the release payload without any HTTP call |
| `--from string` | source track to promote from |
| `--keep-edit-on-failure` | skip the auto-discard cleanup on failure (debug) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--release-name string` | pick the source release with this name (disambiguator) |
| `--release-notes string` | override carry-over with this text (applied to the app's default language) |
| `--release-notes-dir string` | override carry-over with per-locale files (\.txt, optional default.txt) |
| `--staged float` | start a staged rollout at this fraction (0 < f ≤ 1.0) |
| `--to string` | destination track to promote to |
| `--version-code int` | pick the source release with this versionCode (disambiguator) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay releases resume
> gplay releases resume — Set the latest release on --track back to status=inProgress, continuing the rollout at the userFraction it was halted at.
Set the latest release on --track back to status=inProgress, continuing the rollout at the userFraction it was halted at.
Targets the latest release on the track; when two releases coexist pass --version-code N or --release-name \ to pick one.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay releases resume [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| `--confirm` | required to roll out / resume / complete a release on production (reaches real users) |
| `--dry-run` | validate inputs and preview the transition without any HTTP call |
| `--keep-edit-on-failure` | skip the auto-discard cleanup on failure (debug) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--release-name string` | pick the release with this name (disambiguator) |
| `--track string` | target track (internal, alpha, beta, production, or any closed-track name) |
| `--version-code int` | pick the release with this versionCode (disambiguator when the track holds more than one) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay releases rollout
> gplay releases rollout — Set the staged-rollout fraction (--to) on the latest release of --track.
Set the staged-rollout fraction (--to) on the latest release of --track. Status becomes inProgress if it wasn't already.
Targets the latest release on the track; when two releases coexist (e.g. inProgress + halted) pass --version-code N or --release-name \ to pick one, otherwise the command refuses rather than guess.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay releases rollout [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| `--confirm` | required to roll out / resume / complete a release on production (reaches real users) |
| `--dry-run` | validate inputs and preview the transition without any HTTP call |
| `--keep-edit-on-failure` | skip the auto-discard cleanup on failure (debug) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--release-name string` | pick the release with this name (disambiguator) |
| `--to string` | target rollout fraction (0 < f ≤ 1.0), e.g. 0.05 |
| `--track string` | target track (internal, alpha, beta, production, or any closed-track name) |
| `--version-code int` | pick the release with this versionCode (disambiguator when the track holds more than one) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay releases upload
> gplay releases upload — Upload an AAB and attach it to a release on the given track.
Upload an AAB and attach it to a release on the given track.
Performs the full Edit lifecycle in one call: edits.insert → bundles.upload → tracks.update → edits.commit
Targeting production defaults to a draft release (ADR-0002) unless --complete or --staged is supplied. Any string is accepted as --track so closed-test tracks with custom names just work.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay releases upload [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ---------------------------- | --------------------------------------------------------------------------------------------- |
| `--complete` | force the release status to completed (1.0 user fraction) |
| `--confirm` | explicit confirmation required for production publishes (--complete / --staged on production) |
| `--draft` | force the release status to draft |
| `--dry-run` | validate inputs and preview the release payload without any HTTP call |
| `--keep-edit-on-failure` | skip the auto-discard cleanup on failure (debug) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--release-notes string` | release notes text (applied to the app's default language) |
| `--release-notes-dir string` | directory of \.txt files (with optional default.txt fallback) |
| `--staged float` | start a staged rollout at this fraction (0 < f ≤ 1.0) |
| `--track string` | target track (internal, alpha, beta, production, or any closed-track name) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay reviews
> gplay reviews — Read and reply to user reviews
Read and reply to user reviews
## Usage
[Section titled “Usage”](#usage)
```txt
gplay reviews [flags]
gplay reviews [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| ------------------------------------------------------- | ------------------------------------------------------ |
| [`gplay reviews list`](/docs/reference/reviews/list/) | List recent user reviews for a package (last 7 days) |
| [`gplay reviews reply`](/docs/reference/reviews/reply/) | Post a developer reply to a review (single or --batch) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay reviews list
> gplay reviews list — List the user reviews the Google Play API exposes for --package.
List the user reviews the Google Play API exposes for --package.
The API only returns reviews from the LAST 7 DAYS; a WARN line to that effect is always printed to stderr (long-history retrieval via GCS CSV reports is on the backlog). Results are auto-paginated until exhausted.
\--stars filters client-side and accepts a single rating (1), an inclusive range (1-2), or a set (1,3,5); each rating must be 1..5. --limit N caps the final count after filtering (0 = no cap).
Default table columns: date, stars, locale, reviewId, summary. Override with --columns stars,reviewId,... (--output json is the {"reviews":\[...]} pass-through reflecting the filtered set; --output markdown renders a Markdown table.)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay reviews list [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `--columns string` | comma-separated table columns to show (default: date,stars,locale,reviewId,summary) |
| `--limit int` | cap the result count after filtering (0 = no cap) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--stars string` | client-side star filter: 1, 1-2, or 1,3,5 (each rating 1..5) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay reviews reply
> gplay reviews reply — Post developer responses to user reviews.
Post developer responses to user reviews.
Single: gplay reviews reply --package P --review-id ID --reply "Thanks!" Batch: gplay reviews reply --package P --batch replies.tsv gplay reviews reply --package P --batch - (read TSV from stdin)
The batch stream is TSV: one \\t\ line per reply. Blank lines and lines starting with # are skipped; a reply containing tabs or newlines must be double-quoted (RFC 4180 quoting). Each line is posted sequentially; a per-line failure is reported on stderr and does not abort the rest. The process exits non-zero with the highest exit code seen across rows.
\--review-id/--reply and --batch are mutually exclusive. --dry-run parses the input and prints the planned actions without calling the API. --output json echoes the API response (single) or a {"results":\[...]} envelope (batch).
## Usage
[Section titled “Usage”](#usage)
```txt
gplay reviews reply [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `--batch string` | post replies from a TSV file (\\t\), or - for stdin |
| `--dry-run` | parse and print the planned replies without calling the API |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--reply string` | the developer reply text (single mode) |
| `--review-id string` | the id of the review to reply to (single mode) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay schema
> gplay schema — Query an embedded, offline projection of the Android Publisher API (the Schema index) — does a method exist, what does it send and return, w…
Query an embedded, offline projection of the Android Publisher API (the Schema index) — does a method exist, what does it send and return, what fields and enums does a type carry.
This command is OFFLINE: it makes no API call and needs no credentials. It queries an index compiled into the binary, derived from the committed Discovery snapshot.
The query matches, case-insensitively, across three projections:
* the native RPC method id (e.g. `edits.tracks.update`)
* a REST path fragment (e.g. `tracks/{track}`)
* a schema / type name (e.g. `Track`)
A matched method shows its HTTP method, REST path, parameters, and a one-hop inline expansion of its request/response schema (fields, types, enums, Google's verbatim descriptions); nested types are shown by name. A matched schema is rendered directly.
\--list print the compact catalog (id · http · path) of all methods --method GET|POST|PATCH|PUT|DELETE filter the method surface by HTTP verb (combinable)
A query that matches nothing prints a note on stderr and exits 0.
\[experimental] — the surface, especially the --output json shape, may still change.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay schema [query] [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `--list` | print the compact catalog of all methods |
| `--method string` | filter methods by HTTP verb (GET, POST, PATCH, PUT, DELETE) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay team
> gplay team — Manage the Developer account's members and permissions (users, grants)
Manage the Developer account's members and permissions (users, grants)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay team [flags]
gplay team [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| --------------------------------------------------- | ----------------------------------------------- |
| [`gplay team grants`](/docs/reference/team/grants/) | List and manage members' per-app access |
| [`gplay team users`](/docs/reference/team/users/) | List and manage the Developer account's members |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay team grants
> gplay team grants — List and manage members' per-app access
List and manage members' per-app access
## Usage
[Section titled “Usage”](#usage)
```txt
gplay team grants [flags]
gplay team grants [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| ----------------------------------------------------------------- | ------------------------------------------------------------ |
| [`gplay team grants list`](/docs/reference/team/grants/list/) | List per-app access grants across the Developer account |
| [`gplay team grants remove`](/docs/reference/team/grants/remove/) | Remove a member's access to one app (keeps their membership) |
| [`gplay team grants set`](/docs/reference/team/grants/set/) | Grant or adjust a member's per-app access (upsert) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay team grants list
> gplay team grants list — List the per-app Grants of the Developer account's members: which member has which app-level permissions on which package.
List the per-app Grants of the Developer account's members: which member has which app-level permissions on which package.
The Play Developer API has no grants-list endpoint (a Grant is a field of the User), so this reads the full member list (paginated to completion) and projects the grants. Filter with --package \ ("who can touch this app") or --user \ ("what apps this person touches"); neither lists all grants.
Default table columns: user, package, permissions. --output json is a faithful {"grants":\[…]} projection.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay team grants list [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| `--columns string` | comma-separated table columns to show (default: user,package,permissions) |
| `--developer-id string` | Play Console Developer account id (overrides the active Account's, env, and project-local) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | filter to one app's grantees (who can touch this app) |
| `--user string` | filter to one member's grants (what apps this person touches) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay team grants remove
> gplay team grants remove — Remove 's access to one app (--package) via grants.delete, WITHOUT removing them from the Developer account — only the pe…
Remove \'s access to one app (--package) via grants.delete, WITHOUT removing them from the Developer account — only the per-app grant is deleted. To off-board a member entirely, use `gplay team users remove` instead.
This is destructive, so it refuses without --confirm (exit 3, naming the flag); CI=true never auto-confirms. Use --dry-run to preview the target with no HTTP call.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay team grants remove --package [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| `--confirm` | authorize the removal (required — this is destructive) |
| `--developer-id string` | Play Console Developer account id (overrides the active Account's, env, and project-local) |
| `--dry-run` | preview the target without any HTTP call |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | the app (package name) whose access to remove |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay team grants set
> gplay team grants set — Grant or adjust 's access to one app (--package) via an upsert: gplay reads the member's current grants, then creates the gr…
Grant or adjust \'s access to one app (--package) via an upsert: gplay reads the member's current grants, then creates the grant if absent or updates it if present. Express permissions in friendly form — --role \ XOR --permissions \ — resolved in app scope. Run `gplay team permissions --scope app` to list them.
Use --dry-run to rehearse: it reads the current state (an idempotent read, no write) and, with --output json, reports the resolved verb (create/update), the permission diff (current → desired, with add/remove), and the `requires` array. An admin-conferring grant requires the named --grant-admin (exit 3).
## Usage
[Section titled “Usage”](#usage)
```txt
gplay team grants set --package [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| `--developer-id string` | Play Console Developer account id (overrides the active Account's, env, and project-local) |
| `--dry-run` | rehearse: read current state and report the verb + diff without writing |
| `--grant-admin` | acknowledge conferring admin (required when the permission set includes admin) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | the app (package name) to grant access to |
| `--permissions strings` | permission aliases or raw CAN\_\* enums (repeatable or comma-separated) |
| `--role string` | role bundle to grant (viewer, reviewer, tester-manager, release-manager, admin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay team users
> gplay team users — List and manage the Developer account's members
List and manage the Developer account's members
## Usage
[Section titled “Usage”](#usage)
```txt
gplay team users [flags]
gplay team users [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| --------------------------------------------------------------- | ----------------------------------------------------------------------- |
| [`gplay team users add`](/docs/reference/team/users/add/) | Invite a member with account-wide permissions |
| [`gplay team users list`](/docs/reference/team/users/list/) | List the Developer account's members and their account-wide permissions |
| [`gplay team users remove`](/docs/reference/team/users/remove/) | Off-board a member from the Developer account |
| [`gplay team users set`](/docs/reference/team/users/set/) | Replace a member's account-wide permissions (declarative) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay team users add
> gplay team users add — Invite as a member of the Developer account with account-wide permissions, via users.create.
Invite \ as a member of the Developer account with account-wide permissions, via users.create. Express permissions in friendly form — --role \ XOR --permissions \ — resolved in account scope (the \_GLOBAL family). Run `gplay team permissions` to list aliases and bundles.
add is the routine tier: no confirmation gate, and CI-scriptable. But an admin-conferring add (--role admin, or a permission set including the all-permissions enum) requires the named --grant-admin — handing out full control is never silent.
Use --dry-run to preview the resolved payload with no HTTP; with --output json it emits a `requires` array naming the safety flags the live write needs, so an agent can discover the gate before running it.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay team users add [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| `--developer-id string` | Play Console Developer account id (overrides the active Account's, env, and project-local) |
| `--dry-run` | preview the resolved payload without any HTTP call |
| `--grant-admin` | acknowledge conferring admin (required when the permission set includes admin) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--permissions strings` | permission aliases or raw CAN\_\* enums (repeatable or comma-separated) |
| `--role string` | role bundle to grant (viewer, reviewer, tester-manager, release-manager, admin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay team users list
> gplay team users list — List every member of the Developer account with their account-wide permissions, reading users.list paginated to completion (no sile…
List every member of the Developer account with their account-wide permissions, reading users.list paginated to completion (no silent truncation). Account-wide admins (the all-permissions permission) are marked in the ADMIN column.
The Developer account is resolved through the gplay cascade (later wins): the active Account's developer-id, the project-local .gplay/config.local.json, GPLAY\_DEVELOPER\_ID, then --developer-id. Set one with `gplay auth login --developer-id`; an unresolved id fails with exit 10.
Default table columns: email, access, admin, permissions, grants. Override with --columns email,admin,... --output json is the verbatim users.list response (a single {"users":\[…]} object across all pages).
## Usage
[Section titled “Usage”](#usage)
```txt
gplay team users list [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| `--columns string` | comma-separated table columns to show (default: email,access,admin,permissions,grants) |
| `--developer-id string` | Play Console Developer account id (overrides the active Account's, env, and project-local) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay team users remove
> gplay team users remove — Remove from the Developer account via users.delete, revoking all their access at once.
Remove \ from the Developer account via users.delete, revoking all their access at once.
This is destructive and irreversible, so it refuses without --confirm (exit 3, naming the flag); CI=true never auto-confirms. Use --dry-run to preview the target with no HTTP call. To remove a member's access to a single app without off-boarding them, use `gplay team grants remove` instead.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay team users remove [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| `--confirm` | authorize the off-boarding (required — this is destructive) |
| `--developer-id string` | Play Console Developer account id (overrides the active Account's, env, and project-local) |
| `--dry-run` | preview the target without any HTTP call |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay team users set
> gplay team users set — Replace 's account-wide permissions declaratively via users.patch — the whole set is sent, not merged.
Replace \'s account-wide permissions declaratively via users.patch — the whole set is sent, not merged. Express the set in friendly form: --role \ XOR --permissions \ (account scope). Run `gplay team permissions` to list aliases and bundles.
A bare `set` (no --role, --permissions, or --clear) is refused (exit 2) so a forgotten flag can never silently blank the permissions; empty them on purpose with --clear. A permission-reducing set is a normal previewable statement (not gated); conferring admin still requires the named --grant-admin (exit 3).
Use --dry-run to preview the resolved payload with no HTTP; with --output json it emits a `requires` array naming any safety flag the live write needs.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay team users set [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| `--clear` | replace the permission set with an empty set |
| `--developer-id string` | Play Console Developer account id (overrides the active Account's, env, and project-local) |
| `--dry-run` | preview the resolved payload without any HTTP call |
| `--grant-admin` | acknowledge conferring admin (required when the permission set includes admin) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--permissions strings` | permission aliases or raw CAN\_\* enums (repeatable or comma-separated) |
| `--role string` | role bundle to set (viewer, reviewer, tester-manager, release-manager, admin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay testers
> gplay testers — Manage the Google Groups authorized to test a track
Manage the Google Groups authorized to test a track
## Usage
[Section titled “Usage”](#usage)
```txt
gplay testers [flags]
gplay testers [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| ----------------------------------------------------- | ---------------------------------------------------- |
| [`gplay testers list`](/docs/reference/testers/list/) | List the Google Groups authorized to test a track |
| [`gplay testers set`](/docs/reference/testers/set/) | Replace the Google Groups authorized to test a track |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay testers list
> gplay testers list — List the authorized audience of --track: the Google Groups that may test it.
List the authorized audience of --track: the Google Groups that may test it. The testers resource exposes only Google Groups — individual tester emails are not supported by the API — so this lists groups exclusively.
Reads the audience inside a read-only Edit (open → testers.get → discard); nothing is committed. Replacing the audience is the job of `gplay testers set`.
\--output json is the raw testers.get payload (ADR-0003); --output markdown renders a Markdown table.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay testers list [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--track string` | track whose testers to list (any closed-track name) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay testers set
> gplay testers set — Replace the authorized audience of --track with the Google Groups passed to --group.
Replace the authorized audience of --track with the Google Groups passed to --group. testers set is declarative: it replaces the WHOLE list (it maps 1:1 to testers.update — there is no add/remove). The testers resource exposes only Google Groups; individual tester emails are not supported by the API.
A bare `set` with neither --group nor --clear is refused (exit 2) so a forgotten --group can never silently wipe the list; empty the audience on purpose with --clear. There is no --confirm: a closed test track is low-stakes and reversible.
Writes inside an implicit Edit (open → testers.update → commit). Use --dry-run to validate and preview the payload without any HTTP call, and --keep-edit-on-failure to skip the auto-discard cleanup for debugging.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay testers set [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------------ | ---------------------------------------------------------------------------------------- |
| `--clear` | replace the tester list with an empty set (close the closed test) |
| `--dry-run` | validate inputs and preview the tester list without any HTTP call |
| `--group strings` | Google Group email(s) authorized to test (repeatable or comma-separated) |
| `--keep-edit-on-failure` | skip the auto-discard cleanup on failure (debug) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--track string` | track whose testers to replace (any closed-track name) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay tracks
> gplay tracks — Inspect and create release tracks (standard and custom closed)
Inspect and create release tracks (standard and custom closed)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay tracks [flags]
gplay tracks [command]
```
## Subcommands
[Section titled “Subcommands”](#subcommands)
| Command | Description |
| ------------------------------------------------------- | -------------------------------------------------------------------- |
| [`gplay tracks create`](/docs/reference/tracks/create/) | Create a custom closed-testing track |
| [`gplay tracks list`](/docs/reference/tracks/list/) | List every track configured for a package |
| [`gplay tracks view`](/docs/reference/tracks/view/) | Show the full state of a single track (is anything wrong right now?) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay tracks create
> gplay tracks create — Create a custom closed-testing track named .
Create a custom closed-testing track named \.
The create endpoint supports exactly one type (CLOSED\_TESTING) and the DEFAULT (phone) form factor, so there is no --type / --form-factor flag — every created track is closed. Open / internal track creation has no API path. Creating a track that already exists surfaces the API error (exit 30); gplay does not fake idempotency.
Runs inside an implicit Edit (open → tracks.create → commit). --dry-run previews the TrackConfig without any HTTP; --keep-edit-on-failure skips the auto-discard cleanup on failure (debug). No --confirm: a closed test track is low-stakes and reversible.
## Usage
[Section titled “Usage”](#usage)
```txt
gplay tracks create [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------------ | ---------------------------------------------------------------------------------------- |
| `--dry-run` | validate inputs and preview the TrackConfig without any HTTP call |
| `--keep-edit-on-failure` | skip the auto-discard cleanup on failure (debug) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay tracks list
> gplay tracks list — List every track configured for --package: the four standard tracks (internal, alpha, beta, production) plus any custom closed tracks t…
List every track configured for --package: the four standard tracks (internal, alpha, beta, production) plus any custom closed tracks the service account can see. Each row summarizes the track's top release (the highest version code on it); a standard track that has never been used still appears, with empty release columns.
Reads the tracks inside a read-only Edit (open → tracks.list → discard); nothing is committed. Single-track listing of every coexisting release is the job of `gplay releases list --track `.
Default table columns: track, kind, release, status, userFraction, versionCodes. Override with --columns track,status,... (--output json is the raw tracks.list payload; --output markdown renders a Markdown table.)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay tracks list [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------- |
| `--columns string` | comma-separated table columns to show (default: track,kind,release,status,userFraction,versionCodes) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay tracks view
> gplay tracks view — Show the full state of --track: every release coexisting on it (draft, inProgress, halted, completed) one row each, with the track's de…
Show the full state of --track: every release coexisting on it (draft, inProgress, halted, completed) one row each, with the track's derived kind (standard or custom). A halted rollout is rendered as !HALTED so it is impossible to miss at a glance.
Reads the track inside a read-only Edit (open → tracks.get → discard); nothing is committed. Cross-track listing is the job of `gplay tracks list`; mutating verbs (promote, rollout, halt, resume) live under `gplay releases`.
Default table columns: name, status, userFraction, versionCodes, notes. Override with --columns name,status,... (--output json is the raw tracks.get payload; --output markdown renders a Markdown table.)
## Usage
[Section titled “Usage”](#usage)
```txt
gplay tracks view [flags]
```
## Flags
[Section titled “Flags”](#flags)
| Flag | Description |
| ------------------ | -------------------------------------------------------------------------------------------- |
| `--columns string` | comma-separated table columns to show (default: name,status,userFraction,versionCodes,notes) |
| `--output string` | output format: table, json, or markdown (default: auto — table on TTY, json in pipes/CI) |
| `--package string` | Android package name (overrides .gplay/config.json pin) |
| `--track string` | track to inspect (internal, alpha, beta, production, or any closed-track name) |
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |
# gplay version
> gplay version — Print gplay version
Print gplay version
## Usage
[Section titled “Usage”](#usage)
```txt
gplay version [flags]
```
## Global flags
[Section titled “Global flags”](#global-flags)
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--account string` | name of a stored Account to use (overrides env and active Account) |
| `--retry int` | retry transient failures (transport errors, 5xx, 429) up to N times with exponential backoff (default: 0, no retry) |
| `--service-account string` | path to a service-account JSON, or inline JSON content (overrides --account, env, and active Account) |
| `--timeout duration` | per-request API timeout, e.g. 30s or 2m (default: 60s for control-plane calls, none for uploads) |
| `-v, --verbose` | log flow steps to stderr (info level) |