name: Mobile Release on: workflow_dispatch: inputs: tag: description: 'Tag to deploy (e.g., v1.2.3-beta.1)' required: true type: string concurrency: group: mobile-release-${{ github.ref }} cancel-in-progress: false jobs: # Check if this is a prerelease (alpha, beta, rc) check-release-type: name: Check Release Type runs-on: ubuntu-latest outputs: prerelease: ${{ steps.check.outputs.prerelease }} previous_tag: ${{ steps.prev_tag.outputs.tag }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ inputs.tag }} - name: Check if prerelease id: check run: | TAG="${{ inputs.tag }}" if [[ "$TAG" == *-* ]]; then echo "prerelease=true" >> "$GITHUB_OUTPUT" echo "This is a prerelease: $TAG" else echo "prerelease=false" >> "$GITHUB_OUTPUT" echo "This is a stable release: $TAG" fi - name: Get previous tag id: prev_tag run: | TAG="${{ inputs.tag }}" PREV_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "") echo "tag=$PREV_TAG" >> "$GITHUB_OUTPUT" echo "Previous tag: $PREV_TAG" # Check if mobile code changed since last tag check-mobile-changes: name: Check Mobile Changes runs-on: ubuntu-latest needs: [check-release-type] if: needs.check-release-type.outputs.prerelease == 'true' outputs: mobile_changed: ${{ steps.check.outputs.changed }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ inputs.tag }} - name: Check for mobile changes id: check run: | PREV_TAG="${{ needs.check-release-type.outputs.previous_tag }}" CURRENT_TAG="${{ inputs.tag }}" if [ -z "$PREV_TAG" ]; then echo "No previous tag found, assuming mobile changed" echo "changed=true" >> "$GITHUB_OUTPUT" exit 0 fi # Check if any files in apps/mobile or shared packages changed CHANGES=$(git diff --name-only "$PREV_TAG" "$CURRENT_TAG" -- apps/mobile/ packages/shared/ | wc -l) if [ "$CHANGES" -gt 0 ]; then echo "changed=true" >> "$GITHUB_OUTPUT" echo "Mobile changes detected: $CHANGES files" git diff --name-only "$PREV_TAG" "$CURRENT_TAG" -- apps/mobile/ packages/shared/ else echo "changed=false" >> "$GITHUB_OUTPUT" echo "No mobile changes since $PREV_TAG" fi # Deploy mobile app using fingerprint-based detection # This action automatically: # 1. Calculates the native fingerprint # 2. Checks if a compatible build already exists # 3. If no build exists → triggers eas build # 4. If build exists → reuses it # 5. Pushes OTA update to the channel deploy: name: Deploy Mobile runs-on: ubuntu-latest needs: [check-release-type, check-mobile-changes] if: | needs.check-release-type.outputs.prerelease == 'true' && needs.check-mobile-changes.outputs.mobile_changed == 'true' timeout-minutes: 60 defaults: run: working-directory: apps/mobile steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ inputs.tag }} - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile working-directory: . - name: Build shared package run: pnpm --filter @tracearr/shared build - name: Setup Expo uses: expo/expo-github-action@main with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} - name: Validate secrets run: | if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then echo "::error::EXPO_TOKEN secret is not set" exit 1 fi - name: Sync version from git tag run: | # Extract version from tag (v1.2.3-alpha.4 -> 1.2.3) VERSION="${{ inputs.tag }}" VERSION="${VERSION#v}" # Remove 'v' prefix VERSION="${VERSION%%-*}" # Remove prerelease suffix (-alpha.4, -beta.1, etc.) echo "Setting app version to: $VERSION" # Update app.json with the version jq --arg v "$VERSION" '.expo.version = $v' app.json > tmp.json && mv tmp.json app.json # Verify the change ACTUAL=$(jq -r '.expo.version' app.json) if [ "$ACTUAL" != "$VERSION" ]; then echo "::error::Version sync failed. Expected $VERSION, got $ACTUAL" exit 1 fi echo "Version synced: $ACTUAL" - name: Decode Google Service Account run: | mkdir -p credentials echo "${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}" | base64 -d > credentials/google-service-account.json - name: Deploy with fingerprint uses: expo/expo-github-action/continuous-deploy-fingerprint@main id: deploy with: profile: preview branch: preview working-directory: apps/mobile auto-submit-builds: true env: EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.EXPO_APPLE_APP_SPECIFIC_PASSWORD }} GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} - name: Output deployment info run: | echo "## Deployment Results" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "**Fingerprint**: \`${{ steps.deploy.outputs.fingerprint }}\`" >> "$GITHUB_STEP_SUMMARY" echo "**Build needed**: ${{ steps.deploy.outputs.build-new-build }}" >> "$GITHUB_STEP_SUMMARY" if [ -n "${{ steps.deploy.outputs.ios-build-id }}" ]; then echo "**iOS Build ID**: ${{ steps.deploy.outputs.ios-build-id }}" >> "$GITHUB_STEP_SUMMARY" fi if [ -n "${{ steps.deploy.outputs.android-build-id }}" ]; then echo "**Android Build ID**: ${{ steps.deploy.outputs.android-build-id }}" >> "$GITHUB_STEP_SUMMARY" fi if [ -n "${{ steps.deploy.outputs.update-id }}" ]; then echo "**Update ID**: ${{ steps.deploy.outputs.update-id }}" >> "$GITHUB_STEP_SUMMARY" fi # Fallback: Manual OTA if fingerprint action has issues # This job only runs if deploy fails and we want to try OTA anyway ota-fallback: name: OTA Fallback runs-on: ubuntu-latest needs: [check-release-type, check-mobile-changes, deploy] if: | always() && needs.deploy.result == 'failure' && needs.check-release-type.outputs.prerelease == 'true' && needs.check-mobile-changes.outputs.mobile_changed == 'true' timeout-minutes: 15 defaults: run: working-directory: apps/mobile steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ inputs.tag }} - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile working-directory: . - name: Build shared package run: pnpm --filter @tracearr/shared build - name: Setup Expo uses: expo/expo-github-action@main with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} - name: Push OTA update with retry run: | TAG="${{ inputs.tag }}" echo "::warning::Deploy failed, attempting OTA fallback for $TAG" for attempt in 1 2 3; do echo "Attempt $attempt of 3..." if eas update \ --channel preview \ --message "Release $TAG (fallback)" \ --non-interactive; then echo "OTA update successful on attempt $attempt" exit 0 fi if [ $attempt -lt 3 ]; then echo "Attempt $attempt failed, waiting before retry..." sleep $((30 * attempt)) fi done echo "::error::OTA update failed after 3 attempts" exit 1 # Summary job that reports status summary: name: Release Summary runs-on: ubuntu-latest needs: [check-release-type, check-mobile-changes, deploy, ota-fallback] if: always() steps: - name: Report status env: TAG: ${{ inputs.tag }} PRERELEASE: ${{ needs.check-release-type.outputs.prerelease }} MOBILE_CHANGED: ${{ needs.check-mobile-changes.outputs.mobile_changed || 'skipped' }} DEPLOY_RESULT: ${{ needs.deploy.result }} FALLBACK_RESULT: ${{ needs.ota-fallback.result }} run: | { echo "## Mobile Release Summary" echo "" echo "**Tag**: $TAG" echo "**Prerelease**: $PRERELEASE" echo "**Mobile Changed**: $MOBILE_CHANGED" echo "" if [[ "$PRERELEASE" != "true" ]]; then echo "ℹ️ Skipped: Not a prerelease (stable releases don't trigger mobile builds)" elif [[ "$MOBILE_CHANGED" != "true" ]]; then echo "ℹ️ Skipped: No mobile code changes detected" else echo "### Deployment Status" echo "" if [[ "$DEPLOY_RESULT" == "success" ]]; then echo "✅ **Deploy**: Success" echo "" echo "The fingerprint-based deployment completed successfully." echo "- If native changes were detected, a new build was triggered" echo "- If only JS changes, an OTA update was pushed" echo "" echo "Check the [Expo dashboard](https://expo.dev) for details." elif [[ "$FALLBACK_RESULT" == "success" ]]; then echo "⚠️ **Deploy**: Failed (fingerprint action issue)" echo "✅ **OTA Fallback**: Success" echo "" echo "The main deploy failed but OTA fallback succeeded." echo "Users will receive the JS update on next app launch." else echo "❌ **Deploy**: $DEPLOY_RESULT" echo "❌ **OTA Fallback**: $FALLBACK_RESULT" echo "" echo "Both deployment methods failed. Check workflow logs for details." fi fi } >> "$GITHUB_STEP_SUMMARY"