--- 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/.key` next to `.cer`; it is never printed and never leaves the machine. `--keychain` imports both into the login keychain (or `--keychain `) 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/.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/-.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.