Skip to content

feat(play): replace Google Play store listing images through Hands - #810

Merged
stamp-build[bot] merged 2 commits into
mainfrom
agent/codex-android-devops/play-store-images
Oct 27, 2026
Merged

stamp-build[bot] merged 2 commits into
mainfrom
agent/codex-android-devops/play-store-images

Conversation

@stamp-build

@stamp-build stamp-build Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

林舟 (Codex-Android-DevOPS) from slock-android opened this pull request through Stamp
View in Stamp →


Why

Screenshots and graphics are the other half of the store listing. The designer's export for Raft (phone, 7-inch and 10-inch tablet, feature graphic; en / zh-Hans / ja) should go to Play in the same edit as the text, and be checked against Play's rules before anything is sent.

What

Transport: the same POST …/distributions/play/store-listing also accepts multipart/form-data. It carries a request JSON part and one file part per image. The JSON is the #809 body plus images:[{language, imageType, images:[{part, sha256, aiGeneratedState?}]}].

I chose multipart over staging in R2 and streaming. Store images are small (51 files, 9.4 MB) and don't need to be kept in Hands, so there is nothing to stage, seal or clean up. The cost is a cap of 24 MiB of images per push; split larger sets with --locales.

Worker (validates before any Play edit is opened)

  • imageType accepts Google's full 9-value enum. Implemented: phone, 7-inch, 10-inch, TV and Wear screenshots, icon, featureGraphic, tvBanner. appImageTypeUnspecified returns image_type_not_supported, never "invalid".
  • Per-image limits (each file is also capped at 15,728,640 bytes, Google's upload limit):
Image Format Size Count
Screenshots JPEG or 24-bit PNG; alpha/tRNS rejected each side 320–3840 px; long side at most 2× short side up to 8; phone at least 2
Wear screenshots JPEG or 24-bit PNG 1:1, at least 384 px up to 8
featureGraphic JPEG or 24-bit PNG 1024×500 1
tvBanner JPEG or 24-bit PNG 1280×720 1
icon 32-bit PNG 512×512, at most 1024 KB 1
  • Each declared sha256 must match its part, and parts must match declarations 1:1.
  • The receipt records, per image: sha256, size, content type, pixel size, aiGeneratedState and the Play image id.
  • If the adapter doesn't acknowledge the images, the worker fails closed with play_images_not_acknowledged and a receipt.

Play adapter

  • Adds edits.images.list, deleteall and upload (with aiGeneratedState).
  • Text is applied first, so a language created in the same push exists before its images. Images for a language with no listing return image_language_has_no_listing.
  • A set Play already holds with the same sha256 sequence is skipped. Otherwise the adapter deletes the set and uploads in order.
  • Google's Image.sha256 is recorded, not enforced: each image gets sha256 (ours), provider_sha256 and sha256_match: true|false|null; a mismatch is an image_warnings entry, not a failure. Committed sets are read back from a fresh edit (count must match). Our own declared-vs-uploaded sha256 check stays fail-closed.
  • The enum, sha256, sizes and counts are re-checked at the adapter boundary.

CLI: hands store-listing push <app> [--listing …] --images <dir> [--ai-generated-state …] [--commit]

  • Two folder layouts:
    • the designer export, <dir>/<device>/<lang>/NN.png, with devices phone, tablet-7, tablet-10, feature-graphic, icon, tv, wear and tv-banner;
    • <dir>/<lang>/<imageType>/NN.png.
  • Language folders use an explicit table: en→en-US, zh-Hans→zh-CN, zh-Hant→zh-TW, ja→ja-JP, ko→ko-KR. Play tags pass through unchanged.
  • A default/ folder applies to every target language that has no set of its own.
  • Files upload in filename order, compared numerically.

Sources

  • androidpublisher v3 discovery document, revision 20261027: imageType and aiGeneratedState enums, upload max size.
  • Play Console Help answer/9866151 (image requirements).

Unknowns

  • That Image.sha256 is the hash of the uploaded bytes isn't documented (fastlane supply relies on it). sha256_match in the receipt will show it on the first real upload.
  • Two rules are strict readings of Play's text: phone screenshots need at least 2 per language, and the icon must be a 32-bit PNG.
  • Screenshots are capped at 3840 px, although Play's large-screen guidance mentions up to 7680.
  • The 24 MiB cap keeps a push under the Workers RPC message limit (about 32 MiB, from memory).

Deploy

Follow #809's order (docs/google-play-operations.md); no migration beyond #809's. A new worker with an old adapter fails closed on any push with images.

Tests

  • play-adapter: vitest 107/107, tsc clean. Covers:
    • deleteall and ordered uploads in the same edit as the text;
    • the aiGeneratedState query;
    • skipping unchanged sets;
    • dry run leaving live images untouched;
    • failing closed on sha256 mismatch;
    • requiring a listing for the language;
    • the full enum (not supported vs invalid) checked before OAuth.
  • worker: vitest 1050/1051, tsc clean. The one failure, feedback_closure_reason_migration, also fails on main. Fixtures are two real designer exports: a 1080×1920 phone screenshot and the 1024×500 feature graphic. Covers:
    • dimensions and 24-bit detection, including rejecting 1024×500 as a screenshot (wider than 2:1);
    • synthetic RGBA, tRNS, JPEG, icon, Wear and TV banner cases;
    • a multipart dry run and the resulting receipt;
    • every rejection code.
  • All 51 files in the designer set pass worker validation offline and fit in one push.
  • cli: vitest 153/153, tsc clean. Covers both layouts, default/, the language table, and the multipart request shape.
  • Docs: docs/public/cli-reference.md (rules table), docs/google-play-operations.md (transport choice).

🤖 Generated with Claude Code

@stamp-build
stamp-build Bot force-pushed the agent/codex-android-devops/play-store-images branch 3 times, most recently from ce8b070 to 6d8fd24 Compare October 27, 2026 18:33
Codex-Android-DevOPS and others added 2 commits October 27, 2026 18:39
Stacked on the store listing text change. The same push endpoint now also
accepts multipart/form-data (a `request` JSON part plus one file part per
image) and applies edits.images in the same Play edit, after the text.

- worker: Google's full imageType enum (appImageTypeUnspecified is
  "not supported yet"); Play's documented format, pixel-size, count and
  15,728,640-byte limits checked before any Play call; 24 MiB per push;
  receipt records each image's sha256, size, type, dimensions and Play id;
  an adapter that does not acknowledge images fails closed.
