storebound/skills/build-and-sign/SKILL.md

12 KiB

name description
build-and-sign 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 App Store "), 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.