publish-ios.sh (7700B)
1 #!/bin/bash 2 set -euo pipefail 3 4 REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" 5 SECRETS="$REPO_ROOT/.asc/secrets.sh" 6 7 if [[ ! -f "$SECRETS" ]]; then 8 echo "Error: $SECRETS not found. See Scripts/secrets.sh.example for the required format." 9 exit 1 10 fi 11 12 source "$SECRETS" 13 14 LOCAL_XCCONFIG="$REPO_ROOT/Generated/Local.xcconfig" 15 16 xcconfig_value() { 17 local name="$1" 18 [[ -f "$LOCAL_XCCONFIG" ]] || return 0 19 awk -F= -v key="$name" ' 20 $1 ~ "^[[:space:]]*" key "[[:space:]]*$" { 21 value = $2 22 sub(/^[[:space:]]+/, "", value) 23 sub(/[[:space:]]+$/, "", value) 24 print value 25 exit 26 } 27 ' "$LOCAL_XCCONFIG" 28 } 29 30 build_setting() { 31 local name="$1" 32 local value="${!name:-}" 33 if [[ -z "$value" ]]; then 34 value="$(xcconfig_value "$name")" 35 fi 36 printf '%s' "$value" 37 } 38 39 CROSSMATE_PUSH_BASE_URL="$(build_setting CROSSMATE_PUSH_BASE_URL)" 40 CROSSMATE_ENGAGEMENT_SOCKET_URL="$(build_setting CROSSMATE_ENGAGEMENT_SOCKET_URL)" 41 CROSSMATE_SHARE_LINK_BASE_URL="$(build_setting CROSSMATE_SHARE_LINK_BASE_URL)" 42 CROSSMATE_SHARE_LINK_HOST="$(build_setting CROSSMATE_SHARE_LINK_HOST)" 43 44 require_build_setting() { 45 local name="$1" 46 local value="${!name:-}" 47 if [[ -z "$value" ]]; then 48 echo "Error: $name is not set in $SECRETS or $LOCAL_XCCONFIG." 49 exit 1 50 fi 51 } 52 53 require_build_setting "CROSSMATE_PUSH_BASE_URL" 54 55 SCHEME="Crossmate" 56 # Archive into the Xcode Archives library at a unique per-build path so 57 # Organizer indexes every build and TestFlight crashes symbolicate 58 # automatically. A fixed/volatile path (e.g. /tmp) makes each publish 59 # overwrite the previous build's dSYM, leaving superseded builds' crashes 60 # permanently unsymbolicatable. One timestamp drives both the date dir 61 # and the archive name so they can't disagree across a midnight boundary. 62 ARCHIVE_STAMP="$(date '+%Y-%m-%d %H.%M.%S')" 63 ARCHIVE_DIR="$HOME/Library/Developer/Xcode/Archives/${ARCHIVE_STAMP%% *}" 64 ARCHIVE_PATH="$ARCHIVE_DIR/$SCHEME $ARCHIVE_STAMP.xcarchive" 65 EXPORT_PATH="/tmp/Crossmate-export" 66 EXPORT_PLIST="/tmp/Crossmate-ExportOptions.plist" 67 IPA_PATH="$EXPORT_PATH/Crossmate.ipa" 68 DEV_P12="$REPO_ROOT/.asc/dev.p12" 69 DIST_P12="$REPO_ROOT/.asc/ios-signing/dist-headless.p12" 70 TMP_KEYCHAIN="$REPO_ROOT/.asc/build.keychain-db" 71 72 CHECK_ONLY=false 73 if [[ "${1:-}" == "--check" ]]; then 74 CHECK_ONLY=true 75 fi 76 77 cd "$REPO_ROOT" 78 79 if ! git diff --quiet || ! git diff --cached --quiet; then 80 echo "Error: Git repository is dirty. Commit or stash changes before publishing." 81 exit 1 82 fi 83 84 # Capture the caller's exact keychain configuration before touching anything, 85 # so cleanup restores their real prior state rather than assuming login-only. 86 # `security` prints each path quoted and indented; strip both. 87 unquote_keychain_path() { 88 local line="$1" 89 line="${line#"${line%%[![:space:]]*}"}" 90 line="${line%\"}" 91 line="${line#\"}" 92 printf '%s' "$line" 93 } 94 95 ORIGINAL_KEYCHAINS=() 96 while IFS= read -r line; do 97 [[ -n "$line" ]] && ORIGINAL_KEYCHAINS+=("$(unquote_keychain_path "$line")") 98 done < <(security list-keychains -d user) 99 100 ORIGINAL_DEFAULT_KEYCHAIN="$(unquote_keychain_path "$(security default-keychain -d user 2>/dev/null || true)")" 101 102 if [[ ${#ORIGINAL_KEYCHAINS[@]} -eq 0 ]]; then 103 echo "Error: could not read the current keychain search list; refusing to modify it." 104 exit 1 105 fi 106 107 # Cleanup must tolerate a partially completed setup: it is installed before the 108 # temporary keychain exists, and any step below can die under `set -e`. 109 cleanup_keychain() { 110 echo "==> Restoring keychain search list..." 111 if [[ ${#ORIGINAL_KEYCHAINS[@]} -gt 0 ]]; then 112 security list-keychains -d user -s "${ORIGINAL_KEYCHAINS[@]}" || true 113 fi 114 if [[ -n "$ORIGINAL_DEFAULT_KEYCHAIN" ]]; then 115 security default-keychain -d user -s "$ORIGINAL_DEFAULT_KEYCHAIN" || true 116 fi 117 security delete-keychain "$TMP_KEYCHAIN" 2>/dev/null || true 118 rm -f "$REPO_ROOT/private_keys/AuthKey_${KEY_ID}.p8" 119 rmdir "$REPO_ROOT/private_keys" 2>/dev/null || true 120 } 121 trap cleanup_keychain EXIT 122 123 echo "==> Setting up temporary keychain..." 124 security delete-keychain "$TMP_KEYCHAIN" 2>/dev/null || true 125 security create-keychain -p "$TMP_KEYCHAIN_PASS" "$TMP_KEYCHAIN" 126 security unlock-keychain -p "$TMP_KEYCHAIN_PASS" "$TMP_KEYCHAIN" 127 security set-keychain-settings -lut 21600 "$TMP_KEYCHAIN" 128 security import "$DEV_P12" -k "$TMP_KEYCHAIN" -P "$DEV_P12_PASS" \ 129 -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild 130 security import "$DIST_P12" -k "$TMP_KEYCHAIN" -P "$DIST_P12_PASS" \ 131 -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild 132 security set-key-partition-list -S apple-tool:,apple:,codesign:,productbuild: \ 133 -s -k "$TMP_KEYCHAIN_PASS" "$TMP_KEYCHAIN" 134 security list-keychains -d user -s "$TMP_KEYCHAIN" "${ORIGINAL_KEYCHAINS[@]}" 135 136 mkdir -p "$ARCHIVE_DIR" 137 echo "==> Archiving $SCHEME to $ARCHIVE_PATH..." 138 xcodebuild \ 139 -scheme "$SCHEME" \ 140 -project Crossmate.xcodeproj \ 141 -configuration Release \ 142 -destination 'generic/platform=iOS' \ 143 -archivePath "$ARCHIVE_PATH" \ 144 CROSSMATE_PUSH_BASE_URL="$CROSSMATE_PUSH_BASE_URL" \ 145 CROSSMATE_ENGAGEMENT_SOCKET_URL="${CROSSMATE_ENGAGEMENT_SOCKET_URL:-}" \ 146 CROSSMATE_SHARE_LINK_BASE_URL="${CROSSMATE_SHARE_LINK_BASE_URL:-}" \ 147 CROSSMATE_SHARE_LINK_HOST="${CROSSMATE_SHARE_LINK_HOST:-}" \ 148 archive 149 150 echo "==> Writing export options..." 151 cat > "$EXPORT_PLIST" <<PLIST 152 <?xml version="1.0" encoding="UTF-8"?> 153 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 154 "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 155 <plist version="1.0"> 156 <dict> 157 <key>method</key> 158 <string>app-store-connect</string> 159 <key>signingStyle</key> 160 <string>manual</string> 161 <key>teamID</key> 162 <string>$TEAM_ID</string> 163 <key>signingCertificate</key> 164 <string>iPhone Distribution</string> 165 <key>provisioningProfiles</key> 166 <dict> 167 <key>net.inqk.crossmate</key> 168 <string>Crossmate iOS Distribution</string> 169 <key>net.inqk.crossmate.notificationservice</key> 170 <string>Crossmate Notification Service iOS Distribution</string> 171 </dict> 172 <key>destination</key> 173 <string>export</string> 174 <key>stripSwiftSymbols</key> 175 <true/> 176 <key>manageAppVersionAndBuildNumber</key> 177 <false/> 178 </dict> 179 </plist> 180 PLIST 181 182 echo "==> Exporting IPA..." 183 xcodebuild \ 184 -exportArchive \ 185 -archivePath "$ARCHIVE_PATH" \ 186 -exportPath "$EXPORT_PATH" \ 187 -exportOptionsPlist "$EXPORT_PLIST" 188 189 echo "==> Checking entitlements in exported IPA..." 190 CHECK_DIR="/tmp/Crossmate-ipa-check" 191 rm -rf "$CHECK_DIR" 192 unzip -q "$IPA_PATH" -d "$CHECK_DIR" 193 INFO_PLIST="$CHECK_DIR/Payload/Crossmate.app/Info.plist" 194 PUSH_BASE_URL=$(/usr/libexec/PlistBuddy -c "Print :CrossmatePushBaseURL" "$INFO_PLIST") 195 APS_ENVIRONMENT=$(/usr/libexec/PlistBuddy -c "Print :CrossmateAPSEnvironment" "$INFO_PLIST") 196 if [[ "$PUSH_BASE_URL" != https://* ]]; then 197 echo "Error: exported app has invalid CrossmatePushBaseURL: $PUSH_BASE_URL" 198 exit 1 199 fi 200 if [[ "$APS_ENVIRONMENT" != "production" ]]; then 201 echo "Error: exported app has unexpected CrossmateAPSEnvironment: $APS_ENVIRONMENT" 202 exit 1 203 fi 204 echo "--- iOS app entitlements ---" 205 codesign -d --entitlements - "$CHECK_DIR/Payload/Crossmate.app" 206 rm -rf "$CHECK_DIR" 207 208 if $CHECK_ONLY; then 209 echo "==> Check complete. Skipping upload." 210 exit 0 211 fi 212 213 echo "==> Uploading to App Store Connect..." 214 mkdir -p "$REPO_ROOT/private_keys" 215 cp "$REPO_ROOT/.asc/AuthKey_${KEY_ID}.p8" "$REPO_ROOT/private_keys/" 216 xcrun iTMSTransporter \ 217 -m upload \ 218 -assetFile "$IPA_PATH" \ 219 -apiKey "$KEY_ID" \ 220 -apiIssuer "$ISSUER_ID" \ 221 -v informational 222 223 echo "==> Done!"