12 KiB
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
- Read.
storebound apple signing status: is the bundle id registered (elsestorebound apple bundle), which capabilities it has vsapple.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.extraBundleIdsfor 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'sapple.signing.capabilitiesand 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 statusandvalidatewarn on the same. - Pick the style.
apple.signing.style:automatic(default):xcodebuild -allowProvisioningUpdateswith the API key creates and fetches what it needs.ensureonly 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.
- Ensure.
storebound apple signing ensure --dry, show it, then without--dryafter 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:
ensurecreates 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>.keynext to<id>.cer; it is never printed and never leaves the machine.--keychainimports 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-installskips that.
- 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:
- Build.
storebound apple build --dry --version 1.4.0shows the project it found (Capacitorios/App, React Nativeios/<Name>.xcworkspace, Flutterios/Runner, UnityUnity-iPhone, orapple.build.workspace/project/scheme), the build number (next free one from App Store Connect, or--build-number N), the ExportOptions.plist (methodapp-store-connect,destinationexport, team, style, profiles) and both xcodebuild commands. Then run it without--dry(minutes; full logs inbuild/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, soapple build && apple uploadstops 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 doctorflags it).- Capacitor: the archive ships the web assets
npx cap synccopied last. Setbuild.prebuild(e.g."npm run build && npx cap sync"; per storeapple.prebuild/play.prebuild) andapple build/play buildrun it first (once perflow run;--no-prebuildskips it). Then the copiedpublic/index.htmlmust be newer than the source (last commit outside ios/ android/ node_modules/ store/ .storebound/,<webDir>/index.html), else the build stops;--allow-stale-webonly with the owner's OK. The output names the web bundle (sha256 + mtime). In a monorepo (apps/native/capacitor.config.tswith its own android/ and ios/) the one Capacitor project found one or two levels down is used and named; with several, setplay.build.dirandapple.build.workspace/project. React Native / CocoaPods:pod installfirst (the dry run warns when Pods/ is missing). Flutter: storebound runsflutter build ios --config-onlywith the build number. Unity: build the Xcode project from Unity first. - Capacitor needs a UTF-8 locale; storebound sets
LANG/LC_ALLfor xcodebuild.
- Capacitor: the archive ships the web assets
- Upload.
storebound apple upload --build N --dry(the IPA of build N; without--build, the newest IPA underbuild/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-existingonly re-checks a build this command uploaded before (flows resuming). Then without--dryafter a yes. Default--via apiis the App Store Connect API build upload (Build Uploads: reserve, upload the parts, commit with the MD5, poll the upload state; any OS).--via altoolusesxcrun altool --upload-packagewith 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--timeoutit stops with exit 7; the same command again resumes the build upload (raise--timeout). - Next.
storebound apple encryption --build N,storebound apple testflight distribute --build N --group Team --dry(testflightskill),storebound apple listing --version x.y.z --build N(appstore-first-release).
Android
- 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 fourANDROID_*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 ownsigningConfigs.releasesigns (e.g. Flutter'skey.properties). - Build.
storebound play build --dryshows the Gradle project (android/for Capacitor, React Native and Flutter, the root for native, a Unity export withlauncher/+unityLibrary/; orplay.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 inbuild/android/<package>-<versionCode>.aab. The app module'sdefaultConfigmust read the project properties storebound passes;play build(andcheck) warn before Gradle runs whenversionCode/versionNameis a literal or comes from an env var, with the line to use: GroovyversionCode((project.findProperty('versionCode') ?: 1) as Integer)andversionName project.findProperty('versionName') ?: '1.0'; KotlinversionCode = (project.findProperty("versionCode") as String?)?.toInt() ?: 1andversionName = project.findProperty("versionName") as String? ?: "1.0"(keep your current values as the defaults).android.injected.version.codeis passed too but does NOT overridedefaultConfigforbundleRelease(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.dirin the Gradle project'slocal.properties, then~/Library/Android/sdk(macOS) /~/Android/Sdk(Linux) /%LOCALAPPDATA%\Android\Sdk(Windows), and handed to Gradle throughANDROID_HOME/ANDROID_SDK_ROOT;local.propertiesis never written. None found: it stops before Gradle and lists the places checked.storebound doctorshows the SDK path and where it came from. - 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--dryafter a yes. Tracks: internal, alpha (closed), beta (open). Production staysstorebound play release --version-code N --production draftplus 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 statusshows this as "without key"; import that Mac's .p12, orensuremakes 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-buildgives a free one;apple builduses it by default. - Play "Version code N has already been used":
play next-version-code;play builduses it by default.