130 lines
12 KiB
Markdown
130 lines
12 KiB
Markdown
---
|
|
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.
|
|
It also compares three places a capability must be and names what is missing where: the App ID, the config's
|
|
`apple.signing.capabilities` and the iOS project's `.entitlements` (`aps-environment` = PUSH_NOTIFICATIONS,
|
|
`com.apple.developer.game-center` = GAME_CENTER, associated domains, Sign in with Apple, iCloud, app groups, ...;
|
|
an entitlements file must also be set in CODE_SIGN_ENTITLEMENTS). `apple status` and `validate` warn on the same.
|
|
2. **Pick the style.** `apple.signing.style`:
|
|
- `automatic` (default): `xcodebuild -allowProvisioningUpdates` with the API key creates and fetches what it needs.
|
|
`ensure` only adds missing capabilities to the App ID: it never creates certificates or profiles (Xcode owns
|
|
them; its managed profile is renewed with the new capabilities at the next archive). Easiest for one developer.
|
|
- `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), then reads the App ID back (a few
|
|
tries, about 45 s at most) and fails with the API's answers when one did not stick. Never report a capability
|
|
as added from the POST alone. With manual signing, adding one makes existing profiles stale: `ensure` creates
|
|
new ones in the same run.
|
|
- lists provisioning profiles storebound made that nothing uses ("storebound <bundle id> App Store <date>"), with
|
|
the exact delete command; deleting them is the owner's call, storebound never runs it.
|
|
- manual signing only from here on. 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 (`build/ios/export/<version>-<build>/App.ipa`, a fresh folder per build), version and build. **Note the build
|
|
number**: later steps use exactly it. Any archive or export failure exits non-zero, so `apple build && apple upload`
|
|
stops there; an export failure shows the IDEDistribution log's real error lines. xcodebuild runs with Apple's tool
|
|
folders first on PATH (a Homebrew rsync breaks Xcode's IPA step: "exportArchive Copy failed"; `storebound doctor`
|
|
flags it).
|
|
- 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.
|
|
5. **Upload.** `storebound apple upload --build N --dry` (the IPA of build N; without `--build`, the newest IPA under
|
|
`build/ios/export`): bundle id, version, build, and exit 1 when that build number is already on App Store Connect
|
|
(a stale IPA from a failed build: build again). `--allow-existing` only re-checks a build this command uploaded
|
|
before (flows resuming). 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).
|
|
The upload is never silent: the size at the start, a line per part, a heartbeat every 20 s ("still uploading,
|
|
4m30s, 312/890 MB (35%), part 4/12"), the same while altool runs and while the build is processed. Past
|
|
`--timeout` it stops with exit 7; the same command again resumes the build upload (raise `--timeout`).
|
|
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`. The app module's `defaultConfig` must read the project properties
|
|
storebound passes; `play build` (and `check`) warn before Gradle runs when `versionCode` / `versionName` is a
|
|
literal or comes from an env var, with the line to use:
|
|
Groovy `versionCode((project.findProperty('versionCode') ?: 1) as Integer)` and
|
|
`versionName project.findProperty('versionName') ?: '1.0'`; Kotlin
|
|
`versionCode = (project.findProperty("versionCode") as String?)?.toInt() ?: 1` and
|
|
`versionName = project.findProperty("versionName") as String? ?: "1.0"` (keep your current values as the
|
|
defaults). `android.injected.version.code` is passed too but does NOT override `defaultConfig` for
|
|
`bundleRelease` (checked with AGP 8.13); don't count on it. After the build storebound reads the versionCode back
|
|
from the bundle; another number than planned is a hard stop (the AAB is not copied).
|
|
The Android SDK is found the way Android Studio finds it: `ANDROID_HOME`, `ANDROID_SDK_ROOT`, `sdk.dir` in the
|
|
Gradle project's `local.properties`, then `~/Library/Android/sdk` (macOS) / `~/Android/Sdk` (Linux) /
|
|
`%LOCALAPPDATA%\Android\Sdk` (Windows), and handed to Gradle through `ANDROID_HOME` / `ANDROID_SDK_ROOT`;
|
|
`local.properties` is never written. None found: it stops before Gradle and lists the places checked.
|
|
`storebound doctor` shows the SDK path and where it came from.
|
|
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.
|