#!/usr/bin/env bash # compile-ios.sh — build signed iOS IPA and upload to TestFlight (App Store Connect) # # 需要的 CI secrets(缺任意一个则优雅跳过,不阻塞流水线): # IOS_DIST_CERT_P12_BASE64 Apple Distribution 证书 (.p12) 的 base64 # IOS_DIST_CERT_PASSWORD .p12 导出密码 # IOS_PROVISIONING_PROFILE_BASE64 App Store 类型 provisioning profile (.mobileprovision) 的 base64 # IOS_TEAM_ID Apple Developer Team ID(10 位) # APPSTORE_API_KEY_ID App Store Connect API Key ID # APPSTORE_API_ISSUER_ID App Store Connect API Issuer ID # APPSTORE_API_KEY_P8_BASE64 API Key (.p8) 的 base64 # # 详见 docs/ios-signing.md。 set -euo pipefail # shellcheck source=scripts/ci/_env.sh . "$(dirname "$0")/_env.sh" TAG="$1" VER="${TAG#v}" # CFBundleVersion(build 号)必须单调递增,否则 TestFlight 拒绝重复上传。 MAJOR="$(echo "$VER" | cut -d. -f1)" MINOR="$(echo "$VER" | cut -d. -f2)" PATCH="$(echo "$VER" | cut -d. -f3)" BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH )) echo "==> compile-ios: version=${VER} build=${BUILD}" # --- 凭证缺失则跳过(账号/证书尚未配置时不阻塞发版)--- if [ -z "${IOS_DIST_CERT_P12_BASE64:-}" ] || \ [ -z "${IOS_PROVISIONING_PROFILE_BASE64:-}" ] || \ [ -z "${APPSTORE_API_KEY_P8_BASE64:-}" ]; then echo "==> compile-ios: SKIP — iOS signing secrets not configured (see docs/ios-signing.md)" exit 0 fi WORK="$(mktemp -d)" cleanup() { security delete-keychain "$KEYCHAIN" 2>/dev/null || true rm -rf "$WORK" rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/jiu-ci.mobileprovision" 2>/dev/null || true } trap cleanup EXIT # --- 1. 临时 keychain 导入发行证书 --- KEYCHAIN="$WORK/jiu-ci.keychain-db" KEYCHAIN_PWD="ci-temp-$$" security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" security set-keychain-settings -lut 21600 "$KEYCHAIN" security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" echo "${IOS_DIST_CERT_P12_BASE64}" | base64 --decode > "$WORK/dist.p12" security import "$WORK/dist.p12" -k "$KEYCHAIN" \ -P "${IOS_DIST_CERT_PASSWORD:-}" -T /usr/bin/codesign -T /usr/bin/security # 允许 codesign 非交互访问私钥 security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null # 把临时 keychain 加入搜索列表(保留默认 login keychain) security list-keychains -d user -s "$KEYCHAIN" login.keychain-db # --- 2. 安装 provisioning profile --- PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles" mkdir -p "$PROFILES_DIR" echo "${IOS_PROVISIONING_PROFILE_BASE64}" | base64 --decode > "$WORK/profile.mobileprovision" # 解出 profile 的 Name 与 UUID(ExportOptions 需要) security cms -D -i "$WORK/profile.mobileprovision" > "$WORK/profile.plist" PROFILE_NAME="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$WORK/profile.plist")" PROFILE_UUID="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$WORK/profile.plist")" cp "$WORK/profile.mobileprovision" "$PROFILES_DIR/${PROFILE_UUID}.mobileprovision" echo "==> compile-ios: profile '${PROFILE_NAME}' (${PROFILE_UUID})" # --- 3. App Store Connect API Key(供上传使用)--- API_KEYS_DIR="$HOME/.appstoreconnect/private_keys" mkdir -p "$API_KEYS_DIR" echo "${APPSTORE_API_KEY_P8_BASE64}" | base64 --decode > "$API_KEYS_DIR/AuthKey_${APPSTORE_API_KEY_ID}.p8" # --- 4. 同步版本号 --- sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" client/pubspec.yaml # --- 5. 生成 ExportOptions.plist(manual 签名,app-store 分发)--- BUNDLE_ID="com.yanmei.jiu" cat > "$WORK/ExportOptions.plist" < methodapp-store teamID${IOS_TEAM_ID} signingStylemanual uploadBitcode uploadSymbols provisioningProfiles ${BUNDLE_ID}${PROFILE_NAME} EOF # --- 6. 构建签名 IPA --- cd client flutter build ipa --release \ --build-name="${VER}" \ --build-number="${BUILD}" \ --export-options-plist="$WORK/ExportOptions.plist" \ "--dart-define=BASE_URL=https://jiu.51yanmei.com" \ "--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \ "--dart-define=APP_VERSION=${TAG}" cd .. IPA="$(ls client/build/ios/ipa/*.ipa | head -1)" if [ -z "$IPA" ] || [ ! -f "$IPA" ]; then echo "ERROR: IPA not found" >&2 exit 1 fi echo "==> compile-ios: built ${IPA}" # --- 7. 上传 TestFlight(App Store Connect)--- echo "==> compile-ios: uploading to TestFlight" xcrun altool --upload-app -f "$IPA" -t ios \ --apiKey "${APPSTORE_API_KEY_ID}" \ --apiIssuer "${APPSTORE_API_ISSUER_ID}" echo "==> compile-ios: done — uploaded build ${BUILD} to TestFlight"