storebound/skills/build-and-sign/SKILL.md
CRT 41319562d8 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.
2026-10-11 23:42:48 +08:00

8.2 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.
  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.