crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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!"