name: iOS Build & Deploy on: push: tags: - "ios-v*" workflow_dispatch: inputs: upload_to_testflight: description: "Upload to TestFlight after build" required: false default: "true" type: choice options: - "true" - "false" concurrency: group: ios-build-${{ github.ref }} cancel-in-progress: true jobs: test: name: Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" cache: "npm" - name: Install backend dependencies & test run: cd backend && npm ci && npm test -- --ci - name: Install frontend dependencies & test run: cd frontend && npm ci && npm test -- --run --reporter=basic build-ios: name: Build iOS App needs: test runs-on: macos-14 env: SCHEME: App WORKSPACE: ios/App/App.xcworkspace ARCHIVE_PATH: build/App.xcarchive EXPORT_OPTIONS: scripts/export-options.plist steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" cache: "npm" - name: Install root dependencies run: npm ci - name: Install frontend dependencies run: cd frontend && npm ci - name: Build web bundle run: npm run build:web - name: Ensure iOS platform & sync Capacitor run: | if [ ! -d "ios/App/App.xcodeproj" ]; then echo "Adding iOS platform..." npx cap add ios fi npx cap sync ios - name: Install CocoaPods run: cd ios/App && pod install --repo-update - name: Install Apple certificates & provisioning profile uses: apple-actions/import-codesign-certs@v2 with: p12-file-base64: ${{ secrets.IOS_P12_BASE64 }} p12-password: ${{ secrets.IOS_P12_PASSWORD }} - name: Install provisioning profile run: | mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles echo "${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/${{ secrets.IOS_PROVISION_PROFILE_UUID }}.mobileprovision - name: Set version from tag if: startsWith(github.ref, 'refs/tags/ios-v') run: | TAG_VERSION="${GITHUB_REF#refs/tags/ios-v}" if ! echo "$TAG_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then echo "::error::Invalid tag format '$TAG_VERSION'. Expected semver: X.Y.Z" exit 1 fi MAJOR=$(echo "$TAG_VERSION" | cut -d. -f1) MINOR=$(echo "$TAG_VERSION" | cut -d. -f2) PATCH=$(echo "$TAG_VERSION" | cut -d. -f3) BUILD=${{ github.run_number }} echo "Setting MARKETING_VERSION=$MAJOR.$MINOR.$PATCH" echo "Setting CURRENT_PROJECT_VERSION=$BUILD" sed -i '' "s/MARKETING_VERSION = [^;]*;/MARKETING_VERSION = $MAJOR.$MINOR.$PATCH;/" ios/App/App.xcodeproj/project.pbxproj sed -i '' "s/CURRENT_PROJECT_VERSION = [^;]*;/CURRENT_PROJECT_VERSION = $BUILD;/" ios/App/App.xcodeproj/project.pbxproj - name: Build Xcode archive run: | set -o pipefail xcodebuild archive \ -workspace "$WORKSPACE" \ -scheme "$SCHEME" \ -configuration Release \ -destination "generic/platform=iOS" \ -archivePath "$ARCHIVE_PATH" \ -allowProvisioningUpdates \ CODE_SIGN_STYLE=Manual \ DEVELOPMENT_TEAM="${{ secrets.IOS_TEAM_ID }}" \ PROVISIONING_PROFILE_SPECIFIER="${{ secrets.IOS_PROVISION_PROFILE_NAME }}" \ 2>&1 | xcpretty --color - name: Export IPA run: | plutil -replace teamID -string "${{ secrets.IOS_TEAM_ID }}" "$EXPORT_OPTIONS" plutil -replace provisioningProfiles.net.hacecalor.farmafinder -string "${{ secrets.IOS_PROVISION_PROFILE_NAME }}" "$EXPORT_OPTIONS" set -o pipefail xcodebuild -exportArchive \ -archivePath "$ARCHIVE_PATH" \ -exportOptionsPlist "$EXPORT_OPTIONS" \ -exportPath build \ -allowProvisioningUpdates \ 2>&1 | xcpretty --color - name: Upload artifact (IPA) uses: actions/upload-artifact@v4 with: name: FarmaFinder-iOS path: build/*.ipa retention-days: 30 - name: Upload to TestFlight if: > (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ios-v')) || (github.event_name == 'workflow_dispatch' && inputs.upload_to_testflight == 'true') env: APP_STORE_API_KEY_ID: ${{ secrets.APP_STORE_API_KEY_ID }} APP_STORE_API_ISSUER_ID: ${{ secrets.APP_STORE_API_ISSUER_ID }} run: | IPA_PATH=$(find build -name "*.ipa" -maxdepth 1 | head -1) if [ -z "$IPA_PATH" ]; then echo "::error::No IPA found in build/" exit 1 fi API_PRIVATE_KEY_PATH="$RUNNER_TEMP/app_store_api_key.p8" echo "${{ secrets.APP_STORE_API_PRIVATE_KEY }}" > "$API_PRIVATE_KEY_PATH" xcrun notarytool submit "$IPA_PATH" \ --key-id "$APP_STORE_API_KEY_ID" \ --issuer "$APP_STORE_API_ISSUER_ID" \ --key "$API_PRIVATE_KEY_PATH" \ --wait rm -f "$API_PRIVATE_KEY_PATH" - name: Summary run: | IPA_PATH=$(find build -name "*.ipa" -maxdepth 1 | head -1) if [ -n "$IPA_PATH" ]; then IPA_SIZE=$(du -h "$IPA_PATH" | cut -f1) echo "### iOS Build Summary" >> $GITHUB_STEP_SUMMARY echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY echo "|------|-------|" >> $GITHUB_STEP_SUMMARY echo "| IPA Size | $IPA_SIZE |" >> $GITHUB_STEP_SUMMARY echo "| Tag | ${GITHUB_REF#refs/tags/} |" >> $GITHUB_STEP_SUMMARY echo "| Build # | ${{ github.run_number }} |" >> $GITHUB_STEP_SUMMARY else echo "::error::IPA not found in build/" exit 1 fi