148 KiB
Changelog
Unreleased
0.8.19 · 2026-10-20
shots capture/renderfind Playwright's chromium in a project's CommonJS playwright whether it comes on the namespace or underdefault; when the module has nochromium.launch, the error names the resolved file and its exports (was "Cannot read properties of undefined (reading 'launch')").- Heavy app-style headlines again:
shots fontsdownloads the weights the templates use (the fullwghtaxis of a variable Google family, else the used range, else the static weights), so a family like Plus Jakarta Sans no longer falls back to its thin 400 instance.shots fontswarns about weights a family or a local file ({ weight }) lacks;shots checkwarns per image when a title was drawn at another weight (font-weight). - palette.bg as a string: one colour (flat) or a CSS gradient string now renders as given instead of a white
background; anything else fails config validation ("palette.bg must be a colour, a gradient or an array of 2-3
colours").
shots checkwarns when the title ink has low contrast against the pixels behind it (contrast). - Photo backgrounds:
palette.image(+overlay) andshots.feature.backgroundput a cover-cropped photo under a dark overlay (the Play feature graphic, optionally the screenshots);shots checkchecks the file and its size.
0.8.18 · 2026-10-20
- No more "Reparsing as ES module" warning for a
.tsconfig in a CommonJS project (Node's MODULE_TYPELESS_PACKAGE_JSON); the app does not need"type": "module". - Capture sign-in keeps App Review's account apart: the docs use
captureAuthfor a local demo sign-in, andchecknotes a capture against localhost that usesreviewDemoAccount(the accountapple review-detailssends).
0.8.17 · 2026-10-20
shots capture --source webcan sign in first:shots.capture.authis a sign-in request (run once per browser context, cookies kept; body and headers use${cred:NAME}from.storebound/credentials.jsonor${env:NAME}, literal secrets are refused, a failed status stops with the status only) or astorageStatefile.storebound shots loginopens a fresh visible browser for a demo sign-in by hand and saves.storebound/auth.json(0600, counts only). A shot that lands on a sign-in page warns "session expired? run storebound shots login again". Secrets and cookie values are masked in logs and the capture report.
0.8.16 · 2026-10-20
apps folders add [<folder>]adds the folder (it listed and added nothing); a template config is added with a note, a folder without a config is refused with the reason and the fix (pitfall 81).- MCP: the plugin's
.mcp.jsonno longer spawns a plugin path that may be gone (ENOENT, CONNECTION_CLOSED): it falls back tostoreboundon PATH, thennpx -y storebound@latest;mcpandserveskip the self-update (pitfall 82). - Store build minimums in one dated table (
cli/lib/store-minimums.ts):play upload/play releaserefuse an AAB below Play's target API level (36 since 2026-08-31) andapple uploadan IPA built below Xcode 26 / the 26 SDKs (since 2026-04-28), with the file to change;scan/validatereport the same (play-target-sdk,apple-min-sdk) (pitfall 83).
0.8.15 · 2026-10-19
- Mac App Store screenshots.
apple.mac: trueadds themacsize (2880 x 1800;shots checktakes exactly Apple's four 16:10 sizes, no alpha) andapple screenshots --platform macosuploads it asAPP_DESKTOPto the macOS version.validatenotes a macOS version without Mac screenshots. Art pull mapsAPP_DESKTOPtomac/. - Landscape layout in both templates: copy column plus the frame in a Mac window or bare (
shots.landscape), for Mac, Play Chromebook (play.chromebook; no API type,play imageslinks the console) and, withshots.landscape.playTablets, Play's 7" / 10" tablet slots. - Safe margins: text and logo keep 7 % of the width left / right and 5 % of the height at the top in every
template (feature graphics too);
shots checkfails boxes under 6 % / 4 %, measured from the render's layout data. - Contact sheet: the
sheet.pngwait scales with the images, sizes and machine load, with a heartbeat, instead of a flat 30 s. - iPhone Duo skip message says plainly that the API has no Duo type and names the console page.
0.8.14 · 2026-10-19
-
Header and Search Results: upload, add for review, place once approved.
storebound apple creative-assetsuploads each file once into the Asset Library (MD5 upsert: a rejected file or one that failed processing twice is not uploaded again, an archived copy is unarchived), and places it on the version as soon as Apple approved it: the version page's slots offer only approved assets. Until then every locale and placement says "uploaded , waiting for Apple's approval; runstorebound apple creative-assetsagain later", and a re-run places it (an approved old asset stays shown meanwhile).--add-for-reviewadds the unreviewed assets to the app's draft review submission (never submits; Submit for Review stays the owner's);--verify(MCPapple_creative_assets_verify) reads every slot back read-only with the same waiting state, andvalidate/ the report list waiting files. A live app without an editable version uses the live version; if App Store Connect refuses a placement there, the run says so, leaves the live page unchanged (the new placement goes first, the old one is removed only after Apple accepted it), links the live version page and does not retry the other live slots. Art without words needs no copy per locale:<renders>/creative/header.png/search.*fill every language without its own file, uploaded once and placed per locale (the dry run counts files, not slots). Pitfall 76 rewritten. -
iPhone Duo. Apple's published App Store Connect API spec (re-checked: still the 2026-10-06 file) has no Duo screenshot or preview type, so
apple screenshotskeeps skipping the Duo sets with the console link; the check / validate note now says the console takes Duo and the API does not yet. -
macOS (tvOS, visionOS) versions are no longer invisible.
apple status,listing,listing pull,resubmit,submit,releaseandscreenshotstake--platform ios|macos|tvos|visionos(MCP tools and the desktop's owner actions:platform); without it they use the platform that has the version they need (a rejected one, an UNRESOLVED_ISSUES submission, an editable one) and refuse, naming both, when several platforms do. Every version and review submission lookup is per platform, soresubmitfinds the macOS submission back from App Review andlisting --version 0.8.7works on the rejected macOS 0.8.7 while the iOS 0.8.7 is live.apple status,validate,release status, the report and the desktop label every version with its platform; validate and release status warn about another platform's editable or rejected version.whats-new apply --platformnames it in the push command. Pitfall 79. -
Other platforms in store text are refused (both stores). One checker (
cli/lib/cross-store.ts) finds Android, Google Play (Games), Play Store, Galaxy Store, Amazon Appstore, Steam, Microsoft Store, AppGallery (and localized forms) in App Store text (Guideline 2.3.10) and iOS, iPhone, iPad, App Store, Apple Arcade, Game Center, TestFlight in Play text (Metadata policy), per locale, field and line with a fix.apple listing,play listing,play release/play uploadrelease notes,listing set,whats-new check/apply,storebound editand the desktop editor refuse before anything is sent (dry runs show the hits);checkandvalidatereport blockers;scanreportsother-platforms(2.3.10) andplay-other-platforms(Play: Metadata). A functional mention stays only through the owner'slisting.allowPlatformMentions: [{ store, text, reason }]. -
Per-store release notes. Lines tagged
[apple]/[play](or a[play]line heading a block) in a shared notes source go only into that store's notes; the draft leaves untagged other-platform lines out of the wrong store and says so; each store's limit counts its own notes. -
storebound apiwrites piped or redirected are refused loudly.--yeswith stdout not a TTY now exits 1 with "refused: writes throughstorebound apineed an interactive terminal ... Nothing was sent." on stderr instead of a silent dry run.
0.8.13 · 2026-10-18
-
iPhone Duo screenshots.
apple.iphoneDuo: trueadds the foldable's outer (1398 x 2034) and inner (2007 x 2853) sets, portrait or landscape, toshots render,shots check(exact sizes, no alpha) and the gallery. App Store Connect API 4.5.1 has no DuoScreenshotDisplayType, soapple screenshotsskips them with a message and the deep link to the version page's App Previews and Screenshots tab (one constant,IPHONE_DUO_DISPLAY, takes the enum names once Apple adds them).check/validatenote a missing Duo set: optional now, required from April 2027 for iOS 27.1 SDK builds (a warning after that date, never a blocker), and point Play to the tablet sets (no foldable slot). -
Required iPhone class follows the console.
validatenow requires "iPhone with Dynamic Island (medium display)" (APP_IPHONE_61) per locale, met by a 6.9" or 6.5" set that App Store Connect scales down (an ok row, not a warning). Labels match the console: "iPhone with Dynamic Island (medium display)", "iPhone Duo", "iPad 13" display". -
App Store Header and Search Results.
storebound apple creative-assets [--dry]puts a product page header (3840 x 1646, 21:9) and a search results asset (3:2, 1920 x 1280 to 3840 x 2560), image or video (5-30 s, 30/60 fps), per locale from<renders>/<lang>/creative/, or one universal 5244 x 2950 PNG, into the App Asset Library and selects them (PRODUCT_PAGE_HEADER_ASSET,APP_STORE_SEARCH_RESULTS_ASSET) once Apple approved them; MD5 upsert, read back. Local checks: size, no alpha, video length / fps / container (ffprobe, else skipped with a note).checkandvalidatenote when there is none (Play: the feature graphic and promo video are the counterparts). MCPapple_creative_assets; configapple.creativeAssets; pitfall 76.
0.8.12 · 2026-10-17
-
Shareable readiness report.
storebound reportputsscan,checkandvalidateinto one self-contained HTML page (.storebound/reports/readiness-<date>.html, inline CSS, no scripts, light / dark, print-friendly) and opens it in the browser: a verdict per store ("Ready to submit", "N blockers", "Not checked" with the reason when a store key is missing), findings per store by severity with guideline, evidence, fix and an Open in App Store Connect / Play Console button, and the questions only the owner can answer as a checklist. Credentials, key ids, emails and the review demo account are masked.--md,--json,--no-open,--offline,--out. The desktop's Checks tab has a Report button (same code path) and its Markdown export uses the same renderer; MCP gets a read-onlyreporttool. -
Remote config and remote paywalls (App Review 2.3.1, Play Deceptive behavior).
storebound scan(andvalidate;checkas a warning) reportsremote-featuresfor both stores when the app ships a remote config / feature flag SDK (Firebase Remote Config, Statsig, LaunchDarkly, GrowthBook, Optimizely, PostHog feature flags, Unleash, ConfigCat, Amplitude Experiment, Split) or a remote paywall (RevenueCat Paywalls, Superwall, Adapty, Qonversion, Purchasely, Apphud) and the review notes do not mention it. Warn only: storebound never writes the note; the config templates carry a commented example line. Code obfuscators are a low note, minify alone nothing (store-pitfalls 74).
0.8.11 · 2026-10-17
- Guest-play apps rejected as a login wall (Play).
reviewAccess.instructionswithsignInRequired: falseholds guest steps:storebound checkwarns when a Play app has a first-run gate (name entry, code / transfer entry) or in-app purchases and no steps, and theplay-sign-in-detailsplan then declares one entry without credentials, the dialog's required full-access box as the owner's step (store-pitfalls 73).
0.8.10 · 2026-10-15
- Neutral names only. Docs, pitfalls, tests and comments use example names instead of the projects that reported issues; the history was rewritten the same way. Older npm versions are withdrawn.
- Releases: a candidate whose build jobs were cancelled by a newer push says so and gives the tag move and
--resumeline.
0.8.9 · 2026-10-15
- Play edits Play will not send for review are committed anyway. After a rejection, with other pending changes or
managed publishing, Play answers validate / commit with 400 "Changes cannot be sent for review automatically. Please
set the query parameter changesNotSentForReview to true". storebound now commits the same edit again (edits.commit only: validate refuses the parameter with 400 "Unknown name")
with that parameter (a dry run validates that way and says the real run will commit so), then prints "committed;
Play did not send it for review: press Send changes for review in Publishing overview: " (the owner's button).
--not-sent-for-reviewonplay release,upload,rollout,listingandimages(MCP / desktopnot_sent_for_review) sets it from the start. The API cannot read that changes wait to be sent, so storebound records its commit in.storebound/andplay statusshows "changes committed
0.8.8 · 2026-10-15
- Console pages as absolute deep links. Every finding, fix hint, error and plan step that points to an App Store
Connect or Play Console page now prints the readable path plus the absolute URL of that page for the app
(
cli/lib/console-links.ts; apple.appId, play.developerId and play.appId pick the app, the app list otherwise). validate, scan, declarations and release plan JSON carryurl, and the desktop shows it as an Open button. A test fails on a plain console direction without a URL; AGENTS.md rule 11 and pitfall 71 tell agents the same.
0.8.7 · 2026-10-15
- First in-app purchases left out of a review are caught. Apple reviews the first in-app purchase of a type (and
the first subscription of a group) only together with an app version.
validateandrelease plannow block when one that never passed review is not in the open review submission, andapple submitrefuses to leave one out;apple status,release statusand the desktop warn when a version waits for or is in review without it ("not sellable after approval; add it with the next version, or withdraw the submission and resubmit with it").apple iap-reviewsays "ready, not submitted yet" instead of calling READY_TO_SUBMIT complete.
0.8.6 · 2026-10-14
-
Desktop: Fix with AI on scan findings that need a code change (NSAllowsArbitraryLoads in Info.plist, a missing foregroundServiceType, purpose strings, target SDK, ...). The app runs your local Claude Code headless in the app folder with file tools only (no commands, no network, no git push), streams its progress, stops after 10 minutes or on Cancel, and shows the diff with Keep and Undo (exactly the files it changed), then rescans. It refuses while the folder has uncommitted files. Without Claude Code installed the button copies the prompt instead (Copy for AI).
-
Desktop: a Fix button on findings that storebound can fix itself. Scan and validate findings carry a machine-readable
action(--json); the Checks tab runs it as a write with its dry run in the native dialog before anything changes. privacy-labels writes the detected answers into the config (scan --write-only, the dialog shows the config diff); missing Play listings, a missing App Store localization or copyright and the App Review contact push the listing files / config to the store (text only). Nothing submits, releases or publishes. -
Desktop: long findings (a file path in a build folder) wrap inside the Store readiness card instead of pushing the store chips out of it; the release timeline's rail no longer breaks after the current step; the rollout bar stays under its stage.
-
scan: function words of the listing languages (German "eine", "der", French "les", Turkish "bir", ...) no longer count as repeated keywords in the Play full description. -
import type { ShipConfig } from 'storebound'type-checks outside Bun. The package ships declarations for ES module and CommonJS projects (dist/types,dist/types-cjs) and a JS entry for Node; Bun keeps the .ts source. 0.8.5 named onlycli/lib/config.ts, so tsc (next build, vue-tsc, ts-loader,tsc --noEmit) failed on a scaffolded storebound.config.ts and a working Next app stopped building. A test packs the entry and type-checks the template in both project types under bundler, node16, nodenext and node10 with no @types/node and skipLibCheck off, and loads it with import() and require() under Node. Pitfall 69. -
checkfails on the scaffolder's own placeholders: "2026 Example Studio" and example.com / .org / .net URLs and addresses inapp,accountsandreviewAccesswould reach the stores as the app's copyright, contact and legal pages.
0.8.5 · 2026-10-14
Releases take one command and minutes: every push to main is a release candidate (full CI, the acceptance matrix and
the Linux / Windows installers stored per commit), a version tag only promotes it, and bun tools/release.ts does
the rest on the Mac.
- A Chrome storebound starts never outlives its run. The console window's Chrome is ended on exit, closed
gracefully on Ctrl+C / SIGTERM / SIGHUP, and ended by a watchdog when the run dies in any way (SIGKILL included).
Every start first ends headless storebound Chromes earlier runs left behind (an orphan kept the owner's own Chrome
from opening from the macOS Dock) and names orphaned windows. Headless runs on macOS prefer Chromium / Chrome for
Testing over Google Chrome.app.
console fillcloses the window on an error too. Pitfall 68. - Every push to main is a release candidate. ci.yml runs the full CI, the acceptance matrix (Linux + Windows x
Node 20 / 22 / Bun) and the installer builds in parallel and stores the green result as the draft
rc-<sha>. A version-only bump commit (proven file by file) reuses its parent's CI and matrix and rebuilds only the installers. Av*tag promotes the candidate (no rebuild, no retest) and fails at once on a red commit. bun tools/release.ts <version> --yes: bump and CHANGELOG cut, fast gate, commit, push, tag; the Mac build and notarization in parallel with Forgejo; macOS acceptance cached per CLI key; add, verify, publish (each Mac file crosses the owner's uplink once); /Applications/storebound.app updated; the npm commands printed.--rehearsalruns it all as a throwaway prerelease and deletes it afterwards.
0.8.4 · 2026-10-14
Existing products' prices never change without --allow-price-changes; PPP pricing compares like with like and keeps
high-income markets at par. --json prints only JSON. Automatic signing creates no profiles, and capabilities are
verified against the App ID, the config and the build. check requires a copyright; What's new drafts come from
release notes and flag jargon.
-
apple signing ensurewith automatic signing creates no profiles or certificates. Withsigning.style: automatic(the default) it only adds missing capabilities to the App ID; Xcode owns certificates and profiles, and the dry run says so. Provisioning profiles storebound made that nothing uses are listed by name with the exact delete command (never run by storebound). -
Capability writes are read back. After adding a capability,
ensurereads the App ID again (a few tries, about 45 s at most) and fails with the API's answers when one is missing, instead of reporting success. -
Capabilities compared in three places.
apple status,apple signing status/ensure --dryandvalidatecompare the App ID,apple.signing.capabilitiesand the iOS project's.entitlements(push, Game Center, associated domains, Sign in with Apple, iCloud, app groups, ...; also an entitlements file not set in CODE_SIGN_ENTITLEMENTS) and name what is missing where. -
No false alarm for entitlements on another branch. A checkout without
.entitlementsis a warning about this checkout only ("built from another branch?"), naming the current branch and the local branches / worktrees that have one; it never claims the build lacks them. With a built IPA in build/ios/export, its real entitlements (codesign, or the Mach-O signature when codesign is missing) are compared as "uploaded build N"; without one the output says the App Store Connect API does not expose a build's entitlements. -
storebound apiwrites outside a terminal say that they need an interactive terminal by design and name the dedicated command for common operations (bundle id capabilities:apple signing ensure, IAPs, listing, ...). -
--jsonprints only JSON on stdout. Every command run with--jsonsends its log, step and table lines to stderr (one place:withJsonOutputin cli/lib/log.ts, wrapped around every command); the last commands that printed JSON with console.log useprintJson. A test runs every command that reads--jsonagainst the fake stores and parses its stdout (prices ppp --jsonprinted log lines first). -
PPP prices compare like with like.
prices ppptakes the store's own conversion of the US price (VAT included, today's rate: Play convertRegionPrices, Apple equalizations; offline base x rate x (1 + VAT)) as par and applies the PPP price level to it. Defaults: only lower prices (--only-decreases), only economies with a price level at most 50 % of the US (--max-level), World Bank high-income economies keep par (--include-high-incometo include them), mainland China at par on Apple and skipped on Play, never above par (--max-ratio,--min-ratio; the old--floor/--ceilingstill work). Germany keeps EUR 5.99 for USD 4.99 (was EUR 3.59). A top-10 revenue market that drops more than 20 % is warned about (from your sales reports, else a static list). Regions that keep their price stay out of the sheet (keptin--json). -
Creating products never changes a live price.
play products,prices import,play subscriptionsandapple subscriptionschange prices of existing products only with--allow-price-changes, in a terminal too: one "y" covered them before. With the flag they get their own question listing each old -> new; dry runs show a separate "price changes on existing products (held)" section. -
whats-newdrafts from player-facing notes before the git log. It takeswhatsNew.source, thendocs/RELEASE-<version>.md, then the CHANGELOG.md section of the version (else Unreleased); version from--release, else package.json. Only without one (or with--from-git) it drafts from commit messages, and says so. -
Internal jargon warnings in release notes. The draft and
whats-new checkname SDK / vendor names, developer and version-control words, file paths, issue ids and code identifiers, each with a plain alternative; warnings never block. One list in cli/lib/jargon.ts;whatsNew.allowWordskeeps a word. The whats-new skill says to write from what players feel, never from commit messages.
0.8.3 · 2026-10-14
Docs: no private account names in the published package. npx storebound prompts work on Node again (the [Y/n]
prompt of storebound desktop crashed on Windows).
npx storeboundprompts work on Node.storebound desktop(and every [y/N] or type-the-id prompt) crashed on Node at the question with "console is not async iterable" (Windows also hit a libuv assertion at exit): prompts now read stdin throughrt.readLine(node:readline, closed after one line, EOF is "no"). A test keeps Bun-only stdin iteration out of cli/, and the Windows release job runs the Node prompt smoke on the runner.- Release privacy scan of signed macOS builds leaves out Apple code signatures only. The Developer ID certificate in a signature names the developer; the .app, DMG and updater bundle are scanned as copies without signatures, and any hit elsewhere still fails the release.
0.8.2 · 2026-10-14
The desktop app edits everything the consoles do (listings, app info, catalog and prices, TestFlight, store art), with
a dry run before every store write, and reads everything on its own within the stores' rate limits. Play status comes
from the live store page (Live / In review / Rolling out); art pull takes icons and screenshots from the stores;
errors are clear messages instead of raw API text.
-
Store art of a live app comes from the stores, not a re-render.
storebound art pull [--store apple|play] [--write] [--force]downloads every App Store screenshot set and app preview per locale and display type (the original size fromimageAsset.templateUrl), the App Store icon (the newest build's icon at 1024 px, else the public artwork) and per Play language the icon, feature graphic and phone / 7" / 10" / TV / Wear screenshots (the original files,=s0, checked against Play's sha256) into the store art layout (store/renders/<lang>/<size>/NN.png,feature/1024x500.*, the icons atapp-store-1024.png/play.icon).store/renders/pulled.jsonrecords each file's sha256, the store, the URL host (never the URL) and the store's own checksum, soplay imagesandapple screenshots/apple previewssee a pulled file as the store's and upload nothing. An upsert: a re-run changes nothing, a store change updates an unedited pull, and a local file that differs (your own render, an edit after the pull) is kept and listed unless--force. Read-only on the stores.storebound onboardruns it too (--no-artskips it). Pulled art never goes into git: when the app folder is in a repo and the renders folder is not ignored yet,art pull --write(andinit/onboard) add it to the repo root .gitignore (e.g./store/renders/). -
check / validate with pulled art. Missing render sources (the icon artwork, the logo, screenshot captions) are notes instead of problems once the art is pulled; pulled files pass the local store checks as the store's own, and a set the store had nothing for (a Play language on the default language's graphics, a locale without that set) is not demanded locally (validate reports the store side).
-
Desktop: the live store art. The Store art tab shows the art that is live on both stores (thumbnails per store, language and set; same / differs / not pulled per image) with Pull from stores (dry run, dialog, then the files), and store-only apps get the tab read-only. Replace with a file (the lightbox, and each scene's frame in the art editor) copies a picked PNG / JPEG into the app folder after a dry run with the store checks (
art.replace; native pickerimage_pick). docs/console-parity.md: 76 rows, 57 complete, 19 with the reason. -
Parallel Play reads no longer break each other. Google Play keeps one open edit per app, so two reads of one app at once (the desktop's auto-read) deleted each other's edits ("This Edit has been deleted") and the Releases tab then said "not on Google Play yet". Every Play edit of a package now runs under a machine-wide per-package lock (
~/.storebound/locks, wait with timeout and heartbeat; other packages stay parallel), and a read whose edit was deleted is redone once with a new edit (writes are not). A store that failed to read shows "Couldn't read Google Play" and keeps its last good releases with "couldn't refresh". When the stores' current versions differ, the card header says "Current · Play 4.1.9 · App Store 4.1.2" and each lane shows its own state ("4.1.2 · Live (4.1.9 not submitted)"); "Not on this store" only for an app with no record there (pitfall 58). -
Google Play status is decided, never "maybe".
play status, the portfolio and the desktop read the live version from the public store page ("Version" in About this app): the same version = Live, 404 = In review (managed publishing: In review / ready to publish), an older one = In review (live: 1.0.0), Rolling out N% / Halted / Draft from the API. Only a network failure says "Couldn't check the store page", and it keeps the last known answer with its time. "In review or not yet published" and "Google may still be reviewing" are gone. -
Desktop reads everything by itself. No more "Not read yet" and a Read button on every page: on start, when the window shows again and every 15 minutes while it is open (Settings: auto-read on / off, interval), every app's status, releases and submission state, TestFlight, catalog, prices, listings, health, reviews, analytics and checks are read in the background when their saved copy is older than its TTL (5 min to 6 h). Saved reads show at once with "read 3 min ago"; a failed refresh keeps them with "couldn't refresh: ". One scheduler keeps it inside the stores' limits: 4 at a time per store, starts spaced per account, slower below 20 % of App Store Connect's hourly limit (read from
X-Rate-Limit) and a pause at 100 left, exponential backoff with jitter on 429 / RESOURCE_EXHAUSTED, the visible app first. "Refresh all" in the toolbar, a refresh icon per card, writes refresh what they changed, and a status line: "Reading 12 of 48 · App Store limit 3,412/3,600 left this hour". -
Health: open a top issue. Full title, type, counts, versionCode range, Android API range, last report, a sample stack trace (
storebound health --issue <id>/--signature <id>, new readhealth_issue), Open in Play Console, Copy for AI, and a likely cause for known signatures (cli/lib/crash-hints.ts; first entry: the CapacitorBridge.getPermissionStatesNullPointerException fromcheckPermissions()during an activity transition). -
Google API errors in words, not JSON. Google's error bodies are parsed centrally (
cli/lib/google-errors.ts) into typed findings:api-disabled(403 SERVICE_DISABLED: service, Cloud project id, the console page to turn it on),no-app-access(403 PERMISSION_DENIED: which app, the Users and permissions step),auth-revoked(401, invalid_grant) andquota(429, RESOURCE_EXHAUSTED).health,analyticsand the health JSON say one line with the URL ("Crash and ANR data needs the Google Play Developer Reporting API turned on in Google Cloud project . Turn it on: ..."); the raw text stays indetailand the log. The desktop Health tab shows that line with "Turn it on in Google Cloud" and "Check again", the raw text only in a collapsed Details; the Portfolio shows a Cloud project that blocks several apps once ("1 Google Cloud project needs an API turned on: affects ..."). Pitfall 57. -
Findings are results, not errors, in the desktop.
scan,check,validateandhealthexit 1 when they find problems (by design); the sidecar now reports anoutcome(ok / findings / declined / failed) and, for a real failure, the command's ownerrorline (stderr's last "error: ..." line). The Checks tab lists the findings instead of "Exit code 1", findings results are cached, and every place that showed a bare "Exit code N" shows the error line. -
Play "Live" only when the store page proves it. The Play Developer API has no review state: a production release reads
completedwhile Google still reviews it.play statusnow checks the public store page (no auth, fast timeout, cached 5 minutes) and addspublicPageto its JSON: page 404 = "Sent to Google (in review or not yet published)", 200 = live for the first release, an update is "Released to production (Google may still be reviewing)" unless the page shows its versionName, offline = unknown. New release phasessentandreleasedin the desktop Releases tab, Portfolio,release status | planand the CLI JSON. Pitfall 56. -
One row per release across the stores. Versions are matched without trailing .0 parts (App Store "1.0" and Play "1.0.0" are one row and one timeline); each side still shows the store's own string.
0.8.1 · 2026-10-13
apple build exports each build into its own folder and fails loudly on any export error; apple upload refuses a
stale IPA whose build number is already on App Store Connect; xcodebuild runs with Apple's tools first on PATH (a
Homebrew rsync broke exportArchive). The Play feature graphic keeps the whole phone on the canvas.
-
Feature graphic fits the phone. The Play feature graphic (1024 x 500) cut the phone off at the bottom (the game template's 7 deg phone spanned about y 24..552).
shots.feature.fit'contain' (default) sizes the device so its rotated outline stays inside with a 24 px margin, in both templates; 'bleed' keeps the old crop on purpose;shots.feature.scale(0.5-1.5) resizes it; the config is validated.shots renderrecords each device's measured box in report.json andshots checkfails a feature graphic whose device leaves the canvas (warns for a screenshot's front device off a side or the top). Pitfall 55. -
apple buildexports each build intobuild/ios/export/<version>-<build>/(cleaned first), runs xcodebuild with Apple's tool folders first on PATH (a Homebrew rsync broke Xcode's IPA step: "exportArchive Copy failed"), shows the IDEDistribution log's real error lines and exits non-zero on any export failure (pitfalls 53, 54). -
apple uploadfails with exit 1 when the IPA's build number is already on App Store Connect (a stale IPA after a failed build) instead of reporting it VALID;--allow-existingre-checks,--build Npicks that build's IPA. The release and beta flows use both. -
storebound doctorflags a non-Apple rsync first on PATH; desktop publish calls rsync by absolute path without--chmod. -
Site: the download notice says per OS: macOS signed and notarized, Windows unsigned for now (SmartScreen: More info › Run anyway), Linux unsigned.
0.8.0 · 2026-10-13
One product: the CLI and the desktop app share one version, the store accounts and the app folder list, and
npx storebound desktop installs and opens the app. storebound onboard brings live apps in without questions the
stores and the code already answer. Consoles without an API are driven by the agent (console plan / verify).
Plus many fixes from real releases.
- One version. The desktop app has the CLI's version from now on; the next release is 0.8.0 for both.
package.json'sversionis the authority (desktopVersionis gone);tools/desktop-version.tskeeps the plugin manifests and the app's copies equal (CI, andtests/desktop-release.test.ts/src-tauri/tests/d5.rsas the drift tests),desktop-release.ts publishrefuses a folder of another version, and the release order is the app first, thennpm publish. The desktop 0.1.0 release candidate was never published, so nothing public changes. npx storebound desktopjust works. The app has this CLI's version: it opens on the current folder. Not installed: it downloads the app for this system (the CLI's own version, else the newest published), checks size, SHA-256 and the minisign signature, installs it for this user without admin rights (macOS: from the read-only DMG into /Applications or ~/Applications, no xattr changes; Windows: the per-user NSIS installer, silent; Linux: the AppImage in ~/.local/bin with a.desktopentry and thestorebound://handler) and opens it. An older app is updated the same way (never while it runs), a newer one is opened with a note (downgrade only with--force). It asks once ([Y/n]);--yesskips the question; without a terminal and--yesit prints the command and exits 3.--dry/--jsonchange nothing,--downloadonly saves the checked installer. The publish script also writesdesktop/dl/<version>/latest.json(redeploytools/vds/storebound-desktop-publishon the server).- Store accounts, one store. Credential profiles in
~/.storebound/credentialsare shared by the CLI and the desktop app: the app writes its keys there (files 0600, folders 0700) and shows the profiles the CLI made; accounts of earlier desktop builds move from the OS keychain into profiles on first start (ids, studios and app mappings kept; the keychain entry is deleted after the profile reads back). A removed account's key file goes tocredentials/.removed/. Precedence: env, then a profile the project names, then the project's.storebound/credentials.json, then the default profile (the only one, ordefault). A project names its profile withaccount:in the config,"account"in its.storebound/credentials.json, or--account. storebound accountslists the profiles (masked) and which one this folder uses;accounts importmoves the store keys of a project's.storebound/credentials.jsoninto a profile (key files copied to~/.storebound/credentials/<name>/, the same Apple key id or service account e-mail = the same profile, the keystore and review contact stay in the project, a backup of the project file is kept, safe to run again;--dryfirst, never prints values);accounts use <profile>points a project at one.- App folders, one list.
~/.storebound/projects.jsonis the desktop's App folders and the CLI'sstorebound apps folders | add | remove;storebound desktopin an app folder andws initadd to it, andwswithout a workspace file works over it. The app syncs it at start, when the list is shown and before astorebound://openlink, so a folder added in the terminal is selected without a question. - Desktop: a folder whose
package.jsonpins another storebound version shows "This project uses storebound X, the app runs Y".storebound-desktop --version.ws desktop import / exportcarry the accounts' profile names.
Onboarding apps that are already live, without owner questions for what the stores and the code prove (lessons 43-52, from setting storebound up on 6 live apps):
storebound onboard [--write](MCPonboard, dry run by default). Read-only on both stores, never changes a store price. Imports the live one-time products and subscriptions of both stores intoproducts/subscriptions: ids, type, listings per language, usd = the US price (else Play's own USD price for new regions), Apple's price schedule base territory as an override when it is not the USA, an item on one store only kept withplatforms;catalog diffcomes out clean. An item with no USD price anywhere is named with its base price, not left out silently. Fills app.contactEmail / website / supportUrl / privacyPolicyUrl / marketingUrl from Play's app details and App Store Connect where the config holds a template value (real values that differ are reported and kept); the App Review contact goes into.storebound/credentials.jsonreviewContact(masked in output, the phone never printed). Sign-in for review is decided from Apple's review detail (demoAccountRequired; a demo account set there is imported intoreviewDemoAccount, never printed) and the code's auth signals, never a default yes. With accounts and noaccounts.deletionUrlit looks for the deletion page on the app's own site (/delete-account, /account/delete, /delete, /account-deletion, links in the privacy policy that mention deletion); not found stays a finding. Then the owner questions that are left, in one batch.- App Store keywords count characters, not bytes. Live READY_FOR_SALE fields hold Thai 95 characters / 265
bytes, Turkish 100 / 105, Polish 99 / 105:
check,check --aso,listing check,listing translateandaso suggestcount characters again (lesson 4 was wrong; lesson 43). validate: a live app with only the 6.5" iPhone set (APP_IPHONE_65) passes, App Store Connect accepts it for the 6.9" requirement; a 6.9" set is a recommendation (also the two 13" iPad types). Play: a non-default language without its own icon / feature graphic uses the default language's (a note), only the default language's missing graphics block.- Play Reporting API off: the finding names the Cloud project and the exact link to enable
playdeveloperreporting.googleapis.comfor it (apps check,apps list,health,validate);apps listno longer fails but lists the Play apps of this machine's app folders and store-only entries. storebound accounts default <profile>(--clear): the machine's default credential profile, a pointer file in the credentials folder, shown inaccounts list. Order: env > the profile a project names > the project's own keysthis default > the only profile (or the one named
default).- Store-only apps:
storebound apps add --store apple|play --app-id <id> --account <profile>adds an app that is live but has no project on this machine to~/.storebound/projects.json(storeApps);apps foldersshows it as "no local project",apps remove apple:<id>takes it out. The desktop shows it read-only ("Store only": status, reviews, analytics) under the account with that profile, and keeps the entries when it writes the folder list. wschildren get the app's own account choice when the workspace names no account (before: no profile at all).
Fixes from a real first release of a second game (Capacitor 8 monorepo, lessons 35 to 37):
- Fix (
scan, review-prompt 3.2.2(x)): a bare "rate" next to "bonus" (const rate = F.genRate(lv, bonus.gen(r))) is no longer a reward for rating. The signal needs a rate-us / leave-a-review phrase next to a reward, a reward "for rating / reviewing", or a reward granted within a few lines of a system review prompt call. - Fix (
scan, account findings and the declarations proposal): import, require and export-from lines are never sign-in / sign-up / deletion evidence (import './buildings/register'was), auth endpoints must be an absolute path, a URL or a${base}/...template, credential fields and guest-account creation count; accounts only the config claims read "from config (accounts.enabled)". - Fix (
scan, 5.1.1(v)):accounts.inAppalone no longer clears the finding: without deletion code it stays, medium, "claimed by config (accounts.inApp), not found in code". - Fix (
privacy-manifest --propose): NSPrivacyCollectedDataTypes is built fromappPrivacy.types(Apple's manifest keys for type and purposes, linked, tracking); NSPrivacyTracking stays true only with app-level domains.checkwarns once when the app manifest's list is empty butappPrivacy.typesmarks types collected. - Fix (
console plan apple-app-privacy):appPrivacy.typesis authoritative: a type it does not list is "not collected" (certain), never derived from dataSafety or asked. Without it,appPrivacy.tracking: falseand no ad / attribution SDK answer Advertising Data, Customer Support, Environment Scanning, Hands and Head "not collected" unless a support SDK or AR code is found.console planscans the project for these signals. - Fix (
console plan play-advertising-id/play-app-content): "Does your app use advertising ID?" comes from ad / attribution SDKs, the AD_ID permission in the source or merged manifest (or itstools:node="remove"),ads.appAdsTxtorconsole.appContent.advertisingId; a Data safety Device ID alone is not the advertising ID. No signal: No (certain); signals that disagree: asked. scan.ignore[].evidence: accept a finding by the text of an evidence line (matchkeeps matching the title).scanandcheckname every entry that matches no finding.- Fix (
init):.storebound/credentials.jsonstarts with empty strings only. Placeholder-looking values ("your-…",<…>, "xxx", "/absolute/path/to/…", "example") count as missing in every credential read;checkwarns about them anddoctorlists each credential as set, missing or placeholder.
Console facts from a second real game (2026-10-13): the owner now comes back once for Play's App content instead of once per card.
- Play App content card Saves are the agent's: saving a declaration (privacy policy, ads, app access, advertising ID,
government, financial, health, target audience, the content rating questionnaire and Summary, data safety) is not
public; Google gets it only with the next "Send changes for review" in Publishing overview, which stays the
owner's. New button class
declaration-save;declarationSaveAllowedlets exactly Save / Kaydet through on an App content card page;publishingControlrefuses "Send changes for review", "Submit N changes for review", Submit, Publish and "Restart review" everywhere. The plans' final button is "Send changes for review";console fillpresses the content rating Summary's and the target audience's Save itself. console planprints an owner batch first (ownerBatch): every owner step of the plan in ONE list, in the order the agent needs them (open answers, the IARC Terms of Use box with its exact place before the run, Send changes for review after).console plan play-app-content --all(MCPall: true) walks every card inline in order and ends with that one owner list.- IARC questionnaire: page 1's Next does nothing until the IARC Terms of Use box is ticked (an owner-only agreement).
It is in the owner batch before the run;
console fillwaits for the tick instead of pressing a dead Next. - Sign in details: with
reviewDemoAccountin.storebound/credentials.jsonthe agent types the demo account (never printed); only without it is it the owner's. - New App (App Store Connect): a programmatic value set on Name and SKU left the dialog's state empty; the plan says
"type, don't set value" and
console filltypes every text field key by key. play.appIdcaptured automatically:console verify play-app-record --url <url after Create app>reads the id from the redirect and shows theplay.appIdchange as a dry diff;--writeupserts it into the config. New read-onlystorebound play app-id: finds the app by package name in captured app list pages (10 per page, a new app is not on page 1), then the same dry diff and--write.- Pitfalls 38-41.
TestFlight distribute from real use (2026-10-13, pitfall 42):
- Fix (
apple testflight distribute): it failed with 400 right after printing the build state, because App Store Connect refusesGET /v1/betaGroups?filter[builds]=although Apple's spec declares the filter (and the spec has no GET of a build's groups). A build's groups are now read per target group withGET /v1/betaGroups/{id}/relationships/builds. An internal group with all builds is reported as "already available to (internal, all builds)" and never sent the build (App Store Connect refuses that); internal groups without all builds and external groups get it through the relationship; a re-run adds nothing. - Fix (
apple testflight distribute,testers): a group inapple.testflight.groupsthat is not on App Store Connect yet says so and namesstorebound apple testflight groups sync; a group in neither lists the groups that exist (wasunknown group "Team" ()). - Guard:
tests/asc-query-guard.test.tswalks every App Store Connect GET with a query thatcli/builds (template literals and the URLSearchParams they interpolate) and fails offline on a path that is no declared GET, a query parameter the operation does not declare incli/api/apple.gen.ts, or one in the newAPPLE_LIVE_REJECTEDlist (declared, refused live).storebound api applerefuses those parameters too. The fake App Store Connect in the tests answers 400 PARAMETER_ERROR.ILLEGAL to such parameters, as the live API does. - Releases on git.crt.fyi come from a workflow. A
v*tag push runs.forgejo/workflows/release.yml: version checks, the full CI, the Linux and Windows installers, the privacy scan, then one Forgejo release with short notes (the CHANGELOG summary, install line, links), the installers and SHA256SUMS; older release entries are deleted (tags stay). The Mac adds the notarized DMG withdesktop-release.ts upload-forgejo. A dry run (workflow_dispatch) makes a drafttest-<sha>and deletes it.desktop.ymlis folded into release.yml; ci.yml and release.yml share their setup (.forgejo/actions/).
0.7.1 · 2026-10-13
- Fix: README links and images on npmjs.com resolved against npmjs.com and gave 404 (
docs/README.md, the logo). Every README link and image is now absolute (git.crt.fyi), and a test keeps it that way.
0.7.0 · 2026-10-13
Agent-first: the AI does the whole job ("do it, don't ask") and the owner presses only the final public buttons, with
every compliance question asked once, in one batch. Consoles without an API get console plan / console verify
(start URL, steps, answer sheet, never-press list) for App Privacy, Play App content, IARC, Create app and the first
production release. The desktop app sends for review itself (dry-run plan first). The CLI runs on Node 20+
(npx storebound). Plus many store fixes from real releases (IAP resubmits, Invalid Binary, Data safety, uploads).
-
Fix (
check): "N Play App content answer(s) are not in the config" no longer counts the two steps that stay the owner's however complete the config is (ticking the IARC Terms of Use, typing the demo account's username and password), so a fully answered project checks clean. The site'scheckshot (tools/site-term-shots.ts) now shows that ready project: a freshinitplus icon files,apple.name, the App content answers and the demo account. -
The CLI runs on Node 20+ (owner decision, 2026-10-13):
npx storebound initworks,bunx storeboundkeeps working. The npm package'sbinis one Node ESM bundle,dist/storebound.mjs(built byprepackwithbun tools/build-node.ts, zero runtime dependencies, playwright still an optional peer);enginesnames Node 20. Everything the CLI needs from its runtime goes throughcli/lib/rt.ts(spawn / spawnSync with Bun's defaults and exit codes, which with PATHEXT, sleep, a small HTTP server for shots render / video, glob, hashing, gunzip, stdin, SQLite, the package folder); templates, the shots pages and Play's Data safety form are embedded bytools/gen-embedded.tsinstead of Bun text imports, so the bundle and the compiled desktop sidecar carry them.storebound.config.tsloads on Node too (Node's own type stripping, or a fallback on Node 20).storebound installrun with Node writesnpxMCP commands.bun run cibuilds the bundle and smoke-tests it with the realnode(--version,help,checksame as Bun,scan,release plan --help,init, an MCP handshake and tool call);tests/node-compat.test.tsfails on any direct Bun API use incli/.bin/storeboundfalls back to Node when Bun is not installed. Development, tests and CI stay on Bun. -
console verify apple-app-privacychecked against real page text (2026-10-12): the summary sections ("Data Used to Track You" and the others) list category names only and are skipped; a type's lines may come joined by " | "; Apple's comma style ("X, and Y") and the straight apostrophe parse. The tracking radio label of a type that already ends in "Data" no longer reads "crash data data". The Play Data safety page path (app-content/data-privacy-security, with Export / Import CSV) is confirmed. -
Fix (
play build/checkversionCode source warning): a variable set from the property first (def code = (project.findProperty('versionCode') ?: ...).toInteger(), thenversionCode code; alsoext.and Kotlinval) is followed to its definition and no longer flagged. -
Fix (
apple iap-submit,apple resubmit): after the owner canceled a submission, every IAP version was DEVELOPER_REJECTED and the plan skipped them all ("change it in App Store Connect (or --new-version)", then "nothing to submit"). Apple accepts the same version again (201 READY_FOR_REVIEW, then the submit 200), so DEVELOPER_REJECTED in-app purchase, subscription and subscription group versions now go in as they are, listed as "add (developer rejected, re-submitted as is)"; an empty draft is reused.--new-versionis only for APPROVED or REJECTED products (pitfall 31). -
play buildandcheck: warn before Gradle runs when the app module'sdefaultConfigdoes not read-PversionCode/-PversionName(a literal, an env var, ...), naming the file and line and the exact Groovy or Kotlin line to use (both built with AGP 8.13).android.injected.version.codedoes not overridedefaultConfigforbundleRelease(checked with AGP 8.13); the docs no longer suggest it. An AAB with another versionCode than planned is now a hard stop (it was a warning) and is not copied (pitfall 32). -
Console plan
play-production-release: the "Do you want to restart your review?" dialog that "Submit N changes for review" opens while a review is running is a known step; "Restart review" is the owner's button and in neverPress (pitfall 33). -
Desktop: send for review from the window (owner decision, 2026-10-12). New owner actions (desktop only, never MCP tools):
owner.apple_submit(newstorebound apple submit --build <n> [--version x.y.z]: the steps ofapple resubmitfor a first submission: build n attached by number, the open review submission reused, the ready IAPs / subscriptions with it, submitted with the "not ready yet" retry; plan table without--yes),owner.apple_resubmit(apple resubmit --build <n> [--resolve-iaps]),owner.apple_iap_submit(apple iap-submit [--with-version]) andowner.play_production(play release --version-code <n> --production completed | inProgress | draft). Each is a dry run from the window; the real run needs the host's token minted after the native dialog, and only then serve adds--yes(apple submit / resubmit) and answers the command's question.OWNER_ALLOWED_ARGSties every otherwise forbidden flag (--yes,submit,--production,--csv,--allow-price-changes) to its one owner action, a dry run never carries an owner flag, and the MCP server still has no submit, resubmit or production release (tests/serve.test.ts,tests/desktop-d3.test.ts,src-tauri/tests/d3_writes.rs). The submit dialog says the app goes to review and may go live by itself once approved.- Releases tab, "Send for review": the App Store submission state from
apple status(waiting / in review, UNRESOLVED_ISSUES with its items and which ones get marked resolved, empty drafts labelled harmless), the uploaded builds to pick by number (processing ones disabled, nothing preselected), Submit for review, Resubmit (with "also mark rejected IAP items resolved") and Submit in-app purchases (with the editable version); Google Play versionCodes by number (one already live cannot be picked), Send to production as everyone, a staged share or a draft (a never-published app), the managed-publishing warning fromplay.managedPublishing, and the console plans of production countries and "Send changes for review" (no API) with a deep link. Every button shows the plan (dry run,--jsonwith the plan table under serve) in the window first, then the native dialog; the real run's progress lines stream into the panel and Stop cancels it. The old "submit stays in the console" note is gone. - App info, "Console steps without an API": every
console planform (the new Play store settings category, App content and its cards, production countries and production release included) with a plan panel (start page and deep link, steps, answer sheet, never-press buttons, the owner's final button, Copy plan for an AI agent) and the read-onlyconsole verify(page text pasted, or the API for the App Store record and age rating). The Play forms card is folded into it. - Price gate:
owner.prices_importpasses--allow-price-changesitself after the dialog; newowner.prices_play_products,owner.prices_play_subscriptions,owner.prices_apple_subscriptions(Catalog: "Price changes") run that sync with the flag. A write that ends with exit 3 (declined: a held price change) shows "Waiting for your decision", not a failure. - Data safety card: the deletion question as the CSV answers it (
dataDeletion, fromaccounts.deletionUrl,dataSafety.dataDeletion, ...;project.declarationssendsdataDeletion,dataDeletionSource,dataDeletionUrl), older sidecars still shown frompartialDeletion. - TestFlight: "Create missing groups" (
apple testflight groups sync, dry run first). Dependencies: the Android SDK asplay buildfinds it (system.toolsandroidSdk).project.inspectaddsplayRelease(managed publishing, countries, category). Job toasts: upload lines ("312/890 MB (35%), part 4/12") drive the progress bar, and the last 40 lines are in the toast's log. - Covered by the existing views:
validatefindings ("production has 0 countries", the category reminder),checkwarnings (stale web assets withoutbuild.prebuild, Play category, App content answers owed, privacy manifest tracking mismatch ITMS-91064, the defaultConfig versionCode / versionName source warning) andscanfindings show in Checks;play imagesskipped languages and optional tablet sets show in the dry run of the native dialog. - Boot check: four send-for-review cases (ready and UNRESOLVED_ISSUES, on the old D1 and the new D3 saved state)
with store answers from
src/dev/submit-fixtures.ts: the Releases texts, the build and versionCode pickers, the plan panels, a console plan from the real sidecar, every app tab and Dependencies, by DOM text; the dialog is answered Cancel, nothing is sent.
- Releases tab, "Send for review": the App Store submission state from
-
Safety (
apple testflight groups): run bare, it created the configured beta groups right away, with no plan. Now it only lists the groups and printswould create "Team", "Friends". Creating isgroups sync(orgroups --yes). Flows treat baregroupsas a read andgroups syncas a write, and the MCP tool runsgroups sync(dry by default). -
Fix (
apple resubmit,apple iap-submit): after an Invalid Binary rejection the submit kept answering 409 "Version is not ready to be submitted yet" for 25 minutes because the app version item of the UNRESOLVED_ISSUES submission was still REJECTED (pitfall 30). REJECTED items are now marked resolved before the submit (PATCH /v1/reviewSubmissionItems/{id}resolved: true): the app version item inapple resubmit(a fixed build goes in), in-app purchase / subscription items only with the new--resolve-iaps. The plan table shows "mark resolved" rows. On "not ready" the items are read again: one in scope is resolved, one out of scope stops the run at once with the item and how to resolve it; backoff retries only when nothing is left REJECTED. -
Fix: an empty READY_FOR_REVIEW draft cannot be canceled (409 "Resource is not in cancellable state"). A draft the run created and could not use is left as is, stray empty drafts get no cancel command,
apple statuslabels them "empty draft (harmless; App Store Connect keeps it, not cancellable)" and--cancel-submissiontreats that 409 as harmless.apple statusmarks REJECTED items as not resolved. -
Fix (
apple iap-submit): resubmitting after a rejection created a second, empty draft next to the rejected submission and stopped with a wrong "list lag" message (pitfall 30). The open review submissions are read first (GET /v1/apps/{id}/reviewSubmissions, READY_FOR_REVIEW, UNRESOLVED_ISSUES, WAITING_FOR_REVIEW, IN_REVIEW, with their items): one with UNRESOLVED_ISSUES is reused (only missing items added, then resubmitted), a draft is reused, one waiting for or in review means nothing is sent; a new draft only when none is open. 409ITEM_PART_OF_ANOTHER_SUBMISSIONis reported with the other submission's id and that one is used (a draft the run created is canceled again); it is never read as a duplicate. The submit is retried with backoff and heartbeat lines while Apple answers "Version is not ready to be submitted yet" (up to 10 minutes). -
New
storebound apple resubmit --build <n> [--version x.y.z]: attaches build n (by number, VALID) to the rejected version, reuses the UNRESOLVED_ISSUES submission, adds missing IAP / subscription items and resubmits with the retry. A plan table without--yes(the owner's step); never in MCP or flows.apple statuslists an UNRESOLVED_ISSUES submission with its items and their states and prints the resubmit command. -
ITMS-91064 (pitfall 29): a real build was refused as Invalid Binary a minute after the upload because the app's PrivacyInfo.xcprivacy had NSPrivacyTracking true with an empty NSPrivacyTrackingDomains (tracking came from an ad SDK whose own manifest declared it). Rule in every manifest: true needs a non-empty list, a non-empty list needs true;
check,scanandprivacy-manifestfail on either mismatch with the exact ITMS text. App Privacy "used for tracking" no longer requires the app manifest's true:checkfinds SDK manifests that declare tracking (Pods, frameworks, SwiftPM checkouts, Carthage, Capacitor plugins, the last built IPA) and says so as info, names known tracking SDKs from the dependencies, and only warns when nothing declares it.privacy-manifest --propose [--tracking-domains]prints the app manifest with true only forappPrivacy.trackingDomains(the app's own domains); the scan proposal says false for SDK tracking.apple uploadreads every manifest in the IPA (app root, Frameworks, bundles; XML or binary plists) and stops before any network call;apple buildchecks the export. -
Play console steps without an API, from a real first release (2026-10-12; pitfalls 27 and 28). New agent-only
console plan/console verifyforms (also in the MCPconsole_plan/console_verifylists):play-store-settings(the App category:play listingcannot set it, so a new app showed "Not selected"; the first Edit > App or game > the Category[role=option]> Save),play-app-content(every card of/app-content/overviewin order with its page, its "Start declaration" button, its plan and one batch of owner questions) and the cardsplay-privacy-policy,play-ads,play-sign-in-details(the demo account is typed by the owner, never printed),play-advertising-id,play-government-apps,play-financial-features,play-health-apps;play-production-countries(edits.countryAvailability is read-only) andplay-production-release(Edit release > Next > Save; the owner's "Send changes for review", a real click). New config:play.category(checked against Play's game / app lists; default from an exact App Store category match),play.countries,play.managedPublishing,console.contentRating.details(the IARC follow-ups each Yes reveals),console.appContent,social,products[].randomRewards(loot boxes Yes / No from the catalog).checkwarns without a Play category and counts the App content answers still owed;validateadds the category reminder and "production has 0 countries";play statusshows the category as a console step;play releasewarns that an approved production release goes live by itself while managed publishing is off. -
IARC content rating plan: page 1's IARC Terms of Use box is an agreement (never pressed, never ticked: the click gate now refuses "terms of use"); the questionnaire is one long page whose Yes answers reveal follow-ups; the Summary's final button is "Save", not "Submit"; App content forms deep-link to their card pages.
-
Every console plan ships
inPageLoop, a ready-made in-page helper (window.__sb.run, polled through window.__log): one evaluate call times out at about 45 s on big Play pages. Play plans andconsole fillclick the inner[role=checkbox]of material checkboxes like the material radios. -
App Store: appstore-first-release and store-iap-setup send first IAPs with
apple iap-submit --with-version(verified on a real first release), not the console;console verify apple-app-privacyreads "Published … ago by …" as the published signal. -
Fix (
play data-safety):PSL_SUPPORT_DATA_DELETION_BY_USERis Play's general "Do you provide a way for users to request that their data is deleted?". storebound answered it frompartialDeletion(data deletion without deleting the account), so an app with a delete-account page declared "no". Nowaccounts.deletionUrlanswers yes, withPSL_DATA_DELETION_URLset topartialDeletionUrlor the delete-account page. Without a page, the answer comes from the newdataSafety.dataDeletion: 'yes' | 'no' | 'auto90', or else fromdeletionRequest(the same question under the form's old id, no longer deprecated).checkflags any answer other than yes next to a delete-account URL. -
Price changes of existing products need
--allow-price-changes(play products,play subscriptions,apple subscriptions,prices import;apple iapnever changes an existing price).--yes, also the oneflow run --apply --yesadds to write steps, confirms creates, first prices and other updates only. Without the flag a run writes everything else, printsprice change on <id> (<from> -> <to>) needs the owner: re-run with --allow-price-changesper product and exits 3 (declined), so a flow stops with "step N declined". The terminal question (no--yes) and the desktop owner action still cover them. Dry runs mark those rowsowner; flows refuse the flag in steps and never add it (lib/price-gate.ts, tests/price-gate.test.ts). Activating the purchase option of an existing Play product is a normal update now (--yesconfirms it). -
CLI wording: when
` / `console verify `), not "open it yourself" / "fill them yourself". Sign-in, agreements and the final Submit / Create / Publish stay the owner's. Other "by hand" output (fastlane import, catalog pull, ppp, scan) points at the config edit instead. `tests/do-it-dont-ask.test.ts` now scans the CLI's output strings too.console fillcannot open or fill part of a form (console UI changed, an unknown page or dialog step), it says what is left and that the calling agent does it with its browser tool (`storebound console plan -
Agent rules: a first, top-level rule "Do it, don't ask" in
docs/agents-core.md(and so AGENTS.md, GEMINI.md, the Copilot and Cursor rules), README ("How agents use storebound"), the store-pitfalls skill and the openings of console-autofill, play-first-release, appstore-first-release and store-declarations. The agent does API calls, uploads, builds, exports, console navigation and forms itself and runs the real command after the dry run; the owner presses only the final public or irreversible buttons and answers the facts no file proves in one batch at the start. Skill text that left navigation, draft saves, exports or uploads to the owner is rewritten;tests/do-it-dont-ask.test.tsguards it. Pitfall 26. -
Fix (
play data-safety): apps with accounts got Play 400 "Invalid safety labels declaration: Line 7: Response ID missing". Play's current form asksPSL_SUPPORT_DATA_DELETION_BY_USERas a single choice with three options; storebound wrote it as a bare TRUE / FALSE. It now writes one row per option with TRUE on the picked one, anddataSafety.partialDeletionistrue | false | 'auto90': true =DATA_DELETION_YES(plusPSL_DATA_DELETION_URL), false =DATA_DELETION_NO,'auto90'=DATA_DELETION_NO_AUTO_DELETED("No, but user data is automatically deleted within 90 days"); left out,checknames it as missing.- Play removed
PSL_DATA_COLLECTION_USER_REQUEST_DELETEfrom the form, so it is no longer written and has no rule.dataSafety.deletionRequestis deprecated and optional;checkprints a note to remove it.profilechecks,scanand the desktop app no longer ask for it (the desktop Data safety card shows the deletion answer instead). - Google's empty form, exported from Play Console on 2026-10-12, ships as
cli/data/play-data-safety-form.csv(embedded in the binary). Every row's Answer requirement (and label) comes from it,dataSafety.extrarows included (they were guessed as MULTIPLE_CHOICE / MAYBE_REQUIRED). NewformPairIssues: every (Question ID, Response ID) pair must be in that form, orcheck,project.declarations,play data-safety --dryand the real run (before the POST) report ", is not in Play's current Data safety form ...; Play answers 400". Refresh the file when Play changes the form (Play Console > App content > Data safety > Export to CSV, nothing answered).
templates/data-safety.csv(the filled example frominit) regenerated against the form. Pitfall 21.
- Play removed
-
Fix (
play build): the Android SDK is found the way Android Studio finds it, so a non-interactive shell (noANDROID_HOME) with a gitignoredlocal.propertiesno longer ends in Gradle's "SDK location not found". Order:ANDROID_HOME,ANDROID_SDK_ROOT,sdk.dirin the Gradle project'slocal.properties, then~/Library/Android/sdk(macOS) /~/Android/Sdk(Linux) /%LOCALAPPDATA%\Android\Sdk(Windows); only a folder withplatforms/orplatform-tools/counts. Gradle gets it throughANDROID_HOMEandANDROID_SDK_ROOT;local.propertiesis never written. None found: the build stops before Gradle and lists every place checked. The dry run andstorebound doctor(also--json:androidSdk) show the SDK path and where it came from. Pitfall 25. -
Fix (
apple upload): never silent. The IPA size at the start, a line per part (every 10th from 20 parts), a heartbeat every 20 s while a part is in flight ("still uploading, 4m30s, 312/890 MB (35%), part 4/12"), the same heartbeat while--via altoolruns (now in the background, stopped at the deadline) and while the build is processed (apple testflight waittoo). Each part has a 15 min limit and is retried;--timeoutnow also covers the parts and stops with exit 7 and the hint to run the same command again (it resumes the build upload). Every App Store Connect API call has a 2 min limit (a hung connection is retried when safe, never a silent wait). -
Fix (
flow run, every confirming command): a declined confirmation is no longer a success.confirm()printsdeclined: <reason>and exits 3 when the owner answers no, when there is no terminal and no--yes, or underservewithout the owner's dialog;flow runstops at that step with "step N declined: " (statusdeclined), never "ok".flow run --apply --yesis the owner's approval for the run: write steps get--yes(reads, waits and owner steps do not);--yeswithout--applyis refused. Commands that went on with a partial write after a declined part (play productsprices,testflight testers --invite,distribute --submit-review) now exit 3 too. Pitfall 24. -
Fix (
apple build,play build, Capacitor): a native build no longer ships old web assets silently. Newbuild.prebuild(per storeapple.prebuild/play.prebuild): a shell command run in the project root before the archive / bundle (e.g.npm run build && npx cap sync; printed, output streamed, nonzero exit stops the build,--dryonly prints it,--no-prebuildskips it, once perflow runinstead of once per store). After it, the copiedios/App/App/public/index.html/android/app/src/main/assets/public/index.htmlmust be newer than the last git commit outside ios/ android/ node_modules/ store/ .storebound/ and<webDir>/index.html, else the build stops with "web assets are older than the source (copied ..., source ...)";--allow-stale-weboverrides.checkwarns when they are stale and no prebuild is set. The build output and the dry-run plan (webin--json) show the web bundle that went in: sha256 (12 hex) and mtime of the copied index.html. Pitfall 23.- Monorepos:
play buildfailed with "no Gradle project (settings.gradle) in android or ." when the Capacitor app lives inapps/native(capacitor.config.ts, android/, ios/ there).apple build,play build, the staleness check andchecknow use the root's capacitor.config., else the one found one or two levels down (package.json workspaces, apps/, packages/*, direct child folders; node_modules skipped) and name it ("Capacitor project: apps/native"). Several: they are listed andplay.build.dir/apple.build.workspace|projectis asked for; an explicit config value always wins.
- Monorepos:
-
Fix (
play build,play upload,play release): real AGP bundles (e.g. Capacitor 8) no longer fail with "AAB manifest without package / versionCode". The aapt2 proto manifest is read with Resources.proto's field numbers. Pitfall 22. -
Fix (
play data-safety): apps with accounts got Play 400 "Invalid safety labels declaration: Response missing for PSL_ACCOUNT_DELETION_URL". WhendataSafety.accountCreationis notPSL_ACM_NONE, the CSV now carriesPSL_ACCOUNT_DELETION_URL=accounts.deletionUrlandPSL_SUPPORT_DATA_DELETION_BY_USER= the newdataSafety.partialDeletion(delete some or all data without deleting the account; ask the owner), plusPSL_DATA_DELETION_URL= newdataSafety.partialDeletionUrl(defaultaccounts.deletionUrl) when that is true.- Required-row rules in one table (
DATA_SAFETY_RULES, per data typetypeRules,dataSafetyIssues): account creation -> deletion rows, a selected data type -> collected / shared, ephemeral, required / optional, purposes,PSL_HAS_OUTSIDE_APP_ACCOUNTSrefused with sign-up methods.storebound check,project.declarationsandplay data-safety --drysaymissing: <id> (<when>; set <config field>); the real run stops before the POST. - A
dataSafety.extrarow with a generated question id now replaces that question instead of adding a second one. - Sources: support.google.com/googleplay/android-developer/answer/10787469 , support.google.com/googleplay/android-developer/answer/13327111 . Pitfall 21.
- Required-row rules in one table (
-
Agent-first console mode.
storebound console plan <form> [--json](read-only, offline;--lookupasks the App Store Connect API for the app id) gives an AI agent everything to fill a console form with its own browser tool: the start URL (deep links from apple.appId, play.developerId, the new play.appId and the remembered ids), ordered steps (what to click by visible text), the answer sheet (each answer certain or ask-owner, with the console's exact labels, curly quotes noted), the never-press list (Publish, Submit, Create, agreements, help panels), the known UI facts per form and how to verify.storebound console verify <form> [--from-text <file|->](read-only, exit 1 on a mismatch) checks the result: App Privacy from the page text (per-type "Used for …", linked, tracking), Play Create app from the input states, content rating and target audience from their summaries, the App Store record and age rating from the API. MCP:console_plan,console_verify(read-only;console fillstays off MCP). The console-autofill skill now puts the agent-driven mode first;console fillis the optional scripted fast path. Pitfall 20. -
App Privacy, verified on a real console (2026-10-12): two information steps in each type dialog ("Let’s define two important terms…", "To help put tracking into context…"), purpose checkboxes without an accessible name (matched by the nearest parent text), "Definitions and Examples" never pressed. The scripted engine handles all of it.
-
Play Create app, verified through Claude in Chrome (2026-10-12): the form's own URL
/console/u/0/developers/<id>/create-new-app(the engine opens it directly with a developer id); the App package name field (fromplay.packageName, max 150) and its read-only "Check availability" ("Package name available"), which storebound presses; the<material-radio>gotcha (the native input does not update the form's model: the engine clicks the inner[role=radio]); the app dashboard/developers/<id>/app/<appId>/app-dashboard(the app id is remembered and deep-links the App content forms); after Create app the owner adds the new app to the service account (Users and permissions > the service account > Add app), never an agent. The old note "the first AAB upload sets the package name" is gone. -
Confirmed (2026-10-12): the earlier default copy of the owner's Google cookies DID sign the owner out of Play Console in their real Chrome (pitfall 18). The copy has been opt-in since the change below.
-
Fix (
play images): a phone-only project no longer aborts with "no sevenInchScreenshots ... (render first)". Play requires the 512 icon, the 1024 x 500 feature graphic and at least 2 screenshots (Play Console Help answer 9866151); 7" / 10" tablet screenshots are optional.play images(dry and real) uploads what exists (icon, feature graphic, phone screenshots; tablet, TV and Wear sets only when rendered) and printstablet screenshots: none (optional; the app isn't eligible for tablet featuring without them). Missing required sets are still errors, with the fix.- New
play.tablets?: boolean(default: true when tablet renders exist, else false).false:shots render,shots check,shots gallery,validateandplay imagesnever ask for tablet sets.true: rendered, and missing ones are errors (local renders and the listing on Play). - A missing optional set never deletes the screenshots Play has for it: the dry run shows
keeps <type> on Play; new--prunedeletes them (shown aswould deletein the dry run; not exposed over MCP). Pitfall 19. - Languages: the ones
play listingpushes. A status=draft / status=translate section is skipped (skipped de-DE: no Play listing yet (machine-translated draft, not reviewed ...), or--include-drafts), and a language Play has no listing for (404 "Listing for language 'de-DE' not found.") is skipped with an info line instead of aborting.
- New
-
Safety (
console fill): storebound no longer copies the owner's Chrome cookies by default. A copied Google session reached Play Console once, then Google answered/v3/signin/rejected: Google binds its session cookies to the device (Chrome's Device Bound Session Credentials) and signs out sessions whose cookies look stolen, which can sign the owner out of their real Chrome. Sources: blog.google/security/protecting-cookies-with-device-bound-session-credentials , knowledge.workspace.google.com/admin/security/investigate-and-take-action-on-suspicious-session-cookies .- Default: storebound's own profile per store (
~/.storebound/console-profile/<store>); the owner signs in once by hand (the first run prints what to do) and it is kept. - storebound starts that Chrome itself (
--user-data-dir,--remote-debugging-port=0) and attaches over CDP: no--enable-automation,--disable-blink-features=AutomationControlled(navigator.webdriverfalse), none of Playwright's launch switches, Chrome's own user agent. Graceful close by CDPBrowser.close. --use-chrome-session google|apple: explicit opt-in to the old copy, now of only that store's cookies; Google's prints a warning about device-bound sessions. Apple's is opt-in too (Apple publishes no binding, but no guarantee).- Google's
/signin/rejectedpage is said plainly: "Google refused sign-in in this window". - The desktop dialog no longer says it uses the Chrome sign-in. Pitfall 18.
- Default: storebound's own profile per store (
-
App Privacy purposes (wrong answers in
console fill apple-app-privacy,project.declarationsandscan): Play "advertising" became both Third-Party Advertising and Developer's Advertising or Marketing (Device ID, Coarse Location, Product Interaction, Performance Data). Now one Apple purpose per Play purpose, following Apple's definitions (developer.apple.com/app-store/app-privacy-details/): advertising -> Third-Party Advertising, analytics -> Analytics, personalization -> Product Personalization, functionality / fraud / account / communications -> App Functionality. Developer's Advertising or Marketing is No unless the owner says so; with a Play "communications" purpose console fill asks. -
New
appPrivacy.types: the owner's App Privacy answers per Apple data type id (all 35: USER_ID, DEVICE_ID, GAMEPLAY_CONTENT, ADVERTISING_DATA, CUSTOMER_SUPPORT, ENVIRONMENT_SCANNING, HANDS, HEAD ...), each{ collected?, linked?, tracking?, purposes? }(THIRD_PARTY_ADVERTISING, DEVELOPERS_ADVERTISING, ANALYTICS, PRODUCT_PERSONALIZATION, APP_FUNCTIONALITY, OTHER_PURPOSES), validated against Apple's lists.console fill apple-app-privacyfills them as certain answers (they win over what dataSafety suggests); types with no Play equivalent work from the block alone; anything unset stays "(you decide)".scan --writeproposescollected+purposesfor detected types the block does not answer; neverlinked/tracking(the SDK disclosures do not prove them).checkcompares the block with the iOS project, both ways: a type used for tracking needsNSUserTrackingUsageDescriptionandNSPrivacyTrackingtrue (and the reverse is flagged), and the app'sNSPrivacyCollectedDataTypes(manifest keys mapped to App Privacy ids) must match the types marked collected.- templates/app-privacy.md, the store-declarations, console-autofill and store-pitfalls skills document it.
-
console fill play-app-record: Play Console's Create app form, the Play counterpart ofapple-app-record(the Play Developer API cannot create apps).- Account:
play.developerId, the workspace account's id or the remembered one; else the only account the login sees; else the chooser entry namedplay.developerAccount(new optional field: the account's display name, e.g. an organisation account). Several accounts with none set, or a name that matches none or two: it stops and lists them. Remembered in~/.storebound/console-accounts.json. - Navigation: All apps > the page-level Create app button, through a dedicated allowlist (
createAppAllowed: exact name "Create app" / "Uygulama oluştur", button or link, on the app list, no Create app form open, not inside a form or dialog). The form's own Create app stays refused. - Fills App name (
play.name, new, elseapp.name; 30 characters checked), Default language (Play Console label, e.g. "English (United States) – en-US"; matched by code in a Turkish console), App or game (profile) and Free or paid (play.free, new, else Free for IAP / ads only) as an owner check: outlined, with the note that a free app can never become paid. The Developer Program Policies and US export laws declarations are never ticked (anagreementfield is outlined whatever its answer). The log and guidance say the package name is set by the first AAB upload (play upload --track internal/flow run release). - In
--dry, theconsole filllist, the MCP / desktop form list and the desktop App info "Play Console forms" card.
- Account:
-
shots checkno longer reportsi drawn with a system fontfor captions with ligatures ("Outfits" in en / de / nl with Montserrat). The font usage probes (one span per character, read with CDPCSS.getPlatformFontsForNode) sat side by side in one line, so Chrome shaped "f" + "i" into the fi ligature across them and the lone "i" probe reported no glyph of ours. Each probe is now its own block. The Google Fontstext=subsets were fine (they keep the GSUB ligatures); regression test with a real Montserrattext=subset (tests/glyph-ligatures.test.ts). -
--help/-hon every command, subcommand and group (storebound shots render --help,storebound apple --help) prints the usage (the module's usage lines, the description and the options it accepts) and exits 0 before anything loads; it used to start the command (a full render). An unknown flag (--dyr) exits 2 with the usage before anything runs (cli/data/flags.json,bun tools/gen-flags.ts).storebound shotsalone lists the shots commands.storebound ads apple ...is reachable again (the plainadscommand used to swallow it). -
shots render --scenestakes scene ids (the usage said numbers); an unknown id fails at once with the list of ids instead of a 30 s page timeout. -
Faster CI:
bun run ciruns as a task graph (independent steps at once,[step]-prefixed output, fail fast, time per step) with the tests split into parallel shards (CPU cores - 1, max 8) balanced by recorded durations, browser suites in shards of their own. Newbun run ci:quickfor the inner loop: typecheck plus the tests related to the change against origin/main (through imports) plus scrub. The desktop Rust host runs only whensrc-taurior the sidecar build changed (CI=fullalways; Forgejo CI sets it). Fixed sleeps in tests became event waits. -
ASO from official and public data only (no scraping, no third-party ASO services):
aso competitors [--term <kw>] [--storefront us,tr] [--top 10]: per keyword of the locale's keyword field (and--term) the top apps in the iTunes Search API (name, developer, rating count, first release, last update, genre localized to the storefront), the words in their names that the name / subtitle / keywords do not cover (only words of two or more developers: a one-developer word is likely a brand) and the apps new in a term's top N since the last snapshot (.storebound/aso/competitors/<date>.json). Same pace asaso rank(one call every 3.5 s, stop on 403 / 429). Competitor subtitles are not in any public API and are not shown.aso suggest [--locale] [--fresh] [--offline] [--write]: keyword candidates from the own name / subtitle / description (words and two-word phrases), competitor names, the localized category names (from the Search API'sgenres) and, with Apple Ads credentials, the Apple Ads Platform API keyword suggestions (POST /v1/suggestions/keywords/query, relative popularity 0-100). Each candidate shows relevance (own text, competitor names), competition (top apps with the word in their name) and popularity (Apple Ads only, else "unknown"). Proposes a keyword field per locale that fits 100 bytes with the existing rules (no spaces, no duplicates, plural-insensitive, no name / subtitle words, no filler or category words; current terms kept first unless--fresh) as old -> new;--writeputs it intostore/appstore.mdwith a draft line thatcheck --asoandaso auditreport until the owner deletes it.aso audit: one offline report:check --asofindings, the cross-locale free keyword room, rank trend arrows from theaso ranksnapshots, competitor coverage gaps, Play short description use and keyword density, App Store conversion from the analytics snapshots, Product Page Optimization / Play listing experiment ideas (suggestions only: PPO cannot test the subtitle; Play experiments stay in Play Console) and the next 3 actions.- MCP: read-only
aso_competitors(saves no snapshot),aso_suggest(never writes),aso_audit(offline). - Desktop: an ASO card on the Growth tab (
aso.audit): keyword field state, keywords ranked, coverage gaps, the rank trend of the best three keywords (visx, rank 1 at the top), the rank table and the next 3 actions. examples/demo-apphas listing files (store/appstore.md,store/listing.md) to try the ASO commands on.
-
Local font files in
shots.fonts:title,bodyandfallback[]take, besides a Google Fonts family, a path (relative to the config) or{ family, file, weight?, style?, license? }(TTF / OTF / WOFF / WOFF2, validated by extension and signature; the family comes from the name table when not given).shots fontschecks the file's cmap per language, tries the listed fallbacks in order before the automatic Noto ones (pl title: ę ł ą Ł not in ER Display -> Noto Sans Display), errors when nothing covers a character, and records the file in fonts.json with its path, sha256 and license. Render andshots videoserve it in place at/fonts/local/...(never copied); the hash in fonts.css invalidates the capture cache, and render refuses a font file changed sinceshots fonts.shots checktreats the local font and listed fallbacks as configured and warns when a local font has no license note. -
Idempotency: every store write is an upsert; running a command twice, or again after a crash or a lost answer, leaves exactly one of each thing (table of every write path:
docs/idempotency.md).- New
cli/lib/upsert.ts:findOrCreatelooks again with a fresh GET right before a create and asks a journal of recent creates (.storebound/created.json, a direct GET by id) so a re-run during the store's list lag reuses what the last run made;createOncetreats 409 / ALREADY_EXISTS as found and never sends a create blindly again after a timeout or 5xx (it looks first; only keys the store keeps unique get one more try). - The App Store Connect, Google Play, RevenueCat and
storebound apiclients retry 5xx / network errors only for requests that are safe twice (GET, PUT, PATCH, DELETE, relationship adds, state actions, price schedules); a POST create without a clear answer throwsAmbiguousWriteError. Plan runners treat a 409 duplicate as done and stop on an unclear create instead of retrying. - Uses it in
apple iap,apple subscriptions(groups, subscriptions, localizations, promotional offers, offer code campaigns, custom codes, review screenshot by MD5),apple offer-codes,apple cpp create,apple events create,apple ppo create,apple testflight groups,apple bundle,apple review-details(only changed fields),apple release --phased on,apple adhoc register/profile,apple signing ensure(a lost certificate answer keeps the key and finds the certificate by its public key),apple upload(a COMPLETE build upload is not sent again, an unfinished one is resumed),analytics setup,reviews send,revenuecat sync,play subscriptions(subscriptions and offers). - Review submissions (
apple iap-submit,cpp submit,events submit): the draft is reused or created once, an item already in it is not added again. - Screenshot and preview sets keep the leading files that already match (MD5, position, delivered), replace the rest in order and fix the order if Apple shows another; a crash midway converges on the next run without duplicates.
- Play edits: one open at a time per run, deleted on every error path, committed only on success, deleted instead of
committed when nothing changed (
ctl.unchanged()); redone only when nothing went live, never after a commit without a clear answer.play imagescompares sha256 first and replaces only changed image types;play releasereuses a versionCode Play already has and skips identical track releases;play rolloutcommits nothing when the track is already like that;play uploadis done when the versionCode is on the track already. - One writer per app and store: write runs take
.storebound/locks/<store>-<app>.lock(dry runs and reads do not); a second run stops with who holds it; a lock of a process that is gone is taken over;--force-unlock. tests/idempotency.test.tswith stateful store doubles (tests/helpers/fake-stores.ts): write commands run twice with zero writes on the second run, a crash in the middle of a screenshot set, IAP and subscription creates whose answer is lost, 409 duplicates, the list lag after a crash, Play edit error paths and an unclear commit, the lock. Pitfall 9 indocs/pitfalls.mdand the store-pitfalls skill.- The last open rows are closed (
docs/idempotency.mdhas none left):apple listing: the App Store version is created throughfindOrCreate(a fresh look right before the create, the journal for the list lag, a lost answer looks first); localizations throughcreateOnce(409 = found, then only the differing fields are PATCHed); unchanged localizations and an attached build are left alone.play listing: compares again inside the write edit and PUTs only differing details and languages; nothing left to change = the edit is deleted, not committed.apple appinfo: categories, age rating, content rights compared with the live values, only differences PATCHed.play data-safety: Play cannot read the form back, so the sha256 of the last uploaded CSV is kept in.storebound/data-safety.sha256; the same CSV is not sent again unless--force(MCPforce).- Twice-run tests for
apple listing(plus a lost version create and a 409 localization),play listing(plus a store that became current before the edit),apple appinfo,play data-safety,apple promoted,apple win-back,apple preorder,apple testflight testers,play uploadandprices import(Play).
- New
-
Faster
shots capture(web) andshots render(a 22-language game took about 50 min to capture, serially, with a new context and a full game boot per frame):--concurrency N(andshots.capture.concurrency): parallel browser contexts in one Chromium; defaults from the CPU count (capture min(3, cores / 4), render min(4, cores / 2)). Output files do not depend on the order; progress lines carry language / device / frame, n/total and an ETA. Render runs ffmpeg asynchronously and makes the contact sheets after the images.- Per-frame timing (goto, waitFor, eval, delay, screenshot, total) in each progress line and in
.storebound/capture-report.json, with time per step and the slowest frames at the end. shots.capture.session: 'per-language': one page per language and device, booted once; shots switch withnavigate: 'spa'(pushState + popstate, then waitFor / eval) or'reload'; a failed precondition falls back to a fresh page (reuseTimeoutMs).- Input-hash cache (
.storebound/capture-cache.json: shot entry, URL, frame size, session, copy text, app build fromshots.capture.buildor the served HTML + scripts, fonts): unchanged frames are skipped;--only-missing,--force. MCPshots_capture(concurrency,mode) andshots_render(concurrency).
-
apple listing --dry,play listing --dryandapple appinfo --dryprint a real plan (also--json), read-only: the version decision ofapple listing(keep, update the editable version's string while PREPARE_FOR_SUBMISSION, create, or refuse: number taken, not higher than a reviewed version, another version in review / pending release), the build to attach, and per locale / language create or update with old -> new per field (long text cut with its length, unchanged fields summarized); app info name / subtitle / privacy URL, categories, age rating and content rights the same way. Locales whose section isstatus=draftorstatus=translate(listing translate) are skipped with the reason;--include-draftssends them. The real run prints the same plan, asks in a terminal and applies exactly it (unchanged localizations are no longer PATCHed, Play PUTs only changed languages); what's new is left out on an app's first version. -
apple.name: the App Store name in the primary locale (at most 30 characters, validated) for when the plain name is taken on the App Store ("already being used").app.namestays the Play and on-device name.console fill apple-app-recordfills Name from it (elseapp.name), andapple listing,apple listing pull(a pulled name equal to it is not written into the file), the listing completeness view,check --aso, the console app picker andscan's beta-wording rule use it for every locale without its own### nameblock.checkreports an App Store name over 30 characters in any locale and hints when every locale uses the plainapp.name(hintsin--json). -
Pitfalls from real use become guardrails: new skill
store-pitfalls(read before any store task, linked from the agent rules) anddocs/pitfalls.md(changelog of lessons; CONTRIBUTING: every real-use bug adds an entry and, where possible, a check or test).apple iap-reviewnames config products missing from App Store Connect's list (it lags a minute after a create) and exits 1 instead of skipping them silently;apple iapandplay productsre-read the list after creating and warn when a new product is not in it yet.cli/lib/dry.tsdryPlan(title, rows): the standard dry-run plan table.apple iap --drynow reads localizations, price schedule and availability and prints what a real run would add;apple iap-reviewandplay productsend their dry runs with the table. A test scanscli/**for--drybranches that print only a count (allowlist with TODOs for commands being fixed elsewhere).
-
Screenshot glyphs (a Turkish render showed fallback glyphs for "ş" / "ğ" while
shots checkpassed): the cause was not subsetting (css2text=already takes glyphs from every subset) but the title font: Lilita One has no ş ğ İ and most of Latin Extended-A, so Chrome silently drew them with another font.shots fontschecks every character of every language (copy.json + config languages) against the cmap of the faces that draw it (new pure-TS cmap reader for TTF / OTF / TTC / WOFF / WOFF2, no dependency), adds an automatic Noto fallback per missing character (Noto Sans Display for titles at a heavy weight, Noto Sans, Noto Sans