name: Swift
on:
  workflow_call:
  workflow_dispatch:

jobs:
  build:
    runs-on: ${{ matrix.runs-on }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - sdk: macosx
            runs-on: macos-13
            platform: macOS
            destination: platform=macOS
            xcode: "15.0"
          - sdk: iphoneos
            runs-on: macos-13
            platform: iOS
            destination: generic/platform=iOS
            xcode: "15.0"
          # TODO: Enable when GH Actions has macos-14 runners. Until this, this depends
          # on self-hosted runners which can be less reliable and have issues with things
          # like sccache.
          # See https://github.com/firezone/firezone/actions/runs/6608338431/job/17946908445
          # - sdk: macosx
          #   runs-on: macos-14
          #   platform: macOS
          #   destination: platform=macOS
          # - sdk: iphoneos
          #   runs-on: macos-14
          #   platform: iOS
          #   destination: generic/platform=iOS
    permissions:
      contents: read
      id-token: 'write'
    defaults:
      run:
        working-directory: ./swift/apple
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup-rust
        with:
          targets: aarch64-apple-darwin aarch64-apple-ios
      - uses: actions/cache/restore@v3
        name: Restore Swift DerivedData Cache
        with:
          path: ~/Library/Developer/Xcode/DerivedData
          key: ${{ matrix.runs-on }}-${{ runner.arch }}-swift-${{ hashFiles('swift/*', 'rust/**/*.rs', 'rust/**/*.toml', 'rust/**/*.lock}') }}
          restore-keys: |
            ${{ matrix.runs-on }}-${{ runner.arch }}-swift-
      - name: Install the Apple build certificate and provisioning profile
        env:
          BUILD_CERT: ${{ secrets.APPLE_BUILD_CERTIFICATE_BASE64 }}
          BUILD_CERT_PASS: ${{ secrets.APPLE_BUILD_CERTIFICATE_P12_PASSWORD }}
          INSTALLER_CERT: ${{ secrets.APPLE_MAC_INSTALLER_CERTIFICATE_BASE64 }}
          INSTALLER_CERT_PASS: ${{ secrets.APPLE_MAC_INSTALLER_CERTIFICATE_P12_PASSWORD }}
          KEYCHAIN_PASS: ${{ secrets.APPLE_RUNNER_KEYCHAIN_PASSWORD }}
          IOS_APP_PP: ${{ secrets.APPLE_IOS_APP_PROVISIONING_PROFILE }}
          IOS_NE_PP: ${{ secrets.APPLE_IOS_NE_PROVISIONING_PROFILE }}
          MACOS_APP_PP: ${{ secrets.APPLE_MACOS_APP_PROVISIONING_PROFILE }}
          MACOS_NE_PP: ${{ secrets.APPLE_MACOS_NE_PROVISIONING_PROFILE }}
        run: |
          BUILD_CERT_PATH=$RUNNER_TEMP/build_certificate.p12
          INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.cer

          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
          PP_PATH=~/Library/MobileDevice/Provisioning\ Profiles
          mkdir -p "$PP_PATH"

          # import certificate and provisioning profiles from secrets
          echo -n "$BUILD_CERT" | base64 --decode -o $BUILD_CERT_PATH

          # Matrix won't let us access secrets (for good reason), so use an explicit conditional here instead
          if [ "${{ matrix.platform }}" = "iOS" ]; then
            echo -n "$IOS_APP_PP" | base64 --decode -o "$PP_PATH"/app.mobileprovision
            echo -n "$IOS_NE_PP" | base64 --decode -o "$PP_PATH"/ne.mobileprovision
          elif [ "${{ matrix.platform }}" = "macOS" ]; then
            echo -n "$MACOS_APP_PP" | base64 --decode -o "$PP_PATH"/app.provisionprofile
            echo -n "$MACOS_NE_PP" | base64 --decode -o "$PP_PATH"/ne.provisionprofile

            # Submission to the macOS app store requires an installer package
            # which must be signed separately.
            echo -n "$INSTALLER_CERT" | base64 --decode -o $INSTALLER_CERT_PATH
          else
            echo "Platform not supported"
            exit 1
          fi

          # create temporary keychain
          security create-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN_PATH

          # import certificate to keychain
          security import $BUILD_CERT_PATH -P "$BUILD_CERT_PASS" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH

          if [ "${{ matrix.platform }}" = "macOS" ]; then
            security import $INSTALLER_CERT_PATH -P "$INSTALLER_CERT_PASS" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          fi
          security list-keychain -d user -s $KEYCHAIN_PATH
      - name: Build and sign app
        id: build
        env:
          # Build universal binary
          ONLY_ACTIVE_ARCH: no
          # Needed because `productbuild` doesn't support picking this up automatically like Xcode does
          INSTALLER_CODE_SIGN_IDENTITY: "3rd Party Mac Developer Installer: Firezone, Inc. (47R2M6779T)"
          REQUESTED_XCODE_VERSION: ${{ matrix.xcode }}
        run: |
          # Set Xcode version to use if provided
          [[ ! -z "$REQUESTED_XCODE_VERSION" ]] && sudo xcode-select -s /Applications/Xcode_$REQUESTED_XCODE_VERSION.app

          # Copy xcconfig
          cp Firezone/xcconfig/release.xcconfig Firezone/xcconfig/config.xcconfig

          # App Store Connect requires a new build version on each upload and it must be an integer.
          # See https://developer.apple.com/documentation/xcode/build-settings-reference#Current-Project-Version
          seconds_since_epoch=$(date +%s)
          sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9]/CURRENT_PROJECT_VERSION = $seconds_since_epoch/" \
            Firezone.xcodeproj/project.pbxproj

          # Unfortunately the macOS app requires an installer package to make it into the App Store,
          # while iOS requires an ipa. The process for building each of these is slightly different.
          if [ "${{ matrix.platform }}" = "iOS" ]; then
            # Build archive
            xcodebuild archive \
              -archivePath $RUNNER_TEMP/Firezone.xcarchive \
              -configuration Release \
              -scheme Firezone \
              -sdk ${{ matrix.sdk }} \
              -destination '${{ matrix.destination }}'
            # Export IPA
            xcodebuild \
              -exportArchive \
              -archivePath $RUNNER_TEMP/Firezone.xcarchive \
              -exportPath $RUNNER_TEMP/ \
              -exportOptionsPlist Firezone/ExportOptions.plist

            # Save resulting file to use for upload
            echo "app_bundle=$RUNNER_TEMP/Firezone.ipa" >> "$GITHUB_OUTPUT"
          elif [ "${{ matrix.platform }}" = "macOS" ]; then
            # Build app bundle
            xcodebuild build \
              -configuration Release \
              -scheme Firezone \
              -sdk ${{ matrix.sdk }} \
              -destination '${{ matrix.destination }}'
            # Move it from randomized build output dir to somewhere we can find it
            mv ~/Library/Developer/Xcode/DerivedData/Firezone-*/Build/Products/Release/Firezone.app $RUNNER_TEMP/.
            # Create signed installer pkg
            productbuild \
              --sign "${{ env.INSTALLER_CODE_SIGN_IDENTITY }}" \
              --component $RUNNER_TEMP/Firezone.app /Applications $RUNNER_TEMP/Firezone.pkg

            # Save resulting file to use for upload
            echo "app_bundle=$RUNNER_TEMP/Firezone.pkg" >> "$GITHUB_OUTPUT"
          else
            echo "Unsupported platform"
            exit 1
          fi
      - name: Upload build to App Store Connect
        if: ${{ github.event_name == 'workflow_dispatch' || (github.ref == 'refs/heads/main' && contains(github.event.head_commit.modified, 'elixir/VERSION')) }}
        env:
          ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}
          API_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}
          API_KEY: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}
        run: |
          # set up private key from env
          mkdir -p ~/private_keys
          echo "$API_KEY" > ~/private_keys/AuthKey_$API_KEY_ID.p8

          # Submit app to App Store Connect
          xcrun altool \
            --upload-app \
            -f ${{ steps.build.outputs.app_bundle }} \
            -t ${{ matrix.platform }} \
            --apiKey $API_KEY_ID \
            --apiIssuer $ISSUER_ID
      - uses: actions/cache/save@v3
        if: ${{ github.ref == 'refs/heads/main' }}
        name: Save Swift DerivedData Cache
        with:
          path: ~/Library/Developer/Xcode/DerivedData
          # Swift benefits heavily from build cache, so aggressively write a new one
          # on each build on `main` and attempt to restore it in PR builds with broader restore-key.
          key: ${{ matrix.runs-on }}-${{ runner.arch }}-swift-${{ hashFiles('swift/*', 'rust/**/*.rs', 'rust/**/*.toml', 'rust/**/*.lock}') }}
