| name | app-publishing |
| description | Mac, Windows, Linux, iOS and Android app building, signing, and publishing to App Store Connect and Google Play and other platforms. Use when: building release versions, fixing App Store validation errors, uploading to stores, or debugging code signing issues. |
| argument-hint | Describe the build/publish task or validation error to fix |
App Building and Publishing
Scope: This skill applies to the habits-cortex native app (habits-cortex/), not per-stack apps generated from Base. Habit authors distribute via .habit files imported into habits-cortex.
iOS App Store Connect
Build and Upload Command
export APP_STORE_CONNECT_API_KEY_ID=<key-id> \
APP_STORE_CONNECT_API_ISSUER_ID=<issuer-id> \
APP_STORE_CONNECT_API_KEY_BASE64="<base64-encoded-.p8-key>" && \
npx tsx build-release.ts --platform ios --upload-ios
Common Validation Errors and Fixes
ITMS-90717: Invalid App Icon (Alpha Channel)
Error: Invalid large app icon. The app icon in '<path>' can't be transparent or contain an alpha channel.
Fix:
sips --setProperty hasAlpha false app-icon.png
convert app-icon.png -alpha off app-icon-no-alpha.png
Prevention: Always use RGB images without transparency for iOS app icons.
ITMS-90171: Invalid Bundle Structure (libapp.a in Resources)
Error: Invalid bundle structure. The 'Cortex.app/libapp.a' binary file is not permitted.
Root Cause: In project.yml, the Externals folder is listed in sources: without specifying a build phase. XcodeGen scans it, finds .a files, and incorrectly adds them to the Resources build phase.
Fix: Add buildPhase: none to the Externals source entry in project.yml:
sources:
- path: Sources
- path: Assets.xcassets
- path: Externals
buildPhase: none
- path: habits-cortex_iOS
Then regenerate the Xcode project:
cd src-tauri/gen/apple && xcodegen generate --spec project.yml
Why this works: libapp.a is already correctly referenced as a dependency with embed: false, which links it properly. The buildPhase: none tells XcodeGen to include the folder in the project navigator for organization only, not copy its contents to the app bundle.
ITMS-90683: Missing Privacy Strings in Info.plist
Error: Missing purpose string in Info.plist - Your app's code references one or more APIs that access sensitive user data...
Common required keys:
NSLocationWhenInUseUsageDescription - Location access
NSCameraUsageDescription - Camera access
NSMicrophoneUsageDescription - Microphone access
NSPhotoLibraryUsageDescription - Photo library access
NSBluetoothAlwaysUsageDescription - Bluetooth access
NSLocalNetworkUsageDescription - Local network access
Fix: Add the required keys to project.yml in the info.properties section:
info:
path: habits-cortex_iOS/Info.plist
properties:
NSLocationWhenInUseUsageDescription: "This app uses your location to provide location-based services."
NSCameraUsageDescription: "This app uses the camera to scan QR codes."
Then regenerate with xcodegen generate --spec project.yml.
Automatic Patching
Important: The src-tauri/gen/apple/ directory is regenerated by tauri ios init. Manual changes to project.yml will be lost if the iOS project is reinitialized.
To handle this, both the CI workflow and build-release.ts automatically patch project.yml before building:
- CI/CD: The "Patch iOS project.yml" step in
.github/workflows/create-habits-cortex-app.yml applies all fixes after tauri ios init
- Local builds:
build-release.ts calls patchIOSProject() before building, which:
- Adds
buildPhase: none to Externals (ITMS-90171 fix)
- Adds
NSLocationWhenInUseUsageDescription (ITMS-90683 fix)
- Regenerates the Xcode project with xcodegen if available
This ensures App Store validation fixes persist across fresh iOS project initializations.
CI/CD Integration
The workflow at .github/workflows/create-habits-cortex-app.yml handles iOS builds and uploads:
- name: Build iOS
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_API_KEY }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER }}
APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
run: |
cd habits-cortex
npx tsx build-release.ts --platform ios --output ./release --upload-ios
Required GitHub secrets:
APPLE_API_KEY - App Store Connect API Key ID
APPLE_API_ISSUER - App Store Connect API Issuer ID
APPLE_API_KEY_BASE64 - Base64-encoded .p8 private key
Key Files
| File | Purpose |
|---|
habits-cortex/build-release.ts | Build orchestration script |
habits-cortex/src-tauri/gen/apple/project.yml | XcodeGen project definition |
habits-cortex/src-tauri/gen/apple/habits-cortex_iOS/Info.plist | iOS app metadata |
habits-cortex/app-icon.png | Source app icon (must be RGB, no alpha) |
Android Play Store
Build and Upload Command
cd habits-cortex
npx env-cmd -f ../.secrets npx tsx build-release.ts --platform android --upload-android
Required environment variables (in .secrets):
ANDROID_KEYSTORE_BASE64 - Base64-encoded release keystore
ANDROID_KEYSTORE_PASSWORD - Keystore password
ANDROID_KEY_ALIAS - Key alias (e.g., release-key)
ANDROID_KEY_PASSWORD - Key password
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 - Base64-encoded service account JSON
GOOGLE_PLAY_PACKAGE_NAME - Package name (e.g., com.codenteam_oss.habits)
GOOGLE_PLAY_TRACK - Release track (internal, alpha, beta, production)
Common Upload Errors and Fixes
Signing Key Mismatch (403 Error)
Error:
The Android App Bundle was signed with the wrong key.
Found: SHA1: FE:94:1E:DA:..., expected: SHA1: CB:19:2F:72:...
Root Cause: The ANDROID_KEYSTORE_BASE64 in .secrets contains a different keystore than the one originally used to upload to Google Play.
Diagnosis: Check the SHA1 fingerprint of your keystore:
source .secrets && keytool -list -v -keystore /path/to/release.keystore -storepass "$ANDROID_KEYSTORE_PASSWORD" | grep SHA1
Fix: Use the correct keystore. The original keystore is stored at:
habits-cortex/certs/release.keystore
Update .secrets with the correct base64:
base64 -i habits-cortex/certs/release.keystore | tr -d '\n' > /tmp/keystore_base64.txt
Prevention: Always keep the original keystore backed up. Once an app is uploaded to Google Play with a key, all future uploads must use the same key.
Version Code Already Used
Error:
Version code 1000008 has already been used.
Root Cause: The version code in tauri.conf.json hasn't been incremented since the last upload.
Fix: Bump the version in habits-cortex/src-tauri/tauri.conf.json:
{
"version": "1.0.9"
}
Then clean and rebuild:
rm -rf release/*.aab src-tauri/gen/android/app/build/outputs
npx env-cmd -f ../.secrets npx tsx build-release.ts --platform android --upload-android
Note: Tauri generates the Android version code from the version string. 1.0.9 becomes 1000009.
Debug AAB in Release Directory
Error: Upload fails because the script tries to upload debug builds instead of release builds.
Root Cause: Old debug AABs (e.g., app-arm64-debug.aab) exist in the release directory from previous builds.
Fix: Clean all artifacts before building:
rm -rf release/*.aab release/*.apk src-tauri/gen/android/app/build/outputs
Key Files
| File | Purpose |
|---|
habits-cortex/src-tauri/tauri.conf.json | Version configuration |
habits-cortex/src-tauri/gen/android/app/build.gradle.kts | Android build config |
habits-cortex/certs/release.keystore | Original release signing keystore |
habits-cortex/certs/play-service-account.json | Google Play service account |
Tauri Build System
The build-release.ts script handles:
- Bundle generation (cortex-bundle-all.js with all bits)
- Platform-specific builds (iOS, Android, macOS)
- Code signing (automatic with environment variables)
- App Store Connect upload (with
--upload-ios flag)
Supported Flags
| Flag | Description |
|---|
--platform <ios|android|macos> | Target platform |
--output <path> | Output directory for artifacts |
--upload-ios | Upload IPA to App Store Connect after build |
--skip-bundle | Skip bundle generation (use existing) |