Signing, builds, uploads and an originality check

apple signing status|ensure: bundle id capabilities, a distribution certificate from a key made locally (kept in .storebound/signing, never printed), App Store profiles installed for Xcode; Developer ID 403 explained.
apple build / apple upload: xcodebuild archive + export with a generated ExportOptions.plist, next build number, App Store Connect API build upload (or altool), processing wait. Native, Capacitor, React Native, Flutter, Unity projects.
play build / play upload: Gradle bundleRelease with the upload key passed through the environment, next versionCode read back from the AAB, testing tracks only.
originality: offline Guideline 4.3 check (name, listing text, icon and screenshot perceptual hashes, project structure).
New skill build-and-sign; zero new dependencies.
This commit is contained in:
CRTkafa 2026-10-11 23:42:48 +08:00
parent 346d80e07e
commit 41319562d8
36 changed files with 2786 additions and 28 deletions

View file

@ -0,0 +1,92 @@
---
name: build-and-sign
description: Sign, build and upload a mobile app or game from the terminal - App Store Connect API signing (bundle id capabilities, distribution certificate, provisioning profiles), xcodebuild archive and export with a generated ExportOptions.plist, IPA upload through the App Store Connect API or altool with processing wait, and the Android counterpart (Gradle bundleRelease with the upload key, AAB to a testing track). Native, Capacitor, React Native, Flutter and Unity-exported projects. Use when the user wants to build, sign, archive, export, upload or ship a new build, hits code signing or provisioning errors, or needs a build number / versionCode.
---
# Build, sign and upload
The CLI is `storebound` (`storebound help`). Credentials only from env or `.storebound/credentials.json`
(gitignored): `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_P8` (Admin key for signing), `PLAY_SERVICE_ACCOUNT`, and the
Android upload key `ANDROID_KEYSTORE`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`.
Never ask for them in chat, never print them. Shared agent rules: `docs/agents-core.md`.
**Every write is dry first, then asks.** Certificates, profiles and uploads change the owner's developer account:
show the `--dry` output, run it for real only after a clear yes (the command asks again in the terminal; `--yes` only
when the owner said so for this run). The MCP server offers only `apple_signing_status` (read-only) from this skill.
## iOS
1. **Read.** `storebound apple signing status`: is the bundle id registered (else `storebound apple bundle`), which
capabilities it has vs `apple.signing.capabilities`, the distribution certificates of the team and whether this
Mac holds a private key for one (a key file in `.storebound/signing/` or an identity in the keychain), App Store
profiles per bundle id (app + `apple.signing.extraBundleIds` for extensions) and whether Xcode has them installed.
2. **Pick the style.** `apple.signing.style`:
- `automatic` (default): `xcodebuild -allowProvisioningUpdates` with the API key creates and fetches what it needs.
Nothing to do in step 3 unless capabilities are missing. Easiest for one developer on one Mac.
- `manual`: certificate and profiles from step 3, named in ExportOptions.plist. Best for CI and teams; set the app
target to manual signing with the installed profile in Xcode (Signing & Capabilities) once.
3. **Ensure.** `storebound apple signing ensure --dry`, show it, then without `--dry` after a yes:
- adds missing capabilities (never removes one; extra ones are only reported). Adding a capability makes existing
profiles invalid: `ensure` creates new ones in the same run.
- creates an Apple Distribution certificate only when no valid one has its key on this machine. The RSA key is made
locally, kept 0600 in `.storebound/signing/<id>.key` next to `<id>.cer`; it is never printed and never leaves the
machine. `--keychain` imports both into the login keychain (or `--keychain <path>`) so xcodebuild can sign.
Back the key up (encrypted). Apple allows 3 distribution certificates per team: at the limit, revoking one is
the owner's decision (developer.apple.com > Certificates).
- Developer ID certificates (Mac apps outside the App Store): only the **Account Holder** can create them; the API
answers 403 for every other key, and storebound says so. The Account Holder makes it in Xcode > Settings >
Accounts > Manage Certificates and exports a .p12.
- creates or reuses an App Store profile per bundle id and installs it into
`~/Library/Developer/Xcode/UserData/Provisioning Profiles` (Xcode 16+) and `~/Library/MobileDevice/Provisioning
Profiles` (older Xcode). `--no-install` skips that.
4. **Build.** `storebound apple build --dry --version 1.4.0` shows the project it found (Capacitor `ios/App`, React
Native `ios/<Name>.xcworkspace`, Flutter `ios/Runner`, Unity `Unity-iPhone`, or `apple.build.workspace` /
`project` / `scheme`), the build number (next free one from App Store Connect, or `--build-number N`), the
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
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.
5. **Upload.** `storebound apple upload --dry` (default: the newest IPA in `build/ios/export`): bundle id, version,
build, and a refusal when that build number is already on App Store Connect. Then without `--dry` after a yes. Default
`--via api` is the App Store Connect API build upload (Build Uploads: reserve, upload the parts, commit with the
MD5, poll the upload state; any OS). `--via altool` uses `xcrun altool --upload-package` with the API key. Apple
lists both (and Xcode / Transporter); neither is deprecated. storebound then waits until the build is VALID
(exit 0), INVALID / FAILED (exit 1, Apple emails the ITMS reason) or `--timeout` (default 60m, exit 7). Run long
uploads in the background with a log: `storebound apple upload --yes > .storebound/upload.log 2>&1 &` (only
after the owner's yes).
6. **Next.** `storebound apple encryption --build N`, `storebound apple testflight distribute --build N --group Team
--dry` (`testflight` skill), `storebound apple listing --version x.y.z --build N` (`appstore-first-release`).
## Android
1. **Upload key.** Generate once (`keytool -genkeypair -v -storetype PKCS12 -keystore upload.p12 -alias upload
-keyalg RSA -keysize 4096 -validity 10950`), keep it outside git, put the four `ANDROID_*` values in env or
`.storebound/credentials.json`. storebound hands them to Gradle as injected signing properties through the
environment, never on the command line. Without them the project's own `signingConfigs.release` signs (e.g.
Flutter's `key.properties`).
2. **Build.** `storebound play build --dry` shows the Gradle project (`android/` for Capacitor, React Native and
Flutter, the root for native, a Unity export with `launcher/` + `unityLibrary/`; or `play.build.dir` / `module` /
`flavor`), the task (`:app:bundleRelease`; Flutter: `flutter build appbundle`), the next free versionCode from Play
(or `--version-code N`) and where the key comes from. Then without `--dry`. The AAB lands in
`build/android/<package>-<versionCode>.aab`; storebound reads the versionCode back from the bundle and warns when
the build file ignored the one it passed (read `-PversionCode` there, or `android.injected.version.code`).
3. **Upload.** `storebound play upload --track internal --dry` (read-only: checks the versionCode is free, shows
what is on the track now and the release notes languages). Then without `--dry` after a yes. Tracks: internal,
alpha (closed), beta (open). Production stays `storebound play release --version-code N --production draft` plus
the owner's click in the Console (`play-first-release`, `release-management`). A never-published app only takes
drafts: `--status draft`.
## Gotchas
- "No signing certificate iOS Distribution found": the certificate exists on the team but its key is on another Mac.
`apple signing status` shows this as "without key"; import that Mac's .p12, or `ensure` makes a new one.
- "Provisioning profile doesn't include the currently selected device / capability": a capability was added after the
profile was made. `apple signing ensure` (profiles are recreated when they are not ACTIVE).
- Xcode 16+ reads profiles only from `~/Library/Developer/Xcode/UserData/Provisioning Profiles`: profiles copied into
the old folder alone are not seen.
- ITMS-90189 / "bundle version must be higher": the build number is taken. `apple next-build` gives a free one;
`apple build` uses it by default.
- Play "Version code N has already been used": `play next-version-code`; `play build` uses it by default.