build.prebuild and a Capacitor web-asset staleness guard for apple build / play build

apple build and play build run build.prebuild (or apple.prebuild / play.prebuild) first, once per flow run;
--no-prebuild skips it, --dry only prints it. A Capacitor project's copied public/index.html must then be newer
than the last source commit and <webDir>/index.html, else the build stops (--allow-stale-web overrides). check
warns when stale without a prebuild; the build output and dry plan show the web bundle's sha256 and mtime.
The Capacitor project is also found in a monorepo (apps/native etc.); several ask for play.build.dir /
apple.build.*. Pitfall 23.
This commit is contained in:
CRTkafa 2026-10-12 08:44:05 +08:00
parent f694af80eb
commit 5fbb9e63e3
14 changed files with 600 additions and 25 deletions

View file

@ -45,7 +45,14 @@ when the owner said so for this run). The MCP server offers only `apple_signing_
ExportOptions.plist (`method` app-store-connect, `destination` export, team, style, profiles) and both xcodebuild
commands. Then run it without `--dry` (minutes; full logs in `build/ios/xcodebuild-*.log`). It ends with the IPA
path, version and build. **Note the build number**: later steps use exactly it.
- Capacitor: `npx cap sync ios` after the web build first. React Native / CocoaPods: `pod install` first (the dry
- Capacitor: the archive ships the web assets `npx cap sync` copied last. Set `build.prebuild` (e.g.
`"npm run build && npx cap sync"`; per store `apple.prebuild` / `play.prebuild`) and `apple build` / `play build`
run it first (once per `flow run`; `--no-prebuild` skips it). Then the copied `public/index.html` must be newer
than the source (last commit outside ios/ android/ node_modules/ store/ .storebound/, `<webDir>/index.html`),
else the build stops; `--allow-stale-web` only with the owner's OK. The output names the web bundle (sha256 +
mtime). In a monorepo (`apps/native/capacitor.config.ts` with its own android/ and ios/) the one Capacitor
project found one or two levels down is used and named; with several, set `play.build.dir` and
`apple.build.workspace` / `project`. React Native / CocoaPods: `pod install` first (the dry
run warns when Pods/ is missing). Flutter: storebound runs `flutter build ios --config-only` with the build
number. Unity: build the Xcode project from Unity first.
- Capacitor needs a UTF-8 locale; storebound sets `LANG` / `LC_ALL` for xcodebuild.

View file

@ -36,6 +36,9 @@ after: []
onError: []
```
`continueOnError: true` lets a step fail without stopping the flow.
A web build before the native builds (Capacitor: `npm run build && npx cap sync`) belongs in `build.prebuild` in the
config rather than an `sh:` step: `apple build` and `play build` run it themselves, and within one `flow run` it runs
once (the second build step prints "prebuild skipped (already ran in this flow run)").
## Modes
- `flow run release VERSION=1.2.0` (default): read steps (check, validate, scan, release plan, next-build, status ...)

View file

@ -1,6 +1,6 @@
---
name: store-pitfalls
description: Known pitfalls with App Store Connect, Google Play and storebound itself, learned from real releases, and what to do about each - store lists that lag right after a create, dry runs that print a count instead of a plan, App Store names taken on a storefront, keyword limits in bytes, Data safety answers without code evidence, missing font glyphs in screenshots, console sign-in and sessions, Capacitor monorepo plugins, duplicates from retries, re-runs and parallel runs, App Store search ranks that are only a trend, Play without a search API, Play's Create app form (no API, opener and submit share a name, Free is final), App Privacy purposes over-mapped (Play advertising is Third-Party Advertising only), an unknown console page (bounded wait and a debug dump), a signed-in console read as a sign-in page (Google account widget frames), console forms driven agent-first. Read it before any store task, and when a store command does something you did not expect.
description: Known pitfalls with App Store Connect, Google Play and storebound itself, learned from real releases, and what to do about each - store lists that lag right after a create, dry runs that print a count instead of a plan, App Store names taken on a storefront, keyword limits in bytes, Data safety answers without code evidence, missing font glyphs in screenshots, console sign-in and sessions, Capacitor monorepo plugins, duplicates from retries, re-runs and parallel runs, App Store search ranks that are only a trend, Play without a search API, Play's Create app form (no API, opener and submit share a name, Free is final), App Privacy purposes over-mapped (Play advertising is Third-Party Advertising only), an unknown console page (bounded wait and a debug dump), a signed-in console read as a sign-in page (Google account widget frames), console forms driven agent-first, stale Capacitor web assets. Read it before any store task, and when a store command does something you did not expect.
---
# Store pitfalls
@ -250,7 +250,6 @@ Each entry: **symptom** → cause → what to do → what guards it now. The ful
- **Sources:** https://support.google.com/googleplay/android-developer/answer/10787469 (Data safety form, CSV import),
https://support.google.com/googleplay/android-developer/answer/13327111 (account deletion: delete-account link;
deleting other data is a separate question).
## Adding a lesson
Every bug found in real use gets an entry in `docs/pitfalls.md` and here, and, where possible, a check or a test
that fails without the fix (see CONTRIBUTING.md).
@ -262,3 +261,25 @@ that fails without the fix (see CONTRIBUTING.md).
was wrong with the bundle.
- **Do:** update storebound. Don't rebuild or change the Gradle setup for this error.
- **Guard:** `aabInfo` in `cli/lib/gradle.ts`; a byte-level test that doesn't use storebound's own encoder.
## 23. Capacitor: native build ships old web assets
- **Symptom:** `storebound flow run release --apply` went straight into `apple build` (xcodebuild archive) while
`ios/App/App/public` and `android/app/src/main/assets/public` were 3.5 hours older than the latest source commit:
the build would have shipped the old game.
- **Cause:** a Capacitor app's native projects contain a copy of the web build that `npx cap sync` made last. Neither
xcodebuild nor Gradle rebuilds or re-copies it; nothing ran the web build + cap sync before the native build.
- **Do:** set `build.prebuild` (e.g. `"npm run build && npx cap sync"`; per store `apple.prebuild` /
`play.prebuild`). `apple build` / `play build` run it first in the project root (printed, output streamed, a
nonzero exit stops the build; `--dry` only prints it; `--no-prebuild` skips it); inside one `flow run` the shared
command runs once, not per store. Never pass `--allow-stale-web` to get past the check without the owner knowing
that the old web build ships.
- **Guard:** after the prebuild, a Capacitor project's copied `public/index.html` must be newer than the last git
commit outside ios/ android/ node_modules/ store/ .storebound/ and than `<webDir>/index.html`, else the build stops
with "web assets are older than the source (copied ..., source ...)" (`webStep` / `webStaleness` in
`cli/lib/web-assets.ts`). `storebound check` warns when the assets are stale and no prebuild is set. The build output
and the dry-run plan show the web bundle that goes in (sha256 of the copied index.html and its mtime).
`tests/web-assets.test.ts`.
- **Monorepo:** with the Capacitor app in `apps/native` (its own capacitor.config.ts, android/, ios/), `play build`
once failed with "no Gradle project (settings.gradle) in android or .". The builds, the staleness check and `check`
now find the one Capacitor project one or two levels down (workspaces, apps/*, packages/*) and name it; several are
listed and need `play.build.dir` / `apple.build.workspace|project` (`capacitorProject` in `cli/lib/web-assets.ts`).