Updates are agent steps: --yes ships an update of a live app (Play production, App Store submit / release) without an owner gate; the first release stays the owner's (exit 3 OWNER_STEP, --first-release); apple release is a ship step

This commit is contained in:
CRTkafa 2026-10-26 18:09:50 +07:00
parent c247b7e54a
commit ae9d26d3f6
28 changed files with 562 additions and 44 deletions

View file

@ -15,6 +15,12 @@ storebound plus you, the AI agent, do the work. The owner is not your pair of ha
testers. (Also the owner's: canceling a submission, starting or stopping a store experiment, a console form's final
Submit / Create that a console plan lists in `buttons.final`, signing in and granting account permissions.) For
these you prepare everything, show the dry run, and the owner presses one button or says yes.
**Updates are agent steps:** a new version of an app that is already on the store (Play: the production track holds
a published release; App Store: a version of the platform is or was on sale) is yours end to end, no owner gate
per update: `storebound play release <aab> --production completed --yes`, `storebound play upload --track
production --yes`, `storebound apple submit --build <n> --yes`, `storebound apple release --manual-release --yes`
(docs/bypass.md). Only the **first-ever release** of an app stays the owner's (exit 3 `OWNER_STEP`: after the
owner's yes re-run with `--first-release`), plus removal from sale, live price changes and anything that costs money.
**Bypass mode** (`storebound settings bypass status`): when the owner turned it on, you run these owner steps too:
MCP `owner_step` (dry run first, then `dry_run: false`), `storebound api ... --yes --confirm <operationId>`, and
console plans' `buttons.final`. Say in one line what you pressed; every action is logged to

View file

@ -11,9 +11,34 @@ storebound settings bypass off # back to the owner pressing every final butt
It is stored in `~/.storebound/settings.json` (this machine only), never in a project's config or repo.
## Updates are agent steps (no bypass needed)
The owner's standing rule: a new version of an app that is **already on the store** ships end to end by the agent or the
pipeline, with no owner gate per update: submit for review, promote to production, roll out, release an approved
version. This holds with bypass mode off, in a terminal and in CI:
| Store | Update commands (all take `--yes`; `--json` / `--result-file` for the [ship-step contract](ship-step.md)) |
|---|---|
| Google Play | `storebound play release <app.aab> --production completed --yes` (or `--production inProgress --fraction 0.05`), or `storebound play upload <app.aab> --track production --yes` |
| App Store | `storebound apple submit --build <n> --yes`, then (manual release) `storebound apple release --manual-release --yes` |
What makes it an update is read from the store, never from the config: Play's production track holds a completed /
inProgress / halted release; the App Store platform has a version that is or was `READY_FOR_SALE`
(`READY_FOR_DISTRIBUTION`, `REPLACED_WITH_NEW_VERSION`). The result line carries `releaseKind: "update"`.
Still the owner's, bypass mode or not (the commands refuse with exit 3, `error.code` `OWNER_STEP`, "first release of this
app"): the **first-ever release** of an app (the first production release on Play, the first submission or release on
the App Store), removal from sale, live price changes and anything that costs money. The owner approves a first
release once: after the owner's yes in chat re-run with `--first-release` (or turn on bypass mode). A person at a
terminal answering the normal confirmation prompt (no `--yes`) is the owner's approval too, and so is the desktop's
owner dialog. Re-running an update that is on the store already is exit 7, nothing sent.
MCP is unchanged: it still does not offer `--yes` or `--production`; an update goes through the CLI in a terminal or
pipeline (or `owner_step` in bypass mode).
## What it unlocks
- MCP: the `owner_step` tool appears. It runs one of: `apple submit`, `apple resubmit`, `apple iap-submit`, `apple release`,
- MCP: the `owner_step` tool appears. It runs one of (a first release also passes the gate in bypass mode): `apple submit`, `apple resubmit`, `apple iap-submit`, `apple release`,
`apple preorder`, `play release`, `play rollout`, `reviews` (replies), `apple testflight` (testers), `play products`,
`play subscriptions`, `apple iap`, `apple subscriptions`, `prices` (live prices). Dry run first (`dry_run` defaults to
true), then `dry_run: false` runs it with `--yes`.

View file

