build-nym-vpn-apple #1663
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: build-nym-vpn-apple | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| paths: | |
| - ".github/workflows/build-nym-vpn-apple.yml" | |
| - "nym-vpn-apple/**" | |
| workflow_dispatch: | |
| inputs: | |
| release_type: | |
| description: | | |
| Select build mode: | |
| 🚜 pr — Pull Request | |
| 🧪 qa — QA Release | |
| 🚢 ship — Ship Release | |
| type: choice | |
| options: | |
| - pr | |
| - qa | |
| - ship | |
| default: "pr" | |
| build_ios: | |
| description: "📱 iOS app (only applies for QA/Ship builds)" | |
| required: false | |
| type: boolean | |
| default: false | |
| build_macos: | |
| description: "🖥️ macOS app (only applies for QA/Ship builds)" | |
| required: false | |
| type: boolean | |
| default: false | |
| workflow_call: | |
| inputs: | |
| release_type: | |
| description: "Build type: pr | qa | ship (raw or labeled)" | |
| type: string | |
| default: "pr" | |
| outputs: | |
| RUST_VERSION: | |
| value: ${{ jobs.build-apple.outputs.RUST_VERSION }} | |
| schedule: | |
| - cron: "0 1 * * *" | |
| env: | |
| CARGO_TERM_COLOR: always | |
| UPLOAD_DIR_IOS: ios_artifacts | |
| RAW_RELEASE_TYPE: ${{ (github.event_name == 'pull_request' && 'pr') || inputs.release_type || 'pr' }} | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-apple: | |
| if: github.actor != 'dependabot[bot]' | |
| runs-on: AppleSilicon | |
| timeout-minutes: 90 | |
| outputs: | |
| UPLOAD_DIR_IOS: ${{ env.UPLOAD_DIR_IOS }} | |
| RUST_VERSION: ${{ steps.rust-version.outputs.rustc }} | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 1 | |
| - name: Dotenv Action | |
| uses: xom9ikk/dotenv@v2.4.0 | |
| - name: Normalize release_type | |
| run: | | |
| set -euo pipefail | |
| echo "🔍 Normalizing release type and platform flags..." | |
| echo "--------------------------------------------------" | |
| sel="${{ env.RAW_RELEASE_TYPE }}" | |
| ios="${{ inputs.build_ios || 'false' }}" | |
| macos="${{ inputs.build_macos || 'false' }}" | |
| # Determine normalized mode (pr / qa / ship) | |
| case "$sel" in | |
| pr|PR) mode=pr ;; | |
| qa|QA) mode=qa ;; | |
| ship|SHIP) mode=ship ;; | |
| *) mode=pr ;; | |
| esac | |
| # If triggered by schedule, force QA and build both platforms | |
| if [[ "${GITHUB_EVENT_NAME:-}" == "schedule" ]]; then | |
| mode=qa | |
| ios=true | |
| macos=true | |
| echo "🕒 Scheduled run detected → forcing QA with iOS+macOS" | |
| fi | |
| echo "🧩 Selected mode (raw): $sel" | |
| echo "🧩 Normalized mode: $mode" | |
| echo "📱 Build iOS: $ios" | |
| echo "🖥️ Build macOS: $macos" | |
| # Validate at least one platform selected for QA or Ship | |
| if [[ "$mode" =~ ^(qa|ship)$ ]]; then | |
| if [[ "$ios" != "true" && "$macos" != "true" ]]; then | |
| echo "❌ ERROR: At least one platform (iOS or macOS) must be selected for QA or Ship builds." | |
| exit 1 | |
| fi | |
| fi | |
| # Santa's-menu QA code is gated behind `#if SANTA`. Compile it in for | |
| # `qa` only; left OUT of `pr` and `ship` so it is physically absent | |
| # from App Store binaries. | |
| if [[ "$mode" == "qa" ]]; then santa=1; else santa=0; fi | |
| # Export environment variables for later steps | |
| echo "RELEASE_TYPE=$mode" >> "$GITHUB_ENV" | |
| echo "BUILD_IOS=$ios" >> "$GITHUB_ENV" | |
| echo "BUILD_MACOS=$macos" >> "$GITHUB_ENV" | |
| echo "NYM_SANTA=$santa" >> "$GITHUB_ENV" | |
| echo "✅ Environment successfully configured:" | |
| echo " RELEASE_TYPE=$mode" | |
| echo " BUILD_IOS=$ios" | |
| echo " BUILD_MACOS=$macos" | |
| echo " NYM_SANTA=$santa" | |
| echo "--------------------------------------------------" | |
| - name: Show selected release type | |
| run: echo "RELEASE_TYPE=${{ env.RELEASE_TYPE }}" | |
| - name: Install rust toolchain | |
| uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| components: rustfmt, clippy | |
| targets: x86_64-apple-darwin aarch64-apple-ios aarch64-apple-ios-sim | |
| - name: Install cargo-swift | |
| uses: baptiste0928/cargo-install@v3 | |
| with: | |
| crate: cargo-swift | |
| version: ${{ env.CARGO_SWIFT_VERSION }} | |
| - name: Install cargo-license | |
| if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
| uses: baptiste0928/cargo-install@v3 | |
| with: | |
| crate: cargo-license | |
| - name: Install Protoc | |
| uses: arduino/setup-protoc@v3 | |
| with: | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Install Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ env.GOLANG_VERSION }} | |
| cache-dependency-path: wireguard/libwg/go.sum | |
| - name: Get workspace version | |
| id: workspace-version | |
| uses: nicolaiunrein/cargo-get@master | |
| with: | |
| subcommand: workspace.package.version --entry nym-vpn-core | |
| - name: Install cargo-edit | |
| uses: baptiste0928/cargo-install@v3 | |
| with: | |
| crate: cargo-edit | |
| - name: Add Homebrew, Ruby, and user Python tools to PATH | |
| run: | | |
| set -euxo pipefail | |
| echo "/opt/homebrew/bin" >> "$GITHUB_PATH" | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| export PATH="/opt/homebrew/bin:$HOME/.local/bin:$PATH" | |
| RUBY_PREFIX="$(brew --prefix ruby)" | |
| echo "${RUBY_PREFIX}/bin" >> "$GITHUB_PATH" | |
| export PATH="${RUBY_PREFIX}/bin:$PATH" | |
| # Ensure xcbeautify is available for xcodebuild output formatting | |
| which xcbeautify || brew install xcbeautify | |
| # Show versions for debugging | |
| brew --version | |
| ruby --version | |
| gem --version | |
| - name: Update prebundled servers | |
| if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
| working-directory: nym-vpn-apple/scripts | |
| run: | | |
| sh UpdatePrebundledServers.sh | |
| - name: Enable QA mode | |
| if: env.RELEASE_TYPE == 'qa' | |
| working-directory: nym-vpn-apple | |
| run: | | |
| if /usr/libexec/PlistBuddy -c "Print :IsCiBuild" "NymVPNDaemon/Resources/Info.plist" >/dev/null 2>&1; then | |
| /usr/libexec/PlistBuddy -c "Set :IsCiBuild true" "NymVPNDaemon/Resources/Info.plist" | |
| else | |
| /usr/libexec/PlistBuddy -c "Add :IsCiBuild string true" "NymVPNDaemon/Resources/Info.plist" | |
| fi | |
| echo "✅ Updated Info.plist contents:" | |
| /usr/libexec/PlistBuddy -c "Print" "NymVPNDaemon/Resources/Info.plist" | |
| - name: Update licences | |
| if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
| run: | | |
| cargo license -j \ | |
| --avoid-dev-deps \ | |
| --current-dir ./nym-vpn-core \ | |
| --filter-platform x86_64-apple-darwin \ | |
| --avoid-build-deps \ | |
| > ./nym-vpn-apple/NymVPN/Resources/LibLicences.json | |
| - name: Prepare Keychain and import Developer ID certificate | |
| working-directory: nym-vpn-apple | |
| id: setup-keychain | |
| env: | |
| DEVELOPER_ID_CERT_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERT }} | |
| DEVELOPER_ID_CERT_PASS: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERT_PASS }} | |
| DEVELOPER_ID_INSTALLER_CERT_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT }} | |
| DEVELOPER_ID_INSTALLER_CERT_PASS: ${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT_PASS }} | |
| run: | | |
| set -euo pipefail | |
| KEYCHAIN_PASSPHRASE=$(echo $(date +%s%N) | sha256sum | head -c 10) | |
| KEYCHAIN_NAME="$(date +%d-%m-%y)_$(echo $(date +%s%N) | sha256sum | head -c 10).keychain-db" | |
| KEYCHAIN_PATH="$HOME/Library/codemagic-cli-tools/keychains/$KEYCHAIN_NAME" | |
| echo "🔐 Initializing Codemagic keychain..." | |
| keychain initialize --path "$KEYCHAIN_PATH" --password "$KEYCHAIN_PASSPHRASE" | |
| keychain show-info | |
| echo "📥 Importing Developer ID certificate..." | |
| echo "$DEVELOPER_ID_CERT_BASE64" | base64 --decode > developer_id.p12 | |
| keychain add-certificates \ | |
| --certificate developer_id.p12 \ | |
| --certificate-password "$DEVELOPER_ID_CERT_PASS" \ | |
| --allow-app productbuild \ | |
| --allow-app codesign \ | |
| --allow-app productsign | |
| echo "📥 Importing Developer ID Installer certificate..." | |
| echo "$DEVELOPER_ID_INSTALLER_CERT_BASE64" | base64 --decode > developer_id_installer.p12 | |
| keychain add-certificates \ | |
| --certificate developer_id_installer.p12 \ | |
| --certificate-password "$DEVELOPER_ID_INSTALLER_CERT_PASS" \ | |
| --allow-app productbuild \ | |
| --allow-app codesign \ | |
| --allow-app productsign | |
| echo "✅ Keychain and certificates imported successfully." | |
| echo "Set key partition list..." | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSPHRASE" "$KEYCHAIN_PATH" | |
| echo "✅ Shared key partition with apple tools." | |
| echo "keychain_path=$KEYCHAIN_PATH" >> "$GITHUB_OUTPUT" | |
| - name: Fetch and import signing files | |
| working-directory: nym-vpn-apple | |
| env: | |
| APP_STORE_CONNECT_KEY_IDENTIFIER: ${{ secrets.APP_STORE_CONNECT_API_KEY }} | |
| APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} | |
| APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }} | |
| CERTIFICATE_PRIVATE_KEY: ${{ secrets.IOS_DISTRIBUTION_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| echo "📦 Fetching iOS signing files from App Store Connect..." | |
| app-store-connect fetch-signing-files net.nymtech.vpn \ | |
| --type IOS_APP_ADHOC \ | |
| --create \ | |
| --delete-stale-profiles \ | |
| --platform IOS | |
| echo "📦 Downloading macOS provisioning profiles..." | |
| app-store-connect profiles list \ | |
| --type MAC_APP_DIRECT \ | |
| --state ACTIVE \ | |
| --name "NymVPN macOS + Daemon" \ | |
| --save | |
| app-store-connect profiles list \ | |
| --type MAC_APP_DIRECT \ | |
| --state ACTIVE \ | |
| --name "NymVPN daemon" \ | |
| --save | |
| app-store-connect profiles list \ | |
| --type MAC_APP_DIRECT \ | |
| --state ACTIVE \ | |
| --name "NymVPN macOS Widget" \ | |
| --save | |
| keychain add-certificates | |
| echo "✅ Signing files successfully fetched and imported." | |
| - name: Configure sccache | |
| if: ${{ env.RELEASE_TYPE != 'ship' }} | |
| run: | | |
| set -euxo pipefail | |
| echo "🧱 Configuring sccache (incremental disabled — incompatible)..." | |
| which sccache || brew install sccache | |
| export RUSTC_WRAPPER="$(which sccache)" | |
| export SCCACHE_DIR="$HOME/.cache/sccache" | |
| export SCCACHE_CACHE_SIZE="50G" | |
| export SCCACHE_IDLE_TIMEOUT="0" | |
| export CARGO_INCREMENTAL=0 # ✅ disable incremental for sccache | |
| echo "RUSTC_WRAPPER=$RUSTC_WRAPPER" >> "$GITHUB_ENV" | |
| echo "SCCACHE_DIR=$SCCACHE_DIR" >> "$GITHUB_ENV" | |
| echo "SCCACHE_CACHE_SIZE=$SCCACHE_CACHE_SIZE" >> "$GITHUB_ENV" | |
| echo "SCCACHE_IDLE_TIMEOUT=$SCCACHE_IDLE_TIMEOUT" >> "$GITHUB_ENV" | |
| echo "CARGO_INCREMENTAL=$CARGO_INCREMENTAL" >> "$GITHUB_ENV" | |
| mkdir -p "$SCCACHE_DIR" | |
| - name: Update core version in app | |
| if: ${{ env.RELEASE_TYPE != 'pr' }} | |
| run: | | |
| set -euo pipefail | |
| cargo install cargo-get | |
| # Get version from Cargo.toml workspace | |
| LIB_VERSION=$(cargo get workspace.package.version --entry nym-vpn-core/Cargo.toml) | |
| echo "📦 Core library version: ${LIB_VERSION}" | |
| # Path to Swift source file | |
| app_version_file="nym-vpn-apple/ServicesMutual/Sources/AppVersionProvider/AppVersionProvider.swift" | |
| # Validate and update | |
| if [[ -f "$app_version_file" ]]; then | |
| # macOS/BSD sed syntax: -i '' | |
| sed -i '' -E 's|(public static let libVersion = ")[^"]*(")|\1'"$LIB_VERSION"'\2|' "$app_version_file" | |
| echo "✅ libVersion updated to ${LIB_VERSION} in ${app_version_file}." | |
| else | |
| echo "❌ Error: AppVersionProvider.swift file not found at ${app_version_file}" | |
| exit 1 | |
| fi | |
| - name: Update localizations Crowdin | |
| if: ${{ env.RELEASE_TYPE != 'pr' }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} | |
| CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} | |
| run: | | |
| brew tap crowdin/crowdin | |
| brew install crowdin@4 || brew upgrade crowdin@4 || true | |
| # Make sure the crowdin binary is on PATH for this step | |
| export PATH="$(brew --prefix crowdin@4)/bin:$PATH" | |
| which crowdin || echo "crowdin not in PATH" | |
| crowdin download translations \ | |
| --config crowdin-configs/macos-ios.yml \ | |
| --project-id "$CROWDIN_PROJECT_ID" \ | |
| --token "$CROWDIN_PERSONAL_TOKEN" | |
| - name: Build core | |
| working-directory: nym-vpn-apple/scripts | |
| env: | |
| VPNLIB_SENTRY_DSN: ${{ secrets.VPND_SENTRY_DSN }} | |
| RUSTC_WRAPPER: ${{ env.RUSTC_WRAPPER }} | |
| SCCACHE_DIR: ${{ env.SCCACHE_DIR }} | |
| SCCACHE_CACHE_SIZE: ${{ env.SCCACHE_CACHE_SIZE }} | |
| SCCACHE_IDLE_TIMEOUT: ${{ env.SCCACHE_IDLE_TIMEOUT }} | |
| RELEASE: ${{ env.RELEASE_TYPE != 'pr' }} # macOS.mk and iOS.mk both respect RELEASE=true|false | |
| run: | | |
| echo "🧱 Using sccache wrapper: $RUSTC_WRAPPER" | |
| sh BuildCore.sh | |
| - name: Tests | |
| if: ${{ env.RELEASE_TYPE == 'pr' }} | |
| working-directory: nym-vpn-apple/ServicesMutual | |
| env: | |
| ACCOUNT_REPORT_PATH: ${{ github.workspace }}/account-report.md | |
| run: | | |
| set -uo pipefail | |
| which jq >/dev/null || brew install jq | |
| # Run tests — don't abort on failure, we still want summary + report | |
| set +e | |
| xcodebuild test \ | |
| -scheme ServicesMutual-Package \ | |
| -destination 'platform=macOS' \ | |
| CODE_SIGNING_ALLOWED=NO \ | |
| -only-testing:ConnectionTypesTests \ | |
| -resultBundlePath TestResults.xcresult \ | |
| | xcbeautify --renderer github-actions | |
| TEST_STATUS=${PIPESTATUS[0]} | |
| set -e | |
| # Test summary → step summary | |
| BUNDLE="TestResults.xcresult" | |
| if [[ -d "$BUNDLE" ]]; then | |
| JSON="$(xcrun xcresulttool get test-results summary --path "$BUNDLE" --format json 2>/dev/null)" | |
| RESULT=$(jq -r '.result // "Unknown"' <<<"$JSON") | |
| PASSED=$(jq -r '.passedTests // 0' <<<"$JSON") | |
| FAILED=$(jq -r '.failedTests // 0' <<<"$JSON") | |
| SKIPPED=$(jq -r '.skippedTests // 0' <<<"$JSON") | |
| ICON="✅"; [[ "$RESULT" != "Passed" ]] && ICON="❌" | |
| { | |
| echo "## 🧪 ConnectionTypes tests" | |
| echo "" | |
| echo "$ICON **$RESULT** — ${PASSED} passed, ${FAILED} failed, ${SKIPPED} skipped" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [[ "$FAILED" != "0" ]]; then | |
| { | |
| echo "" | |
| echo "| Test | Failure |" | |
| echo "| --- | --- |" | |
| jq -r '.testFailures[]? | "| \(.testName) | \((.failureText // "") | gsub("[\r\n]+"; " ")) |"' <<<"$JSON" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| else | |
| echo "⚠️ No test results bundle found." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # Account report → step summary | |
| if [[ -f "$ACCOUNT_REPORT_PATH" ]]; then | |
| cat "$ACCOUNT_REPORT_PATH" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "account-report.md not produced" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| exit "$TEST_STATUS" | |
| - name: Upload test artifacts | |
| if: ${{ always() && env.RELEASE_TYPE == 'pr' }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: connectiontypes-tests | |
| path: | | |
| nym-vpn-apple/ServicesMutual/TestResults.xcresult | |
| account-report.md | |
| if-no-files-found: warn | |
| retention-days: 1 | |
| - name: Clean up test results | |
| if: ${{ always() && env.RELEASE_TYPE == 'pr' }} | |
| working-directory: nym-vpn-apple/ServicesMutual | |
| run: rm -rf TestResults.xcresult | |
| - name: Resolve iOS build number (qa/ship only) | |
| if: ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') }} | |
| working-directory: nym-vpn-apple | |
| env: | |
| APP_STORE_CONNECT_KEY_IDENTIFIER: ${{ secrets.APP_STORE_CONNECT_API_KEY }} | |
| APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} | |
| APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| PROJECT="$( | |
| xcodebuild -showBuildSettings \ | |
| -workspace "NymVPN.xcworkspace" \ | |
| -scheme "NymVPN" \ | |
| -configuration "Release" 2>/dev/null | | |
| awk '/CURRENT_PROJECT_VERSION/ { print $3; exit }' | |
| )" | |
| echo "🚜 Resolve build number (max across all TestFlight versions)" | |
| LATEST="$( | |
| app-store-connect builds list \ | |
| --app-id 6471254143 \ | |
| --platform IOS \ | |
| --silent \ | |
| --no-color \ | |
| --json 2>/dev/null \ | |
| | jq '[.[].attributes.version | tonumber] | max // 0' | |
| )" | |
| REQUIRED=$((LATEST + 1)) | |
| TARGET=$(( PROJECT > REQUIRED ? PROJECT : REQUIRED )) | |
| if (( TARGET != PROJECT )); then | |
| echo "🚜 Bump build: $PROJECT → $TARGET (TestFlight latest=$LATEST)" | |
| fastlane mac bump_build build:"$TARGET" | |
| else | |
| echo "✅ Keep build: $PROJECT (TestFlight latest=$LATEST)" | |
| fi | |
| # Export for downstream steps (build, Zulip) | |
| echo "IOS_BUILD_NUMBER=$TARGET" >> "$GITHUB_ENV" | |
| echo "IOS_TESTFLIGHT_LATEST=$LATEST" >> "$GITHUB_ENV" | |
| - name: Resolve iOS build number (PR only) | |
| if: ${{ env.RELEASE_TYPE == 'pr' }} | |
| working-directory: nym-vpn-apple | |
| run: | | |
| set -euo pipefail | |
| IOS_BUILD_NUMBER="$( | |
| xcodebuild -showBuildSettings \ | |
| -workspace "NymVPN.xcworkspace" \ | |
| -scheme "NymVPN" \ | |
| -configuration "Release" 2>/dev/null | | |
| awk '/CURRENT_PROJECT_VERSION/ { print $3; exit }' | |
| )" | |
| echo "IOS_BUILD_NUMBER=$IOS_BUILD_NUMBER" >> "$GITHUB_ENV" | |
| echo "🔢 PR build number (no bump): $IOS_BUILD_NUMBER" | |
| - name: Build iOS (Release, signed) | |
| working-directory: nym-vpn-apple | |
| env: | |
| APP_STORE_CONNECT_KEY_IDENTIFIER: ${{ secrets.APP_STORE_CONNECT_API_KEY }} | |
| APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} | |
| APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| echo "🚜 Update signing (iOS only)" | |
| # Save project.pbxproj before use-profiles mutates it, so macOS | |
| # Release signing settings are preserved for the archive step. | |
| cp NymVPN.xcodeproj/project.pbxproj NymVPN.xcodeproj/project.pbxproj.pre-ios-signing | |
| xcode-project use-profiles --archive-method "ad-hoc" --code-signing-setup-verbose-logging | |
| echo "🚀 Building signed iOS Release with Codemagic CLI..." | |
| xcode-project build-ipa \ | |
| --workspace "NymVPN.xcworkspace" \ | |
| --scheme "NymVPN" \ | |
| --config "Release" \ | |
| --clean \ | |
| --no-color \ | |
| --log-stream stdout \ | |
| 2>&1 | xcbeautify --renderer github-actions | |
| echo "✅ Signed archive + .ipa exported" | |
| echo "🔢 Build used: $IOS_BUILD_NUMBER (TestFlight latest=${IOS_TESTFLIGHT_LATEST:-n/a})" | |
| # Capture iOS version (MARKETING_VERSION) for later steps (Zulip) | |
| IOS_VERSION=$( | |
| xcodebuild -showBuildSettings \ | |
| -workspace "NymVPN.xcworkspace" \ | |
| -scheme "NymVPN" \ | |
| -configuration "Release" 2>/dev/null | | |
| awk '/MARKETING_VERSION/ { print $3; exit }' | |
| ) | |
| if [[ -z "${IOS_VERSION:-}" ]]; then | |
| echo "❌ Could not determine iOS MARKETING_VERSION" | |
| exit 1 | |
| fi | |
| echo "IOS_VERSION=$IOS_VERSION" >> "$GITHUB_ENV" | |
| echo "✅ iOS version captured: $IOS_VERSION ($IOS_BUILD_NUMBER)" | |
| - name: Generate what to test notes for iOS | |
| if: ${{ env.RELEASE_TYPE != 'pr' && env.BUILD_IOS == 'true' }} | |
| working-directory: nym-vpn-apple | |
| run: | | |
| set -euo pipefail | |
| DATE_TIME="$(date '+%Y-%m-%d %H:%M:%S')" | |
| BRANCH_NAME="${GITHUB_REF_NAME}" | |
| # Get ONLY the latest commit | |
| LATEST_COMMIT="$(git log -1 --pretty=format:'%h, %ar, message: %s')" | |
| # Build release notes (real newlines so TestFlight renders line breaks) | |
| RELEASE_NOTES="$(printf 'Date: %s\nBranch: %s\nLatest commit:\n- %s' "$DATE_TIME" "$BRANCH_NAME" "$LATEST_COMMIT")" | |
| # Build whats_new.json with proper JSON escaping | |
| export RELEASE_NOTES | |
| ruby -rjson -e ' | |
| notes = ENV.fetch("RELEASE_NOTES") | |
| payload = [{ language: "en-US", locale: "en-US", whats_new: notes }] | |
| puts JSON.pretty_generate(payload) | |
| ' > whats_new.json | |
| echo "Generated whats_new.json:" | |
| cat whats_new.json | |
| - name: Upload to TestFlight | |
| if: ${{ env.RELEASE_TYPE != 'pr' && env.BUILD_IOS == 'true' }} | |
| working-directory: nym-vpn-apple | |
| env: | |
| APP_STORE_CONNECT_KEY_IDENTIFIER: ${{ secrets.APP_STORE_CONNECT_API_KEY }} | |
| APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} | |
| APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| echo "🚀 Uploading to TestFlight..." | |
| app-store-connect publish \ | |
| --beta-build-localizations=@file:whats_new.json \ | |
| --beta-group='External QA' \ | |
| --beta-group='Nym Team External' \ | |
| --testflight | |
| # --beta-group="World" \ | |
| # --expire-build-submitted-for-review | |
| echo "✅ Upload completed successfully." | |
| - name: Restore project signing for macOS | |
| working-directory: nym-vpn-apple | |
| run: | | |
| set -euo pipefail | |
| # Restore project.pbxproj to undo any changes made by | |
| # xcode-project use-profiles (iOS ad-hoc signing step). | |
| # This ensures macOS Release targets retain their original | |
| # manual signing settings from the committed project file. | |
| if [[ -f NymVPN.xcodeproj/project.pbxproj.pre-ios-signing ]]; then | |
| echo "🔄 Restoring project.pbxproj from pre-iOS-signing backup..." | |
| mv NymVPN.xcodeproj/project.pbxproj.pre-ios-signing NymVPN.xcodeproj/project.pbxproj | |
| echo "✅ macOS signing settings restored." | |
| else | |
| echo "ℹ️ No backup found (iOS build may have been skipped). Using project as-is." | |
| fi | |
| - name: Archive macOS | |
| id: archive-macos | |
| working-directory: nym-vpn-apple | |
| env: | |
| MACOSX_DEPLOYMENT_TARGET: 14.0 | |
| DEVELOPMENT_TEAM: ${{ secrets.APPLE_TEAM_ID }} | |
| DEVELOPER_ID_APP_IDENTITY: "Developer ID Application: Nym Technologies SA (${{ secrets.APPLE_TEAM_ID }})" | |
| run: | | |
| set -euo pipefail | |
| ARCHIVE_PATH="$PWD/build/NymVPN.xcarchive" | |
| EXPORT_PATH="$PWD/builds" | |
| EXPORT_PLIST="$PWD/export_options.plist" | |
| mkdir -p "$EXPORT_PATH" | |
| # Create export options plist with explicit profile-to-bundle-ID mapping | |
| cat > "$EXPORT_PLIST" <<PLIST | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>method</key><string>developer-id</string> | |
| <key>teamID</key><string>${DEVELOPMENT_TEAM}</string> | |
| <key>signingStyle</key><string>manual</string> | |
| <key>signingCertificate</key><string>Developer ID Application</string> | |
| <key>stripSwiftSymbols</key><true/> | |
| <key>provisioningProfiles</key> | |
| <dict> | |
| <key>net.nymtech.vpn</key> | |
| <string>NymVPN macOS + Daemon</string> | |
| <key>net.nymtech.vpn.daemon</key> | |
| <string>NymVPN daemon</string> | |
| <key>net.nymtech.vpn.macOSwidget</key> | |
| <string>NymVPN macOS Widget</string> | |
| </dict> | |
| </dict> | |
| </plist> | |
| PLIST | |
| echo "Debug: Checking workspace and scheme..." | |
| ls -la NymVPN.xcworkspace | |
| ls -la NymVPN.xcodeproj/xcshareddata/xcschemes/ | |
| echo "Debug: Code signing identity: ${DEVELOPER_ID_APP_IDENTITY}" | |
| echo "Debug: Development team: ${DEVELOPMENT_TEAM}" | |
| echo "Available signing identities:" | |
| security find-identity -v -p codesigning | |
| echo "🔐 Resolving macOS provisioning profile specifiers..." | |
| PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles" | |
| DAEMON_PROFILE_SPEC="" | |
| WIDGET_PROFILE_SPEC="" | |
| for profile in "$PROFILE_DIR"/*.provisionprofile "$PROFILE_DIR"/*.mobileprovision; do | |
| [[ -f "$profile" ]] || continue | |
| name=$(security cms -D -i "$profile" 2>/dev/null | plutil -extract Name raw -o - -- -) | |
| case "$name" in | |
| "NymVPN macOS + Daemon") DAEMON_PROFILE_SPEC="$name" ;; | |
| "NymVPN macOS Widget") WIDGET_PROFILE_SPEC="$name" ;; | |
| esac | |
| done | |
| if [[ -z "$DAEMON_PROFILE_SPEC" || -z "$WIDGET_PROFILE_SPEC" ]]; then | |
| echo "❌ Could not resolve macOS profile specifiers" | |
| echo " Daemon: '${DAEMON_PROFILE_SPEC:-NOT FOUND}'" | |
| echo " Widget: '${WIDGET_PROFILE_SPEC:-NOT FOUND}'" | |
| exit 1 | |
| fi | |
| echo "✅ Daemon profile: $DAEMON_PROFILE_SPEC" | |
| echo "✅ Widget profile: $WIDGET_PROFILE_SPEC" | |
| echo "Archiving macOS app..." | |
| xcodebuild archive \ | |
| -workspace "NymVPN.xcworkspace" \ | |
| -scheme "NymVPNDaemon" \ | |
| -configuration "Release" \ | |
| -archivePath "$ARCHIVE_PATH" \ | |
| -destination "generic/platform=macOS" \ | |
| CODE_SIGN_STYLE=Manual \ | |
| DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM}" \ | |
| CODE_SIGN_IDENTITY="${DEVELOPER_ID_APP_IDENTITY}" \ | |
| MACOS_DAEMON_PROFILE_SPECIFIER="${DAEMON_PROFILE_SPEC}" \ | |
| MACOS_WIDGET_PROFILE_SPECIFIER="${WIDGET_PROFILE_SPEC}" \ | |
| ENABLE_HARDENED_RUNTIME=YES \ | |
| OTHER_CODE_SIGN_FLAGS="--timestamp" \ | |
| | xcbeautify --renderer github-actions | |
| # Verify archive was created | |
| if [[ ! -d "$ARCHIVE_PATH" ]]; then | |
| echo "Archive not created at $ARCHIVE_PATH" | |
| exit 1 | |
| fi | |
| echo "Exporting macOS app..." | |
| xcodebuild -exportArchive \ | |
| -archivePath "$ARCHIVE_PATH" \ | |
| -exportPath "$EXPORT_PATH" \ | |
| -exportOptionsPlist "$EXPORT_PLIST" | |
| # Find the exported app | |
| APP_PATH="$(find "$EXPORT_PATH" -maxdepth 1 -type d -name 'NymVPN*.app' -print -quit)" | |
| if [[ -z "$APP_PATH" || ! -d "$APP_PATH" ]]; then | |
| echo "❌ Could not find exported app in $EXPORT_PATH" | |
| ls -la "$EXPORT_PATH" | |
| exit 1 | |
| fi | |
| echo "✅ App exported to: $APP_PATH" | |
| echo "app_path=$APP_PATH" >> "$GITHUB_OUTPUT" | |
| - name: Create PKG | |
| if: ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') && env.BUILD_MACOS == 'true' }} | |
| id: make-pkg | |
| working-directory: nym-vpn-apple | |
| env: | |
| INSTALLER_SIGNING_IDENTITY: "Developer ID Installer: Nym Technologies SA (${{ secrets.APPLE_TEAM_ID }})" | |
| run: | | |
| set -euo pipefail | |
| which xmlstarlet || brew install xmlstarlet | |
| KEYCHAIN_PATH="${{ steps.setup-keychain.outputs.keychain_path }}" | |
| APP_PATH="${{ steps.archive-macos.outputs.app_path }}" | |
| APP_PLIST_PATH="$APP_PATH/Contents/Info.plist" | |
| PRODUCT_NAME="$(plutil -extract CFBundleName raw $APP_PLIST_PATH)" | |
| PRODUCT_BUNDLE_IDENTIFIER="$(plutil -extract CFBundleIdentifier raw $APP_PLIST_PATH)" | |
| MARKETING_VERSION="$(plutil -extract CFBundleShortVersionString raw $APP_PLIST_PATH)" | |
| MIN_OS_VERSION="$(plutil -extract LSMinimumSystemVersion raw $APP_PLIST_PATH)" | |
| STAGING_DIRECTORY="$PWD/build/pkg" | |
| PKG_PATH="$STAGING_DIRECTORY/NymVPN-$MARKETING_VERSION.pkg" | |
| STAGING_APP_DIR="$STAGING_DIRECTORY/macos" | |
| COMPONENT_PLIST_FILE="$STAGING_DIRECTORY/component.plist" | |
| DIST_XML_FILE="$STAGING_DIRECTORY/Distribution" | |
| UNSIGNED_PKG_PATH="$STAGING_DIRECTORY/NymVPN-$MARKETING_VERSION-unsigned.pkg" | |
| NESTED_PKG_PATH="$STAGING_DIRECTORY/$PRODUCT_BUNDLE_IDENTIFIER.pkg" | |
| # Directory with pkg scripts | |
| SCRIPTS_DIR="$PWD/Installer/Scripts" | |
| # Directory with pkg resources (images) | |
| RESOURCES_DIR="$PWD/Installer/Resources" | |
| # Ensure that staging directory is empty and exists | |
| rm -rf "$STAGING_DIRECTORY" || true | |
| mkdir -p "$STAGING_DIRECTORY" | |
| # Copy NymVPN.app into staging directory for the app | |
| echo "Copying $APP_PATH to $STAGING_APP_DIR" | |
| mkdir "$STAGING_APP_DIR" | |
| cp -R "$APP_PATH" "$STAGING_APP_DIR" | |
| ls -la "$STAGING_APP_DIR" | |
| # Generate the component property list. | |
| pkgbuild --analyze --root "$STAGING_APP_DIR" "$COMPONENT_PLIST_FILE" | |
| # Force the installation package (.pkg) to not be relocatable. | |
| # This ensures the package components install in /Applications | |
| plutil -replace "0.BundleIsRelocatable" -bool NO "$COMPONENT_PLIST_FILE" | |
| # Allow downgrades | |
| plutil -replace "0.BundleIsVersionChecked" -bool NO "$COMPONENT_PLIST_FILE" | |
| # Build a temporary package using the component property list. | |
| pkgbuild --root "$STAGING_APP_DIR" \ | |
| --identifier "$PRODUCT_BUNDLE_IDENTIFIER" \ | |
| --version "$MARKETING_VERSION" \ | |
| --install-location "/Applications" \ | |
| --scripts "$SCRIPTS_DIR" \ | |
| --min-os-version "$MIN_OS_VERSION" \ | |
| --compression latest \ | |
| --component-plist "$COMPONENT_PLIST_FILE" \ | |
| "$NESTED_PKG_PATH" | |
| # Synthesize the distribution for the temporary package. | |
| productbuild --synthesize --package "$NESTED_PKG_PATH" "$DIST_XML_FILE" | |
| # Customize installer: | |
| # - enable_anywhere=false - restrict installation to system volume only, i.e no installs on external drives | |
| # - enable_currentUserHome=false - prevent installation into home dir | |
| # - enable_localSystem=true - enable installation into root | |
| # Reference: https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html | |
| xmlstarlet edit --inplace \ | |
| --subnode '//installer-gui-script' --type elem -n 'domains' \ | |
| --append '//installer-gui-script/domains' --type attr -n 'enable_anywhere' --value 'false' \ | |
| --append '//installer-gui-script/domains' --type attr -n 'enable_currentUserHome' --value 'false' \ | |
| --append '//installer-gui-script/domains' --type attr -n 'enable_localSystem' --value 'true' \ | |
| "$DIST_XML_FILE" | |
| # Add element to prompt user to close the app before installation | |
| xmlstarlet edit --inplace \ | |
| --subnode '//installer-gui-script' \ | |
| --type elem --name "pkg-ref" \ | |
| --var new_node '$prev' \ | |
| --append '$new_node' --type attr -n 'id' --value 'net.nymtech.vpn' \ | |
| --subnode '$new_node' --type elem -n 'must-close' \ | |
| --var new_node '$prev' \ | |
| --subnode '$new_node' --type elem -n 'app' \ | |
| --append '$prev' --type attr -n 'id' --value 'net.nymtech.vpn' \ | |
| "$DIST_XML_FILE" | |
| # Add title which acts as an app name | |
| xmlstarlet edit --inplace \ | |
| --subnode '//installer-gui-script' \ | |
| --type elem --name "title" --value "$PRODUCT_NAME" \ | |
| "$DIST_XML_FILE" | |
| # Add background image for light mode | |
| # xmlstarlet edit --inplace \ | |
| # --subnode '//installer-gui-script' \ | |
| # --type elem --name "background" \ | |
| # --var new_node '$prev' \ | |
| # --append '$new_node' --type attr -n 'alignment' --value 'topleft' \ | |
| # --append '$new_node' --type attr -n 'file' --value 'background-light.png' \ | |
| # --append '$new_node' --type attr -n 'mime-type' --value 'image/png' \ | |
| # --append '$new_node' --type attr -n 'scaling' --value 'proportional' \ | |
| # "$DIST_XML_FILE" | |
| # Add background image for dark mode | |
| # xmlstarlet edit --inplace \ | |
| # --subnode '//installer-gui-script' \ | |
| # --type elem --name "background-darkAqua" \ | |
| # --var new_node '$prev' \ | |
| # --append '$new_node' --type attr -n 'alignment' --value 'topleft' \ | |
| # --append '$new_node' --type attr -n 'file' --value 'background-dark.png' \ | |
| # --append '$new_node' --type attr -n 'mime-type' --value 'image/png' \ | |
| # --append '$new_node' --type attr -n 'scaling' --value 'proportional' \ | |
| # "$DIST_XML_FILE" | |
| echo "✅ Distribution XML:" | |
| cat "$DIST_XML_FILE" | |
| # Synthesize the final package from the distribution. | |
| productbuild --distribution "$DIST_XML_FILE" --resources "$RESOURCES_DIR" --package-path "$STAGING_DIRECTORY" "$UNSIGNED_PKG_PATH" | |
| echo "✅ Unsigned PKG is written to: $UNSIGNED_PKG_PATH" | |
| # Sign separately since productbuild gets stuck on keychain password prompt when signing | |
| productsign --timestamp \ | |
| --keychain "$KEYCHAIN_PATH" \ | |
| --sign "$INSTALLER_SIGNING_IDENTITY" \ | |
| "$UNSIGNED_PKG_PATH" "$PKG_PATH" | |
| echo "✅ Signed PKG is written to: $PKG_PATH" | |
| echo "pkg_path=$PKG_PATH" >> "$GITHUB_OUTPUT" | |
| - name: Write App Store Connect API key | |
| if: ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') && env.BUILD_MACOS == 'true' }} | |
| id: asc-key | |
| env: | |
| ASC_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| KEY_DIR="$HOME/private_keys" | |
| mkdir -p "$KEY_DIR" | |
| KEY_PATH="$KEY_DIR/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY }}.p8" | |
| echo "$ASC_PRIVATE_KEY" > "$KEY_PATH" | |
| echo "key_path=$KEY_PATH" >> "$GITHUB_OUTPUT" | |
| echo "✅ ASC API key written to $KEY_PATH" | |
| - name: Notarize PKG (Developer Installer ID) | |
| if: ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') && env.BUILD_MACOS == 'true' }} | |
| id: notarize-pkg | |
| working-directory: nym-vpn-apple | |
| env: | |
| ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} | |
| ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY }} | |
| ASC_KEY_PATH: ${{ steps.asc-key.outputs.key_path }} | |
| run: | | |
| set -euxo pipefail | |
| which jq || brew install jq | |
| PKG="${{ steps.make-pkg.outputs.pkg_path }}" | |
| test -f "$PKG" || { echo "❌ PKG not found at $PKG"; exit 1; } | |
| test -f "$ASC_KEY_PATH" || { echo "❌ Missing ASC key at $ASC_KEY_PATH"; exit 1; } | |
| OUT_JSON="$PWD/build/pkg/notarytool-log.json" | |
| echo "📤 Submitting $PKG for notarization…" | |
| xcrun notarytool submit "$PKG" \ | |
| --key "$ASC_KEY_PATH" \ | |
| --key-id "$ASC_KEY_ID" \ | |
| --issuer "$ASC_ISSUER_ID" \ | |
| --wait \ | |
| --output-format json | tee "$OUT_JSON" | |
| STATUS=$(jq -r .status "$OUT_JSON") | |
| REQUEST_ID=$(jq -r .id "$OUT_JSON") | |
| if [[ "$STATUS" != "Accepted" ]]; then | |
| echo "❌ Notarization failed ($STATUS)" | |
| echo "🪵 Fetching detailed log for Request ID: $REQUEST_ID" | |
| LOG_PATH="$PWD/build/pkg/notarytool-log-detailed.json" | |
| xcrun notarytool log "$REQUEST_ID" \ | |
| --key "$ASC_KEY_PATH" \ | |
| --key-id "$ASC_KEY_ID" \ | |
| --issuer "$ASC_ISSUER_ID" \ | |
| --output-format json | tee "$LOG_PATH" | |
| echo "📋 Detailed log saved at: $LOG_PATH" | |
| cat "$LOG_PATH" | |
| exit 1 | |
| fi | |
| echo "✅ Notarization status: Accepted" | |
| echo "📎 Stapling PKG…" | |
| xcrun stapler staple "$PKG" | |
| xcrun stapler validate "$PKG" | |
| echo "pkg_path=$PKG" >> "$GITHUB_OUTPUT" | |
| - name: Generate artifact name | |
| if: ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') && env.BUILD_MACOS == 'true' }} | |
| run: | | |
| TIMESTAMP=$(date +"%Y%m%d-%H%M") | |
| SAFE_BRANCH=$(echo "${GITHUB_REF_NAME}" | tr '/' '-') | |
| echo "ARTIFACT_NAME=apple-${RELEASE_TYPE}-${TIMESTAMP}-${SAFE_BRANCH}" >> "$GITHUB_ENV" | |
| - name: Rename PKG for upload | |
| if: ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') && env.BUILD_MACOS == 'true' }} | |
| working-directory: nym-vpn-apple/build/pkg | |
| run: | | |
| set -euo pipefail | |
| ORIGINAL_PKG=$(basename "${{ steps.notarize-pkg.outputs.pkg_path || steps.make-pkg.outputs.pkg_path }}") | |
| # Strip "apple-" prefix → keep timestamp-branch only | |
| PREFIX_PART=$(echo "${ARTIFACT_NAME}" | sed 's/^apple-//') | |
| NEW_NAME="${PREFIX_PART}-${ORIGINAL_PKG}" | |
| echo "📦 Renaming package:" | |
| echo " Old: $ORIGINAL_PKG" | |
| echo " New: $NEW_NAME" | |
| mv "$ORIGINAL_PKG" "$NEW_NAME" | |
| # Export relative path (relative to repository root) | |
| RELATIVE_PATH="nym-vpn-apple/build/pkg/$NEW_NAME" | |
| echo "renamed_pkg_relative_path=$RELATIVE_PATH" >> "$GITHUB_ENV" | |
| echo "✅ Renamed and ready: $RELATIVE_PATH" | |
| - name: Create ZIP archive from signed PKG (ship) | |
| if: ${{ env.RELEASE_TYPE == 'ship' && env.BUILD_MACOS == 'true' }} | |
| working-directory: nym-vpn-apple | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${renamed_pkg_relative_path:-}" ]]; then | |
| echo "❌ renamed_pkg_relative_path not set (did Rename PKG for upload run?)" | |
| exit 1 | |
| fi | |
| # Resolve signed, renamed PKG path (ship-20251119-2202-feat-sparkle-NymVPN-2.13.0.pkg) | |
| PKG_BASENAME="$(basename "${renamed_pkg_relative_path}")" | |
| PKG_PATH="$PWD/build/pkg/$PKG_BASENAME" | |
| if [[ ! -f "$PKG_PATH" ]]; then | |
| echo "❌ Signed PKG not found at $PKG_PATH" | |
| ls -la "$PWD/build/pkg" | |
| exit 1 | |
| fi | |
| echo "📦 Using signed PKG: $PKG_PATH" | |
| echo "📄 Reading MARKETING_VERSION from Xcode build settings…" | |
| VERSION=$( | |
| xcodebuild -showBuildSettings \ | |
| -workspace "NymVPN.xcworkspace" \ | |
| -scheme "NymVPNDaemon" \ | |
| -configuration "Release" 2>/dev/null | | |
| awk '/MARKETING_VERSION/ { print $3; exit }' | |
| ) | |
| if [[ -z "$VERSION" ]]; then | |
| echo "❌ Could not determine MARKETING_VERSION from Xcode (MARKETING_VERSION)" | |
| exit 1 | |
| fi | |
| echo "✅ MARKETING_VERSION: $VERSION" | |
| ARCHIVE_DIR="$PWD/build/pkg" | |
| # 🚩 This is the **final** PKG name we want user-facing | |
| FINAL_PKG_NAME="NymVPN-${VERSION}.pkg" | |
| FINAL_PKG_PATH="$ARCHIVE_DIR/$FINAL_PKG_NAME" | |
| echo "📛 Copying signed PKG to final name: $FINAL_PKG_PATH" | |
| cp "$PKG_PATH" "$FINAL_PKG_PATH" | |
| # 🎁 Create zip with the PKG at the zip root (no extra "pkg" folder) | |
| pushd "$ARCHIVE_DIR" >/dev/null | |
| NOTARISATION_ARCHIVE="NymVPN-${VERSION}.zip" | |
| echo "🧷 Creating archive: $NOTARISATION_ARCHIVE" | |
| # No --keepParent, and run from ARCHIVE_DIR so zip contains just NymVPN-<version>.pkg at root | |
| ditto -c -k "$FINAL_PKG_NAME" "$NOTARISATION_ARCHIVE" | |
| echo "📂 Archive directory contents after zip:" | |
| ls -la | |
| popd >/dev/null | |
| # Export absolute zip path for the Sparkle/sign_update step | |
| echo "zip_archive_path=$ARCHIVE_DIR/$NOTARISATION_ARCHIVE" >> "$GITHUB_ENV" | |
| echo "FINAL_PKG_PATH=$FINAL_PKG_PATH" >> "$GITHUB_ENV" | |
| echo "MACOS_VERSION=$VERSION" >> "$GITHUB_ENV" | |
| - name: Generate Sparkle appcast & SHA-256 hash (sign_update) | |
| if: ${{ env.RELEASE_TYPE == 'ship' && env.BUILD_MACOS == 'true' }} | |
| working-directory: nym-vpn-apple/build/pkg | |
| env: | |
| APPLE_SPARKLE_PRIVATE_KEY: ${{ secrets.APPLE_SPARKLE_PRIVATE_KEY }} | |
| APP_PATH: ${{ steps.archive-macos.outputs.app_path }} | |
| ZIP_ARCHIVE_PATH: ${{ env.zip_archive_path }} | |
| run: | | |
| set -euo pipefail | |
| which jq || brew install jq | |
| if [[ -z "${renamed_pkg_relative_path:-}" ]]; then | |
| echo "❌ renamed_pkg_relative_path not set" | |
| exit 1 | |
| fi | |
| INSTALLER_PKG="$(basename "${renamed_pkg_relative_path}")" | |
| if [[ ! -f "$INSTALLER_PKG" ]]; then | |
| echo "❌ PKG file not found in $(pwd): $INSTALLER_PKG" | |
| ls -la | |
| exit 1 | |
| fi | |
| echo "📦 Using PKG: $INSTALLER_PKG" | |
| if [[ -z "${ZIP_ARCHIVE_PATH:-}" || ! -f "$ZIP_ARCHIVE_PATH" ]]; then | |
| echo "❌ ZIP_ARCHIVE_PATH invalid or missing: $ZIP_ARCHIVE_PATH" | |
| ls -la | |
| exit 1 | |
| fi | |
| ZIP_FILE="$(basename "$ZIP_ARCHIVE_PATH")" | |
| if [[ "$ZIP_FILE" != *.zip ]]; then | |
| echo "❌ ZIP_ARCHIVE_PATH does not look like a .zip: $ZIP_FILE" | |
| exit 1 | |
| fi | |
| # Make sure the zip is in the current working directory (build/pkg) | |
| if [[ ! -f "$ZIP_FILE" ]]; then | |
| echo "📥 Copying zip into working dir…" | |
| cp "$ZIP_ARCHIVE_PATH" . | |
| fi | |
| echo "🧷 Using ZIP for Sparkle signing: $ZIP_FILE" | |
| if [[ -z "${APP_PATH:-}" || ! -d "$APP_PATH" ]]; then | |
| echo "❌ APP_PATH not set or invalid: $APP_PATH" | |
| exit 1 | |
| fi | |
| APP_PLIST="$APP_PATH/Contents/Info.plist" | |
| SHORT_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_PLIST")" | |
| echo "ℹ️ CFBundleShortVersionString: $SHORT_VERSION" | |
| # --- Download latest Sparkle distribution for sign_update --- | |
| SPARKLE_DIR="$RUNNER_TEMP/sparkle" | |
| mkdir -p "$SPARKLE_DIR" | |
| echo "📥 Fetching latest Sparkle release metadata…" | |
| curl -fsSL "https://api.github.com/repos/sparkle-project/Sparkle/releases/latest" \ | |
| -o "$SPARKLE_DIR/release.json" | |
| ASSET_URL=$(jq -r '.assets[] | select(.name | endswith(".tar.xz")) | .browser_download_url' \ | |
| "$SPARKLE_DIR/release.json" | head -n 1) | |
| if [[ -z "$ASSET_URL" || "$ASSET_URL" == "null" ]]; then | |
| echo "❌ Could not find Sparkle .tar.xz asset in latest release" | |
| jq '.' "$SPARKLE_DIR/release.json" || true | |
| exit 1 | |
| fi | |
| echo "⬇️ Downloading Sparkle from: $ASSET_URL" | |
| SPARKLE_TAR="$SPARKLE_DIR/Sparkle.tar.xz" | |
| curl -fsSL "$ASSET_URL" -o "$SPARKLE_TAR" | |
| EXTRACT_DIR="$SPARKLE_DIR/extracted" | |
| mkdir -p "$EXTRACT_DIR" | |
| tar -xf "$SPARKLE_TAR" -C "$EXTRACT_DIR" --strip-components=1 | |
| SPARKLE_BIN="$EXTRACT_DIR/bin" | |
| if [[ ! -x "$SPARKLE_BIN/sign_update" ]]; then | |
| echo "❌ sign_update not found in Sparkle distribution" | |
| ls -R "$EXTRACT_DIR" | |
| exit 1 | |
| fi | |
| if [[ -z "${APPLE_SPARKLE_PRIVATE_KEY:-}" ]]; then | |
| echo "❌ APPLE_SPARKLE_PRIVATE_KEY secret is empty" | |
| exit 1 | |
| fi | |
| # 🔑 Write EdDSA private key to a file and ensure it's removed afterwards | |
| KEY_FILE="$SPARKLE_DIR/sparkle_private_key_file" | |
| printf '%s' "$APPLE_SPARKLE_PRIVATE_KEY" > "$KEY_FILE" | |
| chmod 600 "$KEY_FILE" | |
| trap "rm -f '$KEY_FILE'" EXIT | |
| echo "📝 Signing ZIP with Sparkle EdDSA key…" | |
| # sign_update prints something like: | |
| # sparkle:edSignature="…" length="123456" | |
| SPARKLE_ATTRS=$("$SPARKLE_BIN/sign_update" --ed-key-file "$KEY_FILE" "$ZIP_FILE") | |
| if [[ -z "${SPARKLE_ATTRS:-}" ]]; then | |
| echo "❌ Failed to obtain Sparkle signature attributes from sign_update" | |
| exit 1 | |
| fi | |
| echo "✅ Sparkle attributes: $SPARKLE_ATTRS" | |
| echo "🔐 Computing SHA-256 (ZIP payload)" | |
| shasum -a 256 "$ZIP_FILE" > sha256-hash.txt | |
| echo "✅ sha256-hash.txt:" | |
| cat sha256-hash.txt | |
| DOWNLOAD_URL="https://github.com/nymtech/nym-vpn-client/releases/download/nym-vpn-macOS-v${MACOS_VERSION}/NymVPN-${MACOS_VERSION}.zip" | |
| HOST_LINK="https://nymvpn.com" | |
| PUB_DATE="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S %z')" | |
| echo "🧾 Creating appcast.xml…" | |
| { | |
| echo '<?xml version="1.0" standalone="yes"?>' | |
| echo '<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">' | |
| echo ' <channel>' | |
| echo ' <title>NymVPN</title>' | |
| echo " <link>${HOST_LINK}</link>" | |
| echo ' <item>' | |
| echo " <title>${SHORT_VERSION}</title>" | |
| echo " <pubDate>${PUB_DATE}</pubDate>" | |
| echo " <sparkle:version>${IOS_BUILD_NUMBER}</sparkle:version>" | |
| echo " <sparkle:shortVersionString>${SHORT_VERSION}</sparkle:shortVersionString>" | |
| echo " <sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>" | |
| echo ' <enclosure' | |
| echo " url=\"${DOWNLOAD_URL}\"" | |
| echo ' sparkle:os="macos" sparkle:installationType="package"' | |
| echo ' type="application/octet-stream"' | |
| printf ' %s />\n' "${SPARKLE_ATTRS}" | |
| echo ' </item>' | |
| echo ' </channel>' | |
| echo '</rss>' | |
| } > appcast.xml | |
| echo "✅ Generated appcast.xml:" | |
| cat appcast.xml | |
| PUBLISH_DIR="publish" | |
| rm -rf "$PUBLISH_DIR" | |
| mkdir -p "$PUBLISH_DIR" | |
| cp "$ZIP_FILE" "$PUBLISH_DIR/" | |
| cp "$FINAL_PKG_PATH" "$PUBLISH_DIR/" | |
| cp appcast.xml "$PUBLISH_DIR/" | |
| cp sha256-hash.txt "$PUBLISH_DIR/" | |
| echo "📂 Publish folder contents:" | |
| ls -la "$PUBLISH_DIR" | |
| echo "publish_relative_path=nym-vpn-apple/build/pkg/$PUBLISH_DIR" >> "$GITHUB_ENV" | |
| echo "🧩 Extracting <item> block for nym-websites…" | |
| awk ' | |
| BEGIN { p=0 } | |
| /<item>/{ p=1 } | |
| p { print } | |
| /<\/item>/{ p=0 } | |
| ' appcast.xml > appcast-item.xml | |
| echo "✅ appcast-item.xml:" | |
| cat appcast-item.xml | |
| # Export for later steps | |
| echo "APPCAST_ITEM_RELATIVE_PATH=nym-vpn-apple/build/pkg/appcast-item.xml" >> "$GITHUB_ENV" | |
| - name: Upload artifacts (QA) | |
| if: ${{ env.RELEASE_TYPE == 'qa' && env.BUILD_MACOS == 'true' }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ env.ARTIFACT_NAME }} | |
| path: ${{ env.renamed_pkg_relative_path }} | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Prepare nightly PKG asset | |
| if: ${{ github.event_name == 'schedule' && env.RELEASE_TYPE == 'qa' && env.BUILD_MACOS == 'true' }} | |
| working-directory: nym-vpn-apple/build/pkg | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${renamed_pkg_relative_path:-}" ]]; then | |
| echo "❌ renamed_pkg_relative_path not set" | |
| exit 1 | |
| fi | |
| SRC_BASENAME="$(basename "${renamed_pkg_relative_path}")" | |
| if [[ ! -f "$SRC_BASENAME" ]]; then | |
| echo "❌ Source PKG not found: $SRC_BASENAME" | |
| ls -la | |
| exit 1 | |
| fi | |
| echo "📦 Using source PKG: $SRC_BASENAME" | |
| cp "$SRC_BASENAME" "NymVPN-nightly.pkg" | |
| # Export path for the release step (from repo root) | |
| echo "NIGHTLY_PKG_PATH=nym-vpn-apple/build/pkg/NymVPN-nightly.pkg" >> "$GITHUB_ENV" | |
| echo "✅ Nightly PKG prepared as NymVPN-nightly.pkg" | |
| - name: Publish macOS nightly GitHub Release | |
| if: ${{ github.event_name == 'schedule' && env.RELEASE_TYPE == 'qa' && env.BUILD_MACOS == 'true' }} | |
| uses: softprops/action-gh-release@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| tag_name: macos-nightly | |
| name: "NymVPN macOS nightly" | |
| prerelease: true | |
| draft: false | |
| files: ${{ env.NIGHTLY_PKG_PATH }} | |
| body: | | |
| Nightly macOS build of NymVPN. | |
| - Branch: ${{ github.ref_name }} | |
| - Commit: ${{ github.sha }} | |
| - Built at: ${{ github.run_started_at }} | |
| - name: Check if macOS release tag already exists | |
| if: ${{ env.RELEASE_TYPE == 'ship' && env.BUILD_MACOS == 'true' }} | |
| id: macos_tag | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="nym-vpn-macOS-v${MACOS_VERSION}" | |
| if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| echo "Tag already exists: ${TAG}" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| echo "Tag does not exist: ${TAG}" | |
| fi | |
| - name: Publish macOS GitHub Release | |
| if: ${{ env.RELEASE_TYPE == 'ship' && env.BUILD_MACOS == 'true' && steps.macos_tag.outputs.exists != 'true' }} | |
| uses: softprops/action-gh-release@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| tag_name: nym-vpn-macos-v${{ env.MACOS_VERSION }} | |
| name: "NymVPN macOS v${{ env.MACOS_VERSION }}" | |
| prerelease: false | |
| draft: false | |
| files: | | |
| nym-vpn-apple/build/pkg/publish/NymVPN-${{ env.MACOS_VERSION }}.zip | |
| nym-vpn-apple/build/pkg/publish/sha256-hash.txt | |
| body: | | |
| Release macOS build of NymVPN. | |
| - Version: ${{ env.MACOS_VERSION }} | |
| - Branch: ${{ github.ref_name }} | |
| - Commit: ${{ github.sha }} | |
| - Built at: ${{ github.run_started_at }} | |
| - name: Upload artifacts (Ship + appcast + hash) | |
| if: ${{ env.RELEASE_TYPE == 'ship' && env.BUILD_MACOS == 'true' }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ env.ARTIFACT_NAME }} | |
| path: ${{ env.publish_relative_path }} | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Select upload source for www | |
| if: ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') && env.BUILD_MACOS == 'true' }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "${RELEASE_TYPE}" == "qa" ]]; then | |
| UPLOAD_SOURCE="${renamed_pkg_relative_path}" | |
| echo "UPLOAD_SOURCE=${UPLOAD_SOURCE}" >> "$GITHUB_ENV" | |
| echo "🌐 QA: will upload PKG only → ${UPLOAD_SOURCE}" | |
| else | |
| UPLOAD_SOURCE="${publish_relative_path}" | |
| echo "UPLOAD_SOURCE=${UPLOAD_SOURCE}" >> "$GITHUB_ENV" | |
| echo "🌐 Ship: will upload ZIP + appcast.xml + sha256-hash.txt → ${UPLOAD_SOURCE}" | |
| fi | |
| - name: Upload to www | |
| if: ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') && env.BUILD_MACOS == 'true' }} | |
| uses: easingthemes/ssh-deploy@main | |
| with: | |
| REMOTE_PORT: 22 | |
| ARGS: -avzr | |
| SSH_CMD_ARGS: -o StrictHostKeyChecking=no | |
| env: | |
| SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }} | |
| REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }} | |
| REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }} | |
| TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/nym-vpn-client/macOS/ | |
| SOURCE: ${{ env.UPLOAD_SOURCE }} | |
| - name: Checkout nym-websites | |
| if: ${{ env.RELEASE_TYPE == 'ship' && env.BUILD_MACOS == 'true' }} | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 1 | |
| repository: nymtech/nym-websites | |
| token: ${{ secrets.WEB_REPO_TOKEN_FOR_APPCAST }} | |
| path: nym-websites | |
| - name: Insert new Sparkle <item> into nym-websites appcast.xml | |
| if: ${{ env.RELEASE_TYPE == 'ship' && env.BUILD_MACOS == 'true' }} | |
| working-directory: nym-websites | |
| env: | |
| ITEM_FILE: ${{ github.workspace }}/${{ env.APPCAST_ITEM_RELATIVE_PATH }} | |
| VERSION: ${{ env.MACOS_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| TARGET="websites/nym/www/public/.wellknown/macos-vpn/appcast.xml" | |
| test -f "$ITEM_FILE" || { echo "❌ Missing ITEM_FILE: $ITEM_FILE"; exit 1; } | |
| test -f "$TARGET" || { echo "❌ Missing TARGET: $TARGET"; exit 1; } | |
| tmp="$(mktemp)" | |
| { | |
| head -n 4 "$TARGET" | |
| cat "$ITEM_FILE" | |
| tail -n +5 "$TARGET" | |
| } > "$tmp" | |
| mv "$tmp" "$TARGET" | |
| echo "✅ Updated $TARGET (inserted after line 4)" | |
| sed -n '1,40p' "$TARGET" || true | |
| - name: Create PR in nym-websites | |
| if: ${{ env.RELEASE_TYPE == 'ship' && env.BUILD_MACOS == 'true' }} | |
| uses: peter-evans/create-pull-request@v8 | |
| with: | |
| token: ${{ secrets.WEB_REPO_TOKEN_FOR_APPCAST }} | |
| path: nym-websites | |
| commit-message: Update Appcast.xml with v${{ env.MACOS_VERSION }} | |
| title: Update Appcast.xml with v${{ env.MACOS_VERSION }} | |
| body: | | |
| Automated Sparkle appcast update for macOS v${{ env.MACOS_VERSION }}. | |
| Source: nym-vpn-client build-nym-vpn-apple (ship). | |
| branch: automation/update-macos-appcast-v${{ env.MACOS_VERSION }} | |
| base: main | |
| delete-branch: true | |
| - name: Install Sentry (nightly) | |
| if: ${{ github.event_name == 'schedule' && env.RELEASE_TYPE == 'qa' && env.BUILD_IOS == 'true' }} | |
| run: | | |
| brew install sentry-cli || brew upgrade sentry-cli || true | |
| sentry-cli --version | |
| - name: Upload iOS dSYMs to Sentry (nightly) | |
| if: ${{ github.event_name == 'schedule' && env.RELEASE_TYPE == 'qa' && env.BUILD_IOS == 'true' }} | |
| working-directory: nym-vpn-apple | |
| env: | |
| SENTRY_AUTH_TOKEN: ${{ secrets.APPLE_SENTRY_CLI_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ secrets.APPLE_SENTRY_CLI_ORG }} | |
| SENTRY_PROJECT: ${{ secrets.APPLE_SENTRY_CLI_PROJECT }} | |
| run: | | |
| set -euo pipefail | |
| BASE="$PWD/build/ios/xcarchive" | |
| echo "🔎 Locating iOS xcarchive under: $BASE" | |
| IOS_XCARCHIVE="$(find "$BASE" -maxdepth 1 -type d -name "*.xcarchive" -print -quit 2>/dev/null || true)" | |
| if [[ -z "${IOS_XCARCHIVE:-}" || ! -d "$IOS_XCARCHIVE" ]]; then | |
| echo "❌ No iOS .xcarchive found in $BASE" | |
| ls -la "$BASE" || true | |
| exit 1 | |
| fi | |
| DSYMS_DIR="$IOS_XCARCHIVE/dSYMs" | |
| if [[ ! -d "$DSYMS_DIR" ]]; then | |
| echo "No debug symbols dir found at $DSYMS_DIR, skip publishing to Sentry" | |
| exit 0 | |
| fi | |
| if ! find "$DSYMS_DIR" -name "*.dSYM" -print -quit | grep -q .; then | |
| echo "No .dSYM bundles found in $DSYMS_DIR, skip publishing to Sentry" | |
| exit 0 | |
| fi | |
| echo "📤 Uploading iOS debug files from: $DSYMS_DIR" | |
| sentry-cli debug-files upload \ | |
| --auth-token "$SENTRY_AUTH_TOKEN" \ | |
| --org "$SENTRY_ORG" \ | |
| --project "$SENTRY_PROJECT" \ | |
| --include-sources "$DSYMS_DIR" | |
| - name: Upload macOS dSYMs to Sentry (nightly) | |
| if: ${{ github.event_name == 'schedule' && env.RELEASE_TYPE == 'qa' && env.BUILD_MACOS == 'true' }} | |
| working-directory: nym-vpn-apple | |
| env: | |
| SENTRY_AUTH_TOKEN: ${{ secrets.APPLE_SENTRY_CLI_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ secrets.APPLE_SENTRY_CLI_ORG }} | |
| SENTRY_PROJECT: ${{ secrets.APPLE_SENTRY_CLI_PROJECT }} | |
| run: | | |
| set -euo pipefail | |
| DSYMS_DIR="$PWD/build/NymVPN.xcarchive/dSYMs" | |
| if [[ ! -d "$DSYMS_DIR" ]]; then | |
| echo "No debug symbols dir found at $DSYMS_DIR, skip publishing to Sentry" | |
| ls -la "$PWD/build" || true | |
| exit 0 | |
| fi | |
| if ! find "$DSYMS_DIR" -name "*.dSYM" -print -quit | grep -q .; then | |
| echo "No .dSYM bundles found in $DSYMS_DIR, skip publishing to Sentry" | |
| exit 0 | |
| fi | |
| echo "📤 Uploading macOS debug files from: $DSYMS_DIR" | |
| sentry-cli debug-files upload \ | |
| --auth-token "$SENTRY_AUTH_TOKEN" \ | |
| --org "$SENTRY_ORG" \ | |
| --project "$SENTRY_PROJECT" \ | |
| --include-sources "$DSYMS_DIR" | |
| - name: Compute macOS download URL (www) | |
| if: ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') && env.BUILD_MACOS == 'true' }} | |
| env: | |
| BASE_URL: ${{ secrets.ZULIP_BOT_APPLE_BUILD_SERVER_URL }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${BASE_URL:-}" ]]; then | |
| echo "❌ BASE_URL secret (WWW_MACOS_BASE_URL) is empty" | |
| exit 1 | |
| fi | |
| if [[ "${RELEASE_TYPE}" == "qa" ]]; then | |
| FILE="$(basename "${renamed_pkg_relative_path}")" | |
| URL="${BASE_URL}/nym-vpn-client/macOS/${FILE}" | |
| else | |
| URL="${BASE_URL}/publish/NymVPN-${MACOS_VERSION}.zip" | |
| fi | |
| echo "MACOS_DOWNLOAD_URL=$URL" >> "$GITHUB_ENV" | |
| echo "✅ macOS download URL: $URL" | |
| - name: Compose Zulip message (iOS) | |
| if: >- | |
| ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') | |
| && env.BUILD_IOS == 'true' }} | |
| run: | | |
| set -euo pipefail | |
| BRANCH="${GITHUB_REF_NAME}" | |
| MODE="${RELEASE_TYPE}" | |
| { | |
| echo "MESSAGE_IOS<<EOF" | |
| echo "📱 iOS ${MODE} build" | |
| echo "• Branch: \`${BRANCH}\`" | |
| echo "• Version: \`${IOS_VERSION}\`" | |
| echo "• Build: \`${IOS_BUILD_NUMBER}\`" | |
| echo "EOF" | |
| } >> "$GITHUB_ENV" | |
| - name: Send Zulip message (iOS) | |
| if: >- | |
| ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') | |
| && env.BUILD_IOS == 'true' }} | |
| uses: zulip/github-actions-zulip/send-message@v2 | |
| with: | |
| api-key: ${{ secrets.ZULIP_BOT_APPLE_API_KEY }} | |
| email: ${{ secrets.ZULIP_BOT_APPLE_EMAIL }} | |
| organization-url: ${{ secrets.ZULIP_ORG_URL }} | |
| to: ${{ secrets.ZULIP_BOT_APPLE_CHANNEL }} | |
| type: "stream" | |
| topic: ${{ secrets.ZULIP_BOT_APPLE_TOPIC_IOS }} | |
| content: ${{ env.MESSAGE_IOS }} | |
| - name: Compose Zulip message (macOS) | |
| if: >- | |
| ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') | |
| && env.BUILD_MACOS == 'true' }} | |
| run: | | |
| set -euo pipefail | |
| BRANCH="${GITHUB_REF_NAME}" | |
| MODE="${RELEASE_TYPE}" | |
| if [[ -z "${MACOS_DOWNLOAD_URL:-}" ]]; then | |
| echo "❌ MACOS_DOWNLOAD_URL is empty (Compute macOS download URL step didn't run?)" | |
| exit 1 | |
| fi | |
| { | |
| echo "MESSAGE_MACOS<<EOF" | |
| echo "🖥️ macOS ${MODE} build" | |
| echo "• Branch: \`${BRANCH}\`" | |
| echo "• Download: ${MACOS_DOWNLOAD_URL}" | |
| echo "EOF" | |
| } >> "$GITHUB_ENV" | |
| - name: Send Zulip message (macOS) | |
| if: >- | |
| ${{ (env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship') | |
| && env.BUILD_MACOS == 'true' }} | |
| uses: zulip/github-actions-zulip/send-message@v2 | |
| with: | |
| api-key: ${{ secrets.ZULIP_BOT_APPLE_API_KEY }} | |
| email: ${{ secrets.ZULIP_BOT_APPLE_EMAIL }} | |
| organization-url: ${{ secrets.ZULIP_ORG_URL }} | |
| to: ${{ secrets.ZULIP_BOT_APPLE_CHANNEL }} | |
| type: "stream" | |
| topic: ${{ secrets.ZULIP_BOT_APPLE_TOPIC_MACOS }} | |
| content: ${{ env.MESSAGE_MACOS }} | |
| - name: Cleanup Keychain | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| echo "🧹 Cleaning up Codemagic keychain..." | |
| keychain delete | |
| echo "🧹 Removing downloaded certificates..." | |
| if [ -d ~/Library/MobileDevice/Certificates ]; then | |
| rm -rf ~/Library/MobileDevice/Certificates/* | |
| fi | |
| echo "🧹 Removing downloaded provisioning profiles..." | |
| if [ -d ~/Library/MobileDevice/Provisioning\ Profiles ]; then | |
| rm -rf ~/Library/MobileDevice/Provisioning\ Profiles/* | |
| fi |