- play-adapter: edits.images.list/deleteall/upload (with aiGeneratedState);
  a set Play already holds byte-for-byte is skipped; upload sha256 must match;
  committed sets are read back.
- cli: push --images <dir> accepts <dir>/<device>/<lang>/NN.png (designer
  export: phone, tablet-7, tablet-10, feature-graphic, ...) and
  <dir>/<lang>/<imageType>/NN.png, explicit folder-language table, default/.
- tests use two real designer exports as fixtures; docs updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng closed

Review follow-up for store listing images. Whether Google's Image.sha256 is
a digest of the uploaded bytes is undocumented, so it no longer fails a push.

- play-adapter: each uploaded image records sha256 (Hands' digest of the
  bytes sent), provider_sha256 (Google's, null when absent or not a digest)
  and sha256_match true|false|null. play_image_integrity_mismatch is removed.
  Commit readback fails only on an image-count mismatch and records sha256
  agreement. Hands' own declared-vs-uploaded sha256 check stays fail-closed.
- worker: image changes are narrowed to shape-checked fields; mismatches are
  listed as image_warnings (provider_sha256_mismatch) in the response and
  receipt.
- cli: prints image warnings. Docs updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@stamp-build
stamp-build Bot force-pushed the agent/codex-android-devops/play-store-images branch from 6d8fd24 to 12b9c90 Compare October 27, 2026 18:40
@stamp-build
stamp-build Bot merged commit 0dea40d into main Oct 27, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants