docs/ship-step.md, pitfall 110 (pipeline exit 7 and double uploads), CHANGELOG Unreleased
This commit is contained in:
parent
06a0b424f5
commit
2e888a01cc
4 changed files with 149 additions and 0 deletions
|
|
@ -3,6 +3,12 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Ship-step contract for release pipelines (docs/ship-step.md): `play build | upload | release`, `apple build | upload | submit | resubmit` exit 0 ok, 1 failure, 2 usage, 3 needs the owner (with `error.code` and the console deep link), 4 store refused, 7 nothing to do; `--json` ends with exactly one result line on stdout, `--result-file` writes it atomically (also on failure), `--progress json` gives one JSON line per phase on stderr.
|
||||
- `--env-file <path>` loads KEY=VALUE secrets before the credentials resolve (POSIX: mode 600 or stricter) and every known secret value is scrubbed from stdout, stderr and the result file; `--version-from version.json` takes the version and build ({version, build}, {versionName, versionCode}, {marketing, build}).
|
||||
- Re-runs never upload twice: `play upload` with a versionCode Play has and `apple upload` with a build number App Store Connect has end with exit 7 and what is there; `play release` with the same release and `apple submit` of an already submitted build too.
|
||||
- `play upload --track production` (the pipeline's update step; `--status draft` keeps it for the Console) and `--version-code N` to pick the AAB.
|
||||
- New `storebound status [--app <name|store id|folder>] [--json]`: per platform the live and next version, build, track, review state and rollout %, read-only. `release status --app` also takes a folder or a store id.
|
||||
|
||||
- Desktop: every read goes through one registry: one store call per read however many buttons are clicked, overlapping buttons (Refresh all, card refresh, Check again, Import, Test) are busy and disabled together, a read just finished is not repeated, and all reads share the scheduler's rate limits and backoff.
|
||||
- Desktop: tray mini card on left click (rejections, releases moving, crash alerts, recent low reviews, refresh, quick actions); right click keeps the menu.
|
||||
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ run in the background with a log (`storebound shots render > .storebound/render.
|
|||
| `ws apps import --account <profile>` | API, read-only + local cache | the apps a store account sees, paired with the folders (the desktop's rules) |
|
||||
| `ws desktop export [--out f \| --merge [--apply]]`, `ws desktop import` | local | the workspace to and from the desktop app's state (labels, never keys) |
|
||||
| `release status [--app a,b] [--versions]` | API, read-only | both stores on one train per version: build > testing > review > live > rollout % |
|
||||
| `status [--app <name, store id or folder>] [--json]` | API, read-only | per platform: live and next version, build, track, review state, rollout % (docs/ship-step.md) |
|
||||
| `release plan <version> [--app a]` | API, read-only | what is left for that version on each store, in fix order, with the owner's next step |
|
||||
|
||||
</details>
|
||||
|
|
|
|||
130
docs/ship-step.md
Normal file
130
docs/ship-step.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Ship step contract
|
||||
|
||||
How a release pipeline (a Jenkins node, a Forgejo runner, any CI job) calls storebound without a person. The
|
||||
commands below keep their normal behaviour in a terminal; the contract switches on when a run passes `--json`,
|
||||
`--result-file`, `--progress json`, `--env-file` or `--version-from`. The code is in one module:
|
||||
`cli/lib/ship-step.ts`.
|
||||
|
||||
| Command | step | store |
|
||||
|---|---|---|
|
||||
| `storebound play build` | `build` | `play` |
|
||||
| `storebound play upload` (`--track internal\|alpha\|beta\|production`) | `upload` | `play` |
|
||||
| `storebound play release` (promote / several tracks in one edit) | `release` | `play` |
|
||||
| `storebound apple build` | `build` | `apple` |
|
||||
| `storebound apple upload` | `upload` | `apple` |
|
||||
| `storebound apple submit` | `submit` | `apple` |
|
||||
| `storebound apple resubmit` | `resubmit` | `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"`).
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Code | Meaning | `error.code` |
|
||||
|---|---|---|
|
||||
| 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` |
|
||||
| 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) | - |
|
||||
|
||||
An exit 3 carries `error.consoleUrl`: the absolute console deep link of the page where the owner acts (App Store
|
||||
Connect Business > Agreements, Users and Access > Integrations, the encryption / App Privacy / age rating pages; Play
|
||||
Console API access, Data safety, Content ratings, Publishing overview). The links come from
|
||||
`cli/lib/console-links.ts`, with the app's console ids when the config knows them.
|
||||
|
||||
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`.
|
||||
|
||||
## `--json`: one final line
|
||||
|
||||
stdout carries exactly one line, at the end, also on failure:
|
||||
|
||||
```json
|
||||
{"ok":true,"step":"upload","store":"play","track":"production","version":"1.2.0","build":"42","state":"completed","url":"https://play.google.com/console/u/0/developers/<dev>/app/<app>/tracks/production","warnings":[]}
|
||||
```
|
||||
|
||||
| Key | |
|
||||
|---|---|
|
||||
| `ok` | true for exit 0 and 7 |
|
||||
| `step`, `store` | see the table above |
|
||||
| `track` | Play track(s) (`internal`, `production`, `internal,production`); null on Apple |
|
||||
| `version`, `build` | marketing version and build number / versionCode, as strings |
|
||||
| `state` | `built`, `planned` (dry run), Play release status (`completed`, `draft`, `inProgress`), App Store `processingState` (`VALID`, `PROCESSING`) or version state (`WAITING_FOR_REVIEW`), `failed` |
|
||||
| `url` | the console page to look at |
|
||||
| `warnings[]` | e.g. a versionCode that is on Play but on another track |
|
||||
| `error` | `{code, message, consoleUrl?}` when not ok |
|
||||
| `artifact` | build steps: the AAB / IPA path |
|
||||
|
||||
The command's own `--json` fields follow in the same object when they do not clash (`versionCode`, `sha256`,
|
||||
`tracks`, `aab`, `processingState` ...), so readers of the old output keep working. Every log line goes to stderr.
|
||||
Under `storebound serve` and flows (`STOREBOUND_MACHINE=1`) a bare `--json` keeps the old result line.
|
||||
|
||||
## `--result-file <path>`
|
||||
|
||||
The same JSON, written atomically (a temp file in the same folder, then a rename), on success and on failure. Also
|
||||
written for an unknown flag (exit 2).
|
||||
|
||||
## `--progress json`
|
||||
|
||||
One JSON line on stderr per phase: `{"phase":"upload","pct":40,"message":"4/10 parts"}`. Phases: `start`,
|
||||
`prebuild`, `build`, `sign`, `verify`, `upload` (with `pct`), `commit`, `processing`, `submit`, `done`. Other stderr
|
||||
lines are plain logs: read the lines that start with `{`.
|
||||
|
||||
## `--env-file <path>`
|
||||
|
||||
`KEY=VALUE` lines (`#` comments, `export ` prefix, quotes; `\n` inside double quotes) loaded into the environment
|
||||
before the credentials resolve; the file wins over the environment. On macOS / Linux the file must not be readable by
|
||||
group or others (mode 600 or stricter), else exit 2. On Windows it is read as is. Every value of the file, and every
|
||||
environment variable whose name says secret (`*_KEY`, `*PASSWORD*`, `*TOKEN*`, `ASC_P8`, `PLAY_SERVICE_ACCOUNT` ...),
|
||||
is replaced with `[redacted]` in stdout, stderr and the result file (multi-line keys line by line).
|
||||
|
||||
## `--version-from <version.json>`
|
||||
|
||||
Reads the version and build for the step. Accepted shapes:
|
||||
|
||||
```json
|
||||
{"version": "1.2.0", "build": 42}
|
||||
{"versionName": "1.2.0", "versionCode": 42}
|
||||
{"marketing": "1.2.0", "build": "42"}
|
||||
```
|
||||
|
||||
(`marketingVersion` and `buildNumber` work too.) What it sets: `play build` `--version-name` + `--version-code`,
|
||||
`play upload` `--version-code` (picks the AAB with that versionCode), `play release` `--version-code` (without an
|
||||
.aab), `apple build` `--version` + `--build-number`, `apple upload` `--build`, `apple submit` / `resubmit` `--version`
|
||||
+ `--build`. A flag given by hand must agree with the file (else exit 2). An Android build must be an integer.
|
||||
|
||||
## Idempotency
|
||||
|
||||
- `play upload`: a versionCode already in Play's bundle list is never uploaded again: exit 7 with the track and
|
||||
status it has (a warning when it is not on the asked track: `play release --version-code N --<track> <status>`).
|
||||
- `apple upload`: a build number App Store Connect already has (a build, or a finished build upload) is exit 7 with
|
||||
its `processingState`; nothing is sent.
|
||||
- `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.
|
||||
|
||||
## `storebound status`
|
||||
|
||||
```
|
||||
storebound status --app <name|store id|folder> --json
|
||||
```
|
||||
|
||||
Read-only, before any step: per platform the release reaching users (`live`) and the one above it (`next`), each with
|
||||
`version`, `build`, `track`, `state`, `phase`, `reviewState` and `rolloutPct`. `--app` takes an app folder path, a
|
||||
workspace app name or a store id (bundle id, package name, App Store app id). One JSON line:
|
||||
|
||||
```json
|
||||
{"apps":[{"app":"notes","folder":"/srv/notes","platforms":[{"platform":"android","store":"play","live":{"version":"1.2.0","build":"42","track":"production","state":"inProgress","phase":"rollout","reviewState":null,"rolloutPct":20},"next":null}],"errors":[],"warnings":[]}],"at":"2026-10-23T10:00:00.000Z"}
|
||||
```
|
||||
|
||||
Exit 1 when a store could not be read (`platforms[].error`).
|
||||
|
||||
## A pipeline stage
|
||||
|
||||
```sh
|
||||
storebound play build --version-from version.json --env-file /run/secrets/ship.env --json --result-file out/build.json --progress json --yes
|
||||
storebound play upload --track production --version-from version.json --env-file /run/secrets/ship.env --json --result-file out/upload.json --yes
|
||||
case $? in 0|7) ;; 3) echo "owner: $(jq -r .error.consoleUrl out/upload.json)"; exit 1 ;; *) exit 1 ;; esac
|
||||
```
|
||||
|
|
@ -1311,3 +1311,15 @@ that fails without the fix (see CONTRIBUTING.md).
|
|||
- **Do:** write the notes without other platforms or stores; `[play]` / `[apple]` tags split shared notes.
|
||||
- **Guard:** `storebound check` reports the "What's new" of every Play language, `play build` refuses before Gradle runs
|
||||
(`notesGuard`), `play upload` / `play release` refuse before any edit (`refusePlayNotes`).
|
||||
|
||||
## 110. A pipeline read "exit 7" as a failure, or uploaded the same build twice
|
||||
|
||||
- **What happens:** a CI stage re-ran after a crash (or a retry) and either uploaded the same versionCode / build
|
||||
number again (Play 403 "APK specifies a version code that has already been used", App Store Connect "redundant
|
||||
binary upload") or failed the job on exit 7, which used to mean "processing timeout" for `apple upload` alone.
|
||||
- **Do:** call the ship commands under the ship-step contract (`--json` / `--result-file`, docs/ship-step.md): exit 7
|
||||
is "nothing to do, it is there", with the track and state in the result; exit 3 names what the owner must do with
|
||||
the console deep link (`error.consoleUrl`); exit 4 is the store refusing. Treat 0 and 7 as success.
|
||||
- **Guard:** `cli/lib/ship-step.ts` holds the one exit-code mapping; `play upload` and `apple upload` check the store
|
||||
before sending and stop with exit 7 (`tests/ship-step.test.ts`, fake stores); `--env-file` values are scrubbed from
|
||||
every output.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue