storebound/docs/studios.md
CRT 710c3a53d0 README, docs and site: one story (the whole job, no avoidable rejections, the desktop app, your AI, install)
README is a short front page (about 120 lines) with one real visual per section; every former section moved
unchanged into docs/*.md with a docs/README.md index (journey, rejections, getting-started, commands, growth,
studios, store-art, listing-languages, api, skills, mcp, safety, faq, project). The site tells the same story on one
page: the teaser in the hero, real CLI output as text, the desktop app as a section with light/dark captures of eight
screens and the per-system downloads (desktop/latest.json); /desktop/ redirects to /#download. New capture tools:
tools/site-desktop-shots.ts (fixture-mode desktop frames) and tools/site-term-shots.ts (real CLI output as text and
WebP). tests/docs-links.test.ts checks README, docs and site links resolve.
2026-10-13 03:29:59 +08:00

137 lines
8.2 KiB
Markdown

# Many apps, studios, release trains and workflows
## Many apps and studios
A studio with twenty games, or a developer with apps under two store accounts, works from one folder above the app
folders. `storebound ws init` writes `storebound.workspace.json`:
```json
{
"apps": ["games/*", { "path": "apps/notes", "apple": "solo-apple" }],
"studios": [
{ "name": "Acme Games", "color": "indigo", "apps": ["games/*"],
"accounts": [{ "store": "apple", "profile": "acme-apple" }, { "store": "play", "profile": "acme-play" }] }
],
"accounts": [{ "store": "apple", "profile": "solo-apple", "label": "Solo" }],
"ignore": ["games/old-*"],
"overrides": { "pairs": [], "unpairs": [], "links": [], "appStudio": [] },
"concurrency": 4
}
```
- **apps**: folders or globs (only folders with a `storebound.config.*` count; `node_modules` and dot folders are
skipped). An object entry names the folder's own credential profiles.
- **studios**: name, colour (the desktop's set), the store accounts and the app folders (globs) that belong to it.
- **accounts**: store accounts without a studio. An account is a store and a **credential profile** name, never a key.
- **ignore**: folder globs, `app:<store app key>` and `account:<profile>`, as in the desktop's ignore list.
- **overrides**: the desktop's pairing corrections (`pairs`, `unpairs`, `links` of a store app to a folder,
`appStudio`), with folders named by their path. A `storebound.workspace.ts` with `export default { ... }` works too.
Which keys an app uses: its own entry's profiles, else the single account of that store in its studio, else the
single account of that store in the workspace, else the folder's own credentials (env or `.storebound/`).
**Credential profiles** keep many accounts apart on one machine: `~/.storebound/credentials/<profile>.json` with the
account names of `.storebound/credentials.json` (`PLAY_SERVICE_ACCOUNT`, `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_P8`,
`ASC_VENDOR_NUMBER`; the Android upload keystore belongs to the app and stays in the project;
relative key paths are relative to that folder, which `STOREBOUND_CREDENTIALS_DIR` can move), or env
`STOREBOUND_PROFILE_<NAME>_<KEY>` (for CI). Any command takes `--account <profile>`. `ws list` shows per account whether
its profile has keys; no command prints a key.
`ws status`, `ws check`, `ws validate` and `ws run` read the apps a few at a time (`concurrency`, 1-8, or
`--concurrency`), each app with its own keys handed to its child process only. `ws run` takes any read command and
the MCP tool set; write commands get `--dry` unless `--apply`, and submit, release, rollout, send, start / stop,
testers and `--yes` are refused: those stay with the owner in the app folder.
`ws apps import --account <profile>` lists the apps a store account sees (as `apps list`) into
`.storebound/workspace/` and pairs them with the folders by the desktop's rules (`cli/lib/identity.ts`, the same code):
a folder takes the store apps its config names, the remaining iOS and Android apps pair by id, close id or name,
`unpairs` keep two apart, and the rest are store-only apps that `ws status` reads too.
**The desktop app** keeps the same model (studios, accounts, folders, overrides). `ws desktop import` turns its
`state.json` (or `--state <file>`) into a workspace file and lists the credential profiles to create: the desktop's
keychain is never read. `ws desktop export` prints the workspace as a desktop state (`--out <file>`), and
`--merge --apply` adds it to the desktop's `state.json` while the app is closed (a backup is kept; nothing is
removed; new accounts get their keys in the desktop's Accounts screen).
MCP: `workspace_list` (offline) and `workspace_status` (read-only).
## Release trains
One version, both stores, one track: **build > testing > review > live > rollout**. The CLI and the desktop's Releases
tab share the code (`cli/lib/train.ts`, `cli/lib/portfolio.ts`), so both show the same stage for the same store state.
```
$ storebound release status
app version train App Store Google Play
------ ------- ------- ---------------------- -----------------------------
Puzzle 2.3.0 current rollout 20% (build 45) rollout 10% (build 230)
Puzzle 2.4.0 next build testing (build 240, internal)
```
`release plan <version>` lists what is left for that version on each store in fix order: config problems (`check`),
the version on the store (how to create it when it is missing), store blockers (`validate`, while nothing is
submitted), the next move and the warnings. Owner steps (submit for review, promote to production, release an approved
version, raise or complete a rollout) are named with where the owner does them; storebound does not do them. A store
it cannot read is "unknown", never "not there". Both commands work in an app folder (that app) and in a workspace
(every app, or `--app a,b`); `--json` gives the structured result. MCP: `release_status`, `release_plan` (read-only).
## Repo-local workflows
Your release routine as a file in the repo, run step by step. `storebound init` writes two to start from
(`store/workflows/release.yml`, `beta.yml`; app and game versions):
```yaml
# store/workflows/release.yml (app profile, shortened)
params:
VERSION: "1.0.0"
steps:
- id: next-build
if: apple
run: apple next-build
capture: BUILD
- id: build
if: apple
run: apple build --version ${VERSION} --build-number ${BUILD}
- id: upload
if: apple
run: apple upload --no-wait
# wait, play-build, play-upload, whats-new, listing, validate, scan, plan ...
- id: submit
owner: "App Store Connect: Submit for Review (version ${VERSION}, build ${BUILD}) ..."
```
The plan of that file (`--dry`: nothing runs; without it the reads run and `${BUILD}` is filled in):
```
$ storebound flow run release VERSION=1.2.0 --dry
[flow] 1/12 next-build: storebound apple next-build (plan only)
[flow] 2/12 build: storebound apple build --version 1.2.0 --build-number ${BUILD} --dry (plan only)
[flow] 3/12 upload: storebound apple upload --no-wait --dry (plan only)
[flow] 4/12 wait: storebound apple testflight wait --build ${BUILD} --version 1.2.0 --dry (plan only)
[flow] 5/12 play-build: storebound play build --version-name 1.2.0 --dry (plan only)
[flow] 6/12 play-upload: storebound play upload --track internal --dry (plan only)
[flow] 7/12 whats-new: storebound whats-new apply (if exists store/whats-new: false)
[flow] 8/12 listing: storebound apple listing --version 1.2.0 --build ${BUILD} --dry (plan only)
[flow] 9/12 validate: storebound validate (plan only)
[flow] 10/12 scan: storebound scan (plan only)
[flow] 11/12 plan: storebound release plan 1.2.0 (plan only)
[flow] 12/12 submit: YOUR CLICK App Store Connect: Submit for Review (version 1.2.0, build ${BUILD}). Play Console: promote the internal release and Send changes for review
```
- Read steps (check, validate, scan, release plan, next-build, status) run; **write steps run with `--dry`** unless
`--apply`, and even then each command asks its own confirmation; wait steps check once; `sh:` steps (your own build
commands) run only with `--apply`. `--dry` prints the plan without running anything. `--apply --yes` is your
approval of every write step of the run (no terminal needed); without it, a step whose confirmation cannot be
answered stops the flow as "declined", never "ok".
- Owner steps (submit, release, phased release, rollout, review replies, price imports, tester invitations, Beta App
Review) are never executed, also when written as `run:`; the flow ends with the list of your clicks.
- `capture:` turns a step's last output line into a variable (`${BUILD}`), `if:` skips a step (`apple`, `play`,
`exists <path>`, `${VAR} == x`), `flow:` reuses another flow (`private: true` sub-flows), `before` / `after` /
`onError` are hooks, `continueOnError` keeps going.
- A failed step stops the flow; `--resume` starts again at that step with the captured variables
(`.storebound/flows/<name>.json`), `--from <step>` anywhere. `flow validate` checks commands, variables, duplicate ids
and sub-flow cycles. Several flows can also live in `storebound.workflows.yml|json|ts`.
- MCP: `flow_list` and `flow_run` (the dry mode only; `--apply` stays a terminal command). Skill: `release-flows`.
---
[All docs](README.md) · [README](../README.md)