gplay metadata sync (listings + images)
Manage the store front: per-locale listing text and images, kept as an
on-disk tree and reconciled with Play. Shared conventions (auth, --package,
output, exit codes) are in gplay-cli-usage.
The sync model
gplay treats a local ./metadata tree (<locale>/<field>.txt, plus image
files) as the desired state and reconciles it with Play (ADR-0011). The loop:
pull → edit on disk → validate (offline) → apply --dry-run → apply --confirm
metadata pull rapatriates the live Listings into the tree
(<locale>/title.txt, short_description, full_description, video). It
reads inside a read-only Edit (nothing committed) and is additive, a
field empty online writes no file, a local locale absent online is left
intact. A metadata apply immediately after a pull is a guaranteed no-op.
metadata validate lints the tree offline (no auth, no network):
character limits (title 30, short 80, full 4000), required non-empty fields
(title + full description), and known Play locale codes. Any violation exits
20. Safe in a pre-commit hook or CI gate. (--allow-locale xx-YY
whitelists a locale Google added after this gplay release.)
metadata apply reconciles disk → Play. Additive by default: it
upserts the locales/fields on disk and leaves online-only locales intact
(reported). --prune also deletes online-only locales (it refuses to remove
the app's defaultLanguage). Note a locale counts as "present on disk" only
if its directory holds at least one recognized field file, a folder with
only a README is not managed, and under --prune would be deleted
online.
metadata list summarizes what is live on Play, per locale.
Apply safely
gplay metadata pull --dir ./metadata
# …edit the .txt files…
gplay metadata validate --dir ./metadata # offline lint, exit 20 on error
gplay metadata apply --dir ./metadata --dry-run # ONLINE diff, prints per-locale delta
gplay metadata apply --dir ./metadata --confirm # publishes, live immediately
metadata apply --dry-run reads live Play and prints the delta without
committing, --output json is the diff schema {package, changes[], summary}, so a CI gate is one 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 you at the flag. 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 (nothing published).
The tree stays inside the repo
Every file gplay reads or writes under --dir must resolve inside the
repo once symlinks are followed: a title.txt symlinked to a file outside
the tree, or a pre-placed link where pull will write, is refused rather than
followed. Locale names are checked in BCP 47 (en-US, not en_US) before an
Edit opens, with every offending file named at once. Monorepos that share
translations or assets through symlinks can set
GPLAY_ALLOW_EXTERNAL_SYMLINKS=1 (same shape as GPLAY_READONLY): symlink
egress is then followed, one NOTE per outbound path on stderr, while a ..
escape and any path derived from API data (a locale Play returns, a package
name) stay contained regardless.
Images
gplay metadata images list --package com.example.app
gplay metadata images list --type icon # one image type, all locales
gplay metadata images pull --dir ./metadata
gplay metadata images validate --dir ./metadata # offline lint, exit 20 on error
gplay metadata images apply --dir ./metadata --dry-run # ONLINE per-slot delta
gplay metadata images apply --dir ./metadata --confirm # publishes, live immediately
metadata images mirrors the same list/pull/validate/apply verbs for
per-locale image slots (ADR-0013), with the same discipline, validate
offline, --dry-run, then apply --confirm (atomic: all slots reconcile in
one Edit; any per-slot failure discards it, nothing published).
- Slots. The 9 image types are
icon, featureGraphic, tvBanner,
promoGraphic, phoneScreenshots, sevenInchScreenshots,
tenInchScreenshots, tvScreenshots, wearScreenshots. images list
walks all 9 across every locale that has a Listing; --type
([experimental]) narrows to one type (an unknown value is refused
client-side, exit 20).
validate limits (versioned in-code table; Play's commit stays the
authority): exact dimensions (icon 512×512, feature graphic 1024×500, TV
banner 1280×720); screenshot sides 320–3840 px with 2:1 aspect ratio;
PNG/JPEG only (read from the bytes); ≤8 images per slot.
apply --no-validate bypasses this pre-check.
- Additive, like the text side.
apply uploads on-disk images and
reorders a gallery whose order changed; an online-only image in a managed
slot is left intact unless --prune (destructive, also --confirm-gated).
A slot absent or empty on disk is unmanaged and never touched.
- Scoped applies.
--locale and --type (both repeatable) restrict the
reconciliation to a subset of slots.
- CI gate.
apply --dry-run --output json is the diff schema
{package, slots[], summary}, one line:
jq -e '.summary.upload + .summary.delete + .summary.reorder > 0'.
1---2name: gplay-metadata-sync3description: Sync a Google Play store listing, per-locale text and images, between an on-disk tree and Play with `gplay metadata`. Use when editing store listings or screenshots, migrating listing text into version control, localizing a listing, or gating a listing change in CI before it goes live.4---56# gplay metadata sync (listings + images)78Manage the store front: per-locale listing **text** and **images**, kept as an9on-disk tree and reconciled with Play. Shared conventions (auth, `--package`,10output, exit codes) are in `gplay-cli-usage`.1112## The sync model1314gplay treats a local `./metadata` tree (`<locale>/<field>.txt`, plus image15files) as the desired state and reconciles it with Play (ADR-0011). The loop:1617```18pull → edit on disk → validate (offline) → apply --dry-run → apply --confirm19```2021- **`metadata pull`** rapatriates the live Listings into the tree22 (`<locale>/title.txt`, `short_description`, `full_description`, `video`). It23 reads inside a **read-only Edit** (nothing committed) and is **additive**, a24 field empty online writes no file, a local locale absent online is left25 intact. A `metadata apply` immediately after a `pull` is a guaranteed no-op.26- **`metadata validate`** lints the tree **offline** (no auth, no network):27 character limits (title 30, short 80, full 4000), required non-empty fields28 (title + full description), and known Play locale codes. Any violation exits29 `20`. Safe in a pre-commit hook or CI gate. (`--allow-locale xx-YY`30 whitelists a locale Google added after this gplay release.)31- **`metadata apply`** reconciles disk → Play. **Additive by default**: it32 upserts the locales/fields on disk and leaves online-only locales intact33 (reported). `--prune` also deletes online-only locales (it refuses to remove34 the app's `defaultLanguage`). Note a locale counts as "present on disk" only35 if its directory holds at least one recognized field file, a folder with36 only a README is **not** managed, and under `--prune` would be deleted37 online.38- **`metadata list`** summarizes what is live on Play, per locale.3940## Apply safely4142```bash43gplay metadata pull --dir ./metadata44# …edit the .txt files…45gplay metadata validate --dir ./metadata # offline lint, exit 20 on error46gplay metadata apply --dir ./metadata --dry-run # ONLINE diff, prints per-locale delta47gplay metadata apply --dir ./metadata --confirm # publishes, live immediately48```4950`metadata apply --dry-run` reads live Play and prints the delta **without51committing**, `--output json` is the diff schema `{package, changes[],52summary}`, so a CI gate is one line: `jq -e '.summary.create + .summary.update53> 0'`. A real `apply` **requires `--confirm`** (every committed Listing is live54on the store immediately); without it apply refuses and points you at the55flag. `CI=true` does not auto-confirm. The publish is **atomic**: all56locales patch inside one Edit committed once, and any per-locale failure57discards the Edit (nothing published).5859### The tree stays inside the repo6061Every file gplay reads or writes under `--dir` must resolve **inside the62repo** once symlinks are followed: a `title.txt` symlinked to a file outside63the tree, or a pre-placed link where `pull` will write, is refused rather than64followed. Locale names are checked in BCP 47 (`en-US`, not `en_US`) before an65Edit opens, with every offending file named at once. Monorepos that share66translations or assets through symlinks can set67`GPLAY_ALLOW_EXTERNAL_SYMLINKS=1` (same shape as `GPLAY_READONLY`): symlink68egress is then followed, one `NOTE` per outbound path on stderr, while a `..`69escape and any path derived from API data (a locale Play returns, a package70name) stay contained regardless.7172## Images7374```bash75gplay metadata images list --package com.example.app76gplay metadata images list --type icon # one image type, all locales77gplay metadata images pull --dir ./metadata78gplay metadata images validate --dir ./metadata # offline lint, exit 20 on error79gplay metadata images apply --dir ./metadata --dry-run # ONLINE per-slot delta80gplay metadata images apply --dir ./metadata --confirm # publishes, live immediately81```8283`metadata images` mirrors the same `list/pull/validate/apply` verbs for84per-locale image **slots** (ADR-0013), with the same discipline, validate85offline, `--dry-run`, then `apply --confirm` (atomic: all slots reconcile in86one Edit; any per-slot failure discards it, nothing published).8788- **Slots.** The 9 image types are `icon`, `featureGraphic`, `tvBanner`,89 `promoGraphic`, `phoneScreenshots`, `sevenInchScreenshots`,90 `tenInchScreenshots`, `tvScreenshots`, `wearScreenshots`. `images list`91 walks all 9 across every locale that has a Listing; `--type`92 (`[experimental]`) narrows to one type (an unknown value is refused93 client-side, exit `20`).94- **`validate` limits** (versioned in-code table; Play's commit stays the95 authority): exact dimensions (icon 512×512, feature graphic 1024×500, TV96 banner 1280×720); screenshot sides 320–3840 px with 2:1 aspect ratio;97 PNG/JPEG only (read from the bytes); ≤8 images per slot.98 `apply --no-validate` bypasses this pre-check.99- **Additive, like the text side.** `apply` uploads on-disk images and100 reorders a gallery whose order changed; an online-only image in a managed101 slot is left intact unless `--prune` (destructive, also `--confirm`-gated).102 A slot absent or empty on disk is unmanaged and never touched.103- **Scoped applies.** `--locale` and `--type` (both repeatable) restrict the104 reconciliation to a subset of slots.105- **CI gate.** `apply --dry-run --output json` is the diff schema106 `{package, slots[], summary}`, one line:107 `jq -e '.summary.upload + .summary.delete + .summary.reorder > 0'`.