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.
8.2 KiB
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:
{
"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_modulesand 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>andaccount:<profile>, as in the desktop's ignore list. - overrides: the desktop's pairing corrections (
pairs,unpairs,linksof a store app to a folder,appStudio), with folders named by their path. Astorebound.workspace.tswithexport 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):
# 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
--dryunless--apply, and even then each command asks its own confirmation; wait steps check once;sh:steps (your own build commands) run only with--apply.--dryprints the plan without running anything.--apply --yesis 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: truesub-flows),before/after/onErrorare hooks,continueOnErrorkeeps going.- A failed step stops the flow;
--resumestarts again at that step with the captured variables (.storebound/flows/<name>.json),--from <step>anywhere.flow validatechecks commands, variables, duplicate ids and sub-flow cycles. Several flows can also live instorebound.workflows.yml|json|ts. - MCP:
flow_listandflow_run(the dry mode only;--applystays a terminal command). Skill:release-flows.