- one version: package.json's version is the desktop app's too (desktopVersion removed); tools/desktop-version.ts keeps the plugin manifests and the app's copies equal, desktop-release publish refuses another version, release order app first then npm publish; the unpublished desktop 0.1.0 candidate is noted in the docs and CHANGELOG - storebound desktop installs (per user, no admin: macOS DMG -> /Applications or ~/Applications, Windows NSIS /S, Linux AppImage + .desktop with the storebound:// handler), updates an older app, notes a newer one (downgrade only with --force), asks once ([Y/n], --yes, exit 3 without a terminal), opens on the folder; --dry / --json / --download; the install steps are pure plans (cli/lib/desktop-install.ts); the version's own manifest dl/<version>/latest.json - credentials: one store, the CLI's credential profiles in ~/.storebound/credentials; the desktop writes and reads them (profiles.rs, files 0600), moves old keychain accounts into profiles on first start, shows profiles the CLI made; precedence env > named profile > project file > default profile; config `account`, "account" in the project's credentials.json; storebound accounts list | import (dedup by key id / service account e-mail, key files copied 0600, project file keeps keystore and review contact, backup, --dry, never prints values) | use - app folders: one list ~/.storebound/projects.json shared by the desktop (registry.rs), storebound desktop, apps folders | add | remove, ws init and ws without a workspace file - desktop: notice when a folder pins another storebound version; storebound-desktop --version - tests: version drift, desktop decisions and per-OS plans, credential precedence and import, registry (TS + Rust), keychain migration and profile round trip (Rust), boot check with old keychain state and the new shared state
138 lines
8.4 KiB
Markdown
138 lines
8.4 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; its accounts name the credential profiles the app and the
|
|
CLI share (`~/.storebound/credentials`), so the keys are already there. `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). Without a workspace file, `ws` works over the machine's app folder list
|
|
(`~/.storebound/projects.json`, the desktop's App folders).
|
|
|
|
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)
|