@ -9,6 +9,7 @@ fails without the fix.
| # | date | lesson | guard |
|---|---|---|---|
| 96 | 2026-10 | A pipeline could not ship a production update of a live app: production was treated as the owner's for every release, though the owner's rule is updates end to end and only the first-ever release gated. | `cli/lib/release-kind.ts` reads update vs first from the store; `--yes` ships an update (Play `play release --production` / `play upload --track production`, App Store `apple submit` / `apple release`), a first release with `--yes` is exit 3 `OWNER_STEP` (`--first-release` or bypass approve it), a re-run is exit 7; `tests/update-gate.test.ts` |
| 95 | 2026-10 | The Google service-account keys moved to the owner's credential broker, but storebound still wanted a local `PLAY_SERVICE_ACCOUNT`, and a Play write with a missing permission ran its edit before Google's bare 403. | `cli/lib/broker.ts` (broker.json, key file mode 600, token cache and single flight, grants, `playPreflight` before Play write commands), `playToken` in `cli/lib/google-auth.ts` (broker first, local key fallback when it does not answer), `cli/lib/google-access.ts` (403 SERVICE_DISABLED / PERMISSION_DENIED -> exact API or grant with its link, never retried), `validate` blockers; `tests/credential-broker.test.ts` against a local fake broker. Skill pitfall 122, docs/credential-broker.md. |
| 94 | 2026-10 | An app with managed publishing was approved but never went live: `play status` said "In review / ready to publish" with no next step, and the Publishing overview "Publish N changes" button (production + Data safety) was easy to miss or to confuse with Discard changes. | `console plan play-publish-changes` (final "Publish N changes" is the owner's, Discard changes / Stop managed publishing never-press), `console verify play-publish-changes` (API), the hint in `play status`, `status` and the desktop Releases tab (`cli/lib/play-publish.ts`, `tests/console-play-plans.test.ts`). |
| 93 | 2026-10 | A git-ignored, never-captured frames folder rendered store images with no app or game in them; render only logged 404s and `shots check` said 0 errors. | `shots render` refuses to start while a scene frame is missing (`framesForRender` in `cli/shots/lib.ts`) and fails when any load failed; report.json `missing` is a `frame-missing` error in `shots check` / `validate` (`cli/shots/report.ts`, `tests/shots-missing-frames.test.ts`). |

View file

@ -14,6 +14,7 @@ commands below keep their normal behaviour in a terminal; the contract switches
| `storebound apple upload` | `upload` | `apple` |
| `storebound apple submit` | `submit` | `apple` |
| `storebound apple resubmit` | `resubmit` | `apple` |
| `storebound apple release` (`--manual-release`) | `release` | `apple` |
A write still needs `--yes` in a pipeline: without it the confirmation is declined and the run ends with exit 3
(`CONFIRMATION_REQUIRED`). `apple submit` without `--yes` is a dry run (exit 0, `state: "planned"`).
@ -25,7 +26,7 @@ A write still needs `--yes` in a pipeline: without it the confirmation is declin
| 0 | ok | - |
| 1 | failure (build failed, network, timeout, blockers) | `FAILED`, `TIMEOUT`, `BLOCKERS` |
| 2 | usage (unknown flag, bad value, bad `--version-from` / `--env-file`) | `USAGE`, `ENV_FILE` |
| 3 | needs the owner | `AGREEMENT_PENDING`, `SIGN_IN_REQUIRED`, `COMPLIANCE_ANSWER_MISSING`, `CONSOLE_STEP`, `CONFIRMATION_REQUIRED` |
| 3 | needs the owner | `AGREEMENT_PENDING`, `SIGN_IN_REQUIRED`, `COMPLIANCE_ANSWER_MISSING`, `CONSOLE_STEP`, `CONFIRMATION_REQUIRED`, `OWNER_STEP` |
| 4 | the store refused it (4xx answer, ITMS error, invalid build, rejected upload) | `STORE_REFUSED`, `BUILD_INVALID` |
| 7 | nothing to do: the build / release is on the store already (the result names what is there) | - |
@ -37,6 +38,31 @@ Console API access, Data safety, Content ratings, Publishing overview). The link
Note: `apple upload` and `apple testflight wait` keep their old exit 7 for a processing timeout when the contract is
off; under the contract a timeout is exit 1 `TIMEOUT` and an invalid build exit 4 `BUILD_INVALID`.
## Updates and first releases
The production and review steps (`play upload --track production`, `play release --production completed|inProgress`,
`apple submit`, `apple resubmit`, `apple release --manual-release`) read from the store whether this is an **update** of
an app that is already live or its **first release** (Play: the production track holds a completed / inProgress / halted
release; App Store: a version of the platform is or was on sale; `cli/lib/release-kind.ts`). The result carries
`releaseKind`.
- Update: `--yes` is all it takes. No owner gate, no bypass mode needed ([docs/bypass.md](bypass.md)).
- First release with `--yes`: exit 3, `error.code` `OWNER_STEP`, message "first release of this app ... the owner
approves it once", `error.consoleUrl` the page where the owner acts, nothing written. After the owner's yes: re-run
with `--first-release` (or bypass mode on).
- Already on the store (the versionCode on production with that status, the version submitted or released): exit 7,
nothing sent.
```sh
# Android production update
storebound play release build/android/app.aab --production completed --yes --json --result-file out/release.json --env-file /run/secrets/ship.env
# (or a staged rollout: --production inProgress --fraction 0.05 --yes)
# iOS update: attach the uploaded build, submit, and later release the approved version
storebound apple submit --build 42 --yes --json --result-file out/submit.json --env-file /run/secrets/ship.env
storebound apple release --manual-release --yes --json --result-file out/release.json --env-file /run/secrets/ship.env
case $? in 0|7) ;; 3) echo "owner: $(jq -r .error.consoleUrl out/release.json)"; exit 1 ;; *) exit 1 ;; esac
```
## `--json`: one final line
stdout carries exactly one line, at the end, also on failure:
@ -104,6 +130,7 @@ Reads the version and build for the step. Accepted shapes:
- `play release`: every track already holds the same release: exit 7, nothing committed.
- `apple submit`: the version is already submitted (or further) with this build, or the submission is in review:
exit 7.
- `apple release --manual-release`: the version is released (`READY_FOR_SALE` ...) and none waits for release: exit 7.
## `storebound status`