| 1 | #!/usr/bin/env bash |
| 2 | # build-visionos.sh |
| 3 | # |
| 4 | # Full App Store pipeline for the native SitiVision visionOS app (xcode/): |
| 5 | # 1. Generate — xcodegen generate (SitiVision.xcodeproj is not checked in) |
| 6 | # 2. Archive — xcodebuild archive (automatic signing, Splitfire AB team) |
| 7 | # 3. Export — xcodebuild -exportArchive (method app-store-connect → .ipa) |
| 8 | # 4. Fixplist — rewrite the beta host-OS stamp (BuildMachineOSBuild) + Mach-O |
| 9 | # LC_BUILD_VERSION, then re-sign — see do_fixplist for why |
| 10 | # 5. Upload — xcrun altool (TestFlight / App Store Connect submission) |
| 11 | # |
| 12 | # Unlike build-ios.sh (Tauri + cargo-tauri), SitiVision is a plain XcodeGen |
| 13 | # project, so this pipeline is xcodebuild/xcodegen only — no cargo/pnpm steps. |
| 14 | # The fixplist step follows the same workaround as the sibling splitfire repo's |
| 15 | # Scripts/build_karokowe_appstore.sh (native tvOS/visionOS Xcode projects) and |
| 16 | # this repo's own build-ios.sh (Tauri iOS) — see do_fixplist for the mechanism. |
| 17 | # |
| 18 | # Every configurable value has a hardcoded default that can be overridden by |
| 19 | # setting the corresponding environment variable before invoking the script. |
| 20 | # |
| 21 | # Usage: |
| 22 | # ./scripts/build-visionos.sh # full pipeline |
| 23 | # ./scripts/build-visionos.sh generate # xcodegen generate only |
| 24 | # ./scripts/build-visionos.sh archive # archive only (assumes project generated) |
| 25 | # ./scripts/build-visionos.sh export # export .ipa only (assumes archive already built) |
| 26 | # ./scripts/build-visionos.sh fixplist # patch beta host-OS stamp + re-sign |
| 27 | # ./scripts/build-visionos.sh upload # upload only (assumes .ipa already exported) |
| 28 | # |
| 29 | # make asvision # full pipeline via Makefile |
| 30 | # make asvision-generate |
| 31 | # make asvision-archive |
| 32 | # make asvision-export |
| 33 | # make asvision-fixplist |
| 34 | # make asvision-upload |
| 35 | |
| 36 | set -euo pipefail |
| 37 | |
| 38 | # ── Paths ───────────────────────────────────────────────────────────────────── |
| 39 | |
| 40 | REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 41 | XCODE_DIR="${REPO_ROOT}/xcode" |
| 42 | PROJECT="${XCODE_DIR}/SitiVision.xcodeproj" |
| 43 | BUILD_DIR="${XCODE_DIR}/build" |
| 44 | ARCHIVE_PATH="${BUILD_DIR}/SitiVision.xcarchive" |
| 45 | EXPORT_DIR="${BUILD_DIR}/export" |
| 46 | EXPORT_OPTIONS="${BUILD_DIR}/ExportOptions.plist" |
| 47 | IPA_PATH="${EXPORT_DIR}/SitiVision.ipa" |
| 48 | |
| 49 | # Auto-load the untracked .env.appstore (signing identities + ASC creds) so |
| 50 | # `make asvision` works without the caller exporting them first. See |
| 51 | # .env.appstore.example for the expected keys. |
| 52 | if [[ -f "${REPO_ROOT}/.env.appstore" ]]; then |
| 53 | set -a |
| 54 | # shellcheck disable=SC1091 |
| 55 | . "${REPO_ROOT}/.env.appstore" |
| 56 | set +a |
| 57 | fi |
| 58 | |
| 59 | # Xcode toolchain. App Store Connect rejects binaries built with a beta Xcode or |
| 60 | # beta SDK, so pin a release Xcode here regardless of the global `xcode-select`. |
| 61 | # Override: DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer ./scripts/build-visionos.sh |
| 62 | pick_release_xcode() { |
| 63 | local active; active="$(xcode-select -p 2>/dev/null)" |
| 64 | if [[ -n "$active" && "$active" != *[Bb]eta* ]]; then |
| 65 | printf '%s\n' "$active"; return |
| 66 | fi |
| 67 | local app |
| 68 | for app in /Applications/Xcode*.app; do |
| 69 | case "$app" in *[Bb]eta*) continue ;; esac |
| 70 | [[ -d "$app/Contents/Developer" ]] && { printf '%s\n' "$app/Contents/Developer"; return; } |
| 71 | done |
| 72 | } |
| 73 | DEVELOPER_DIR="${DEVELOPER_DIR:-$(pick_release_xcode)}" |
| 74 | export DEVELOPER_DIR |
| 75 | |
| 76 | # ── Overridable config ───────────────────────────────────────────────────────── |
| 77 | |
| 78 | # Apple Developer team (Splitfire AB — same team as the Tauri app's iOS/macOS |
| 79 | # builds; see project.yml's DEVELOPMENT_TEAM and ../src-tauri/tauri.conf.json). |
| 80 | TEAM_ID="${TEAM_ID:-2TQF86ZACD}" |
| 81 | |
| 82 | # App Store Connect API credentials. Used both for -allowProvisioningUpdates |
| 83 | # (so archive/export can fetch/create the distribution profile headlessly, |
| 84 | # without a signed-in Xcode account) and for the altool upload. |
| 85 | # ASC_API_KEY — Key ID shown in App Store Connect → Users & Access → Keys. |
| 86 | # ASC_ISSUER_ID — Issuer UUID on the same page. |
| 87 | # The private key (.p8) must be present at: |
| 88 | # ~/.appstoreconnect/private_keys/AuthKey_<ASC_API_KEY>.p8 (Xcode's default), or |
| 89 | # ~/private_keys/AuthKey_<ASC_API_KEY>.p8 (ASC_PRIVATE_KEY_PATH override) |
| 90 | ASC_API_KEY="${ASC_API_KEY:-L84N624YQH}" |
| 91 | ASC_ISSUER_ID="${ASC_ISSUER_ID:-b4e8d369-8b7d-4538-8435-643b73237575}" |
| 92 | ASC_PRIVATE_KEY_PATH="${ASC_PRIVATE_KEY_PATH:-${HOME}/private_keys/AuthKey_${ASC_API_KEY}.p8}" |
| 93 | |
| 94 | # Re-sign identity used by the fixplist step (distribution cert for the same |
| 95 | # team as TEAM_ID). Not secret — safe to commit once known. |
| 96 | SIGN_IDENTITY="${SIGN_IDENTITY:-Apple Distribution: Splitfire AB (2TQF86ZACD)}" |
| 97 | |
| 98 | # A shipping macOS build number to stamp for BuildMachineOSBuild during |
| 99 | # fixplist (matches the constant already used by build-ios.sh and the |
| 100 | # sibling splitfire repo's Scripts/build_karokowe_appstore.sh). |
| 101 | RELEASE_OS_BUILD="${RELEASE_OS_BUILD:-25A354}" |
| 102 | |
| 103 | # ── Helpers ─────────────────────────────────────────────────────────────────── |
| 104 | |
| 105 | step() { echo ""; echo "── $* ──────────────────────────────────────────────"; } |
| 106 | ok() { echo "✅ $*"; } |
| 107 | fail() { printf "❌ %b\n" "$*" >&2; exit 1; } |
| 108 | |
| 109 | # True when an Apple build number is a pre-release (beta) seed. Apple stamps |
| 110 | # seeds with a trailing lowercase letter (e.g. 26A5353q); shipping builds |
| 111 | # never carry one (e.g. 25A354). |
| 112 | is_beta_build() { |
| 113 | [[ "${1:-}" =~ [a-z]$ ]] |
| 114 | } |
| 115 | |
| 116 | require_release_xcode() { |
| 117 | [[ -n "${DEVELOPER_DIR}" && -d "${DEVELOPER_DIR}" ]] \ |
| 118 | || fail "No release Xcode found.\n Install a release Xcode, or set DEVELOPER_DIR to its Contents/Developer dir." |
| 119 | case "${DEVELOPER_DIR}" in |
| 120 | *[Bb]eta*) |
| 121 | fail "DEVELOPER_DIR points at a beta Xcode:\n ${DEVELOPER_DIR}\n App Store Connect rejects beta-built binaries. Point DEVELOPER_DIR at a release Xcode." ;; |
| 122 | esac |
| 123 | echo "🛠 Xcode → $(xcodebuild -version 2>/dev/null | head -1) [${DEVELOPER_DIR}]" |
| 124 | } |
| 125 | |
| 126 | require_asc_key() { |
| 127 | [[ -f "${ASC_PRIVATE_KEY_PATH}" ]] \ |
| 128 | || fail "ASC API private key not found: ${ASC_PRIVATE_KEY_PATH}\n Download it from App Store Connect → Users & Access → Integrations → App Store Connect API (once — Apple won't let you re-download), or set ASC_PRIVATE_KEY_PATH." |
| 129 | } |
| 130 | |
| 131 | # Release toolchain stamps used by fixplist to rewrite a bundle's Info.plist |
| 132 | # so App Store Connect reads it as a release-Xcode/SDK build. Derived from the |
| 133 | # pinned release Xcode (DEVELOPER_DIR); every value is overridable via env. |
| 134 | derive_release_toolchain() { |
| 135 | local xv sv xver xbuild sdkver sdkbuild d |
| 136 | xv="$(xcodebuild -version 2>/dev/null)" |
| 137 | sv="$(xcodebuild -version -sdk xros 2>/dev/null)" |
| 138 | xver="$(printf '%s\n' "$xv" | awk '/^Xcode/{print $2}')" |
| 139 | xbuild="$(printf '%s\n' "$xv" | awk '/Build version/{print $3}')" |
| 140 | sdkver="$(printf '%s\n' "$sv" | awk -F': ' '/SDKVersion/{print $2; exit}')" |
| 141 | sdkbuild="$(printf '%s\n' "$sv" | awk -F': ' '/ProductBuildVersion/{print $2; exit}')" |
| 142 | d="${xver//./}"; while [[ -n "$d" && ${#d} -lt 4 ]]; do d="${d}0"; done |
| 143 | REL_DTXCODE="${RELEASE_DTXCODE:-$d}" |
| 144 | REL_XCODE_BUILD="${RELEASE_XCODE_BUILD:-$xbuild}" |
| 145 | REL_SDK_VERSION="${RELEASE_SDK_VERSION:-$sdkver}" |
| 146 | REL_SDK_NAME="${RELEASE_SDK_NAME:-xros${sdkver}}" |
| 147 | REL_SDK_BUILD="${RELEASE_SDK_BUILD:-$sdkbuild}" |
| 148 | [[ -n "$REL_DTXCODE" && -n "$REL_XCODE_BUILD" && -n "$REL_SDK_BUILD" ]] \ |
| 149 | || fail "Could not determine release toolchain stamps from ${DEVELOPER_DIR}." |
| 150 | } |
| 151 | |
| 152 | patch_bundle_plist() { |
| 153 | local plist="$1" |
| 154 | pl() { /usr/libexec/PlistBuddy -c "Print :$1" "$plist" 2>/dev/null || echo ""; } |
| 155 | setpl() { /usr/libexec/PlistBuddy -c "Set :$1 $2" "$plist" 2>/dev/null \ |
| 156 | || /usr/libexec/PlistBuddy -c "Add :$1 string $2" "$plist"; } |
| 157 | setpl BuildMachineOSBuild "${RELEASE_OS_BUILD}" |
| 158 | setpl DTXcode "${REL_DTXCODE}"; setpl DTXcodeBuild "${REL_XCODE_BUILD}" |
| 159 | setpl DTSDKName "${REL_SDK_NAME}"; setpl DTSDKBuild "${REL_SDK_BUILD}" |
| 160 | setpl DTPlatformBuild "${REL_SDK_BUILD}"; setpl DTPlatformVersion "${REL_SDK_VERSION}" |
| 161 | } |
| 162 | |
| 163 | # ── Steps ───────────────────────────────────────────────────────────────────── |
| 164 | |
| 165 | do_generate() { |
| 166 | step "Generate" |
| 167 | |
| 168 | command -v xcodegen >/dev/null || fail "xcodegen not found.\n Install with: brew install xcodegen" |
| 169 | |
| 170 | ( cd "${XCODE_DIR}" && xcodegen generate ) |
| 171 | [[ -d "$PROJECT" ]] || fail "xcodegen ran but ${PROJECT} was not created" |
| 172 | ok "Xcode project generated → ${PROJECT}" |
| 173 | } |
| 174 | |
| 175 | do_archive() { |
| 176 | step "Archive" |
| 177 | |
| 178 | require_release_xcode |
| 179 | require_asc_key |
| 180 | [[ -d "$PROJECT" ]] || fail "Xcode project not found at ${PROJECT}\n Run 'generate' step first." |
| 181 | |
| 182 | # CURRENT_PROJECT_VERSION: unix-seconds build number. |
| 183 | # Strictly increasing across builds → satisfies App Store Connect's |
| 184 | # "each build must have a higher build number than the last" requirement. |
| 185 | BUILD_NUMBER="$(date -u +"%s")" |
| 186 | |
| 187 | mkdir -p "${BUILD_DIR}" |
| 188 | rm -rf "${ARCHIVE_PATH}" |
| 189 | |
| 190 | echo "🏷 buildNumber → ${BUILD_NUMBER}" |
| 191 | echo "👥 team → ${TEAM_ID}" |
| 192 | echo "" |
| 193 | |
| 194 | xcodebuild archive \ |
| 195 | -project "${PROJECT}" \ |
| 196 | -scheme SitiVision \ |
| 197 | -destination "generic/platform=visionOS" \ |
| 198 | -archivePath "${ARCHIVE_PATH}" \ |
| 199 | -configuration Release \ |
| 200 | -allowProvisioningUpdates \ |
| 201 | -authenticationKeyPath "${ASC_PRIVATE_KEY_PATH}" \ |
| 202 | -authenticationKeyID "${ASC_API_KEY}" \ |
| 203 | -authenticationKeyIssuerID "${ASC_ISSUER_ID}" \ |
| 204 | CURRENT_PROJECT_VERSION="${BUILD_NUMBER}" \ |
| 205 | DEVELOPMENT_TEAM="${TEAM_ID}" |
| 206 | |
| 207 | [[ -d "$ARCHIVE_PATH" ]] || fail "Archive succeeded but not found at: $ARCHIVE_PATH" |
| 208 | ok "Archive complete → ${ARCHIVE_PATH}" |
| 209 | } |
| 210 | |
| 211 | do_export() { |
| 212 | step "Export" |
| 213 | |
| 214 | require_release_xcode |
| 215 | require_asc_key |
| 216 | [[ -d "$ARCHIVE_PATH" ]] || fail "Archive not found: ${ARCHIVE_PATH}\n Run 'archive' step first." |
| 217 | |
| 218 | # exportOptionsPlist: automatic signing re-resolves to a *distribution* |
| 219 | # certificate + App Store profile for method app-store-connect, even though |
| 220 | # the archive step itself signs with a development identity (expected — |
| 221 | # Xcode always archives with the dev cert; export re-signs per method). |
| 222 | cat > "${EXPORT_OPTIONS}" <<PLIST |
| 223 | <?xml version="1.0" encoding="UTF-8"?> |
| 224 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 225 | <plist version="1.0"> |
| 226 | <dict> |
| 227 | <key>method</key> |
| 228 | <string>app-store-connect</string> |
| 229 | <key>teamID</key> |
| 230 | <string>${TEAM_ID}</string> |
| 231 | <key>signingStyle</key> |
| 232 | <string>automatic</string> |
| 233 | <key>destination</key> |
| 234 | <string>export</string> |
| 235 | </dict> |
| 236 | </plist> |
| 237 | PLIST |
| 238 | |
| 239 | rm -rf "${EXPORT_DIR}" |
| 240 | |
| 241 | xcodebuild -exportArchive \ |
| 242 | -archivePath "${ARCHIVE_PATH}" \ |
| 243 | -exportPath "${EXPORT_DIR}" \ |
| 244 | -exportOptionsPlist "${EXPORT_OPTIONS}" \ |
| 245 | -allowProvisioningUpdates \ |
| 246 | -authenticationKeyPath "${ASC_PRIVATE_KEY_PATH}" \ |
| 247 | -authenticationKeyID "${ASC_API_KEY}" \ |
| 248 | -authenticationKeyIssuerID "${ASC_ISSUER_ID}" |
| 249 | |
| 250 | [[ -f "$IPA_PATH" ]] || fail "Export succeeded but .ipa not found at: $IPA_PATH" |
| 251 | ok "Export complete → ${IPA_PATH}" |
| 252 | } |
| 253 | |
| 254 | # Why this is needed even with a release Xcode pinned via DEVELOPER_DIR: |
| 255 | # xcodebuild fully respects DEVELOPER_DIR, so DTXcode/DTXcodeBuild/DTSDKBuild |
| 256 | # and every Mach-O's embedded LC_BUILD_VERSION sdk field all come out |
| 257 | # clean/release automatically. But `BuildMachineOSBuild` reflects the literal |
| 258 | # host macOS build — this Mac itself, regardless of which Xcode.app performs |
| 259 | # the build. When the host OS is a beta seed (a build number with a trailing |
| 260 | # lowercase letter, e.g. `26A5353q`), that field alone is enough to make App |
| 261 | # Store Connect's REVIEW-SUBMISSION validation — a separate, stricter pass |
| 262 | # than TestFlight build processing — reject the binary as "Invalid Binary" |
| 263 | # with no further detail, even though TestFlight processing itself reports |
| 264 | # the build as valid. Mirrors build-ios.sh's fixplist step and the sibling |
| 265 | # splitfire repo's Scripts/build_karokowe_appstore.sh, adapted for a plain |
| 266 | # xcodebuild archive/export instead of `cargo tauri ios build`. |
| 267 | do_fixplist() { |
| 268 | step "Fixplist (beta host OS → release)" |
| 269 | |
| 270 | [[ -f "$IPA_PATH" ]] || fail ".ipa not found: ${IPA_PATH}\n Run 'export' step first." |
| 271 | command -v /usr/libexec/PlistBuddy >/dev/null || fail "PlistBuddy not found" |
| 272 | |
| 273 | local work app_dir os_cur |
| 274 | work="$(mktemp -d)" |
| 275 | trap 'rm -rf "$work"' RETURN |
| 276 | |
| 277 | unzip -q "$IPA_PATH" -d "$work" |
| 278 | app_dir="$(find "$work/Payload" -maxdepth 1 -name '*.app' -print -quit)" |
| 279 | [[ -n "$app_dir" && -d "$app_dir" ]] || fail "No .app found inside ${IPA_PATH}" |
| 280 | |
| 281 | os_cur="$(/usr/libexec/PlistBuddy -c 'Print :BuildMachineOSBuild' "$app_dir/Info.plist" 2>/dev/null || echo "")" |
| 282 | if [[ -z "${FORCE_FIXPLIST:-}" ]] && ! is_beta_build "$os_cur"; then |
| 283 | ok "BuildMachineOSBuild (${os_cur}) is already a release seed — no patch needed." |
| 284 | return 0 |
| 285 | fi |
| 286 | |
| 287 | require_release_xcode |
| 288 | derive_release_toolchain |
| 289 | echo "🛠 xcode → ${REL_DTXCODE}/${REL_XCODE_BUILD} sdk → ${REL_SDK_NAME}/${REL_SDK_BUILD}" |
| 290 | echo "🔁 BuildMachineOSBuild ${os_cur:-<unset>} → ${RELEASE_OS_BUILD}" |
| 291 | echo "🔏 identity → ${SIGN_IDENTITY}" |
| 292 | echo "" |
| 293 | |
| 294 | # Patch every bundle's Info.plist: the main .app and any nested extensions. |
| 295 | local bundles=("$app_dir") |
| 296 | while IFS= read -r appex; do bundles+=("$appex"); done \ |
| 297 | < <(find "$app_dir" -name '*.appex' -type d) |
| 298 | local bundle |
| 299 | for bundle in "${bundles[@]}"; do |
| 300 | [[ -f "$bundle/Info.plist" ]] && patch_bundle_plist "$bundle/Info.plist" |
| 301 | done |
| 302 | |
| 303 | # Critical: App Store Connect's review-stage validation reads the `sdk` |
| 304 | # baked into each executable's Mach-O LC_BUILD_VERSION, not just the |
| 305 | # Info.plist DTSDK* keys. Patch every Mach-O in the bundle; minos is |
| 306 | # preserved, only the sdk is (re)set to the release SDK. |
| 307 | local macho minos_cur |
| 308 | while IFS= read -r macho; do |
| 309 | [[ -z "$macho" ]] && continue |
| 310 | minos_cur="$(xcrun vtool -show-build "$macho" 2>/dev/null | awk '/minos/{print $2; exit}')" |
| 311 | minos_cur="${minos_cur:-2.0}" |
| 312 | echo "🔁 Mach-O sdk → ${REL_SDK_VERSION} (minos ${minos_cur}): ${macho#"$app_dir"/}" |
| 313 | xcrun vtool -set-build-version visionos "${minos_cur}" "${REL_SDK_VERSION}" \ |
| 314 | -replace -output "${macho}.vt" "$macho" >/dev/null 2>&1 \ |
| 315 | && mv "${macho}.vt" "$macho" \ |
| 316 | || fail "vtool failed to patch LC_BUILD_VERSION on ${macho#"$app_dir"/}" |
| 317 | done < <(find "$app_dir" -type f -exec sh -c 'file "$1" | grep -q Mach-O' _ {} \; -print) |
| 318 | |
| 319 | # Re-sign nested code deepest-first, then the .app last — editing the |
| 320 | # plist/Mach-O invalidates the original signature. |
| 321 | local nested ent |
| 322 | while IFS= read -r nested; do |
| 323 | [[ -z "$nested" ]] && continue |
| 324 | ent="${work}/$(basename "$nested" | tr '/' '_').entitlements" |
| 325 | echo "✍️ sign nested → ${nested#"$app_dir"/}" |
| 326 | if codesign -d --entitlements :"$ent" "$nested" 2>/dev/null && [[ -s "$ent" ]]; then |
| 327 | codesign --force --timestamp=none --sign "$SIGN_IDENTITY" --entitlements "$ent" "$nested" |
| 328 | else |
| 329 | codesign --force --timestamp=none --sign "$SIGN_IDENTITY" "$nested" |
| 330 | fi |
| 331 | done < <(find "$app_dir" \( -name '*.framework' -o -name '*.dylib' -o -name '*.appex' \) -print \ |
| 332 | | awk '{ print length, $0 }' | sort -rn | cut -d' ' -f2-) |
| 333 | |
| 334 | ent="${work}/app.entitlements" |
| 335 | if codesign -d --entitlements :"$ent" "$app_dir" 2>/dev/null && [[ -s "$ent" ]]; then |
| 336 | codesign --force --timestamp=none --sign "$SIGN_IDENTITY" --entitlements "$ent" "$app_dir" |
| 337 | else |
| 338 | codesign --force --timestamp=none --sign "$SIGN_IDENTITY" "$app_dir" |
| 339 | fi |
| 340 | codesign --verify --deep --strict "$app_dir" || fail "Re-signed app failed signature verification" |
| 341 | echo "🔁 now: DTXcodeBuild=$(/usr/libexec/PlistBuddy -c 'Print :DTXcodeBuild' "$app_dir/Info.plist" 2>/dev/null) BuildMachineOSBuild=$(/usr/libexec/PlistBuddy -c 'Print :BuildMachineOSBuild' "$app_dir/Info.plist" 2>/dev/null)" |
| 342 | |
| 343 | rm -f "$IPA_PATH" |
| 344 | ( cd "$work" && zip -qry "$IPA_PATH" Payload ) |
| 345 | [[ -f "$IPA_PATH" ]] || fail "Repack failed: $IPA_PATH" |
| 346 | |
| 347 | ok "Patched + re-signed → ${IPA_PATH}" |
| 348 | } |
| 349 | |
| 350 | do_upload() { |
| 351 | step "Upload" |
| 352 | |
| 353 | [[ -f "$IPA_PATH" ]] || fail ".ipa not found: ${IPA_PATH}\n Run 'export' step first." |
| 354 | |
| 355 | echo "📤 file → ${IPA_PATH}" |
| 356 | echo "🔑 apiKey → ${ASC_API_KEY}" |
| 357 | echo "🏢 issuer → ${ASC_ISSUER_ID}" |
| 358 | echo "" |
| 359 | |
| 360 | xcrun altool \ |
| 361 | --upload-app \ |
| 362 | --type visionos \ |
| 363 | --file "${IPA_PATH}" \ |
| 364 | --apiKey "${ASC_API_KEY}" \ |
| 365 | --apiIssuer "${ASC_ISSUER_ID}" |
| 366 | |
| 367 | ok "Uploaded to App Store Connect" |
| 368 | } |
| 369 | |
| 370 | # ── Entry point ─────────────────────────────────────────────────────────────── |
| 371 | |
| 372 | STEP="${1:-all}" |
| 373 | |
| 374 | case "$STEP" in |
| 375 | generate) do_generate ;; |
| 376 | archive) do_archive ;; |
| 377 | export) do_export ;; |
| 378 | fixplist) do_fixplist ;; |
| 379 | upload) do_upload ;; |
| 380 | all) |
| 381 | do_generate |
| 382 | do_archive |
| 383 | do_export |
| 384 | do_fixplist |
| 385 | do_upload |
| 386 | echo "" |
| 387 | echo "🎉 Pipeline complete: built and uploaded SitiVision to App Store Connect." |
| 388 | ;; |
| 389 | *) |
| 390 | echo "Usage: $0 [generate|archive|export|fixplist|upload|all]" >&2 |
| 391 | exit 1 |
| 392 | ;; |
| 393 | esac |