10 KiB
10 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. - Pick the style.
apple.signing.style:automatic(default):xcodebuild -allowProvisioningUpdateswith 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.
- Ensure.
storebound apple signing ensure --dry, show it, then without--dryafter a yes:- adds missing capabilities (never removes one; extra ones are only reported). Adding a capability makes existing
profiles invalid:
ensurecreates 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>.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). Adding a capability makes existing
profiles invalid:
- 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, version and build. Note the build number: later steps use exactly 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 --dry(default: the newest IPA inbuild/ios/export): bundle id, version, build, and a refusal when that build number is already on App Store Connect. 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.