From d7d86861950f89b517201aebdad00c66aeaee07f Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Feb 2026 23:34:19 -0800 Subject: [PATCH 01/52] Separate build and upload jobs in deploy workflow Split Android and iOS jobs into separate build and upload jobs to avoid rebuilding when uploads fail. Upload jobs are further split into critical store uploads and non-critical testing uploads that run in parallel. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 357 ++++++++++++++++++++++++----------- 1 file changed, 250 insertions(+), 107 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 426bbc35c366..95f4cbb6fc34 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -81,10 +81,12 @@ jobs: needs: prep secrets: inherit - android: - name: Build and deploy Android HybridApp + android-build: + name: Build Android HybridApp needs: prep runs-on: ubuntu-latest-xl + outputs: + VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} env: SHOULD_BUILD_APP: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} steps: @@ -141,7 +143,6 @@ jobs: run: | op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json op read "op://${{ vars.OP_VAULT }}/upload-key.keystore/upload-key.keystore" --force --out-file ./upload-key.keystore - op read "op://${{ vars.OP_VAULT }}/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json # Copy the keystore to the Android directory for Fullstory cp ./upload-key.keystore Mobile-Expensify/Android @@ -157,7 +158,6 @@ jobs: ANDROID_UPLOAD_KEYSTORE_PASSWORD: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_PASSWORD ANDROID_UPLOAD_KEYSTORE_ALIAS: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_ALIAS ANDROID_UPLOAD_KEY_PASSWORD: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEY_PASSWORD - APPLAUSE_API_KEY: op://${{ vars.OP_VAULT }}/Applause-API-Key/password - name: Get Android native version id: getAndroidVersion @@ -175,39 +175,6 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: Upload Android app to Google Play - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} - run: bundle exec fastlane android ${{ vars.ANDROID_UPLOAD_COMMAND }} - env: - VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} - ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME }} - - - name: Get current Android rollout percentage - if: ${{ github.ref == 'refs/heads/production' }} - id: getAndroidRolloutPercentage - uses: ./.github/actions/javascript/getAndroidRolloutPercentage - with: - GOOGLE_KEY_FILE: ./android-fastlane-json-key.json - PACKAGE_NAME: org.me.mobiexpensifyg - - # Complete the previous version rollout if the current rollout percentage is not -1 (no rollout in progress) or 1 (fully rolled out) - - name: Submit previous production build to 100% - if: ${{ github.ref == 'refs/heads/production' && !contains(fromJSON('["1", "-1"]'), steps.getAndroidRolloutPercentage.outputs.CURRENT_ROLLOUT_PERCENTAGE) }} - run: bundle exec fastlane android complete_hybrid_rollout - continue-on-error: true - - - name: Submit production build for Google Play review and a slow rollout - if: ${{ github.ref == 'refs/heads/production' }} - run: bundle exec fastlane android upload_google_play_production_hybrid_rollout - env: - VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} - - - name: Upload Android build to Browser Stack - if: ${{ fromJSON(env.IS_APP_REPO) && (github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK)) }} - run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ env.aabPath }}" - env: - BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - - name: Install bundletool if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} run: | @@ -239,14 +206,6 @@ jobs: # Unzip just the universal apk unzip -p Expensify.apks universal.apk > Expensify.apk - - name: Upload Android build to Applause - if: ${{ fromJSON(env.IS_APP_REPO) && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} - run: | - APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.') - curl -F "file=@Expensify.apk" \ - "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36008" \ - -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}" - - name: Upload Android APK build artifact if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} # v4 @@ -271,8 +230,68 @@ jobs: name: android-sourcemap-artifact path: /home/runner/work/App/App/Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map - - name: Set current App version in Env - run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" + android-upload-google-play: + name: Upload Android to Google Play + needs: [prep, android-build] + runs-on: ubuntu-latest + steps: + - name: Checkout + # v4 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + + - name: Download Android build artifact + # v4 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + with: + name: android-build-artifact + path: ./ + + - name: Setup Ruby + # v1.229.0 + uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 + with: + bundler-cache: true + + - name: Install New Expensify Gems + run: bundle install + + - name: Setup 1Password CLI + uses: Expensify/GitHub-Actions/setup-certificate-1p@main + with: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SHOULD_LOAD_SSL_CERTIFICATES: 'false' + + - name: Load Google Play credentials from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op read "op://${{ vars.OP_VAULT }}/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json + + - name: Upload Android app to Google Play + run: bundle exec fastlane android ${{ vars.ANDROID_UPLOAD_COMMAND }} + env: + VERSION: ${{ needs.android-build.outputs.VERSION_CODE }} + ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME }} + + - name: Get current Android rollout percentage + if: ${{ github.ref == 'refs/heads/production' }} + id: getAndroidRolloutPercentage + uses: ./.github/actions/javascript/getAndroidRolloutPercentage + with: + GOOGLE_KEY_FILE: ./android-fastlane-json-key.json + PACKAGE_NAME: org.me.mobiexpensifyg + + # Complete the previous version rollout if the current rollout percentage is not -1 (no rollout in progress) or 1 (fully rolled out) + - name: Submit previous production build to 100% + if: ${{ github.ref == 'refs/heads/production' && !contains(fromJSON('["1", "-1"]'), steps.getAndroidRolloutPercentage.outputs.CURRENT_ROLLOUT_PERCENTAGE) }} + run: bundle exec fastlane android complete_hybrid_rollout + continue-on-error: true + + - name: Submit production build for Google Play review and a slow rollout + if: ${{ github.ref == 'refs/heads/production' }} + run: bundle exec fastlane android upload_google_play_production_hybrid_rollout + env: + VERSION: ${{ needs.android-build.outputs.VERSION_CODE }} - name: Warn deployers if Android production deploy failed if: ${{ failure() && github.ref == 'refs/heads/production' }} @@ -293,10 +312,60 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - ios: - name: Build and deploy iOS HybridApp + android-upload-testing: + name: Upload Android to BrowserStack and Applause + needs: [prep, android-build] + runs-on: ubuntu-latest + if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + continue-on-error: true + steps: + - name: Checkout + # v4 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + + - name: Download Android APK artifact + # v4 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + with: + name: android-apk-artifact + path: ./ + + - name: Setup 1Password CLI + uses: Expensify/GitHub-Actions/setup-certificate-1p@main + with: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SHOULD_LOAD_SSL_CERTIFICATES: 'false' + + - name: Load testing credentials from 1Password + id: load-credentials + # v2 + uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 + with: + export-env: false + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + APPLAUSE_API_KEY: op://${{ vars.OP_VAULT }}/Applause-API-Key/password + + - name: Upload Android build to Browser Stack + if: ${{ fromJSON(env.IS_APP_REPO) }} + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@Expensify.apk" + env: + BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + + - name: Upload Android build to Applause + if: ${{ fromJSON(env.IS_APP_REPO) && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + run: | + APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.') + curl -F "file=@Expensify.apk" \ + "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36008" \ + -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}" + + ios-build: + name: Build iOS HybridApp needs: prep runs-on: macos-15-xlarge + outputs: + IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} env: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer SHOULD_BUILD_APP: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} @@ -360,22 +429,8 @@ jobs: op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore/${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }} op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Share_Extension/${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }} op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Notification_Service/${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }} - op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 - - name: Load iOS credentials from 1Password - id: load-credentials - # v2 - uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 - with: - export-env: false - env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - APPLAUSE_API_KEY: op://${{ vars.OP_VAULT }}/Applause-API-Key/password - - - name: Set current App version in Env - run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" - - name: Get iOS native version id: getIOSVersion run: echo "IOS_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT" @@ -393,8 +448,78 @@ jobs: APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME: ${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: Upload release build to TestFlight + - name: Upload iOS build artifact + if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} + # v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ios-build-artifact + path: ${{ env.ipaPath }} + + - name: Upload iOS dSYM artifact if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} + # v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ios-dsym-artifact + path: ${{ env.dsymPath }} + + - name: Upload iOS sourcemap artifact + if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} + # v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ios-sourcemap-artifact + path: /Users/runner/work/App/App/Mobile-Expensify/main.jsbundle.map + + ios-upload-testflight: + name: Upload iOS to TestFlight + needs: [prep, ios-build] + runs-on: macos-15-xlarge + env: + DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer + steps: + - name: Checkout + # v4 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + + - name: Download iOS build artifact + # v4 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + with: + name: ios-build-artifact + path: ./ + + - name: Download iOS dSYM artifact + # v4 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + with: + name: ios-dsym-artifact + path: ./ + + - name: Setup Ruby + # v1.229.0 + uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 + with: + bundler-cache: true + + - name: Install New Expensify Gems + run: bundle install + + - name: Setup 1Password CLI + uses: Expensify/GitHub-Actions/setup-certificate-1p@main + with: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SHOULD_LOAD_SSL_CERTIFICATES: 'false' + + - name: Load iOS credentials from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json + op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json + + - name: Upload release build to TestFlight run: bundle exec fastlane ios upload_testflight_hybrid env: APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} @@ -404,7 +529,7 @@ jobs: APPLE_ID: ${{ vars.APPLE_ID }} - name: Upload DSYMs to Firebase for HybridApp - if: ${{ fromJSON(env.IS_APP_REPO) && fromJSON(env.SHOULD_BUILD_APP) }} + if: ${{ fromJSON(env.IS_APP_REPO) }} run: bundle exec fastlane ios upload_dsyms_hybrid - name: Submit previous production build to 100% @@ -416,39 +541,9 @@ jobs: if: ${{ github.ref == 'refs/heads/production' }} run: bundle exec fastlane ios submit_hybrid_for_rollout env: - VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} + VERSION: ${{ needs.ios-build.outputs.IOS_VERSION }} APPLE_ID: ${{ vars.APPLE_ID }} - - name: Upload iOS build to Browser Stack - if: ${{ fromJSON(env.IS_APP_REPO) && fromJSON(env.SHOULD_BUILD_APP) }} - run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ env.ipaPath }}" - env: - BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - - - name: Upload iOS build to Applause - if: ${{ fromJSON(env.IS_APP_REPO) && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} - run: | - APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.') - curl -F "file=@${{ env.ipaPath }}" \ - "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36005" \ - -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}" - - - name: Upload iOS build artifact - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: ios-build-artifact - path: ${{ env.ipaPath }} - - - name: Upload iOS sourcemap artifact - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: ios-sourcemap-artifact - path: /Users/runner/work/App/App/Mobile-Expensify/main.jsbundle.map - - name: Warn deployers if iOS production deploy failed if: ${{ failure() && github.ref == 'refs/heads/production' }} # v3 @@ -461,13 +556,61 @@ jobs: attachments: [{ color: "#DB4545", pretext: ``, - text: `💥 iOS HybridApp production deploy failed. Please ${{ steps.getIOSVersion.outputs.IOS_VERSION }} in the . 💥`, + text: `💥 iOS HybridApp production deploy failed. Please ${{ needs.ios-build.outputs.IOS_VERSION }} in the . 💥`, }] } env: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + ios-upload-testing: + name: Upload iOS to BrowserStack and Applause + needs: [prep, ios-build] + runs-on: ubuntu-latest + if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + continue-on-error: true + steps: + - name: Checkout + # v4 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + + - name: Download iOS build artifact + # v4 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + with: + name: ios-build-artifact + path: ./ + + - name: Setup 1Password CLI + uses: Expensify/GitHub-Actions/setup-certificate-1p@main + with: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SHOULD_LOAD_SSL_CERTIFICATES: 'false' + + - name: Load testing credentials from 1Password + id: load-credentials + # v2 + uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 + with: + export-env: false + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + APPLAUSE_API_KEY: op://${{ vars.OP_VAULT }}/Applause-API-Key/password + + - name: Upload iOS build to Browser Stack + if: ${{ fromJSON(env.IS_APP_REPO) }} + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@Expensify.ipa" + env: + BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + + - name: Upload iOS build to Applause + if: ${{ fromJSON(env.IS_APP_REPO) && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + run: | + APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.') + curl -F "file=@Expensify.ipa" \ + "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36005" \ + -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}" + web: name: Build and deploy Web needs: prep @@ -561,7 +704,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: ubuntu-latest if: ${{ failure() }} - needs: [android, ios, web] + needs: [android-upload-google-play, ios-upload-testflight, web] steps: - name: Checkout # v4 @@ -577,15 +720,15 @@ jobs: outputs: IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }} - needs: [android, ios, web] + needs: [android-upload-google-play, ios-upload-testflight, web] if: ${{ always() }} steps: - name: Check deployment success on at least one platform id: checkDeploymentSuccessOnAtLeastOnePlatform run: | isAtLeastOnePlatformDeployed="false" - if [ "${{ needs.iOS.result }}" == "success" ] || \ - [ "${{ needs.android.result }}" == "success" ] || \ + if [ "${{ needs.ios-upload-testflight.result }}" == "success" ] || \ + [ "${{ needs.android-upload-google-play.result }}" == "success" ] || \ [ "${{ needs.web.result }}" == "success" ]; then isAtLeastOnePlatformDeployed="true" fi @@ -596,8 +739,8 @@ jobs: id: checkDeploymentSuccessOnAllPlatforms run: | isAllPlatformsDeployed="false" - if [ "${{ needs.iOS.result }}" == "success" ] && \ - [ "${{ needs.android.result }}" == "success" ] && \ + if [ "${{ needs.ios-upload-testflight.result }}" == "success" ] && \ + [ "${{ needs.android-upload-google-play.result }}" == "success" ] && \ [ "${{ needs.web.result }}" == "success" ]; then isAllPlatformsDeployed="true" fi @@ -736,7 +879,7 @@ jobs: name: Post a Slack message when all platforms deploy successfully runs-on: ubuntu-latest if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }} - needs: [prep, android, ios, web, checkDeploymentSuccess, createRelease] + needs: [prep, android-upload-google-play, ios-upload-testflight, web, checkDeploymentSuccess, createRelease] steps: - name: 'Announces the deploy in the #announce Slack room' # v3 @@ -793,11 +936,11 @@ jobs: postGithubComments: uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} - needs: [prep, android, ios, web, checkDeploymentSuccess, createRelease] + needs: [prep, android-upload-google-play, ios-upload-testflight, web, checkDeploymentSuccess, createRelease] secrets: inherit with: version: ${{ needs.prep.outputs.APP_VERSION }} env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} - android: ${{ needs.android.result }} - ios: ${{ needs.ios.result }} + android: ${{ needs.android-upload-google-play.result }} + ios: ${{ needs.ios-upload-testflight.result }} web: ${{ needs.web.result }} From f3251cbc8d98d60a9e65844f553f02fe7da9d9bc Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Feb 2026 23:41:37 -0800 Subject: [PATCH 02/52] Update upload-artifact action to v6 Co-authored-by: Cursor --- .github/workflows/deploy.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 95f4cbb6fc34..34c2d72e3838 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -208,24 +208,24 @@ jobs: - name: Upload Android APK build artifact if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: name: android-apk-artifact path: Expensify.apk - name: Upload Android build artifact if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: name: android-build-artifact path: ${{ env.aabPath }} - name: Upload Android sourcemap artifact if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: name: android-sourcemap-artifact path: /home/runner/work/App/App/Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map @@ -450,24 +450,24 @@ jobs: - name: Upload iOS build artifact if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: name: ios-build-artifact path: ${{ env.ipaPath }} - name: Upload iOS dSYM artifact if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: name: ios-dsym-artifact path: ${{ env.dsymPath }} - name: Upload iOS sourcemap artifact if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: name: ios-sourcemap-artifact path: /Users/runner/work/App/App/Mobile-Expensify/main.jsbundle.map @@ -675,8 +675,8 @@ jobs: HOST: ${{ github.ref == 'refs/heads/production' && vars.WEB_PRODUCTION_HOST || vars.WEB_STAGING_HOST }} - name: Upload web sourcemaps artifact - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: name: web-sourcemaps-artifact path: ./dist/merged-source-map.js.map @@ -687,15 +687,15 @@ jobs: zip -r webBuild.zip dist - name: Upload .tar.gz web build artifact - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: name: web-build-tar-gz-artifact path: ./webBuild.tar.gz - name: Upload .zip web build artifact - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: name: web-build-zip-artifact path: ./webBuild.zip From cfddcf8c539b75e067c0c11ef89a2cac076d555f Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Feb 2026 23:42:47 -0800 Subject: [PATCH 03/52] Update download-artifact action to v7 Co-authored-by: Cursor --- .github/workflows/deploy.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 34c2d72e3838..08d913985d4b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -240,8 +240,8 @@ jobs: uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - name: Download Android build artifact - # v4 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: name: android-build-artifact path: ./ @@ -324,8 +324,8 @@ jobs: uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - name: Download Android APK artifact - # v4 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: name: android-apk-artifact path: ./ @@ -484,15 +484,15 @@ jobs: uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - name: Download iOS build artifact - # v4 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: name: ios-build-artifact path: ./ - name: Download iOS dSYM artifact - # v4 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: name: ios-dsym-artifact path: ./ @@ -575,8 +575,8 @@ jobs: uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - name: Download iOS build artifact - # v4 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: name: ios-build-artifact path: ./ From 558249715cbfea5ff604371c25c7bc2bf74b8b26 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Feb 2026 23:43:39 -0800 Subject: [PATCH 04/52] Update checkout action to v6 Co-authored-by: Cursor --- .github/workflows/deploy.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 08d913985d4b..f61cd387b010 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,8 +21,8 @@ jobs: IS_CHERRY_PICK: ${{ steps.isCherryPick.outputs.IS_CHERRY_PICK }} steps: - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: token: ${{ secrets.OS_BOTIFY_TOKEN }} submodules: true @@ -91,8 +91,8 @@ jobs: SHOULD_BUILD_APP: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} steps: - name: Checkout App and Mobile-Expensify repo - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: submodules: true token: ${{ secrets.OS_BOTIFY_TOKEN }} @@ -236,8 +236,8 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download Android build artifact # v7 @@ -320,8 +320,8 @@ jobs: continue-on-error: true steps: - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download Android APK artifact # v7 @@ -371,8 +371,8 @@ jobs: SHOULD_BUILD_APP: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} steps: - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: submodules: true token: ${{ secrets.OS_BOTIFY_TOKEN }} @@ -480,8 +480,8 @@ jobs: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer steps: - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download iOS build artifact # v7 @@ -571,8 +571,8 @@ jobs: continue-on-error: true steps: - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download iOS build artifact # v7 @@ -617,8 +617,8 @@ jobs: runs-on: ubuntu-latest-xl steps: - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Setup Node uses: ./.github/actions/composite/setupNode @@ -707,8 +707,8 @@ jobs: needs: [android-upload-google-play, ios-upload-testflight, web] steps: - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Post Slack message on failure uses: ./.github/actions/composite/announceFailedWorkflowInSlack From 926fe68f397c744c564cf36acd4a94e8d52b858e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Feb 2026 23:46:01 -0800 Subject: [PATCH 05/52] Set artifact paths for Fastlane after downloading artifacts Since GITHUB_ENV doesn't persist across jobs, we need to set the aabPath, ipaPath, and dsymPath environment variables after downloading the artifacts in the upload jobs. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f61cd387b010..5e12ec703b7c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -246,6 +246,9 @@ jobs: name: android-build-artifact path: ./ + - name: Set aabPath for Fastlane + run: echo "aabPath=$(pwd)/Expensify-release.aab" >> "$GITHUB_ENV" + - name: Setup Ruby # v1.229.0 uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 @@ -497,6 +500,11 @@ jobs: name: ios-dsym-artifact path: ./ + - name: Set artifact paths for Fastlane + run: | + echo "ipaPath=$(pwd)/Expensify.ipa" >> "$GITHUB_ENV" + echo "dsymPath=$(pwd)/Expensify.app.dSYM.zip" >> "$GITHUB_ENV" + - name: Setup Ruby # v1.229.0 uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 From 8252550a6227978743c07392ac0271a0ff6cd1a5 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Feb 2026 23:56:51 -0800 Subject: [PATCH 06/52] Skip artifact-dependent steps on production deploys On production deploys, native builds are skipped so no artifacts are uploaded. Add conditional checks to skip download/upload steps that depend on build artifacts, while still allowing production rollout steps to run. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5e12ec703b7c..c14b3de27f0c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -240,6 +240,7 @@ jobs: uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download Android build artifact + if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -247,6 +248,7 @@ jobs: path: ./ - name: Set aabPath for Fastlane + if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} run: echo "aabPath=$(pwd)/Expensify-release.aab" >> "$GITHUB_ENV" - name: Setup Ruby @@ -271,6 +273,7 @@ jobs: op read "op://${{ vars.OP_VAULT }}/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json - name: Upload Android app to Google Play + if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} run: bundle exec fastlane android ${{ vars.ANDROID_UPLOAD_COMMAND }} env: VERSION: ${{ needs.android-build.outputs.VERSION_CODE }} @@ -487,6 +490,7 @@ jobs: uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download iOS build artifact + if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -494,6 +498,7 @@ jobs: path: ./ - name: Download iOS dSYM artifact + if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -501,6 +506,7 @@ jobs: path: ./ - name: Set artifact paths for Fastlane + if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} run: | echo "ipaPath=$(pwd)/Expensify.ipa" >> "$GITHUB_ENV" echo "dsymPath=$(pwd)/Expensify.app.dSYM.zip" >> "$GITHUB_ENV" @@ -528,6 +534,7 @@ jobs: op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json - name: Upload release build to TestFlight + if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} run: bundle exec fastlane ios upload_testflight_hybrid env: APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} @@ -537,7 +544,7 @@ jobs: APPLE_ID: ${{ vars.APPLE_ID }} - name: Upload DSYMs to Firebase for HybridApp - if: ${{ fromJSON(env.IS_APP_REPO) }} + if: ${{ fromJSON(env.IS_APP_REPO) && (github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK)) }} run: bundle exec fastlane ios upload_dsyms_hybrid - name: Submit previous production build to 100% From 2bc88880f6d0b92da6b8c70050cfbfca5d81130f Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 00:04:31 -0800 Subject: [PATCH 07/52] DRY up staging/cherry-pick condition with SHOULD_DEPLOY_NATIVE output Add SHOULD_DEPLOY_NATIVE output to prep job and reference it throughout the workflow instead of repeating the condition inline. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c14b3de27f0c..4b9db6bd3f23 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,6 +19,8 @@ jobs: TAG: ${{ steps.getTagName.outputs.TAG }} # Is this deploy for a cherry-pick? IS_CHERRY_PICK: ${{ steps.isCherryPick.outputs.IS_CHERRY_PICK }} + # Should we build and deploy native apps? (staging or cherry-pick, not production) + SHOULD_DEPLOY_NATIVE: ${{ github.ref == 'refs/heads/staging' || fromJSON(steps.isCherryPick.outputs.IS_CHERRY_PICK) }} steps: - name: Checkout # v6 @@ -88,7 +90,7 @@ jobs: outputs: VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} env: - SHOULD_BUILD_APP: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + SHOULD_BUILD_APP: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} steps: - name: Checkout App and Mobile-Expensify repo # v6 @@ -240,7 +242,7 @@ jobs: uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download Android build artifact - if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -248,7 +250,7 @@ jobs: path: ./ - name: Set aabPath for Fastlane - if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} run: echo "aabPath=$(pwd)/Expensify-release.aab" >> "$GITHUB_ENV" - name: Setup Ruby @@ -273,7 +275,7 @@ jobs: op read "op://${{ vars.OP_VAULT }}/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json - name: Upload Android app to Google Play - if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} run: bundle exec fastlane android ${{ vars.ANDROID_UPLOAD_COMMAND }} env: VERSION: ${{ needs.android-build.outputs.VERSION_CODE }} @@ -322,7 +324,7 @@ jobs: name: Upload Android to BrowserStack and Applause needs: [prep, android-build] runs-on: ubuntu-latest - if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} continue-on-error: true steps: - name: Checkout @@ -374,7 +376,7 @@ jobs: IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} env: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer - SHOULD_BUILD_APP: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + SHOULD_BUILD_APP: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} steps: - name: Checkout # v6 @@ -490,7 +492,7 @@ jobs: uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download iOS build artifact - if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -498,7 +500,7 @@ jobs: path: ./ - name: Download iOS dSYM artifact - if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -506,7 +508,7 @@ jobs: path: ./ - name: Set artifact paths for Fastlane - if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} run: | echo "ipaPath=$(pwd)/Expensify.ipa" >> "$GITHUB_ENV" echo "dsymPath=$(pwd)/Expensify.app.dSYM.zip" >> "$GITHUB_ENV" @@ -534,7 +536,7 @@ jobs: op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json - name: Upload release build to TestFlight - if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} run: bundle exec fastlane ios upload_testflight_hybrid env: APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} @@ -544,7 +546,7 @@ jobs: APPLE_ID: ${{ vars.APPLE_ID }} - name: Upload DSYMs to Firebase for HybridApp - if: ${{ fromJSON(env.IS_APP_REPO) && (github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK)) }} + if: ${{ fromJSON(env.IS_APP_REPO) && fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} run: bundle exec fastlane ios upload_dsyms_hybrid - name: Submit previous production build to 100% @@ -582,7 +584,7 @@ jobs: name: Upload iOS to BrowserStack and Applause needs: [prep, ios-build] runs-on: ubuntu-latest - if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} continue-on-error: true steps: - name: Checkout From a947ec1228b9b39686531422ea556616a1c4ba63 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 00:05:45 -0800 Subject: [PATCH 08/52] Rename SHOULD_DEPLOY_NATIVE to SHOULD_BUILD_NATIVE Co-authored-by: Cursor --- .github/workflows/deploy.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4b9db6bd3f23..83d2d12e8fe0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,8 +19,8 @@ jobs: TAG: ${{ steps.getTagName.outputs.TAG }} # Is this deploy for a cherry-pick? IS_CHERRY_PICK: ${{ steps.isCherryPick.outputs.IS_CHERRY_PICK }} - # Should we build and deploy native apps? (staging or cherry-pick, not production) - SHOULD_DEPLOY_NATIVE: ${{ github.ref == 'refs/heads/staging' || fromJSON(steps.isCherryPick.outputs.IS_CHERRY_PICK) }} + # Should we build native apps? (only on staging or cherry-pick, not production) + SHOULD_BUILD_NATIVE: ${{ github.ref == 'refs/heads/staging' || fromJSON(steps.isCherryPick.outputs.IS_CHERRY_PICK) }} steps: - name: Checkout # v6 @@ -90,7 +90,7 @@ jobs: outputs: VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} env: - SHOULD_BUILD_APP: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + SHOULD_BUILD_APP: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} steps: - name: Checkout App and Mobile-Expensify repo # v6 @@ -242,7 +242,7 @@ jobs: uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download Android build artifact - if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -250,7 +250,7 @@ jobs: path: ./ - name: Set aabPath for Fastlane - if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: echo "aabPath=$(pwd)/Expensify-release.aab" >> "$GITHUB_ENV" - name: Setup Ruby @@ -275,7 +275,7 @@ jobs: op read "op://${{ vars.OP_VAULT }}/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json - name: Upload Android app to Google Play - if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: bundle exec fastlane android ${{ vars.ANDROID_UPLOAD_COMMAND }} env: VERSION: ${{ needs.android-build.outputs.VERSION_CODE }} @@ -324,7 +324,7 @@ jobs: name: Upload Android to BrowserStack and Applause needs: [prep, android-build] runs-on: ubuntu-latest - if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} continue-on-error: true steps: - name: Checkout @@ -376,7 +376,7 @@ jobs: IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} env: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer - SHOULD_BUILD_APP: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + SHOULD_BUILD_APP: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} steps: - name: Checkout # v6 @@ -492,7 +492,7 @@ jobs: uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download iOS build artifact - if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -500,7 +500,7 @@ jobs: path: ./ - name: Download iOS dSYM artifact - if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -508,7 +508,7 @@ jobs: path: ./ - name: Set artifact paths for Fastlane - if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: | echo "ipaPath=$(pwd)/Expensify.ipa" >> "$GITHUB_ENV" echo "dsymPath=$(pwd)/Expensify.app.dSYM.zip" >> "$GITHUB_ENV" @@ -536,7 +536,7 @@ jobs: op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json - name: Upload release build to TestFlight - if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: bundle exec fastlane ios upload_testflight_hybrid env: APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} @@ -546,7 +546,7 @@ jobs: APPLE_ID: ${{ vars.APPLE_ID }} - name: Upload DSYMs to Firebase for HybridApp - if: ${{ fromJSON(env.IS_APP_REPO) && fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + if: ${{ fromJSON(env.IS_APP_REPO) && fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: bundle exec fastlane ios upload_dsyms_hybrid - name: Submit previous production build to 100% @@ -584,7 +584,7 @@ jobs: name: Upload iOS to BrowserStack and Applause needs: [prep, ios-build] runs-on: ubuntu-latest - if: ${{ fromJSON(needs.prep.outputs.SHOULD_DEPLOY_NATIVE) }} + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} continue-on-error: true steps: - name: Checkout From 67fa88b1fbd48e24817009495c8c89afd9862c02 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 00:06:42 -0800 Subject: [PATCH 09/52] Add submodules to iOS upload job checkout The dSYM upload lane references Mobile-Expensify/iOS/Pods/FirebaseCrashlytics/upload-symbols, so the checkout needs to pull submodules. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 83d2d12e8fe0..bd63b8129fc7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -490,6 +490,9 @@ jobs: - name: Checkout # v6 uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 + with: + token: ${{ secrets.OS_BOTIFY_TOKEN }} + submodules: true - name: Download iOS build artifact if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} From da387f87ec7b089bc058699da246105e46b9ba6a Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 00:17:34 -0800 Subject: [PATCH 10/52] Add Node setup and pod install to iOS upload job The dSYM upload lane uses the Crashlytics upload-symbols binary from Mobile-Expensify/iOS/Pods, so we need to install pods before running the upload. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bd63b8129fc7..b5a568701744 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -538,6 +538,30 @@ jobs: op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json + - name: Setup Node + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} + id: setup-node + uses: ./.github/actions/composite/setupNode + with: + IS_HYBRID_BUILD: 'true' + + - name: Cache Pod dependencies + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} + # v5.0.1 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb + id: pods-cache + with: + path: Mobile-Expensify/iOS/Pods + key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock', 'firebase.json') }} + + - name: Install cocoapods + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) && steps.pods-cache.outputs.cache-hit != 'true' }} + uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 + with: + timeout_minutes: 10 + max_attempts: 5 + command: npm run pod-install + - name: Upload release build to TestFlight if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: bundle exec fastlane ios upload_testflight_hybrid From ac13408027da317b6d17badd4911dcf7f7b2ad2e Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 00:32:28 -0800 Subject: [PATCH 11/52] Use bracket notation for hyphenated job IDs GitHub Actions expressions don't allow dot-notation access to keys containing hyphens. Changed needs.ios-upload-testflight.result to needs['ios-upload-testflight'].result and similarly for android-upload-google-play. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b5a568701744..2cd126e72b15 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -771,8 +771,8 @@ jobs: id: checkDeploymentSuccessOnAtLeastOnePlatform run: | isAtLeastOnePlatformDeployed="false" - if [ "${{ needs.ios-upload-testflight.result }}" == "success" ] || \ - [ "${{ needs.android-upload-google-play.result }}" == "success" ] || \ + if [ "${{ needs['ios-upload-testflight'].result }}" == "success" ] || \ + [ "${{ needs['android-upload-google-play'].result }}" == "success" ] || \ [ "${{ needs.web.result }}" == "success" ]; then isAtLeastOnePlatformDeployed="true" fi @@ -783,8 +783,8 @@ jobs: id: checkDeploymentSuccessOnAllPlatforms run: | isAllPlatformsDeployed="false" - if [ "${{ needs.ios-upload-testflight.result }}" == "success" ] && \ - [ "${{ needs.android-upload-google-play.result }}" == "success" ] && \ + if [ "${{ needs['ios-upload-testflight'].result }}" == "success" ] && \ + [ "${{ needs['android-upload-google-play'].result }}" == "success" ] && \ [ "${{ needs.web.result }}" == "success" ]; then isAllPlatformsDeployed="true" fi @@ -985,6 +985,6 @@ jobs: with: version: ${{ needs.prep.outputs.APP_VERSION }} env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} - android: ${{ needs.android-upload-google-play.result }} - ios: ${{ needs.ios-upload-testflight.result }} + android: ${{ needs['android-upload-google-play'].result }} + ios: ${{ needs['ios-upload-testflight'].result }} web: ${{ needs.web.result }} From 4727e54acd04ff236699a67110b0eff55b1fc291 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 00:43:15 -0800 Subject: [PATCH 12/52] Add build jobs to failure notification needs Include android-build and ios-build in the postSlackMessageOnFailure job's needs array so build failures also trigger Slack notifications. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2cd126e72b15..ced0ee67d44c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -748,7 +748,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: ubuntu-latest if: ${{ failure() }} - needs: [android-upload-google-play, ios-upload-testflight, web] + needs: [android-build, android-upload-google-play, ios-build, ios-upload-testflight, web] steps: - name: Checkout # v6 From afc91ea6af8e9961fbdb2e87e3dda66431560a17 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 00:54:26 -0800 Subject: [PATCH 13/52] Read IPA path from environment in upload_testflight_hybrid Modified the upload_testflight_hybrid lane to read the IPA path from the ipaPath environment variable. This allows the lane to work when run in a separate job from the build, where the lane context is not available. Co-authored-by: Cursor --- fastlane/Fastfile | 1 + 1 file changed, 1 insertion(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7dfcec4c7c30..0e9f1cd1f856 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -531,6 +531,7 @@ platform :ios do desc "Upload HybridApp to TestFlight" lane :upload_testflight_hybrid do upload_to_testflight( + ipa: ENV[KEY_IPA_PATH], app_identifier: ENV["APPLE_ID"], api_key_path: "./ios-fastlane-json-key.json", distribute_external: true, From 8d2c83ef903c4ee2ba5caf714fe542809e98e89d Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 01:21:27 -0800 Subject: [PATCH 14/52] Use dynamic artifact paths instead of hardcoded filenames Use find commands to dynamically locate the AAB, IPA, and dSYM files instead of hardcoding specific filenames. This makes the workflow more robust to changes in build output naming conventions. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ced0ee67d44c..8e12b03c0ad5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -251,7 +251,7 @@ jobs: - name: Set aabPath for Fastlane if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} - run: echo "aabPath=$(pwd)/Expensify-release.aab" >> "$GITHUB_ENV" + run: echo "aabPath=$(find $(pwd) -maxdepth 1 -name '*.aab' | head -1)" >> "$GITHUB_ENV" - name: Setup Ruby # v1.229.0 @@ -513,8 +513,8 @@ jobs: - name: Set artifact paths for Fastlane if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: | - echo "ipaPath=$(pwd)/Expensify.ipa" >> "$GITHUB_ENV" - echo "dsymPath=$(pwd)/Expensify.app.dSYM.zip" >> "$GITHUB_ENV" + echo "ipaPath=$(find $(pwd) -maxdepth 1 -name '*.ipa' | head -1)" >> "$GITHUB_ENV" + echo "dsymPath=$(find $(pwd) -maxdepth 1 -name '*.dSYM.zip' | head -1)" >> "$GITHUB_ENV" - name: Setup Ruby # v1.229.0 From 01b44bab4f4b2345d227ab68ebe82d4155c605fd Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 01:47:57 -0800 Subject: [PATCH 15/52] Add MapBox SDK setup before pod install in iOS upload job Configure the MapBox SDK token before running pod install to ensure CocoaPods can fetch the Mapbox SDK when the pods cache is cold. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8e12b03c0ad5..69598697a055 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -545,6 +545,10 @@ jobs: with: IS_HYBRID_BUILD: 'true' + - name: Configure MapBox SDK + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} + run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + - name: Cache Pod dependencies if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} # v5.0.1 From 93169af813dcd9db956fa5f38491aeda199a37ea Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 02:03:45 -0800 Subject: [PATCH 16/52] Fall back to lane context for IPA path in upload_testflight_hybrid Use environment variable if set (for separate GitHub Actions jobs), otherwise fall back to lane context (for local runs where the build and upload happen in the same lane execution). Co-authored-by: Cursor --- fastlane/Fastfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0e9f1cd1f856..7e086d0b5f52 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -530,8 +530,10 @@ platform :ios do desc "Upload HybridApp to TestFlight" lane :upload_testflight_hybrid do + # Use environment variable if set (for separate upload jobs), otherwise fall back to lane context (for local runs) + ipa_path = ENV[KEY_IPA_PATH] || lane_context[SharedValues::IPA_OUTPUT_PATH] upload_to_testflight( - ipa: ENV[KEY_IPA_PATH], + ipa: ipa_path, app_identifier: ENV["APPLE_ID"], api_key_path: "./ios-fastlane-json-key.json", distribute_external: true, From faa61811eefa81d091b0dfedf9c5db807c02a37f Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 02:14:29 -0800 Subject: [PATCH 17/52] Use recursive find for artifact paths Remove -maxdepth 1 from find commands to allow recursive searching, since download-artifact may restore the full directory structure when artifacts are uploaded with absolute paths. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 69598697a055..7367a4dddfb6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -251,7 +251,7 @@ jobs: - name: Set aabPath for Fastlane if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} - run: echo "aabPath=$(find $(pwd) -maxdepth 1 -name '*.aab' | head -1)" >> "$GITHUB_ENV" + run: echo "aabPath=$(find $(pwd) -name '*.aab' | head -1)" >> "$GITHUB_ENV" - name: Setup Ruby # v1.229.0 @@ -513,8 +513,8 @@ jobs: - name: Set artifact paths for Fastlane if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: | - echo "ipaPath=$(find $(pwd) -maxdepth 1 -name '*.ipa' | head -1)" >> "$GITHUB_ENV" - echo "dsymPath=$(find $(pwd) -maxdepth 1 -name '*.dSYM.zip' | head -1)" >> "$GITHUB_ENV" + echo "ipaPath=$(find $(pwd) -name '*.ipa' | head -1)" >> "$GITHUB_ENV" + echo "dsymPath=$(find $(pwd) -name '*.dSYM.zip' | head -1)" >> "$GITHUB_ENV" - name: Setup Ruby # v1.229.0 From 5506e756e75686819639fc70b1e5e9a33295d107 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 02:51:41 -0800 Subject: [PATCH 18/52] Use dynamic paths for APK/IPA in testing upload jobs Add steps to find the APK and IPA files using recursive find before uploading to BrowserStack and Applause, since the artifacts may be extracted to nested directories. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7367a4dddfb6..19c280df521f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -354,9 +354,13 @@ jobs: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} APPLAUSE_API_KEY: op://${{ vars.OP_VAULT }}/Applause-API-Key/password + - name: Find APK path + id: find-apk + run: echo "APK_PATH=$(find $(pwd) -name '*.apk' | head -1)" >> "$GITHUB_OUTPUT" + - name: Upload Android build to Browser Stack if: ${{ fromJSON(env.IS_APP_REPO) }} - run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@Expensify.apk" + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ steps.find-apk.outputs.APK_PATH }}" env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} @@ -364,7 +368,7 @@ jobs: if: ${{ fromJSON(env.IS_APP_REPO) && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} run: | APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.') - curl -F "file=@Expensify.apk" \ + curl -F "file=@${{ steps.find-apk.outputs.APK_PATH }}" \ "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36008" \ -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}" @@ -645,9 +649,13 @@ jobs: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} APPLAUSE_API_KEY: op://${{ vars.OP_VAULT }}/Applause-API-Key/password + - name: Find IPA path + id: find-ipa + run: echo "IPA_PATH=$(find $(pwd) -name '*.ipa' | head -1)" >> "$GITHUB_OUTPUT" + - name: Upload iOS build to Browser Stack if: ${{ fromJSON(env.IS_APP_REPO) }} - run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@Expensify.ipa" + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ steps.find-ipa.outputs.IPA_PATH }}" env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} @@ -655,7 +663,7 @@ jobs: if: ${{ fromJSON(env.IS_APP_REPO) && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} run: | APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.') - curl -F "file=@Expensify.ipa" \ + curl -F "file=@${{ steps.find-ipa.outputs.IPA_PATH }}" \ "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36005" \ -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}" From b48ce108275c460c54a449fac46a8e2530b2ded0 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 07:52:49 -0800 Subject: [PATCH 19/52] Use camelCase for job names instead of kebab-case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed hyphenated job IDs to camelCase to allow standard dot notation access in GitHub Actions expressions without needing bracket notation. - android-build → androidBuild - android-upload-google-play → androidUploadGooglePlay - android-upload-testing → androidUploadTesting - ios-build → iosBuild - ios-upload-testflight → iosUploadTestflight - ios-upload-testing → iosUploadTesting Co-authored-by: Cursor --- .github/workflows/deploy.yml | 62 ++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 19c280df521f..558a0e5e2ded 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -83,7 +83,7 @@ jobs: needs: prep secrets: inherit - android-build: + androidBuild: name: Build Android HybridApp needs: prep runs-on: ubuntu-latest-xl @@ -221,7 +221,7 @@ jobs: # v6 uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: - name: android-build-artifact + name: androidBuild-artifact path: ${{ env.aabPath }} - name: Upload Android sourcemap artifact @@ -232,9 +232,9 @@ jobs: name: android-sourcemap-artifact path: /home/runner/work/App/App/Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map - android-upload-google-play: + androidUploadGooglePlay: name: Upload Android to Google Play - needs: [prep, android-build] + needs: [prep, androidBuild] runs-on: ubuntu-latest steps: - name: Checkout @@ -246,7 +246,7 @@ jobs: # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: - name: android-build-artifact + name: androidBuild-artifact path: ./ - name: Set aabPath for Fastlane @@ -278,7 +278,7 @@ jobs: if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: bundle exec fastlane android ${{ vars.ANDROID_UPLOAD_COMMAND }} env: - VERSION: ${{ needs.android-build.outputs.VERSION_CODE }} + VERSION: ${{ needs.androidBuild.outputs.VERSION_CODE }} ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME }} - name: Get current Android rollout percentage @@ -299,7 +299,7 @@ jobs: if: ${{ github.ref == 'refs/heads/production' }} run: bundle exec fastlane android upload_google_play_production_hybrid_rollout env: - VERSION: ${{ needs.android-build.outputs.VERSION_CODE }} + VERSION: ${{ needs.androidBuild.outputs.VERSION_CODE }} - name: Warn deployers if Android production deploy failed if: ${{ failure() && github.ref == 'refs/heads/production' }} @@ -320,9 +320,9 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - android-upload-testing: + androidUploadTesting: name: Upload Android to BrowserStack and Applause - needs: [prep, android-build] + needs: [prep, androidBuild] runs-on: ubuntu-latest if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} continue-on-error: true @@ -372,7 +372,7 @@ jobs: "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36008" \ -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}" - ios-build: + iosBuild: name: Build iOS HybridApp needs: prep runs-on: macos-15-xlarge @@ -465,7 +465,7 @@ jobs: # v6 uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: - name: ios-build-artifact + name: iosBuild-artifact path: ${{ env.ipaPath }} - name: Upload iOS dSYM artifact @@ -484,9 +484,9 @@ jobs: name: ios-sourcemap-artifact path: /Users/runner/work/App/App/Mobile-Expensify/main.jsbundle.map - ios-upload-testflight: + iosUploadTestflight: name: Upload iOS to TestFlight - needs: [prep, ios-build] + needs: [prep, iosBuild] runs-on: macos-15-xlarge env: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer @@ -503,7 +503,7 @@ jobs: # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: - name: ios-build-artifact + name: iosBuild-artifact path: ./ - name: Download iOS dSYM artifact @@ -593,7 +593,7 @@ jobs: if: ${{ github.ref == 'refs/heads/production' }} run: bundle exec fastlane ios submit_hybrid_for_rollout env: - VERSION: ${{ needs.ios-build.outputs.IOS_VERSION }} + VERSION: ${{ needs.iosBuild.outputs.IOS_VERSION }} APPLE_ID: ${{ vars.APPLE_ID }} - name: Warn deployers if iOS production deploy failed @@ -608,16 +608,16 @@ jobs: attachments: [{ color: "#DB4545", pretext: ``, - text: `💥 iOS HybridApp production deploy failed. Please ${{ needs.ios-build.outputs.IOS_VERSION }} in the . 💥`, + text: `💥 iOS HybridApp production deploy failed. Please ${{ needs.iosBuild.outputs.IOS_VERSION }} in the . 💥`, }] } env: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - ios-upload-testing: + iosUploadTesting: name: Upload iOS to BrowserStack and Applause - needs: [prep, ios-build] + needs: [prep, iosBuild] runs-on: ubuntu-latest if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} continue-on-error: true @@ -630,7 +630,7 @@ jobs: # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: - name: ios-build-artifact + name: iosBuild-artifact path: ./ - name: Setup 1Password CLI @@ -760,7 +760,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: ubuntu-latest if: ${{ failure() }} - needs: [android-build, android-upload-google-play, ios-build, ios-upload-testflight, web] + needs: [androidBuild, androidUploadGooglePlay, iosBuild, iosUploadTestflight, web] steps: - name: Checkout # v6 @@ -776,15 +776,15 @@ jobs: outputs: IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }} - needs: [android-upload-google-play, ios-upload-testflight, web] + needs: [androidUploadGooglePlay, iosUploadTestflight, web] if: ${{ always() }} steps: - name: Check deployment success on at least one platform id: checkDeploymentSuccessOnAtLeastOnePlatform run: | isAtLeastOnePlatformDeployed="false" - if [ "${{ needs['ios-upload-testflight'].result }}" == "success" ] || \ - [ "${{ needs['android-upload-google-play'].result }}" == "success" ] || \ + if [ "${{ needs.iosUploadTestflight.result }}" == "success" ] || \ + [ "${{ needs.androidUploadGooglePlay.result }}" == "success" ] || \ [ "${{ needs.web.result }}" == "success" ]; then isAtLeastOnePlatformDeployed="true" fi @@ -795,8 +795,8 @@ jobs: id: checkDeploymentSuccessOnAllPlatforms run: | isAllPlatformsDeployed="false" - if [ "${{ needs['ios-upload-testflight'].result }}" == "success" ] && \ - [ "${{ needs['android-upload-google-play'].result }}" == "success" ] && \ + if [ "${{ needs.iosUploadTestflight.result }}" == "success" ] && \ + [ "${{ needs.androidUploadGooglePlay.result }}" == "success" ] && \ [ "${{ needs.web.result }}" == "success" ]; then isAllPlatformsDeployed="true" fi @@ -873,10 +873,10 @@ jobs: run: | # Release asset name should follow the template: fileNameOnRunner#fileNameInRelease files=" - ./android-build-artifact/Expensify-release.aab#android.aab + ./androidBuild-artifact/Expensify-release.aab#android.aab ./android-apk-artifact/Expensify.apk#android.apk ./android-sourcemap-artifact/index.android.bundle.map#android-sourcemap.js.map - ./ios-build-artifact/Expensify.ipa#ios.ipa + ./iosBuild-artifact/Expensify.ipa#ios.ipa ./ios-sourcemap-artifact/main.jsbundle.map#ios-sourcemap.js.map ./web-sourcemaps-artifact/web-merged-source-map.js.map#web-sourcemap.js.map ./web-build-tar-gz-artifact/webBuild.tar.gz#web.tar.gz @@ -935,7 +935,7 @@ jobs: name: Post a Slack message when all platforms deploy successfully runs-on: ubuntu-latest if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }} - needs: [prep, android-upload-google-play, ios-upload-testflight, web, checkDeploymentSuccess, createRelease] + needs: [prep, androidUploadGooglePlay, iosUploadTestflight, web, checkDeploymentSuccess, createRelease] steps: - name: 'Announces the deploy in the #announce Slack room' # v3 @@ -992,11 +992,11 @@ jobs: postGithubComments: uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} - needs: [prep, android-upload-google-play, ios-upload-testflight, web, checkDeploymentSuccess, createRelease] + needs: [prep, androidUploadGooglePlay, iosUploadTestflight, web, checkDeploymentSuccess, createRelease] secrets: inherit with: version: ${{ needs.prep.outputs.APP_VERSION }} env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} - android: ${{ needs['android-upload-google-play'].result }} - ios: ${{ needs['ios-upload-testflight'].result }} + android: ${{ needs.androidUploadGooglePlay.result }} + ios: ${{ needs.iosUploadTestflight.result }} web: ${{ needs.web.result }} From a93aba21ec303d887765991adfa8fff642db5704 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 07:57:06 -0800 Subject: [PATCH 20/52] Propagate build failures to deployment status checks If a build job fails, the corresponding upload job is skipped. This change ensures that build failures are properly reported as "failure" rather than "skipped" in deployment success checks and GitHub comments. - Added platformResults step to determine effective result per platform - If build fails, result is "failure"; otherwise uses upload result - checkDeploymentSuccess now depends on both build and upload jobs - postGithubComments uses the propagated results from checkDeploymentSuccess Co-authored-by: Cursor --- .github/workflows/deploy.yml | 45 +++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 558a0e5e2ded..d6a40a0e4f35 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -776,16 +776,39 @@ jobs: outputs: IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }} - needs: [androidUploadGooglePlay, iosUploadTestflight, web] + ANDROID_RESULT: ${{ steps.platformResults.outputs.ANDROID_RESULT }} + IOS_RESULT: ${{ steps.platformResults.outputs.IOS_RESULT }} + WEB_RESULT: ${{ steps.platformResults.outputs.WEB_RESULT }} + needs: [androidBuild, androidUploadGooglePlay, iosBuild, iosUploadTestflight, web] if: ${{ always() }} steps: + # Determine effective result for each platform (build failure = failure, not skipped) + - name: Determine platform results + id: platformResults + run: | + # Android: if build failed, result is failure; otherwise use upload result + if [ "${{ needs.androidBuild.result }}" == "failure" ]; then + echo "ANDROID_RESULT=failure" >> "$GITHUB_OUTPUT" + else + echo "ANDROID_RESULT=${{ needs.androidUploadGooglePlay.result }}" >> "$GITHUB_OUTPUT" + fi + + # iOS: if build failed, result is failure; otherwise use upload result + if [ "${{ needs.iosBuild.result }}" == "failure" ]; then + echo "IOS_RESULT=failure" >> "$GITHUB_OUTPUT" + else + echo "IOS_RESULT=${{ needs.iosUploadTestflight.result }}" >> "$GITHUB_OUTPUT" + fi + + echo "WEB_RESULT=${{ needs.web.result }}" >> "$GITHUB_OUTPUT" + - name: Check deployment success on at least one platform id: checkDeploymentSuccessOnAtLeastOnePlatform run: | isAtLeastOnePlatformDeployed="false" - if [ "${{ needs.iosUploadTestflight.result }}" == "success" ] || \ - [ "${{ needs.androidUploadGooglePlay.result }}" == "success" ] || \ - [ "${{ needs.web.result }}" == "success" ]; then + if [ "${{ steps.platformResults.outputs.IOS_RESULT }}" == "success" ] || \ + [ "${{ steps.platformResults.outputs.ANDROID_RESULT }}" == "success" ] || \ + [ "${{ steps.platformResults.outputs.WEB_RESULT }}" == "success" ]; then isAtLeastOnePlatformDeployed="true" fi echo "IS_AT_LEAST_ONE_PLATFORM_DEPLOYED=$isAtLeastOnePlatformDeployed" >> "$GITHUB_OUTPUT" @@ -795,9 +818,9 @@ jobs: id: checkDeploymentSuccessOnAllPlatforms run: | isAllPlatformsDeployed="false" - if [ "${{ needs.iosUploadTestflight.result }}" == "success" ] && \ - [ "${{ needs.androidUploadGooglePlay.result }}" == "success" ] && \ - [ "${{ needs.web.result }}" == "success" ]; then + if [ "${{ steps.platformResults.outputs.IOS_RESULT }}" == "success" ] && \ + [ "${{ steps.platformResults.outputs.ANDROID_RESULT }}" == "success" ] && \ + [ "${{ steps.platformResults.outputs.WEB_RESULT }}" == "success" ]; then isAllPlatformsDeployed="true" fi @@ -992,11 +1015,11 @@ jobs: postGithubComments: uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} - needs: [prep, androidUploadGooglePlay, iosUploadTestflight, web, checkDeploymentSuccess, createRelease] + needs: [prep, checkDeploymentSuccess, createRelease] secrets: inherit with: version: ${{ needs.prep.outputs.APP_VERSION }} env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} - android: ${{ needs.androidUploadGooglePlay.result }} - ios: ${{ needs.iosUploadTestflight.result }} - web: ${{ needs.web.result }} + android: ${{ needs.checkDeploymentSuccess.outputs.ANDROID_RESULT }} + ios: ${{ needs.checkDeploymentSuccess.outputs.IOS_RESULT }} + web: ${{ needs.checkDeploymentSuccess.outputs.WEB_RESULT }} From 433d0989cfc2ad0c4d7ca47c5bfe46812334b513 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Feb 2026 08:28:02 -0800 Subject: [PATCH 21/52] Refactor deploy workflow to separate build, upload, and submit jobs This refactoring separates build/upload/submit responsibilities into distinct jobs: **Staging/Cherry-pick deploys:** - Build jobs (androidBuild, iosBuild) - only run for staging/cherry-picks - Upload jobs (androidUploadGooglePlay, iosUploadTestflight) - upload to stores - Testing upload jobs (split into 4 separate jobs for atomic retryability): - androidUploadBrowserStack, androidUploadApplause - iosUploadBrowserStack, iosUploadApplause **Production deploys:** - Submit jobs (androidSubmit, iosSubmit) - handle rollout completion and submission - No builds or uploads needed (uses already-uploaded builds) **Benefits:** - Clear separation of concerns - each job has one purpose - Efficient resource usage - no wasted builds on production deploys - Atomic retryability - testing uploads can be retried independently - Simpler conditionals - job-level if instead of step-level checks - Correct failure reporting - build failures properly propagated through deployment status **Changes:** - Added androidSubmit and iosSubmit jobs for production rollout/submission - Split testing uploads into 4 separate jobs (BrowserStack/Applause per platform) - Added job-level if conditions to build/upload jobs - Updated checkDeploymentSuccess to evaluate staging vs production results correctly - Updated all downstream job dependencies Co-authored-by: Cursor --- .github/workflows/deploy.yml | 205 ++++++++++++++++++++++++----------- 1 file changed, 139 insertions(+), 66 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d6a40a0e4f35..a48402c8d213 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -87,10 +87,9 @@ jobs: name: Build Android HybridApp needs: prep runs-on: ubuntu-latest-xl + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} outputs: VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} - env: - SHOULD_BUILD_APP: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} steps: - name: Checkout App and Mobile-Expensify repo # v6 @@ -166,7 +165,6 @@ jobs: run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT" - name: Build Android app - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} run: bundle exec fastlane android build_hybrid env: ANDROID_UPLOAD_KEYSTORE_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} @@ -178,7 +176,6 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Install bundletool - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} run: | readonly BUNDLETOOL_VERSION="1.18.1" readonly BUNDLETOOL_URL="https://github.com/google/bundletool/releases/download/${BUNDLETOOL_VERSION}/bundletool-all-${BUNDLETOOL_VERSION}.jar" @@ -195,7 +192,6 @@ jobs: fi - name: Generate APK from AAB - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} run: | # Generate apks using bundletool java -jar bundletool.jar build-apks --bundle=${{ env.aabPath }} --output=Expensify.apks \ @@ -209,7 +205,6 @@ jobs: unzip -p Expensify.apks universal.apk > Expensify.apk - name: Upload Android APK build artifact - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} # v6 uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: @@ -217,7 +212,6 @@ jobs: path: Expensify.apk - name: Upload Android build artifact - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} # v6 uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: @@ -225,7 +219,6 @@ jobs: path: ${{ env.aabPath }} - name: Upload Android sourcemap artifact - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} # v6 uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: @@ -236,13 +229,13 @@ jobs: name: Upload Android to Google Play needs: [prep, androidBuild] runs-on: ubuntu-latest + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} steps: - name: Checkout # v6 uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Download Android build artifact - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -250,7 +243,6 @@ jobs: path: ./ - name: Set aabPath for Fastlane - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: echo "aabPath=$(find $(pwd) -name '*.aab' | head -1)" >> "$GITHUB_ENV" - name: Setup Ruby @@ -275,14 +267,43 @@ jobs: op read "op://${{ vars.OP_VAULT }}/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json - name: Upload Android app to Google Play - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: bundle exec fastlane android ${{ vars.ANDROID_UPLOAD_COMMAND }} env: VERSION: ${{ needs.androidBuild.outputs.VERSION_CODE }} ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME }} + androidSubmit: + name: Submit Android for production rollout + needs: [prep, androidBuild] + runs-on: ubuntu-latest + if: ${{ github.ref == 'refs/heads/production' }} + steps: + - name: Checkout + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 + + - name: Setup Ruby + # v1.229.0 + uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 + with: + bundler-cache: true + + - name: Install New Expensify Gems + run: bundle install + + - name: Setup 1Password CLI + uses: Expensify/GitHub-Actions/setup-certificate-1p@main + with: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SHOULD_LOAD_SSL_CERTIFICATES: 'false' + + - name: Load Google Play credentials from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op read "op://${{ vars.OP_VAULT }}/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json + - name: Get current Android rollout percentage - if: ${{ github.ref == 'refs/heads/production' }} id: getAndroidRolloutPercentage uses: ./.github/actions/javascript/getAndroidRolloutPercentage with: @@ -291,18 +312,17 @@ jobs: # Complete the previous version rollout if the current rollout percentage is not -1 (no rollout in progress) or 1 (fully rolled out) - name: Submit previous production build to 100% - if: ${{ github.ref == 'refs/heads/production' && !contains(fromJSON('["1", "-1"]'), steps.getAndroidRolloutPercentage.outputs.CURRENT_ROLLOUT_PERCENTAGE) }} + if: ${{ !contains(fromJSON('["1", "-1"]'), steps.getAndroidRolloutPercentage.outputs.CURRENT_ROLLOUT_PERCENTAGE) }} run: bundle exec fastlane android complete_hybrid_rollout continue-on-error: true - name: Submit production build for Google Play review and a slow rollout - if: ${{ github.ref == 'refs/heads/production' }} run: bundle exec fastlane android upload_google_play_production_hybrid_rollout env: VERSION: ${{ needs.androidBuild.outputs.VERSION_CODE }} - name: Warn deployers if Android production deploy failed - if: ${{ failure() && github.ref == 'refs/heads/production' }} + if: ${{ failure() }} # v3 uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e with: @@ -320,17 +340,37 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - androidUploadTesting: - name: Upload Android to BrowserStack and Applause + androidUploadBrowserStack: + name: Upload Android to BrowserStack needs: [prep, androidBuild] runs-on: ubuntu-latest if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} continue-on-error: true steps: - - name: Checkout - # v6 - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 + - name: Download Android APK artifact + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + with: + name: android-apk-artifact + path: ./ + + - name: Find APK path + id: find-apk + run: echo "APK_PATH=$(find $(pwd) -name '*.apk' | head -1)" >> "$GITHUB_OUTPUT" + + - name: Upload Android build to BrowserStack + if: ${{ fromJSON(env.IS_APP_REPO) }} + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ steps.find-apk.outputs.APK_PATH }}" + env: + BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + androidUploadApplause: + name: Upload Android to Applause + needs: [prep, androidBuild] + runs-on: ubuntu-latest + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} + continue-on-error: true + steps: - name: Download Android APK artifact # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 @@ -344,7 +384,7 @@ jobs: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} SHOULD_LOAD_SSL_CERTIFICATES: 'false' - - name: Load testing credentials from 1Password + - name: Load Applause API key from 1Password id: load-credentials # v2 uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 @@ -358,14 +398,8 @@ jobs: id: find-apk run: echo "APK_PATH=$(find $(pwd) -name '*.apk' | head -1)" >> "$GITHUB_OUTPUT" - - name: Upload Android build to Browser Stack - if: ${{ fromJSON(env.IS_APP_REPO) }} - run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ steps.find-apk.outputs.APK_PATH }}" - env: - BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - - name: Upload Android build to Applause - if: ${{ fromJSON(env.IS_APP_REPO) && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(env.IS_APP_REPO) }} run: | APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.') curl -F "file=@${{ steps.find-apk.outputs.APK_PATH }}" \ @@ -376,11 +410,11 @@ jobs: name: Build iOS HybridApp needs: prep runs-on: macos-15-xlarge + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} outputs: IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} env: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer - SHOULD_BUILD_APP: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} steps: - name: Checkout # v6 @@ -448,7 +482,6 @@ jobs: run: echo "IOS_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT" - name: Build iOS HybridApp - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} run: bundle exec fastlane ios build_hybrid env: APPLE_ID: ${{ vars.APPLE_ID }} @@ -461,7 +494,6 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Upload iOS build artifact - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} # v6 uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: @@ -469,7 +501,6 @@ jobs: path: ${{ env.ipaPath }} - name: Upload iOS dSYM artifact - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} # v6 uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: @@ -477,7 +508,6 @@ jobs: path: ${{ env.dsymPath }} - name: Upload iOS sourcemap artifact - if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} # v6 uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 with: @@ -488,6 +518,7 @@ jobs: name: Upload iOS to TestFlight needs: [prep, iosBuild] runs-on: macos-15-xlarge + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} env: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer steps: @@ -499,7 +530,6 @@ jobs: submodules: true - name: Download iOS build artifact - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -507,7 +537,6 @@ jobs: path: ./ - name: Download iOS dSYM artifact - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: @@ -515,7 +544,6 @@ jobs: path: ./ - name: Set artifact paths for Fastlane - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: | echo "ipaPath=$(find $(pwd) -name '*.ipa' | head -1)" >> "$GITHUB_ENV" echo "dsymPath=$(find $(pwd) -name '*.dSYM.zip' | head -1)" >> "$GITHUB_ENV" @@ -543,18 +571,15 @@ jobs: op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json - name: Setup Node - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} id: setup-node uses: ./.github/actions/composite/setupNode with: IS_HYBRID_BUILD: 'true' - name: Configure MapBox SDK - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - name: Cache Pod dependencies - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} # v5.0.1 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb id: pods-cache @@ -563,7 +588,7 @@ jobs: key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock', 'firebase.json') }} - name: Install cocoapods - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) && steps.pods-cache.outputs.cache-hit != 'true' }} + if: ${{ steps.pods-cache.outputs.cache-hit != 'true' }} uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 with: timeout_minutes: 10 @@ -571,7 +596,6 @@ jobs: command: npm run pod-install - name: Upload release build to TestFlight - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} run: bundle exec fastlane ios upload_testflight_hybrid env: APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} @@ -581,23 +605,54 @@ jobs: APPLE_ID: ${{ vars.APPLE_ID }} - name: Upload DSYMs to Firebase for HybridApp - if: ${{ fromJSON(env.IS_APP_REPO) && fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} + if: ${{ fromJSON(env.IS_APP_REPO) }} run: bundle exec fastlane ios upload_dsyms_hybrid + iosSubmit: + name: Submit iOS for production rollout + needs: [prep, iosBuild] + runs-on: macos-15-xlarge + if: ${{ github.ref == 'refs/heads/production' }} + env: + DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer + steps: + - name: Checkout + # v6 + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 + + - name: Setup Ruby + # v1.229.0 + uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 + with: + bundler-cache: true + + - name: Install New Expensify Gems + run: bundle install + + - name: Setup 1Password CLI + uses: Expensify/GitHub-Actions/setup-certificate-1p@main + with: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SHOULD_LOAD_SSL_CERTIFICATES: 'false' + + - name: Load iOS credentials from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json + - name: Submit previous production build to 100% - if: ${{ github.ref == 'refs/heads/production' }} run: bundle exec fastlane ios complete_hybrid_rollout continue-on-error: true - name: Submit production build for App Store review and a slow rollout - if: ${{ github.ref == 'refs/heads/production' }} run: bundle exec fastlane ios submit_hybrid_for_rollout env: VERSION: ${{ needs.iosBuild.outputs.IOS_VERSION }} APPLE_ID: ${{ vars.APPLE_ID }} - name: Warn deployers if iOS production deploy failed - if: ${{ failure() && github.ref == 'refs/heads/production' }} + if: ${{ failure() }} # v3 uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e with: @@ -615,17 +670,37 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - iosUploadTesting: - name: Upload iOS to BrowserStack and Applause + iosUploadBrowserStack: + name: Upload iOS to BrowserStack needs: [prep, iosBuild] runs-on: ubuntu-latest if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} continue-on-error: true steps: - - name: Checkout - # v6 - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 + - name: Download iOS build artifact + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + with: + name: iosBuild-artifact + path: ./ + + - name: Find IPA path + id: find-ipa + run: echo "IPA_PATH=$(find $(pwd) -name '*.ipa' | head -1)" >> "$GITHUB_OUTPUT" + - name: Upload iOS build to BrowserStack + if: ${{ fromJSON(env.IS_APP_REPO) }} + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ steps.find-ipa.outputs.IPA_PATH }}" + env: + BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + + iosUploadApplause: + name: Upload iOS to Applause + needs: [prep, iosBuild] + runs-on: ubuntu-latest + if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} + continue-on-error: true + steps: - name: Download iOS build artifact # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 @@ -639,7 +714,7 @@ jobs: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} SHOULD_LOAD_SSL_CERTIFICATES: 'false' - - name: Load testing credentials from 1Password + - name: Load Applause API key from 1Password id: load-credentials # v2 uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 @@ -653,14 +728,8 @@ jobs: id: find-ipa run: echo "IPA_PATH=$(find $(pwd) -name '*.ipa' | head -1)" >> "$GITHUB_OUTPUT" - - name: Upload iOS build to Browser Stack - if: ${{ fromJSON(env.IS_APP_REPO) }} - run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ steps.find-ipa.outputs.IPA_PATH }}" - env: - BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - - name: Upload iOS build to Applause - if: ${{ fromJSON(env.IS_APP_REPO) && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} + if: ${{ fromJSON(env.IS_APP_REPO) }} run: | APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.') curl -F "file=@${{ steps.find-ipa.outputs.IPA_PATH }}" \ @@ -760,7 +829,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: ubuntu-latest if: ${{ failure() }} - needs: [androidBuild, androidUploadGooglePlay, iosBuild, iosUploadTestflight, web] + needs: [androidBuild, androidUploadGooglePlay, androidUploadBrowserStack, androidUploadApplause, androidSubmit, iosBuild, iosUploadTestflight, iosUploadBrowserStack, iosUploadApplause, iosSubmit, web] steps: - name: Checkout # v6 @@ -779,22 +848,26 @@ jobs: ANDROID_RESULT: ${{ steps.platformResults.outputs.ANDROID_RESULT }} IOS_RESULT: ${{ steps.platformResults.outputs.IOS_RESULT }} WEB_RESULT: ${{ steps.platformResults.outputs.WEB_RESULT }} - needs: [androidBuild, androidUploadGooglePlay, iosBuild, iosUploadTestflight, web] + needs: [androidBuild, androidUploadGooglePlay, androidSubmit, iosBuild, iosUploadTestflight, iosSubmit, web] if: ${{ always() }} steps: - # Determine effective result for each platform (build failure = failure, not skipped) + # Determine effective result for each platform - name: Determine platform results id: platformResults run: | - # Android: if build failed, result is failure; otherwise use upload result - if [ "${{ needs.androidBuild.result }}" == "failure" ]; then + # Android: use submit result for production, upload result for staging + if [ "${{ github.ref }}" == "refs/heads/production" ]; then + echo "ANDROID_RESULT=${{ needs.androidSubmit.result }}" >> "$GITHUB_OUTPUT" + elif [ "${{ needs.androidBuild.result }}" == "failure" ]; then echo "ANDROID_RESULT=failure" >> "$GITHUB_OUTPUT" else echo "ANDROID_RESULT=${{ needs.androidUploadGooglePlay.result }}" >> "$GITHUB_OUTPUT" fi - # iOS: if build failed, result is failure; otherwise use upload result - if [ "${{ needs.iosBuild.result }}" == "failure" ]; then + # iOS: use submit result for production, upload result for staging + if [ "${{ github.ref }}" == "refs/heads/production" ]; then + echo "IOS_RESULT=${{ needs.iosSubmit.result }}" >> "$GITHUB_OUTPUT" + elif [ "${{ needs.iosBuild.result }}" == "failure" ]; then echo "IOS_RESULT=failure" >> "$GITHUB_OUTPUT" else echo "IOS_RESULT=${{ needs.iosUploadTestflight.result }}" >> "$GITHUB_OUTPUT" @@ -958,7 +1031,7 @@ jobs: name: Post a Slack message when all platforms deploy successfully runs-on: ubuntu-latest if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }} - needs: [prep, androidUploadGooglePlay, iosUploadTestflight, web, checkDeploymentSuccess, createRelease] + needs: [prep, androidUploadGooglePlay, androidSubmit, iosUploadTestflight, iosSubmit, web, checkDeploymentSuccess, createRelease] steps: - name: 'Announces the deploy in the #announce Slack room' # v3 From 09bb630ef4fa6c1d5f65ff32932008714b1136a1 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 13:27:03 -0800 Subject: [PATCH 22/52] Use 32vcpu runners for heavy build jobs Android and web builds are resource-intensive operations that benefit from larger runners. Updated androidBuild and web jobs from 16vcpu to 32vcpu, consistent with buildAdHoc.yml conventions. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c6abd91ec186..89dd5d3569dd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -86,7 +86,7 @@ jobs: androidBuild: name: Build Android HybridApp needs: prep - runs-on: blacksmith-16vcpu-ubuntu-2404 + runs-on: blacksmith-32vcpu-ubuntu-2404 if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} outputs: VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} @@ -739,7 +739,7 @@ jobs: web: name: Build and deploy Web needs: prep - runs-on: blacksmith-16vcpu-ubuntu-2404 + runs-on: blacksmith-32vcpu-ubuntu-2404 steps: - name: Checkout # v6 From 6abb732485d4f5121b91e370b6018478a4eb6844 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 13:44:41 -0800 Subject: [PATCH 23/52] Move version outputs to prep job so submit jobs run on production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The androidSubmit and iosSubmit jobs depended on androidBuild and iosBuild respectively, but those build jobs are skipped on production deploys (when SHOULD_BUILD_NATIVE is false). GitHub Actions skips downstream jobs when their dependencies are skipped, which meant production rollout submissions never ran — a regression from the monolithic job structure. Fix by moving VERSION_CODE and IOS_VERSION computation to the prep job (which already checks out submodules) and removing the build jobs from the submit jobs' needs. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 89dd5d3569dd..00fcc92516b2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,6 +21,8 @@ jobs: IS_CHERRY_PICK: ${{ steps.isCherryPick.outputs.IS_CHERRY_PICK }} # Should we build native apps? (only on staging or cherry-pick, not production) SHOULD_BUILD_NATIVE: ${{ github.ref == 'refs/heads/staging' || fromJSON(steps.isCherryPick.outputs.IS_CHERRY_PICK) }} + VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} + IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} steps: - name: Checkout # v6 @@ -75,6 +77,14 @@ jobs: isCherryPick, ); + - name: Get Android native version + id: getAndroidVersion + run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT" + + - name: Get iOS native version + id: getIOSVersion + run: echo "IOS_VERSION=$(echo '${{ steps.getAppVersion.outputs.VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT" + # Note: we're updating the checklist before running the deploys and assuming that it will succeed on at least one platform deployChecklist: name: Create or update deploy checklist @@ -269,12 +279,12 @@ jobs: - name: Upload Android app to Google Play run: bundle exec fastlane android ${{ vars.ANDROID_UPLOAD_COMMAND }} env: - VERSION: ${{ needs.androidBuild.outputs.VERSION_CODE }} + VERSION: ${{ needs.prep.outputs.VERSION_CODE }} ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME }} androidSubmit: name: Submit Android for production rollout - needs: [prep, androidBuild] + needs: [prep] runs-on: blacksmith-2vcpu-ubuntu-2404 if: ${{ github.ref == 'refs/heads/production' }} steps: @@ -319,7 +329,7 @@ jobs: - name: Submit production build for Google Play review and a slow rollout run: bundle exec fastlane android upload_google_play_production_hybrid_rollout env: - VERSION: ${{ needs.androidBuild.outputs.VERSION_CODE }} + VERSION: ${{ needs.prep.outputs.VERSION_CODE }} - name: Warn deployers if Android production deploy failed if: ${{ failure() }} @@ -610,7 +620,7 @@ jobs: iosSubmit: name: Submit iOS for production rollout - needs: [prep, iosBuild] + needs: [prep] runs-on: macos-15-xlarge if: ${{ github.ref == 'refs/heads/production' }} env: @@ -648,7 +658,7 @@ jobs: - name: Submit production build for App Store review and a slow rollout run: bundle exec fastlane ios submit_hybrid_for_rollout env: - VERSION: ${{ needs.iosBuild.outputs.IOS_VERSION }} + VERSION: ${{ needs.prep.outputs.IOS_VERSION }} APPLE_ID: ${{ vars.APPLE_ID }} - name: Warn deployers if iOS production deploy failed @@ -663,7 +673,7 @@ jobs: attachments: [{ color: "#DB4545", pretext: ``, - text: `💥 iOS HybridApp production deploy failed. Please ${{ needs.iosBuild.outputs.IOS_VERSION }} in the . 💥`, + text: `💥 iOS HybridApp production deploy failed. Please ${{ needs.prep.outputs.IOS_VERSION }} in the . 💥`, }] } env: From 6f03e63fe24f4a22360b2704a31b46e35090b247 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 14:33:52 -0800 Subject: [PATCH 24/52] Gate submit jobs on upload completion for cherry-pick deploys On cherry-pick production deploys, SHOULD_BUILD_NATIVE is true so upload jobs run. The submit jobs must wait for uploads to finish before promoting builds. Add androidUploadGooglePlay and iosUploadTestflight to the submit jobs' needs, using always() && !cancelled() so submits still run when uploads are skipped (non-cherry-pick production). Also guard against upload failures to prevent promoting stale builds. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 00fcc92516b2..8da6d9f6834d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -284,9 +284,9 @@ jobs: androidSubmit: name: Submit Android for production rollout - needs: [prep] + needs: [prep, androidUploadGooglePlay] runs-on: blacksmith-2vcpu-ubuntu-2404 - if: ${{ github.ref == 'refs/heads/production' }} + if: ${{ always() && !cancelled() && github.ref == 'refs/heads/production' && needs.androidUploadGooglePlay.result != 'failure' }} steps: - name: Checkout # v6 @@ -620,9 +620,9 @@ jobs: iosSubmit: name: Submit iOS for production rollout - needs: [prep] + needs: [prep, iosUploadTestflight] runs-on: macos-15-xlarge - if: ${{ github.ref == 'refs/heads/production' }} + if: ${{ always() && !cancelled() && github.ref == 'refs/heads/production' && needs.iosUploadTestflight.result != 'failure' }} env: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer steps: From e6400f2e3d4c79fb07de5258d20e2248c075d8f4 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 15:45:03 -0800 Subject: [PATCH 25/52] Propagate build/upload failures in production deploy status On production cherry-picks where native builds run, if the upload job fails the submit job is skipped, but checkDeploymentSuccess was only looking at the submit result. This masked the real failure as "skipped". Now check for upstream build/upload failures before using the submit result for both Android and iOS platform status reporting. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8da6d9f6834d..34b09936e07c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -865,18 +865,28 @@ jobs: - name: Determine platform results id: platformResults run: | - # Android: use submit result for production, upload result for staging + # Android: use submit result for production, upload result for staging. + # On production cherry-picks, propagate build/upload failures that caused submit to be skipped. if [ "${{ github.ref }}" == "refs/heads/production" ]; then - echo "ANDROID_RESULT=${{ needs.androidSubmit.result }}" >> "$GITHUB_OUTPUT" + if [ "${{ needs.androidBuild.result }}" == "failure" ] || [ "${{ needs.androidUploadGooglePlay.result }}" == "failure" ]; then + echo "ANDROID_RESULT=failure" >> "$GITHUB_OUTPUT" + else + echo "ANDROID_RESULT=${{ needs.androidSubmit.result }}" >> "$GITHUB_OUTPUT" + fi elif [ "${{ needs.androidBuild.result }}" == "failure" ]; then echo "ANDROID_RESULT=failure" >> "$GITHUB_OUTPUT" else echo "ANDROID_RESULT=${{ needs.androidUploadGooglePlay.result }}" >> "$GITHUB_OUTPUT" fi - # iOS: use submit result for production, upload result for staging + # iOS: use submit result for production, upload result for staging. + # On production cherry-picks, propagate build/upload failures that caused submit to be skipped. if [ "${{ github.ref }}" == "refs/heads/production" ]; then - echo "IOS_RESULT=${{ needs.iosSubmit.result }}" >> "$GITHUB_OUTPUT" + if [ "${{ needs.iosBuild.result }}" == "failure" ] || [ "${{ needs.iosUploadTestflight.result }}" == "failure" ]; then + echo "IOS_RESULT=failure" >> "$GITHUB_OUTPUT" + else + echo "IOS_RESULT=${{ needs.iosSubmit.result }}" >> "$GITHUB_OUTPUT" + fi elif [ "${{ needs.iosBuild.result }}" == "failure" ]; then echo "IOS_RESULT=failure" >> "$GITHUB_OUTPUT" else From d71dd9ddf7e5894ffd74bee9c333fcc16ff1a6c0 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 15:54:09 -0800 Subject: [PATCH 26/52] Quote $(pwd) in find commands to fix shellcheck SC2046 Co-authored-by: Cursor --- .github/workflows/deploy.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 34b09936e07c..aef489504dc5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -253,7 +253,7 @@ jobs: path: ./ - name: Set aabPath for Fastlane - run: echo "aabPath=$(find $(pwd) -name '*.aab' | head -1)" >> "$GITHUB_ENV" + run: echo "aabPath=$(find "$(pwd)" -name '*.aab' | head -1)" >> "$GITHUB_ENV" - name: Setup Ruby # v1.229.0 @@ -366,7 +366,7 @@ jobs: - name: Find APK path id: find-apk - run: echo "APK_PATH=$(find $(pwd) -name '*.apk' | head -1)" >> "$GITHUB_OUTPUT" + run: echo "APK_PATH=$(find "$(pwd)" -name '*.apk' | head -1)" >> "$GITHUB_OUTPUT" - name: Upload Android build to BrowserStack if: ${{ fromJSON(env.IS_APP_REPO) }} @@ -406,7 +406,7 @@ jobs: - name: Find APK path id: find-apk - run: echo "APK_PATH=$(find $(pwd) -name '*.apk' | head -1)" >> "$GITHUB_OUTPUT" + run: echo "APK_PATH=$(find "$(pwd)" -name '*.apk' | head -1)" >> "$GITHUB_OUTPUT" - name: Upload Android build to Applause if: ${{ fromJSON(env.IS_APP_REPO) }} @@ -555,8 +555,8 @@ jobs: - name: Set artifact paths for Fastlane run: | - echo "ipaPath=$(find $(pwd) -name '*.ipa' | head -1)" >> "$GITHUB_ENV" - echo "dsymPath=$(find $(pwd) -name '*.dSYM.zip' | head -1)" >> "$GITHUB_ENV" + echo "ipaPath=$(find "$(pwd)" -name '*.ipa' | head -1)" >> "$GITHUB_ENV" + echo "dsymPath=$(find "$(pwd)" -name '*.dSYM.zip' | head -1)" >> "$GITHUB_ENV" - name: Setup Ruby # v1.229.0 @@ -696,7 +696,7 @@ jobs: - name: Find IPA path id: find-ipa - run: echo "IPA_PATH=$(find $(pwd) -name '*.ipa' | head -1)" >> "$GITHUB_OUTPUT" + run: echo "IPA_PATH=$(find "$(pwd)" -name '*.ipa' | head -1)" >> "$GITHUB_OUTPUT" - name: Upload iOS build to BrowserStack if: ${{ fromJSON(env.IS_APP_REPO) }} @@ -736,7 +736,7 @@ jobs: - name: Find IPA path id: find-ipa - run: echo "IPA_PATH=$(find $(pwd) -name '*.ipa' | head -1)" >> "$GITHUB_OUTPUT" + run: echo "IPA_PATH=$(find "$(pwd)" -name '*.ipa' | head -1)" >> "$GITHUB_OUTPUT" - name: Upload iOS build to Applause if: ${{ fromJSON(env.IS_APP_REPO) }} From 4bd09e51ed7346625afd4eeab31dfa31b8fe5c8f Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 16:07:53 -0800 Subject: [PATCH 27/52] Block submit jobs when build fails on cherry-pick deploys When androidBuild/iosBuild fails on a cherry-pick production deploy, the upload jobs are skipped (not failed). The submit jobs' if-guards only checked for upload failure, allowing submit to run against a stale build. Add build jobs to submit needs and check their results too. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index aef489504dc5..2f0d13e2dca9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -284,9 +284,9 @@ jobs: androidSubmit: name: Submit Android for production rollout - needs: [prep, androidUploadGooglePlay] + needs: [prep, androidBuild, androidUploadGooglePlay] runs-on: blacksmith-2vcpu-ubuntu-2404 - if: ${{ always() && !cancelled() && github.ref == 'refs/heads/production' && needs.androidUploadGooglePlay.result != 'failure' }} + if: ${{ always() && !cancelled() && github.ref == 'refs/heads/production' && needs.androidBuild.result != 'failure' && needs.androidUploadGooglePlay.result != 'failure' }} steps: - name: Checkout # v6 @@ -620,9 +620,9 @@ jobs: iosSubmit: name: Submit iOS for production rollout - needs: [prep, iosUploadTestflight] + needs: [prep, iosBuild, iosUploadTestflight] runs-on: macos-15-xlarge - if: ${{ always() && !cancelled() && github.ref == 'refs/heads/production' && needs.iosUploadTestflight.result != 'failure' }} + if: ${{ always() && !cancelled() && github.ref == 'refs/heads/production' && needs.iosBuild.result != 'failure' && needs.iosUploadTestflight.result != 'failure' }} env: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer steps: From 0b43e7f0d7e2df6c6b72b4a887dcdb16c60ca3f1 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 16:29:18 -0800 Subject: [PATCH 28/52] Upload AAB artifact before APK generation steps Move the AAB and sourcemap artifact uploads to run immediately after the Android build, before bundletool install and APK generation. This ensures the Google Play upload job can still run even if the testing- only APK generation steps fail transiently. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2f0d13e2dca9..6dadcf2be61a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -185,6 +185,20 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + - name: Upload Android build artifact + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 + with: + name: androidBuild-artifact + path: ${{ env.aabPath }} + + - name: Upload Android sourcemap artifact + # v6 + uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 + with: + name: android-sourcemap-artifact + path: /home/runner/work/App/App/Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map + - name: Install bundletool run: | readonly BUNDLETOOL_VERSION="1.18.1" @@ -221,20 +235,6 @@ jobs: name: android-apk-artifact path: Expensify.apk - - name: Upload Android build artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: androidBuild-artifact - path: ${{ env.aabPath }} - - - name: Upload Android sourcemap artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: android-sourcemap-artifact - path: /home/runner/work/App/App/Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map - androidUploadGooglePlay: name: Upload Android to Google Play needs: [prep, androidBuild] From 591bdbd07892d3c7b8fc6e91a096b1487178ae99 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 16:46:59 -0800 Subject: [PATCH 29/52] Restrict Applause uploads to staging non-cherry-pick deploys Restore the original guard condition that limits Applause uploads to staging non-cherry-pick deploys only. The SHOULD_BUILD_NATIVE flag was too broad and included production cherry-picks. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6dadcf2be61a..933473675d6a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -378,7 +378,7 @@ jobs: name: Upload Android to Applause needs: [prep, androidBuild] runs-on: blacksmith-2vcpu-ubuntu-2404 - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} + if: ${{ github.repository == 'Expensify/App' && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} continue-on-error: true steps: - name: Download Android APK artifact @@ -708,7 +708,7 @@ jobs: name: Upload iOS to Applause needs: [prep, iosBuild] runs-on: blacksmith-2vcpu-ubuntu-2404 - if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} + if: ${{ github.repository == 'Expensify/App' && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} continue-on-error: true steps: - name: Download iOS build artifact From 6e936f8ea95434deb9c3d34fb618ba52833847a8 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 17:28:40 -0800 Subject: [PATCH 30/52] Add setup-gradle to androidBuild job in deploy workflow Cache the Gradle user home (~/.gradle) between CI runs to eliminate repeated Maven dependency resolution, which averages ~11 minutes per build. Uses the same pinned SHA as buildAdHoc.yml and publishReactNativeAndroidArtifacts.yml. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 933473675d6a..72558fef201c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -133,6 +133,10 @@ jobs: distribution: oracle java-version: ${{ steps.get-java-version.outputs.version }} + - name: Setup Gradle + # v4 + uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 + - name: Setup Ruby # v1.229.0 uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 From a82e7cc12fe7e49024b3d02ce1ec886ae3aae810 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 17:31:58 -0800 Subject: [PATCH 31/52] Retry Android build after clearing Gradle cache on failure If the Android build fails, clear ~/.gradle/caches and retry. A corrupted or stale Gradle cache is a common cause of transient build failures, especially with setup-gradle caching across runs. Applied to both deploy.yml (Fastlane build) and buildAdHoc.yml (Rock Remote Build). Co-authored-by: Cursor --- .github/workflows/buildAdHoc.yml | 31 ++++++++++++++++++++++++++++++- .github/workflows/deploy.yml | 7 ++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/workflows/buildAdHoc.yml b/.github/workflows/buildAdHoc.yml index 9fd322c10a9a..4fd7be0a37b9 100644 --- a/.github/workflows/buildAdHoc.yml +++ b/.github/workflows/buildAdHoc.yml @@ -204,6 +204,36 @@ jobs: - name: Rock Remote Build - Android id: rock-remote-build-android + continue-on-error: true + uses: callstackincubator/android@4cedf4d9b5c167452c96fe67233577e0fde9a025 + env: + GITHUB_TOKEN: ${{ github.token }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + IS_HYBRID_APP: true + with: + variant: 'Adhoc' + sign: true + re-sign: true + ad-hoc: true + keystore-file: './upload-key.keystore' + keystore-store-file: 'upload-key.keystore' + keystore-store-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} + keystore-key-alias: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} + keystore-key-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} + keystore-path: '../tools/buildtools/upload-key.keystore' + comment-bot: false + rock-build-extra-params: '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' + custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }} + validate-elf-alignment: false + + - name: Clear Gradle cache + if: steps.rock-remote-build-android.outcome == 'failure' + run: | + echo "::warning::Android build failed, clearing Gradle caches and retrying…" + rm -rf ~/.gradle/caches + + - name: Rock Remote Build - Android (retry) + if: steps.rock-remote-build-android.outcome == 'failure' uses: callstackincubator/android@4cedf4d9b5c167452c96fe67233577e0fde9a025 env: GITHUB_TOKEN: ${{ github.token }} @@ -219,7 +249,6 @@ jobs: keystore-store-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} keystore-key-alias: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} keystore-key-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} - # Specify the path (relative to the Android source directory) where the keystore should be placed. keystore-path: '../tools/buildtools/upload-key.keystore' comment-bot: false rock-build-extra-params: '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 72558fef201c..f31b92718352 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -179,7 +179,12 @@ jobs: run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT" - name: Build Android app - run: bundle exec fastlane android build_hybrid + run: | + if ! bundle exec fastlane android build_hybrid; then + echo "::warning::Android build failed, clearing Gradle caches and retrying…" + rm -rf ~/.gradle/caches + bundle exec fastlane android build_hybrid + fi env: ANDROID_UPLOAD_KEYSTORE_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} ANDROID_UPLOAD_KEYSTORE_ALIAS: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} From bccb127c32741cea8e0bc43a7d4df5c3721a8cd5 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 18:17:19 -0800 Subject: [PATCH 32/52] Remove stale buildAndroid.yml and disabled e2ePerformanceTests.yml buildAndroid.yml was a stale standalone NewDot callable workflow with no HybridApp support, only called by e2ePerformanceTests.yml which has been disabled since August 2025. Both are removed. Co-authored-by: Cursor --- .github/workflows/buildAndroid.yml | 212 -------------- .github/workflows/e2ePerformanceTests.yml | 319 ---------------------- 2 files changed, 531 deletions(-) delete mode 100644 .github/workflows/buildAndroid.yml delete mode 100644 .github/workflows/e2ePerformanceTests.yml diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml deleted file mode 100644 index 1d66c9f35338..000000000000 --- a/.github/workflows/buildAndroid.yml +++ /dev/null @@ -1,212 +0,0 @@ -name: Build Android app - -on: - workflow_call: - inputs: - type: - description: 'What type of build to run. Must be one of ["release", "adhoc", "e2e", "e2eDelta"]' - type: string - required: true - ref: - description: Git ref to checkout and build - type: string - required: true - artifact-prefix: - description: 'The prefix for build artifact names. This is useful if you need to call multiple builds from the same workflow' - type: string - required: false - default: '' - pull_request_number: - description: The pull request number associated with this build, if relevant. - type: string - required: false - - outputs: - AAB_FILE_NAME: - description: Name of the AAB file produced by this workflow. - value: ${{ jobs.build.outputs.AAB_FILE_NAME }} - APK_FILE_NAME: - description: Name of the APK file produced by this workflow. - value: ${{ jobs.build.outputs.APK_FILE_NAME }} - APK_ARTIFACT_NAME: - description: Name of the APK artifact. - value: ${{ jobs.build.outputs.APK_ARTIFACT_NAME }} - - workflow_dispatch: - inputs: - type: - description: What type of build do you want to run? - required: true - type: choice - options: - - release - - adhoc - - e2e - - e2eDelta - ref: - description: Git ref to checkout and build - required: true - type: string - - pull_request_number: - description: The pull request number associated with this build, if relevant. - type: number - required: false - -jobs: - build: - name: Build Android app - runs-on: blacksmith-16vcpu-ubuntu-2404 - outputs: - AAB_FILE_NAME: ${{ steps.build.outputs.AAB_FILE_NAME }} - APK_FILE_NAME: ${{ steps.build.outputs.APK_FILE_NAME }} - APK_ARTIFACT_NAME: ${{ steps.build.outputs.APK_ARTIFACT_NAME }} - - steps: - - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - with: - ref: ${{ inputs.ref }} - - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - - name: Setup Node - uses: ./.github/actions/composite/setupNode - - - name: Get Java version - id: get-java-version - uses: ./.github/actions/composite/getJavaVersion - - - name: Setup Java - # v4 - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 - with: - distribution: oracle - java-version: ${{ steps.get-java-version.outputs.version }} - - - name: Setup Gradle - # v4 - uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 - - - name: Setup Ruby - # v1.229.0 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 - with: - bundler-cache: true - - - name: Setup 1Password CLI and certificates - uses: Expensify/GitHub-Actions/setup-certificate-1p@main - with: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - SHOULD_LOAD_SSL_CERTIFICATES: 'false' - - - name: Load files from 1Password - working-directory: android/app - env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - run: op read "op://${{ vars.OP_VAULT }}/New Expensify my-upload-key.keystore/my-upload-key.keystore" --force --out-file ./my-upload-key.keystore - - - name: Get package version - id: getPackageVersion - run: echo "VERSION=$(jq -r .version < package.json)" >> "$GITHUB_OUTPUT" - - - name: Get Android native version - id: getAndroidVersion - run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT" - - - name: Setup DotEnv - if: ${{ inputs.type != 'release' }} - run: | - if [ '${{ inputs.type }}' == 'adhoc' ]; then - cp .env.staging .env.adhoc - sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc - echo "PULL_REQUEST_NUMBER=${{ inputs.pull_request_number }}" >> .env.adhoc - else - envFile='' - if [ '${{ inputs.type }}' == 'e2e' ]; then - envFile='tests/e2e/.env.e2e' - else - envFile=tests/e2e/.env.e2edelta - fi - { - echo "EXPENSIFY_PARTNER_NAME=${{ secrets.EXPENSIFY_PARTNER_NAME }}" - echo "EXPENSIFY_PARTNER_PASSWORD=${{ secrets.EXPENSIFY_PARTNER_PASSWORD }}" - echo "EXPENSIFY_PARTNER_USER_ID=${{ secrets.EXPENSIFY_PARTNER_USER_ID }}" - echo "EXPENSIFY_PARTNER_USER_SECRET=${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }}" - echo "EXPENSIFY_PARTNER_PASSWORD_EMAIL=${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }}" - } >> "$envFile" - fi - - - name: Build Android app (retryable) - # v3 - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 - id: build - env: - MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }} - MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} - with: - retry_on: error - retry_wait_seconds: 60 - timeout_minutes: 60 - max_attempts: 3 - command: | - lane='' - case '${{ inputs.type }}' in - 'release') - lane='build';; - 'adhoc') - lane='build_adhoc';; - 'e2e') - lane='build_e2e';; - 'e2eDelta') - lane='build_e2eDelta';; - esac - bundle exec fastlane android "$lane" - - # Refresh environment variables from GITHUB_ENV that are updated when running fastlane - # shellcheck disable=SC1090 - source "$GITHUB_ENV" - - SHOULD_UPLOAD_SOURCEMAPS='false' - if [ -f ./android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map ]; then - SHOULD_UPLOAD_SOURCEMAPS='true' - fi - - { - # aabPath and apkPath are environment varibles set within the Fastfile - echo "AAB_PATH=$aabPath" - echo "AAB_FILE_NAME=$(basename "$aabPath")" - echo "APK_PATH=$apkPath" - echo "APK_FILE_NAME=$(basename "$apkPath")" - echo "SHOULD_UPLOAD_SOURCEMAPS=$SHOULD_UPLOAD_SOURCEMAPS" - echo "APK_ARTIFACT_NAME=${{ inputs.artifact-prefix }}android-apk-artifact" >> "$GITHUB_OUTPUT" - } >> "$GITHUB_OUTPUT" - - - name: Upload Android AAB artifact - if: ${{ steps.build.outputs.AAB_PATH != '' }} - continue-on-error: true - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: ${{ inputs.artifact-prefix }}android-aab-artifact - path: ${{ steps.build.outputs.AAB_PATH }} - - - name: Upload Android APK artifact - if: ${{ steps.build.outputs.APK_PATH != '' }} - continue-on-error: true - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: ${{ steps.build.outputs.APK_ARTIFACT_NAME }} - path: ${{ steps.build.outputs.APK_PATH }} - - - name: Upload Android sourcemaps artifact - if: ${{ steps.build.outputs.SHOULD_UPLOAD_SOURCEMAPS == 'true' }} - continue-on-error: true - # v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: ${{ inputs.artifact-prefix }}android-sourcemaps-artifact - path: ./android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml deleted file mode 100644 index cd2c22f9b62d..000000000000 --- a/.github/workflows/e2ePerformanceTests.yml +++ /dev/null @@ -1,319 +0,0 @@ -name: E2E Performance Tests - -on: - workflow_call: - inputs: - PR_NUMBER: - description: A PR number to run performance tests against. If the PR is already merged, the merge commit will be used. If not, the PR will be merged locally before running the performance tests. - type: string - required: true - - workflow_dispatch: - inputs: - PR_NUMBER: - description: A PR number to run performance tests against. If the PR is already merged, the merge commit will be used. If not, the PR will be merged locally before running the performance tests. - type: string - required: true - -concurrency: - group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-e2e - cancel-in-progress: true - -jobs: - prep: - runs-on: blacksmith-2vcpu-ubuntu-2404 - name: Find the baseline and delta refs, and check for an existing build artifact for that commit - outputs: - BASELINE_REF: ${{ steps.getBaselineRef.outputs.BASELINE_REF }} - DELTA_REF: ${{ steps.getDeltaRef.outputs.DELTA_REF }} - IS_PR_MERGED: ${{ steps.getPullRequestDetails.outputs.IS_MERGED }} - steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 - with: - fetch-depth: 0 # Fetches the entire history - - - name: Determine "baseline ref" (prev merge commit) - id: getBaselineRef - run: | - # Get the name of the current branch - current_branch=$(git rev-parse --abbrev-ref HEAD) - - if [ "$current_branch" = "main" ]; then - # On the main branch, find the previous merge commit - previous_merge=$(git rev-list --merges HEAD~1 | head -n 1) - else - # On a feature branch, find the common ancestor of the current branch and main - git fetch origin main:main - previous_merge=$(git merge-base HEAD main) - fi - echo "$previous_merge" - echo "BASELINE_REF=$previous_merge" >> "$GITHUB_OUTPUT" - - - name: Get pull request details - id: getPullRequestDetails - uses: ./.github/actions/javascript/getPullRequestDetails - with: - GITHUB_TOKEN: ${{ github.token }} - PULL_REQUEST_NUMBER: ${{ inputs.PR_NUMBER }} - USER: ${{ github.actor }} - - - name: Determine "delta ref" - id: getDeltaRef - run: | - if [ '${{ steps.getPullRequestDetails.outputs.IS_MERGED }}' == 'true' ]; then - echo "DELTA_REF=${{ steps.getPullRequestDetails.outputs.MERGE_COMMIT_SHA }}" >> "$GITHUB_OUTPUT" - else - # Set dummy git credentials - git config --global user.email "test@test.com" - git config --global user.name "Test" - - # Fetch head_ref of unmerged PR - git fetch origin ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} --no-tags --depth=1 - - # Merge pull request locally and get merge commit sha - git merge --allow-unrelated-histories -X ours --no-commit ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - git checkout ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - - # Create and push a branch so it can be checked out in another runner - git checkout -b e2eDelta-${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - git push origin e2eDelta-${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - echo "DELTA_REF=e2eDelta-${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }}" >> "$GITHUB_OUTPUT" - fi - - buildBaseline: - name: Build apk from baseline - uses: ./.github/workflows/buildAndroid.yml - needs: prep - secrets: inherit - with: - type: e2e - ref: ${{ needs.prep.outputs.BASELINE_REF }} - artifact-prefix: baseline-${{ needs.prep.outputs.BASELINE_REF }} - - buildDelta: - name: Build apk from delta ref - uses: ./.github/workflows/buildAndroid.yml - needs: prep - secrets: inherit - with: - type: e2eDelta - ref: ${{ needs.prep.outputs.DELTA_REF }} - artifact-prefix: delta-${{ needs.prep.outputs.DELTA_REF }} - - runTestsInAWS: - runs-on: blacksmith-2vcpu-ubuntu-2404 - needs: [prep, buildBaseline, buildDelta] - if: ${{ always() }} - name: Run E2E tests in AWS device farm - steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 - with: - # The OS_BOTIFY_COMMIT_TOKEN is a personal access token tied to osbotify (we need a PAT to access the artifact API) - token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - - - name: Setup Node - uses: ./.github/actions/composite/setupNode - - - name: Make zip directory for everything to send to AWS Device Farm - run: mkdir zip - - - name: Download baseline APK - # v4 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e - id: downloadBaselineAPK - with: - name: ${{ needs.buildBaseline.outputs.APK_ARTIFACT_NAME }} - path: zip - - # The downloaded artifact will be a file named "app-e2e-release.apk" so we have to rename it - - name: Rename baseline APK - run: mv "${{ steps.downloadBaselineAPK.outputs.download-path }}/app-e2e-release.apk" "${{ steps.downloadBaselineAPK.outputs.download-path }}/app-e2eRelease.apk" - - - name: Download delta APK - # v4 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e - id: downloadDeltaAPK - with: - name: ${{ needs.buildDelta.outputs.APK_ARTIFACT_NAME }} - path: zip - - - name: Rename delta APK - run: mv "${{ steps.downloadDeltaAPK.outputs.download-path }}/app-e2edelta-release.apk" "${{ steps.downloadDeltaAPK.outputs.download-path }}/app-e2edeltaRelease.apk" - - - name: Compile test runner to be executable in a nodeJS environment - run: npm run e2e-test-runner-build - - - name: Copy e2e code into zip folder - run: cp tests/e2e/dist/index.js zip/testRunner.ts - - - name: Copy profiler binaries into zip folder - run: cp -r node_modules/@perf-profiler/android/cpp-profiler/bin zip/bin - - - name: Zip everything in the zip directory up - run: zip -qr App.zip ./zip - - - name: Configure AWS Credentials - # v4 - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-west-2 - - - name: Schedule AWS Device Farm test run on main branch - # v3.5.0 - uses: Wandalen/wretry.action@96605825122f94418f745bb54775d049581bbc6b - id: schedule-awsdf-main - with: - action: realm/aws-devicefarm/test-application@7b9a91236c456c97e28d384c9e476035d5ea686b - with: | - name: App E2E Performance Regression Tests - project_arn: ${{ secrets.AWS_PROJECT_ARN }} - device_pool_arn: ${{ secrets.AWS_DEVICE_POOL_ARN }} - app_file: zip/app-e2eRelease.apk - app_type: ANDROID_APP - test_type: APPIUM_NODE - test_package_file: App.zip - test_package_type: APPIUM_NODE_TEST_PACKAGE - test_spec_file: tests/e2e/TestSpec.yml - test_spec_type: APPIUM_NODE_TEST_SPEC - remote_src: false - file_artifacts: | - Customer Artifacts.zip - Test spec output.txt - log_artifacts: debug.log - cleanup: true - timeout: 7200 - - - name: Print logs if run failed - if: failure() - run: | - echo ${{ steps.schedule-awsdf-main.outputs.data }} - unzip "Customer Artifacts.zip" -d mainResults - cat "./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/logcat.txt" || true - cat ./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/debug.log || true - cat "./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/Test spec output.txt" || true - - - name: Announce failed workflow in Slack - if: failure() - # v3 - uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e - with: - status: custom - custom_payload: | - { - channel: '#e2e-announce', - attachments: [{ - color: 'danger', - text: `💥 ${process.env.AS_REPO} E2E Test run failed on workflow 💥`, - }] - } - env: - GITHUB_TOKEN: ${{ github.token }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - - - name: Unzip AWS Device Farm results - if: always() - run: unzip "Customer Artifacts.zip" - - - name: Print AWS Device Farm run results - if: always() - run: | - if ls "./Host_Machine_Files/\$WORKING_DIRECTORY"/output2.md 1> /dev/null 2>&1; then - # Print all the split files - for file in "./Host_Machine_Files/\$WORKING_DIRECTORY/output"*; do - if [ -f "$file" ]; then - cat "$file" - fi - done - else - cat "./Host_Machine_Files/\$WORKING_DIRECTORY/output1.md" - fi - - - name: Check if test failed, if so post the results and add the DeployBlocker label - id: checkIfRegressionDetected - run: | - if grep -q '🔴' "./Host_Machine_Files/\$WORKING_DIRECTORY/output1.md"; then - # Create an output to the GH action that the test failed: - echo "performanceRegressionDetected=true" >> "$GITHUB_OUTPUT" - - gh pr edit ${{ inputs.PR_NUMBER }} --add-label DeployBlockerCash - - # Check if there are any split files - if ls "./Host_Machine_Files/\$WORKING_DIRECTORY"/output2.md 1> /dev/null 2>&1; then - # Post each split file as a separate comment - for file in "./Host_Machine_Files/\$WORKING_DIRECTORY/output"*; do - if [ -f "$file" ]; then - gh pr comment ${{ inputs.PR_NUMBER }} -F "$file" - fi - done - else - gh pr comment ${{ inputs.PR_NUMBER }} -F "./Host_Machine_Files/\$WORKING_DIRECTORY/output1.md" - fi - - gh pr comment ${{ inputs.PR_NUMBER }} -b "@Expensify/mobile-deployers 📣 Please look into this performance regression as it's a deploy blocker." - else - echo "performanceRegressionDetected=false" >> "$GITHUB_OUTPUT" - echo '✅ no performance regression detected' - fi - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Check if test has skipped tests - id: checkIfSkippedTestsDetected - run: | - if grep -q '⚠️' "./Host_Machine_Files/\$WORKING_DIRECTORY/output1.md"; then - # Create an output to the GH action that the tests were skipped: - echo "skippedTestsDetected=true" >> "$GITHUB_OUTPUT" - else - echo "skippedTestsDetected=false" >> "$GITHUB_OUTPUT" - echo '✅ no skipped tests detected' - fi - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: 'Announce skipped tests in Slack' - if: ${{ steps.checkIfSkippedTestsDetected.outputs.skippedTestsDetected == 'true' }} - # v3 - uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e - with: - status: custom - custom_payload: | - { - channel: '#e2e-announce', - attachments: [{ - color: 'danger', - text: `⚠️ ${process.env.AS_REPO} Some of E2E tests were skipped on workflow ⚠️`, - }] - } - env: - GITHUB_TOKEN: ${{ github.token }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - - - name: 'Announce regression in Slack' - if: ${{ steps.checkIfRegressionDetected.outputs.performanceRegressionDetected == 'true' }} - # v3 - uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e - with: - status: custom - custom_payload: | - { - channel: '#quality', - attachments: [{ - color: 'danger', - text: `🔴 Performance regression detected in PR ${{ inputs.PR_NUMBER }}\nDetected in workflow.`, - }] - } - env: - GITHUB_TOKEN: ${{ github.token }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - - cleanupDeltaRef: - needs: [prep, runTestsInAWS] - if: ${{ always() && needs.prep.outputs.IS_PR_MERGED != 'true' }} - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 - - - name: Delete temporary merge branch created for delta ref - run: git push -d origin ${{ needs.prep.outputs.DELTA_REF }} From fedd763cfcdc8367d0f44799feefae7180abad87 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 18:18:25 -0800 Subject: [PATCH 33/52] Add buildAndroid.yml callable workflow using Rock Remote Build Unified callable workflow for Android HybridApp builds that serves both deploy.yml (Release variant) and test builds (Adhoc variant). Uses Rock Remote Build with retry + Gradle cache clear for durability. Produces AAB, APK (via bundletool), and sourcemap artifacts with configurable naming via artifact-prefix. Co-authored-by: Cursor --- .github/workflows/buildAndroid.yml | 250 +++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 .github/workflows/buildAndroid.yml diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml new file mode 100644 index 000000000000..9e6bed068671 --- /dev/null +++ b/.github/workflows/buildAndroid.yml @@ -0,0 +1,250 @@ +name: Build Android HybridApp + +on: + workflow_call: + inputs: + ref: + description: Git ref to checkout and build + type: string + required: true + variant: + description: "'Release' or 'Adhoc'" + type: string + required: true + mobile-expensify-ref: + description: Mobile-Expensify ref to checkout (empty to use submodule at HEAD) + type: string + default: '' + pull-request-number: + description: Pull request number associated with this build + type: string + default: '' + artifact-prefix: + description: Prefix for build artifact names + type: string + default: '' + upload-sourcemaps: + description: Whether to upload sourcemap artifacts + type: boolean + default: true + + outputs: + VERSION_CODE: + description: Android version code from the build + value: ${{ jobs.build.outputs.VERSION_CODE }} + ROCK_ARTIFACT_URL: + description: URL to download the ad-hoc build artifact (adhoc only) + value: ${{ jobs.build.outputs.ROCK_ARTIFACT_URL }} + +jobs: + build: + name: Build Android HybridApp + runs-on: blacksmith-32vcpu-ubuntu-2404 + env: + PULL_REQUEST_NUMBER: ${{ inputs.pull-request-number }} + outputs: + VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} + ROCK_ARTIFACT_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }} + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + submodules: true + ref: ${{ inputs.ref }} + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Checkout Mobile-Expensify to specified branch or commit + if: ${{ inputs.mobile-expensify-ref != '' }} + run: | + cd Mobile-Expensify + git fetch origin ${{ inputs.mobile-expensify-ref }} + git checkout ${{ inputs.mobile-expensify-ref }} + + - name: Configure MapBox SDK + run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + + - name: Setup Node + id: setup-node + uses: ./.github/actions/composite/setupNode + with: + IS_HYBRID_BUILD: 'true' + + - name: Run grunt build + run: | + cd Mobile-Expensify + npm run grunt:build:shared + + - name: Create .env.adhoc file based on staging + if: ${{ inputs.variant == 'Adhoc' }} + run: | + cp .env.staging .env.adhoc + sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + + - name: Inject CI data into JS bundle + if: ${{ inputs.variant == 'Adhoc' && inputs.pull-request-number != '' }} + run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER" + + - name: Get Java version + id: get-java-version + uses: ./.github/actions/composite/getJavaVersion + + - name: Setup Java + # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 + with: + distribution: oracle + java-version: ${{ steps.get-java-version.outputs.version }} + + - name: Setup Gradle + # v4 + uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 + + - name: Setup 1Password CLI and certificates + uses: Expensify/GitHub-Actions/setup-certificate-1p@main + with: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SHOULD_LOAD_SSL_CERTIFICATES: 'false' + + - name: Load files from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json + op read "op://${{ vars.OP_VAULT }}/upload-key.keystore/upload-key.keystore" --force --out-file ./upload-key.keystore + op read "op://${{ vars.OP_VAULT }}/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json + cp ./upload-key.keystore Mobile-Expensify/Android + + - name: Load Android upload keystore credentials from 1Password + id: load-credentials + # v3 + uses: 1password/load-secrets-action@8d0d610af187e78a2772c2d18d627f4c52d3fbfb + with: + export-env: false + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + ANDROID_UPLOAD_KEYSTORE_PASSWORD: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_PASSWORD + ANDROID_UPLOAD_KEYSTORE_ALIAS: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_ALIAS + ANDROID_UPLOAD_KEY_PASSWORD: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEY_PASSWORD + + - name: Get Android native version + id: getAndroidVersion + run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT" + + - name: Configure AWS Credentials + # v6 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Rock Remote Build - Android + id: rock-remote-build-android + continue-on-error: true + uses: callstackincubator/android@4cedf4d9b5c167452c96fe67233577e0fde9a025 + env: + GITHUB_TOKEN: ${{ github.token }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + IS_HYBRID_APP: true + with: + variant: ${{ inputs.variant }} + sign: true + re-sign: true + ad-hoc: ${{ inputs.variant == 'Adhoc' }} + keystore-file: './upload-key.keystore' + keystore-store-file: 'upload-key.keystore' + keystore-store-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} + keystore-key-alias: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} + keystore-key-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} + keystore-path: '../tools/buildtools/upload-key.keystore' + comment-bot: false + rock-build-extra-params: ${{ inputs.variant == 'Adhoc' && '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' || '--extra-params "--profile"' }} + validate-elf-alignment: false + + - name: Clear Gradle cache + if: steps.rock-remote-build-android.outcome == 'failure' + run: | + echo "::warning::Android build failed, clearing Gradle caches and retrying…" + rm -rf ~/.gradle/caches + + - name: Rock Remote Build - Android (retry) + if: steps.rock-remote-build-android.outcome == 'failure' + uses: callstackincubator/android@4cedf4d9b5c167452c96fe67233577e0fde9a025 + env: + GITHUB_TOKEN: ${{ github.token }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + IS_HYBRID_APP: true + with: + variant: ${{ inputs.variant }} + sign: true + re-sign: true + ad-hoc: ${{ inputs.variant == 'Adhoc' }} + keystore-file: './upload-key.keystore' + keystore-store-file: 'upload-key.keystore' + keystore-store-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} + keystore-key-alias: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} + keystore-key-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} + keystore-path: '../tools/buildtools/upload-key.keystore' + comment-bot: false + rock-build-extra-params: ${{ inputs.variant == 'Adhoc' && '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' || '--extra-params "--profile"' }} + validate-elf-alignment: false + + - name: Upload Gradle profile report + if: always() + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: ${{ inputs.artifact-prefix }}gradle-profile-report + path: Mobile-Expensify/Android/build/reports/profile/ + if-no-files-found: ignore + + - name: Set artifact URL output + id: set-artifact-url + if: ${{ inputs.variant == 'Adhoc' }} + run: echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT" + + - name: Find and upload AAB artifact + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: ${{ inputs.artifact-prefix }}androidBuild-artifact + path: Mobile-Expensify/Android/app/build/outputs/bundle/release/*.aab + + - name: Upload Android sourcemap artifact + if: ${{ inputs.upload-sourcemaps }} + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: ${{ inputs.artifact-prefix }}android-sourcemap-artifact + path: Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map + + - name: Install bundletool + run: | + readonly BUNDLETOOL_VERSION="1.18.1" + readonly BUNDLETOOL_URL="https://github.com/google/bundletool/releases/download/${BUNDLETOOL_VERSION}/bundletool-all-${BUNDLETOOL_VERSION}.jar" + curl -L -o bundletool.jar "$BUNDLETOOL_URL" + readonly EXPECTED_SHA="675786493983787ffa11550bdb7c0715679a44e1643f3ff980a529e9c822595c" + SHA="$(sha256sum bundletool.jar | cut -d ' ' -f1)" + if [[ "$SHA" != "$EXPECTED_SHA" ]]; then + echo "SHA mismatch: expected $EXPECTED_SHA but got $SHA" + exit 1 + fi + + - name: Generate APK from AAB + run: | + AAB_PATH=$(find Mobile-Expensify/Android/app/build/outputs/bundle -name '*.aab' | head -1) + java -jar bundletool.jar build-apks --bundle="$AAB_PATH" --output=Expensify.apks \ + --mode=universal \ + --ks=upload-key.keystore \ + --ks-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} \ + --ks-key-alias=${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} \ + --key-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} + unzip -p Expensify.apks universal.apk > Expensify.apk + + - name: Upload Android APK build artifact + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: ${{ inputs.artifact-prefix }}android-apk-artifact + path: Expensify.apk From 14424d234b0230e9056e6d1cb7d7f1ee86ccf1b2 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 18:22:11 -0800 Subject: [PATCH 34/52] Add buildIOS.yml callable workflow using Rock Remote Build Unified callable workflow for iOS HybridApp builds that serves both deploy.yml (Release variant with app-store export) and test builds (Adhoc variant with ad-hoc export). Handles variant-specific provisioning profiles, ExportOptions.plist, and scheme/configuration via conditional steps. Includes CocoaPods caching and installation. Co-authored-by: Cursor --- .github/workflows/buildIOS.yml | 250 +++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 .github/workflows/buildIOS.yml diff --git a/.github/workflows/buildIOS.yml b/.github/workflows/buildIOS.yml new file mode 100644 index 000000000000..192605fc6c5d --- /dev/null +++ b/.github/workflows/buildIOS.yml @@ -0,0 +1,250 @@ +name: Build iOS HybridApp + +on: + workflow_call: + inputs: + ref: + description: Git ref to checkout and build + type: string + required: true + variant: + description: "'Release' or 'Adhoc'" + type: string + required: true + mobile-expensify-ref: + description: Mobile-Expensify ref to checkout (empty to use submodule at HEAD) + type: string + default: '' + pull-request-number: + description: Pull request number associated with this build + type: string + default: '' + artifact-prefix: + description: Prefix for build artifact names + type: string + default: '' + upload-sourcemaps: + description: Whether to upload sourcemap artifacts + type: boolean + default: true + + outputs: + IOS_VERSION: + description: iOS version string from the build + value: ${{ jobs.build.outputs.IOS_VERSION }} + ROCK_ARTIFACT_URL: + description: URL to download the ad-hoc build artifact (adhoc only) + value: ${{ jobs.build.outputs.ROCK_ARTIFACT_URL }} + +jobs: + build: + name: Build iOS HybridApp + runs-on: macos-15-xlarge + env: + DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer + PULL_REQUEST_NUMBER: ${{ inputs.pull-request-number }} + outputs: + IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} + ROCK_ARTIFACT_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }} + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + submodules: true + ref: ${{ inputs.ref }} + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Checkout Mobile-Expensify to specified branch or commit + if: ${{ inputs.mobile-expensify-ref != '' }} + run: | + cd Mobile-Expensify + git fetch origin ${{ inputs.mobile-expensify-ref }} + git checkout ${{ inputs.mobile-expensify-ref }} + + - name: Configure MapBox SDK + run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + + - name: Setup Node + id: setup-node + uses: ./.github/actions/composite/setupNode + with: + IS_HYBRID_BUILD: 'true' + + - name: Create .env.adhoc file based on staging + if: ${{ inputs.variant == 'Adhoc' }} + run: | + cp .env.staging .env.adhoc + sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + + - name: Inject CI data into JS bundle + if: ${{ inputs.variant == 'Adhoc' && inputs.pull-request-number != '' }} + run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER" + + - name: Setup Ruby + # v1.229.0 + uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 + with: + bundler-cache: true + + - name: Install New Expensify Gems + run: bundle install + + - name: Cache Pod dependencies + # v5.0.1 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb + id: pods-cache + with: + path: Mobile-Expensify/iOS/Pods + key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock', 'firebase.json') }} + + - name: Compare Podfile.lock and Manifest.lock + id: compare-podfile-and-manifest + run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') == hashFiles('Mobile-Expensify/iOS/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT" + + - name: Install cocoapods + uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 + if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true' + with: + timeout_minutes: 10 + max_attempts: 5 + command: npm run pod-install + + - name: Setup 1Password CLI and certificates + uses: Expensify/GitHub-Actions/setup-certificate-1p@main + with: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SHOULD_LOAD_SSL_CERTIFICATES: 'false' + + - name: Load Release provisioning profiles from 1Password + if: ${{ inputs.variant == 'Release' }} + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json + op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore/${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }} + op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Share_Extension/${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }} + op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Notification_Service/${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }} + op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 + + - name: Load AdHoc provisioning profiles from 1Password + if: ${{ inputs.variant == 'Adhoc' }} + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc/OldApp_AdHoc.mobileprovision" --force --out-file ./OldApp_AdHoc.mobileprovision + op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Share_Extension/OldApp_AdHoc_Share_Extension.mobileprovision" --force --out-file ./OldApp_AdHoc_Share_Extension.mobileprovision + op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Notification_Service/OldApp_AdHoc_Notification_Service.mobileprovision" --force --out-file ./OldApp_AdHoc_Notification_Service.mobileprovision + op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 + + - name: Create ExportOptions.plist for Release + if: ${{ inputs.variant == 'Release' }} + run: | + cat > Mobile-Expensify/iOS/ExportOptions.plist << EOF + + + + + method + app-store + provisioningProfiles + + ${{ vars.APPLE_ID }} + ${{ vars.APPLE_STORE_PROVISIONING_PROFILE_NAME }} + ${{ vars.APPLE_ID }}.SmartScanExtension + ${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_NAME }} + ${{ vars.APPLE_ID }}.NotificationServiceExtension + ${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME }} + + + + EOF + + - name: Create ExportOptions.plist for AdHoc + if: ${{ inputs.variant == 'Adhoc' }} + run: | + cat > Mobile-Expensify/iOS/ExportOptions.plist << 'EOF' + + + + + method + ad-hoc + provisioningProfiles + + com.expensify.expensifylite.adhoc + (OldApp) AdHoc + com.expensify.expensifylite.adhoc.SmartScanExtension + (OldApp) AdHoc: Share Extension + com.expensify.expensifylite.adhoc.NotificationServiceExtension + (OldApp) AdHoc: Notification Service + + + + EOF + + - name: Get iOS native version + id: getIOSVersion + run: echo "IOS_VERSION=$(jq -r .version < package.json | tr '-' '.')" >> "$GITHUB_OUTPUT" + + - name: Configure AWS Credentials + # v6 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Prepare provisioning profiles JSON + id: prepare-profiles + run: | + if [ "${{ inputs.variant }}" == "Release" ]; then + echo 'PROFILES=[{"name":"${{ vars.APPLE_STORE_PROVISIONING_PROFILE_NAME }}","file":"./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}"},{"name":"${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_NAME }}","file":"./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}"},{"name":"${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME }}","file":"./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}"}]' >> "$GITHUB_OUTPUT" + else + echo 'PROFILES=[{"name":"(OldApp) AdHoc","file":"./OldApp_AdHoc.mobileprovision"},{"name":"(OldApp) AdHoc: Share Extension","file":"./OldApp_AdHoc_Share_Extension.mobileprovision"},{"name":"(OldApp) AdHoc: Notification Service","file":"./OldApp_AdHoc_Notification_Service.mobileprovision"}]' >> "$GITHUB_OUTPUT" + fi + + - name: Rock Remote Build - iOS + id: rock-remote-build-ios + uses: callstackincubator/ios@dd30f7e53eee2ea6a59509793d0a30fbb5c91216 + env: + GITHUB_TOKEN: ${{ github.token }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + IS_HYBRID_APP: true + with: + destination: device + re-sign: true + ad-hoc: ${{ inputs.variant == 'Adhoc' }} + scheme: ${{ inputs.variant == 'Release' && 'Expensify' || 'Expensify AdHoc' }} + configuration: ${{ inputs.variant == 'Release' && 'Release' || 'AdHoc' }} + certificate-file: './Certificates.p12' + provisioning-profiles: ${{ steps.prepare-profiles.outputs.PROFILES }} + comment-bot: false + + - name: Set artifact URL output + id: set-artifact-url + if: ${{ inputs.variant == 'Adhoc' }} + run: echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT" + + - name: Find and upload IPA artifact + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: ${{ inputs.artifact-prefix }}iosBuild-artifact + path: .rock/cache/ios/export/*.ipa + + - name: Find and upload dSYM artifact + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: ${{ inputs.artifact-prefix }}ios-dsym-artifact + path: .rock/cache/ios/export/*.dSYM.zip + if-no-files-found: warn + + - name: Upload iOS sourcemap artifact + if: ${{ inputs.upload-sourcemaps }} + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: ${{ inputs.artifact-prefix }}ios-sourcemap-artifact + path: Mobile-Expensify/main.jsbundle.map From f8b11e672f14a6018dcdb78231c541c83085d716 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 18:22:50 -0800 Subject: [PATCH 35/52] Add buildWeb.yml callable workflow Unified callable workflow for web builds that serves deploy.yml (production/staging with S3 deploy, Cloudflare purge, and storybook) and test builds (adhoc with S3 deploy for PR testing). Build command varies by environment input. Co-authored-by: Cursor --- .github/workflows/buildWeb.yml | 160 +++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 .github/workflows/buildWeb.yml diff --git a/.github/workflows/buildWeb.yml b/.github/workflows/buildWeb.yml new file mode 100644 index 000000000000..6bd3444b36d0 --- /dev/null +++ b/.github/workflows/buildWeb.yml @@ -0,0 +1,160 @@ +name: Build and deploy Web + +on: + workflow_call: + inputs: + ref: + description: Git ref to checkout and build + type: string + required: true + environment: + description: "'production', 'staging', or 'adhoc'" + type: string + required: true + pull-request-number: + description: Pull request number (used for adhoc builds) + type: string + default: '' + upload-artifacts: + description: Whether to upload compressed build artifacts (.tar.gz, .zip) + type: boolean + default: true + deploy-to-s3: + description: Whether to deploy to S3 + type: boolean + default: false + build-storybook: + description: Whether to build storybook docs + type: boolean + default: false + + outputs: + S3_URL: + description: The S3 URL of the deployed web app (adhoc only) + value: ${{ jobs.build.outputs.S3_URL }} + +jobs: + build: + name: Build and deploy Web + runs-on: blacksmith-32vcpu-ubuntu-2404 + env: + PULL_REQUEST_NUMBER: ${{ inputs.pull-request-number }} + outputs: + S3_URL: ${{ steps.set-s3-url.outputs.S3_URL }} + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ inputs.ref }} + + - name: Create .env.adhoc file based on staging + if: ${{ inputs.environment == 'adhoc' }} + run: | + cp .env.staging .env.adhoc + sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + + - name: Inject CI data into JS bundle + if: ${{ inputs.environment == 'adhoc' && inputs.pull-request-number != '' }} + run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER" + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + + - name: Setup Cloudflare CLI + if: ${{ inputs.environment != 'adhoc' }} + run: pip3 install cloudflare==2.19.0 + + - name: Configure AWS Credentials + if: ${{ inputs.deploy-to-s3 }} + # v6 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Build web + run: | + if [ "${{ inputs.environment }}" == "production" ]; then + npm run build + elif [ "${{ inputs.environment }}" == "staging" ]; then + npm run build-staging + else + npm run build-adhoc + fi + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + + - name: Build storybook docs + if: ${{ inputs.build-storybook }} + continue-on-error: true + run: | + if [ "${{ inputs.environment }}" == "production" ]; then + npm run storybook-build + else + npm run storybook-build-staging + fi + + - name: Deploy to S3 (production/staging) + if: ${{ inputs.deploy-to-s3 && inputs.environment != 'adhoc' }} + run: | + aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist ${{ env.S3_BUCKET }}/ + aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association + aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association ${{ env.S3_BUCKET }}/apple-app-site-association + env: + S3_BUCKET: s3://${{ inputs.environment == 'staging' && 'staging-' || '' }}${{ vars.PRODUCTION_S3_BUCKET }} + + - name: Deploy to S3 (adhoc) + if: ${{ inputs.deploy-to-s3 && inputs.environment == 'adhoc' && inputs.pull-request-number != '' }} + run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://ad-hoc-expensify-cash/web/"$PULL_REQUEST_NUMBER" + + - name: Set S3 URL output + id: set-s3-url + if: ${{ inputs.environment == 'adhoc' && inputs.pull-request-number != '' }} + run: echo "S3_URL=https://${{ inputs.pull-request-number }}.pr-testing.expensify.com" >> "$GITHUB_OUTPUT" + + - name: Purge Cloudflare cache + if: ${{ inputs.environment != 'adhoc' }} + run: | + /home/runner/.local/bin/cli4 --verbose --delete hosts=["$HOST"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache + env: + CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} + HOST: ${{ inputs.environment == 'production' && vars.WEB_PRODUCTION_HOST || vars.WEB_STAGING_HOST }} + + - name: Verify deploy + if: ${{ inputs.environment != 'adhoc' }} + run: | + APP_VERSION=$(jq -r .version < package.json) + ./.github/scripts/verifyDeploy.sh "$HOST" "$APP_VERSION" + env: + HOST: ${{ inputs.environment == 'production' && vars.WEB_PRODUCTION_HOST || vars.WEB_STAGING_HOST }} + + - name: Upload web sourcemaps artifact + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: web-sourcemaps-artifact + path: ./dist/merged-source-map.js.map + + - name: Compress web build .tar.gz and .zip + if: ${{ inputs.upload-artifacts }} + run: | + tar -czvf webBuild.tar.gz dist + zip -r webBuild.zip dist + + - name: Upload .tar.gz web build artifact + if: ${{ inputs.upload-artifacts }} + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: web-build-tar-gz-artifact + path: ./webBuild.tar.gz + + - name: Upload .zip web build artifact + if: ${{ inputs.upload-artifacts }} + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: web-build-zip-artifact + path: ./webBuild.zip From c332d3f4361c9838b2f7a69a5f3a5dadfdce589b Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 18:24:49 -0800 Subject: [PATCH 36/52] Update deploy.yml to use callable build workflows Replace inline androidBuild, iosBuild, and web jobs with calls to buildAndroid.yml, buildIOS.yml, and buildWeb.yml callable workflows. Downstream jobs (upload, submit, BrowserStack, Applause) remain unchanged as they consume the same GitHub artifact names. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 358 ++--------------------------------- 1 file changed, 21 insertions(+), 337 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f31b92718352..00ebf2e36b1e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -95,154 +95,13 @@ jobs: androidBuild: name: Build Android HybridApp - needs: prep - runs-on: blacksmith-32vcpu-ubuntu-2404 + needs: [prep] if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} - outputs: - VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} - steps: - - name: Checkout App and Mobile-Expensify repo - # v6 - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - with: - submodules: true - token: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - - name: Setup Node - id: setup-node - uses: ./.github/actions/composite/setupNode - with: - IS_HYBRID_BUILD: 'true' - - - name: Run grunt build - run: | - cd Mobile-Expensify - npm run grunt:build:shared - - - name: Get Java version - id: get-java-version - uses: ./.github/actions/composite/getJavaVersion - - - name: Setup Java - # v4 - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 - with: - distribution: oracle - java-version: ${{ steps.get-java-version.outputs.version }} - - - name: Setup Gradle - # v4 - uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 - - - name: Setup Ruby - # v1.229.0 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 - with: - bundler-cache: true - - - name: Install New Expensify Gems - run: bundle install - - - name: Setup 1Password CLI and certificates - uses: Expensify/GitHub-Actions/setup-certificate-1p@main - with: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - SHOULD_LOAD_SSL_CERTIFICATES: 'false' - - - name: Load files from 1Password - env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - run: | - op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json - op read "op://${{ vars.OP_VAULT }}/upload-key.keystore/upload-key.keystore" --force --out-file ./upload-key.keystore - - # Copy the keystore to the Android directory for Fullstory - cp ./upload-key.keystore Mobile-Expensify/Android - - - name: Load Android upload keystore credentials from 1Password - id: load-credentials - # v2 - uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 - with: - export-env: false - env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - ANDROID_UPLOAD_KEYSTORE_PASSWORD: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_PASSWORD - ANDROID_UPLOAD_KEYSTORE_ALIAS: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_ALIAS - ANDROID_UPLOAD_KEY_PASSWORD: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEY_PASSWORD - - - name: Get Android native version - id: getAndroidVersion - run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT" - - - name: Build Android app - run: | - if ! bundle exec fastlane android build_hybrid; then - echo "::warning::Android build failed, clearing Gradle caches and retrying…" - rm -rf ~/.gradle/caches - bundle exec fastlane android build_hybrid - fi - env: - ANDROID_UPLOAD_KEYSTORE_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} - ANDROID_UPLOAD_KEYSTORE_ALIAS: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} - ANDROID_UPLOAD_KEY_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} - ANDROID_BUILD_TYPE: ${{ vars.ANDROID_BUILD_TYPE }} - GITHUB_ACTOR: ${{ github.actor }} - GITHUB_TOKEN: ${{ github.token }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - - name: Upload Android build artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: androidBuild-artifact - path: ${{ env.aabPath }} - - - name: Upload Android sourcemap artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: android-sourcemap-artifact - path: /home/runner/work/App/App/Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map - - - name: Install bundletool - run: | - readonly BUNDLETOOL_VERSION="1.18.1" - readonly BUNDLETOOL_URL="https://github.com/google/bundletool/releases/download/${BUNDLETOOL_VERSION}/bundletool-all-${BUNDLETOOL_VERSION}.jar" - - # Download jar from GitHub Release - curl -L -o bundletool.jar "$BUNDLETOOL_URL" - - # Validate checksum - readonly EXPECTED_SHA="675786493983787ffa11550bdb7c0715679a44e1643f3ff980a529e9c822595c" - SHA="$(sha256sum bundletool.jar | cut -d ' ' -f1)" - if [[ "$SHA" != "$EXPECTED_SHA" ]]; then - echo "SHA mismatch: expected $EXPECTED_SHA but got $ACTUAL_SHA" - exit 1 - fi - - - name: Generate APK from AAB - run: | - # Generate apks using bundletool - java -jar bundletool.jar build-apks --bundle=${{ env.aabPath }} --output=Expensify.apks \ - --mode=universal \ - --ks=upload-key.keystore \ - --ks-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} \ - --ks-key-alias=${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} \ - --key-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} - - # Unzip just the universal apk - unzip -p Expensify.apks universal.apk > Expensify.apk - - - name: Upload Android APK build artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: android-apk-artifact - path: Expensify.apk + uses: ./.github/workflows/buildAndroid.yml + with: + ref: ${{ github.ref }} + variant: Release + secrets: inherit androidUploadGooglePlay: name: Upload Android to Google Play @@ -427,111 +286,13 @@ jobs: iosBuild: name: Build iOS HybridApp - needs: prep - runs-on: macos-15-xlarge + needs: [prep] if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} - outputs: - IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} - env: - DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer - steps: - - name: Checkout - # v6 - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - with: - submodules: true - token: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - - name: Setup Node - id: setup-node - uses: ./.github/actions/composite/setupNode - with: - IS_HYBRID_BUILD: 'true' - - - name: Setup Ruby - # v1.229.0 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 - with: - bundler-cache: true - - - name: Install New Expensify Gems - run: bundle install - - - name: Cache Pod dependencies - # v5.0.1 - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb - id: pods-cache - with: - path: Mobile-Expensify/iOS/Pods - key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock', 'firebase.json') }} - - - name: Compare Podfile.lock and Manifest.lock - id: compare-podfile-and-manifest - run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') == hashFiles('Mobile-Expensify/iOS/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT" - - - name: Install cocoapods - uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 - if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true' - with: - timeout_minutes: 10 - max_attempts: 5 - command: npm run pod-install - - - name: Setup 1Password CLI and certificates - uses: Expensify/GitHub-Actions/setup-certificate-1p@main - with: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - SHOULD_LOAD_SSL_CERTIFICATES: 'false' - - - name: Load files from 1Password - env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - run: | - op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json - op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore/${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }} - op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Share_Extension/${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }} - op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Notification_Service/${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }} - op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 - - - name: Get iOS native version - id: getIOSVersion - run: echo "IOS_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT" - - - name: Build iOS HybridApp - run: bundle exec fastlane ios build_hybrid - env: - APPLE_ID: ${{ vars.APPLE_ID }} - APPLE_STORE_PROVISIONING_PROFILE_FILE: ${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }} - APPLE_STORE_PROVISIONING_PROFILE_NAME: ${{ vars.APPLE_STORE_PROVISIONING_PROFILE_NAME }} - APPLE_SHARE_PROVISIONING_PROFILE_FILE: ${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }} - APPLE_SHARE_PROVISIONING_PROFILE_NAME: ${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_NAME }} - APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE: ${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }} - APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME: ${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - - name: Upload iOS build artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: iosBuild-artifact - path: ${{ env.ipaPath }} - - - name: Upload iOS dSYM artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: ios-dsym-artifact - path: ${{ env.dsymPath }} - - - name: Upload iOS sourcemap artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: ios-sourcemap-artifact - path: /Users/runner/work/App/App/Mobile-Expensify/main.jsbundle.map + uses: ./.github/workflows/buildIOS.yml + with: + ref: ${{ github.ref }} + variant: Release + secrets: inherit iosUploadTestflight: name: Upload iOS to TestFlight @@ -757,92 +518,15 @@ jobs: web: name: Build and deploy Web - needs: prep - runs-on: blacksmith-32vcpu-ubuntu-2404 - steps: - - name: Checkout - # v6 - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - - - name: Setup Node - uses: ./.github/actions/composite/setupNode - - - name: Setup Cloudflare CLI - run: pip3 install cloudflare==2.19.0 - - - name: Configure AWS Credentials - # v4 - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Build web - run: | - if [[ '${{ github.ref }}' == 'refs/heads/production' ]]; then - npm run build - else - npm run build-staging - fi - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - - name: Build storybook docs - continue-on-error: true - run: | - if [[ ${{ github.ref }} == 'refs/heads/production' ]]; then - npm run storybook-build - else - npm run storybook-build-staging - fi - - - name: Deploy to S3 - run: | - aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist ${{ env.S3_URL }}/ - aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_URL }}/.well-known/apple-app-site-association ${{ env.S3_URL }}/.well-known/apple-app-site-association - aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_URL }}/.well-known/apple-app-site-association ${{env.S3_URL }}/apple-app-site-association - env: - S3_URL: s3://${{ github.ref == 'refs/heads/staging' && 'staging-' || '' }}${{ vars.PRODUCTION_S3_BUCKET }} - - - name: Purge Cloudflare cache - run: | - /home/runner/.local/bin/cli4 --verbose --delete hosts=["$HOST"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache - env: - CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} - HOST: ${{ github.ref == 'refs/heads/production' && vars.WEB_PRODUCTION_HOST || vars.WEB_STAGING_HOST }} - - - name: Verify deploy - run: | - ./.github/scripts/verifyDeploy.sh "$HOST" "${{ needs.prep.outputs.APP_VERSION }}" - env: - HOST: ${{ github.ref == 'refs/heads/production' && vars.WEB_PRODUCTION_HOST || vars.WEB_STAGING_HOST }} - - - name: Upload web sourcemaps artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: web-sourcemaps-artifact - path: ./dist/merged-source-map.js.map - - - name: Compress web build .tar.gz and .zip - run: | - tar -czvf webBuild.tar.gz dist - zip -r webBuild.zip dist - - - name: Upload .tar.gz web build artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: web-build-tar-gz-artifact - path: ./webBuild.tar.gz - - - name: Upload .zip web build artifact - # v6 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: web-build-zip-artifact - path: ./webBuild.zip + needs: [prep] + uses: ./.github/workflows/buildWeb.yml + with: + ref: ${{ github.ref }} + environment: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} + deploy-to-s3: true + build-storybook: true + upload-artifacts: true + secrets: inherit postSlackMessageOnFailure: name: Post a Slack message when any platform fails to build or deploy From fbe534d6db8d5a217b9cad6747210ecd67a368e8 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 18:26:01 -0800 Subject: [PATCH 37/52] Update testBuild.yml and testBuildOnPush.yml to use callable build workflows Replace the single buildApps job (which called buildAdHoc.yml) with direct calls to buildAndroid.yml, buildIOS.yml, and buildWeb.yml. Move postGitHubCommentBuildStarted, postGithubComment, and buildSummary jobs from buildAdHoc.yml into both test build workflows. Co-authored-by: Cursor --- .github/workflows/testBuild.yml | 196 ++++++++++++++++++++++++-- .github/workflows/testBuildOnPush.yml | 146 +++++++++++++++++-- 2 files changed, 324 insertions(+), 18 deletions(-) diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index ba03181ff3d1..af3e3d54a5d7 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -146,16 +146,192 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - buildApps: + postGitHubCommentBuildStarted: + name: Post build started comment + if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' || needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR != '' }} + needs: [prep, getMobileExpensifyPR] + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - name: Add build start comment to Expensify/App PR + if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' }} + # v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + with: + github-token: ${{ github.token }} + script: | + const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ needs.prep.outputs.APP_PR_NUMBER }}, + body: `🚧 @${{ github.actor }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).` + }); + + - name: Add build start comment to Expensify/Mobile-Expensify PR + if: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR != '' }} + # v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + with: + github-token: ${{ secrets.OS_BOTIFY_TOKEN }} + script: | + const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: 'Mobile-Expensify', + issue_number: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }}, + body: `🚧 @${{ github.actor }} has triggered a test Expensify/Mobile-Expensify build. You can view the [workflow run here](${workflowURL}).` + }); + + buildAndroid: + name: Build Android + needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef] + if: ${{ inputs.ANDROID }} + uses: ./.github/workflows/buildAndroid.yml + with: + ref: ${{ needs.prep.outputs.APP_REF }} + variant: Adhoc + mobile-expensify-ref: ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }} + pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} + secrets: inherit + + buildIOS: + name: Build iOS needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef] - uses: ./.github/workflows/buildAdHoc.yml + if: ${{ inputs.IOS }} + uses: ./.github/workflows/buildIOS.yml with: - APP_REF: ${{ needs.prep.outputs.APP_REF }} - APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }} - MOBILE_EXPENSIFY_REF: ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }} - MOBILE_EXPENSIFY_PR: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }} - BUILD_WEB: ${{ inputs.WEB && 'true' || 'false' }} - BUILD_IOS: ${{ inputs.IOS && 'true' || 'false' }} - BUILD_ANDROID: ${{ inputs.ANDROID && 'true' || 'false' }} - TRIGGER_ACTOR: ${{ github.actor }} + ref: ${{ needs.prep.outputs.APP_REF }} + variant: Adhoc + mobile-expensify-ref: ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }} + pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} secrets: inherit + + buildWeb: + name: Build Web + needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef] + if: ${{ inputs.WEB && needs.prep.outputs.APP_PR_NUMBER != '' }} + uses: ./.github/workflows/buildWeb.yml + with: + ref: ${{ needs.prep.outputs.APP_REF }} + environment: adhoc + pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} + upload-artifacts: false + deploy-to-s3: true + build-storybook: false + secrets: inherit + + postGithubComment: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: ${{ always() && (needs.prep.outputs.APP_PR_NUMBER != '' || needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR != '') }} + name: Post a GitHub comment with app download links for testing + needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef, buildWeb, buildAndroid, buildIOS] + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ needs.prep.outputs.APP_REF }} + + - name: Download Artifact + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + + - name: Publish links to apps for download on Expensify/App PR + if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' }} + uses: ./.github/actions/javascript/postTestBuildComment + with: + REPO: App + APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }} + MOBILE_EXPENSIFY_PR_NUMBER: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }} + GITHUB_TOKEN: ${{ github.token }} + ANDROID: ${{ needs.buildAndroid.result }} + IOS: ${{ needs.buildIOS.result }} + WEB: ${{ needs.buildWeb.result }} + ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }} + IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }} + WEB_LINK: https://${{ needs.prep.outputs.APP_PR_NUMBER }}.pr-testing.expensify.com + + - name: Publish links to apps for download on Expensify/Mobile-Expensify PR + if: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR != '' }} + uses: ./.github/actions/javascript/postTestBuildComment + with: + REPO: Mobile-Expensify + MOBILE_EXPENSIFY_PR_NUMBER: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }} + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + ANDROID: ${{ needs.buildAndroid.result }} + IOS: ${{ needs.buildIOS.result }} + ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }} + IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }} + + buildSummary: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: ${{ always() && (needs.prep.outputs.APP_PR_NUMBER != '' || needs.buildAndroid.outputs.ROCK_ARTIFACT_URL != '' || needs.buildIOS.outputs.ROCK_ARTIFACT_URL != '') }} + name: Build Summary + needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef, buildWeb, buildAndroid, buildIOS] + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ needs.prep.outputs.APP_REF }} + + - name: Get Mobile-Expensify ref + id: getMobileExpensifySHA + run: | + if [ -n "${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }}" ]; then + echo "SHA=${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }}" >> "$GITHUB_OUTPUT" + elif [ -d "Mobile-Expensify" ]; then + SUBMODULE_SHA=$(git ls-tree HEAD Mobile-Expensify | awk '{print $3}') + echo "SHA=$SUBMODULE_SHA" >> "$GITHUB_OUTPUT" + fi + + - name: Create build summary + # v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + with: + github-token: ${{ github.token }} + script: | + const prNumber = '${{ needs.prep.outputs.APP_PR_NUMBER }}'; + const webLink = prNumber && '${{ needs.buildWeb.result }}' === 'success' ? `https://${prNumber}.pr-testing.expensify.com` : ''; + const androidLink = '${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }}' || ''; + const iosLink = '${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }}' || ''; + const mobileExpensifySHA = '${{ steps.getMobileExpensifySHA.outputs.SHA }}' || ''; + + const webStatus = webLink ? '✅ Success' : '${{ needs.buildWeb.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + const androidStatus = androidLink ? '✅ Success' : '${{ needs.buildAndroid.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + const iosStatus = iosLink ? '✅ Success' : '${{ needs.buildIOS.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + + const summary = core.summary + .addTable([ + [{data: 'Platform', header: true}, {data: 'Status', header: true}, {data: 'Download', header: true}], + [ + 'Web', + webStatus, + webLink ? `${webLink}` : '-' + ], + [ + 'Android', + androidStatus, + androidLink ? `${androidLink}` : '-' + ], + [ + 'iOS', + iosStatus, + iosLink ? `${iosLink}` : '-' + ], + ]); + + if (mobileExpensifySHA) { + const mobileExpensifyUrl = `https://github.com/Expensify/Mobile-Expensify/commit/${mobileExpensifySHA}`; + const label = '${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }}' ? 'Mobile-Expensify SHA (Custom)' : 'Mobile-Expensify Submodule SHA'; + summary.addRaw(`\n**${label}:** [${mobileExpensifySHA}](${mobileExpensifyUrl})`); + } + + if (prNumber) { + const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`; + summary.addRaw(`\n**PR Link:** ${prUrl}`); + } else { + summary.addRaw(`\n**PR Link:** No PR associated with this commit.`); + } + + summary.write(); diff --git a/.github/workflows/testBuildOnPush.yml b/.github/workflows/testBuildOnPush.yml index 5bc2bbe15063..1b714a4ea48c 100644 --- a/.github/workflows/testBuildOnPush.yml +++ b/.github/workflows/testBuildOnPush.yml @@ -80,14 +80,144 @@ jobs: core.setFailed(`Commit pushed by non-OSBotify actor. No merged pull request found for commit ${context.sha}`); } - buildApps: + postGitHubCommentBuildStarted: + name: Post build started comment + if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' }} needs: [prep] - uses: ./.github/workflows/buildAdHoc.yml + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - name: Add build start comment to Expensify/App PR + # v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + with: + github-token: ${{ github.token }} + script: | + const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ needs.prep.outputs.APP_PR_NUMBER }}, + body: `🚧 @${{ github.actor }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).` + }); + + buildAndroid: + name: Build Android + needs: [prep] + if: ${{ fromJSON(needs.prep.outputs.BUILD_MOBILE) }} + uses: ./.github/workflows/buildAndroid.yml + with: + ref: ${{ needs.prep.outputs.APP_REF }} + variant: Adhoc + pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} + secrets: inherit + + buildIOS: + name: Build iOS + needs: [prep] + if: ${{ fromJSON(needs.prep.outputs.BUILD_MOBILE) }} + uses: ./.github/workflows/buildIOS.yml + with: + ref: ${{ needs.prep.outputs.APP_REF }} + variant: Adhoc + pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} + secrets: inherit + + buildWeb: + name: Build Web + needs: [prep] + if: ${{ fromJSON(needs.prep.outputs.BUILD_WEB) && needs.prep.outputs.APP_PR_NUMBER != '' }} + uses: ./.github/workflows/buildWeb.yml with: - APP_REF: ${{ needs.prep.outputs.APP_REF }} - APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }} - BUILD_WEB: ${{ needs.prep.outputs.BUILD_WEB }} - BUILD_IOS: ${{ needs.prep.outputs.BUILD_MOBILE }} - BUILD_ANDROID: ${{ needs.prep.outputs.BUILD_MOBILE }} - TRIGGER_ACTOR: ${{ github.actor }} + ref: ${{ needs.prep.outputs.APP_REF }} + environment: adhoc + pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} + upload-artifacts: false + deploy-to-s3: true + build-storybook: false secrets: inherit + + postGithubComment: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: ${{ always() && needs.prep.outputs.APP_PR_NUMBER != '' }} + name: Post a GitHub comment with app download links for testing + needs: [prep, buildWeb, buildAndroid, buildIOS] + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ needs.prep.outputs.APP_REF }} + + - name: Download Artifact + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + + - name: Publish links to apps for download on Expensify/App PR + if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' }} + uses: ./.github/actions/javascript/postTestBuildComment + with: + REPO: App + APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }} + GITHUB_TOKEN: ${{ github.token }} + ANDROID: ${{ needs.buildAndroid.result }} + IOS: ${{ needs.buildIOS.result }} + WEB: ${{ needs.buildWeb.result }} + ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }} + IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }} + WEB_LINK: https://${{ needs.prep.outputs.APP_PR_NUMBER }}.pr-testing.expensify.com + + buildSummary: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: ${{ always() && (needs.prep.outputs.APP_PR_NUMBER != '' || needs.buildAndroid.outputs.ROCK_ARTIFACT_URL != '' || needs.buildIOS.outputs.ROCK_ARTIFACT_URL != '') }} + name: Build Summary + needs: [prep, buildWeb, buildAndroid, buildIOS] + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ needs.prep.outputs.APP_REF }} + + - name: Create build summary + # v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + with: + github-token: ${{ github.token }} + script: | + const prNumber = '${{ needs.prep.outputs.APP_PR_NUMBER }}'; + const webLink = prNumber && '${{ needs.buildWeb.result }}' === 'success' ? `https://${prNumber}.pr-testing.expensify.com` : ''; + const androidLink = '${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }}' || ''; + const iosLink = '${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }}' || ''; + + const webStatus = webLink ? '✅ Success' : '${{ needs.buildWeb.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + const androidStatus = androidLink ? '✅ Success' : '${{ needs.buildAndroid.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + const iosStatus = iosLink ? '✅ Success' : '${{ needs.buildIOS.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + + const summary = core.summary + .addTable([ + [{data: 'Platform', header: true}, {data: 'Status', header: true}, {data: 'Download', header: true}], + [ + 'Web', + webStatus, + webLink ? `${webLink}` : '-' + ], + [ + 'Android', + androidStatus, + androidLink ? `${androidLink}` : '-' + ], + [ + 'iOS', + iosStatus, + iosLink ? `${iosLink}` : '-' + ], + ]); + + if (prNumber) { + const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`; + summary.addRaw(`\n**PR Link:** ${prUrl}`); + } else { + summary.addRaw(`\n**PR Link:** No PR associated with this commit.`); + } + + summary.write(); From 1aa3dadb3e2e6f61aa7eade0481de2bf008f1553 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 19 Feb 2026 18:26:12 -0800 Subject: [PATCH 38/52] Remove buildAdHoc.yml All callers now use the callable build workflows directly (buildAndroid.yml, buildIOS.yml, buildWeb.yml). Co-authored-by: Cursor --- .github/workflows/buildAdHoc.yml | 516 ------------------------------- 1 file changed, 516 deletions(-) delete mode 100644 .github/workflows/buildAdHoc.yml diff --git a/.github/workflows/buildAdHoc.yml b/.github/workflows/buildAdHoc.yml deleted file mode 100644 index 4fd7be0a37b9..000000000000 --- a/.github/workflows/buildAdHoc.yml +++ /dev/null @@ -1,516 +0,0 @@ -name: Build and deploy ad-hoc apps - -on: - workflow_call: - inputs: - APP_REF: - description: Git ref to checkout for building - type: string - required: true - APP_PR_NUMBER: - description: Expensify/App pull request number (empty to skip comments and web deploy) - type: string - default: '' - MOBILE_EXPENSIFY_REF: - description: Mobile-Expensify ref to checkout (empty to use submodule at HEAD) - type: string - default: '' - MOBILE_EXPENSIFY_PR: - description: Mobile-Expensify pull request number (empty to skip ME PR comments) - type: string - default: '' - BUILD_WEB: - description: Whether to build the web app - type: string - default: 'true' - BUILD_IOS: - description: Whether to build the iOS app - type: string - default: 'true' - BUILD_ANDROID: - description: Whether to build the Android app - type: string - default: 'true' - TRIGGER_ACTOR: - description: GitHub username who triggered the build - type: string - required: true - -jobs: - postGitHubCommentBuildStarted: - name: Post build started comment - if: ${{ inputs.APP_PR_NUMBER != '' || inputs.MOBILE_EXPENSIFY_PR != '' }} - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Add build start comment to Expensify/App PR - if: ${{ inputs.APP_PR_NUMBER != '' }} - # v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - with: - github-token: ${{ github.token }} - script: | - const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: ${{ inputs.APP_PR_NUMBER }}, - body: `🚧 @${{ inputs.TRIGGER_ACTOR }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).` - }); - - - name: Add build start comment to Expensify/Mobile-Expensify PR - if: ${{ inputs.MOBILE_EXPENSIFY_PR != '' }} - # v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - with: - github-token: ${{ secrets.OS_BOTIFY_TOKEN }} - script: | - const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: 'Mobile-Expensify', - issue_number: ${{ inputs.MOBILE_EXPENSIFY_PR }}, - body: `🚧 @${{ inputs.TRIGGER_ACTOR }} has triggered a test Expensify/Mobile-Expensify build. You can view the [workflow run here](${workflowURL}).` - }); - - web: - name: Build and deploy Web - if: ${{ inputs.BUILD_WEB == 'true' && inputs.APP_PR_NUMBER != '' }} - runs-on: blacksmith-16vcpu-ubuntu-2404 - env: - PULL_REQUEST_NUMBER: ${{ inputs.APP_PR_NUMBER }} - steps: - - name: Checkout - # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ inputs.APP_REF }} - - - name: Create .env.adhoc file based on staging - run: | - cp .env.staging .env.adhoc - sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc - - - name: Inject CI data into JS bundle - run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER" - - - name: Setup Node - uses: ./.github/actions/composite/setupNode - - - name: Configure AWS Credentials - # v6 - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Build web for testing - run: npm run build-adhoc - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - - name: Deploy to S3 for internal testing - run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://ad-hoc-expensify-cash/web/"$PULL_REQUEST_NUMBER" - - androidHybrid: - name: Build Android HybridApp - if: ${{ inputs.BUILD_ANDROID == 'true' }} - runs-on: blacksmith-32vcpu-ubuntu-2404 - env: - PULL_REQUEST_NUMBER: ${{ inputs.APP_PR_NUMBER }} - outputs: - ROCK_ANDROID_ADHOC_INDEX_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }} - steps: - - name: Checkout - # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - submodules: true - ref: ${{ inputs.APP_REF }} - token: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Checkout Mobile-Expensify to specified branch or commit - if: ${{ inputs.MOBILE_EXPENSIFY_REF != '' }} - run: | - cd Mobile-Expensify - git fetch origin ${{ inputs.MOBILE_EXPENSIFY_REF }} - git checkout ${{ inputs.MOBILE_EXPENSIFY_REF }} - echo "Building from https://github.com/Expensify/Mobile-Expensify/pull/${{ inputs.MOBILE_EXPENSIFY_PR }}" - - - name: Compute custom build identifier - id: computeIdentifier - run: | - APP_SHORT_SHA=$(git rev-parse --short HEAD) - MOBILE_EXPENSIFY_SHORT_SHA=$(cd Mobile-Expensify && git rev-parse --short HEAD) - echo "IDENTIFIER=${APP_SHORT_SHA}-${MOBILE_EXPENSIFY_SHORT_SHA}" >> "$GITHUB_OUTPUT" - - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - - name: Setup Node - id: setup-node - uses: ./.github/actions/composite/setupNode - with: - IS_HYBRID_BUILD: 'true' - - - name: Run grunt build - run: | - cd Mobile-Expensify - npm run grunt:build:shared - - - name: Create .env.adhoc file based on staging - run: | - cp .env.staging .env.adhoc - sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc - - - name: Inject CI data into JS bundle - run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER" - - - name: Setup 1Password CLI and certificates - uses: Expensify/GitHub-Actions/setup-certificate-1p@main - with: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - SHOULD_LOAD_SSL_CERTIFICATES: 'false' - - - name: Load files from 1Password - env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - run: | - op read "op://${{ vars.OP_VAULT }}/upload-key.keystore/upload-key.keystore" --force --out-file ./upload-key.keystore - op read "op://${{ vars.OP_VAULT }}/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json - - # Copy the keystore to the Android directory for Fullstory - cp ./upload-key.keystore Mobile-Expensify/Android - - - name: Load Android upload keystore credentials from 1Password - id: load-credentials - # v3 - uses: 1password/load-secrets-action@8d0d610af187e78a2772c2d18d627f4c52d3fbfb - with: - export-env: false - env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - ANDROID_UPLOAD_KEYSTORE_PASSWORD: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_PASSWORD - ANDROID_UPLOAD_KEYSTORE_ALIAS: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_ALIAS - ANDROID_UPLOAD_KEY_PASSWORD: op://${{ vars.OP_VAULT }}/Repository-Secrets/ANDROID_UPLOAD_KEY_PASSWORD - - - name: Configure AWS Credentials - # v6 - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Rock Remote Build - Android - id: rock-remote-build-android - continue-on-error: true - uses: callstackincubator/android@4cedf4d9b5c167452c96fe67233577e0fde9a025 - env: - GITHUB_TOKEN: ${{ github.token }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - IS_HYBRID_APP: true - with: - variant: 'Adhoc' - sign: true - re-sign: true - ad-hoc: true - keystore-file: './upload-key.keystore' - keystore-store-file: 'upload-key.keystore' - keystore-store-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} - keystore-key-alias: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} - keystore-key-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} - keystore-path: '../tools/buildtools/upload-key.keystore' - comment-bot: false - rock-build-extra-params: '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' - custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }} - validate-elf-alignment: false - - - name: Clear Gradle cache - if: steps.rock-remote-build-android.outcome == 'failure' - run: | - echo "::warning::Android build failed, clearing Gradle caches and retrying…" - rm -rf ~/.gradle/caches - - - name: Rock Remote Build - Android (retry) - if: steps.rock-remote-build-android.outcome == 'failure' - uses: callstackincubator/android@4cedf4d9b5c167452c96fe67233577e0fde9a025 - env: - GITHUB_TOKEN: ${{ github.token }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - IS_HYBRID_APP: true - with: - variant: 'Adhoc' - sign: true - re-sign: true - ad-hoc: true - keystore-file: './upload-key.keystore' - keystore-store-file: 'upload-key.keystore' - keystore-store-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} - keystore-key-alias: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} - keystore-key-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} - keystore-path: '../tools/buildtools/upload-key.keystore' - comment-bot: false - rock-build-extra-params: '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' - custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }} - validate-elf-alignment: false - - - name: Upload Gradle profile report - if: always() - # v6 - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f - with: - name: gradle-profile-report - path: Mobile-Expensify/Android/build/reports/profile/ - if-no-files-found: ignore - - - name: Set artifact URL output - id: set-artifact-url - run: echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT" - - iosHybrid: - name: Build and deploy iOS for testing - if: ${{ inputs.BUILD_IOS == 'true' }} - env: - DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer - PULL_REQUEST_NUMBER: ${{ inputs.APP_PR_NUMBER }} - runs-on: macos-15-xlarge - outputs: - ROCK_IOS_ADHOC_INDEX_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }} - steps: - - name: Checkout - # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - submodules: true - ref: ${{ inputs.APP_REF }} - token: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Checkout Mobile-Expensify to specified branch or commit - if: ${{ inputs.MOBILE_EXPENSIFY_REF != '' }} - run: | - cd Mobile-Expensify - git fetch origin ${{ inputs.MOBILE_EXPENSIFY_REF }} - git checkout ${{ inputs.MOBILE_EXPENSIFY_REF }} - echo "Building from https://github.com/Expensify/Mobile-Expensify/pull/${{ inputs.MOBILE_EXPENSIFY_PR }}" - - - name: Compute custom build identifier - id: computeIdentifier - run: | - APP_SHORT_SHA=$(git rev-parse --short HEAD) - MOBILE_EXPENSIFY_SHORT_SHA=$(cd Mobile-Expensify && git rev-parse --short HEAD) - echo "IDENTIFIER=${APP_SHORT_SHA}-${MOBILE_EXPENSIFY_SHORT_SHA}" >> "$GITHUB_OUTPUT" - - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - - name: Setup Node - id: setup-node - uses: ./.github/actions/composite/setupNode - with: - IS_HYBRID_BUILD: 'true' - - - name: Create .env.adhoc file based on staging - run: | - cp .env.staging .env.adhoc - sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc - - - name: Inject CI data into JS bundle - run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER" - - - name: Setup 1Password CLI and certificates - uses: Expensify/GitHub-Actions/setup-certificate-1p@main - with: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - SHOULD_LOAD_SSL_CERTIFICATES: 'false' - - - name: Load files from 1Password - env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - run: | - op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc/OldApp_AdHoc.mobileprovision" --force --out-file ./OldApp_AdHoc.mobileprovision - op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Share_Extension/OldApp_AdHoc_Share_Extension.mobileprovision" --force --out-file ./OldApp_AdHoc_Share_Extension.mobileprovision - op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Notification_Service/OldApp_AdHoc_Notification_Service.mobileprovision" --force --out-file ./OldApp_AdHoc_Notification_Service.mobileprovision - op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 - - - name: Create ExportOptions.plist - run: | - cat > Mobile-Expensify/iOS/ExportOptions.plist << 'EOF' - - - - - method - ad-hoc - provisioningProfiles - - com.expensify.expensifylite.adhoc - (OldApp) AdHoc - com.expensify.expensifylite.adhoc.SmartScanExtension - (OldApp) AdHoc: Share Extension - com.expensify.expensifylite.adhoc.NotificationServiceExtension - (OldApp) AdHoc: Notification Service - - - - EOF - - - name: Configure AWS Credentials - # v6 - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Rock Remote Build - iOS - id: rock-remote-build-ios - uses: callstackincubator/ios@dd30f7e53eee2ea6a59509793d0a30fbb5c91216 - env: - GITHUB_TOKEN: ${{ github.token }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - IS_HYBRID_APP: true - with: - destination: device - re-sign: true - ad-hoc: true - scheme: 'Expensify AdHoc' - configuration: 'AdHoc' - certificate-file: './Certificates.p12' - provisioning-profiles: | - [ - { - "name": "(OldApp) AdHoc", - "file": "./OldApp_AdHoc.mobileprovision" - }, - { - "name": "(OldApp) AdHoc: Share Extension", - "file": "./OldApp_AdHoc_Share_Extension.mobileprovision" - }, - { - "name": "(OldApp) AdHoc: Notification Service", - "file": "./OldApp_AdHoc_Notification_Service.mobileprovision" - } - ] - comment-bot: false - custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }} - - - name: Set artifact URL output - id: set-artifact-url - run: echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT" - - postGithubComment: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: ${{ always() && (inputs.APP_PR_NUMBER != '' || inputs.MOBILE_EXPENSIFY_PR != '') }} - name: Post a GitHub comment with app download links for testing - needs: [web, androidHybrid, iosHybrid] - steps: - - name: Checkout - # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ inputs.APP_REF }} - - - name: Download Artifact - # v7 - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 - - - name: Publish links to apps for download on Expensify/App PR - if: ${{ inputs.APP_PR_NUMBER != '' }} - uses: ./.github/actions/javascript/postTestBuildComment - with: - REPO: App - APP_PR_NUMBER: ${{ inputs.APP_PR_NUMBER }} - MOBILE_EXPENSIFY_PR_NUMBER: ${{ inputs.MOBILE_EXPENSIFY_PR }} - GITHUB_TOKEN: ${{ github.token }} - ANDROID: ${{ needs.androidHybrid.result }} - IOS: ${{ needs.iosHybrid.result }} - WEB: ${{ needs.web.result }} - ANDROID_LINK: ${{ needs.androidHybrid.outputs.ROCK_ANDROID_ADHOC_INDEX_URL }} - IOS_LINK: ${{ needs.iosHybrid.outputs.ROCK_IOS_ADHOC_INDEX_URL }} - WEB_LINK: https://${{ inputs.APP_PR_NUMBER }}.pr-testing.expensify.com - - - name: Publish links to apps for download on Expensify/Mobile-Expensify PR - if: ${{ inputs.MOBILE_EXPENSIFY_PR != '' }} - uses: ./.github/actions/javascript/postTestBuildComment - with: - REPO: Mobile-Expensify - MOBILE_EXPENSIFY_PR_NUMBER: ${{ inputs.MOBILE_EXPENSIFY_PR }} - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - ANDROID: ${{ needs.androidHybrid.result }} - IOS: ${{ needs.iosHybrid.result }} - ANDROID_LINK: ${{ needs.androidHybrid.outputs.ROCK_ANDROID_ADHOC_INDEX_URL }} - IOS_LINK: ${{ needs.iosHybrid.outputs.ROCK_IOS_ADHOC_INDEX_URL }} - - buildSummary: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: ${{ always() && (inputs.APP_PR_NUMBER != '' || needs.androidHybrid.outputs.ROCK_ANDROID_ADHOC_INDEX_URL != '' || needs.iosHybrid.outputs.ROCK_IOS_ADHOC_INDEX_URL != '') }} - name: Build Summary - needs: [web, androidHybrid, iosHybrid] - steps: - - name: Checkout - # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ inputs.APP_REF }} - - - name: Get Mobile-Expensify ref - id: getMobileExpensifySHA - run: | - if [ -n "${{ inputs.MOBILE_EXPENSIFY_REF }}" ]; then - echo "SHA=${{ inputs.MOBILE_EXPENSIFY_REF }}" >> "$GITHUB_OUTPUT" - elif [ -d "Mobile-Expensify" ]; then - SUBMODULE_SHA=$(git ls-tree HEAD Mobile-Expensify | awk '{print $3}') - echo "SHA=$SUBMODULE_SHA" >> "$GITHUB_OUTPUT" - fi - - - name: Create build summary - # v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - with: - github-token: ${{ github.token }} - script: | - const prNumber = '${{ inputs.APP_PR_NUMBER }}'; - const webLink = prNumber && '${{ needs.web.result }}' === 'success' ? `https://${prNumber}.pr-testing.expensify.com` : ''; - const androidLink = '${{ needs.androidHybrid.outputs.ROCK_ANDROID_ADHOC_INDEX_URL }}' || ''; - const iosLink = '${{ needs.iosHybrid.outputs.ROCK_IOS_ADHOC_INDEX_URL }}' || ''; - const mobileExpensifySHA = '${{ steps.getMobileExpensifySHA.outputs.SHA }}' || ''; - - const webStatus = webLink ? '✅ Success' : '${{ needs.web.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; - const androidStatus = androidLink ? '✅ Success' : '${{ needs.androidHybrid.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; - const iosStatus = iosLink ? '✅ Success' : '${{ needs.iosHybrid.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; - - const summary = core.summary - .addTable([ - [{data: 'Platform', header: true}, {data: 'Status', header: true}, {data: 'Download', header: true}], - [ - 'Web', - webStatus, - webLink ? `${webLink}` : '-' - ], - [ - 'Android', - androidStatus, - androidLink ? `${androidLink}` : '-' - ], - [ - 'iOS', - iosStatus, - iosLink ? `${iosLink}` : '-' - ], - ]); - - if (mobileExpensifySHA) { - const mobileExpensifyUrl = `https://github.com/Expensify/Mobile-Expensify/commit/${mobileExpensifySHA}`; - const label = '${{ inputs.MOBILE_EXPENSIFY_REF }}' ? 'Mobile-Expensify SHA (Custom)' : 'Mobile-Expensify Submodule SHA'; - summary.addRaw(`\n**${label}:** [${mobileExpensifySHA}](${mobileExpensifyUrl})`); - } - - if (prNumber) { - const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`; - summary.addRaw(`\n**PR Link:** ${prUrl}`); - } else { - summary.addRaw(`\n**PR Link:** No PR associated with this commit.`); - } - - summary.write(); From 2b9719a7a8a3208b0cb1e67d430ea7e454283a48 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 14:35:32 -0800 Subject: [PATCH 39/52] DRY up test build workflows with buildAdHoc.yml wrapper Re-introduce buildAdHoc.yml as a lightweight callable workflow that handles the shared ad-hoc build orchestration: posting build-started comments, delegating to buildAndroid/buildIOS/buildWeb callable workflows, posting download link comments, and generating the build summary. testBuild.yml and testBuildOnPush.yml now each call buildAdHoc.yml with a single job instead of duplicating ~150 lines of comment/summary logic each. Mobile-Expensify PR handling is conditional on the MOBILE_EXPENSIFY_PR input being provided. Co-authored-by: Cursor --- .github/workflows/buildAdHoc.yml | 226 ++++++++++++++++++++++++++ .github/workflows/testBuild.yml | 198 ++-------------------- .github/workflows/testBuildOnPush.yml | 145 +---------------- 3 files changed, 243 insertions(+), 326 deletions(-) create mode 100644 .github/workflows/buildAdHoc.yml diff --git a/.github/workflows/buildAdHoc.yml b/.github/workflows/buildAdHoc.yml new file mode 100644 index 000000000000..36910297b265 --- /dev/null +++ b/.github/workflows/buildAdHoc.yml @@ -0,0 +1,226 @@ +name: Build and deploy ad-hoc apps for testing + +on: + workflow_call: + inputs: + APP_REF: + description: Git ref to checkout and build + type: string + required: true + APP_PR_NUMBER: + description: App PR number (empty if none) + type: string + default: '' + MOBILE_EXPENSIFY_PR: + description: Mobile-Expensify PR number (empty if none) + type: string + default: '' + MOBILE_EXPENSIFY_REF: + description: Mobile-Expensify ref to checkout (empty to use submodule at HEAD) + type: string + default: '' + BUILD_ANDROID: + description: Whether to build Android ('true' or 'false') + type: string + default: 'true' + BUILD_IOS: + description: Whether to build iOS ('true' or 'false') + type: string + default: 'true' + BUILD_WEB: + description: Whether to build web ('true' or 'false') + type: string + default: 'true' + FORCE_NATIVE_BUILD: + description: Force a full native build, bypassing Rock remote cache + type: string + default: 'false' + +jobs: + postGitHubCommentBuildStarted: + name: Post build started comment + if: ${{ inputs.APP_PR_NUMBER != '' || inputs.MOBILE_EXPENSIFY_PR != '' }} + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - name: Add build start comment to Expensify/App PR + if: ${{ inputs.APP_PR_NUMBER != '' }} + # v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + with: + github-token: ${{ github.token }} + script: | + const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ inputs.APP_PR_NUMBER || 0 }}, + body: `🚧 @${{ github.actor }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).` + }); + + - name: Add build start comment to Expensify/Mobile-Expensify PR + if: ${{ inputs.MOBILE_EXPENSIFY_PR != '' }} + # v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + with: + github-token: ${{ secrets.OS_BOTIFY_TOKEN }} + script: | + const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: 'Mobile-Expensify', + issue_number: ${{ inputs.MOBILE_EXPENSIFY_PR || 0 }}, + body: `🚧 @${{ github.actor }} has triggered a test Expensify/Mobile-Expensify build. You can view the [workflow run here](${workflowURL}).` + }); + + buildAndroid: + name: Build Android + if: ${{ fromJSON(inputs.BUILD_ANDROID) }} + uses: ./.github/workflows/buildAndroid.yml + with: + ref: ${{ inputs.APP_REF }} + variant: Adhoc + mobile-expensify-ref: ${{ inputs.MOBILE_EXPENSIFY_REF }} + pull-request-number: ${{ inputs.APP_PR_NUMBER }} + force-native-build: ${{ inputs.FORCE_NATIVE_BUILD }} + secrets: inherit + + buildIOS: + name: Build iOS + if: ${{ fromJSON(inputs.BUILD_IOS) }} + uses: ./.github/workflows/buildIOS.yml + with: + ref: ${{ inputs.APP_REF }} + variant: Adhoc + mobile-expensify-ref: ${{ inputs.MOBILE_EXPENSIFY_REF }} + pull-request-number: ${{ inputs.APP_PR_NUMBER }} + force-native-build: ${{ inputs.FORCE_NATIVE_BUILD }} + secrets: inherit + + buildWeb: + name: Build Web + if: ${{ fromJSON(inputs.BUILD_WEB) && inputs.APP_PR_NUMBER != '' }} + uses: ./.github/workflows/buildWeb.yml + with: + ref: ${{ inputs.APP_REF }} + environment: adhoc + pull-request-number: ${{ inputs.APP_PR_NUMBER }} + upload-artifacts: false + deploy-to-s3: true + build-storybook: false + secrets: inherit + + postGithubComment: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: ${{ always() && (inputs.APP_PR_NUMBER != '' || inputs.MOBILE_EXPENSIFY_PR != '') }} + name: Post a GitHub comment with app download links for testing + needs: [buildWeb, buildAndroid, buildIOS] + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ inputs.APP_REF }} + + - name: Download Artifact + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + + - name: Publish links to apps for download on Expensify/App PR + if: ${{ inputs.APP_PR_NUMBER != '' }} + uses: ./.github/actions/javascript/postTestBuildComment + with: + REPO: App + APP_PR_NUMBER: ${{ inputs.APP_PR_NUMBER }} + MOBILE_EXPENSIFY_PR_NUMBER: ${{ inputs.MOBILE_EXPENSIFY_PR }} + GITHUB_TOKEN: ${{ github.token }} + ANDROID: ${{ needs.buildAndroid.result }} + IOS: ${{ needs.buildIOS.result }} + WEB: ${{ needs.buildWeb.result }} + ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }} + IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }} + WEB_LINK: https://${{ inputs.APP_PR_NUMBER }}.pr-testing.expensify.com + + - name: Publish links to apps for download on Expensify/Mobile-Expensify PR + if: ${{ inputs.MOBILE_EXPENSIFY_PR != '' }} + uses: ./.github/actions/javascript/postTestBuildComment + with: + REPO: Mobile-Expensify + MOBILE_EXPENSIFY_PR_NUMBER: ${{ inputs.MOBILE_EXPENSIFY_PR }} + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + ANDROID: ${{ needs.buildAndroid.result }} + IOS: ${{ needs.buildIOS.result }} + ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }} + IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }} + + buildSummary: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: ${{ always() && (inputs.APP_PR_NUMBER != '' || needs.buildAndroid.outputs.ROCK_ARTIFACT_URL != '' || needs.buildIOS.outputs.ROCK_ARTIFACT_URL != '') }} + name: Build Summary + needs: [buildWeb, buildAndroid, buildIOS] + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ inputs.APP_REF }} + + - name: Get Mobile-Expensify ref + id: getMobileExpensifySHA + run: | + if [ -n "${{ inputs.MOBILE_EXPENSIFY_REF }}" ]; then + echo "SHA=${{ inputs.MOBILE_EXPENSIFY_REF }}" >> "$GITHUB_OUTPUT" + elif [ -d "Mobile-Expensify" ]; then + SUBMODULE_SHA=$(git ls-tree HEAD Mobile-Expensify | awk '{print $3}') + echo "SHA=$SUBMODULE_SHA" >> "$GITHUB_OUTPUT" + fi + + - name: Create build summary + # v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + with: + github-token: ${{ github.token }} + script: | + const prNumber = '${{ inputs.APP_PR_NUMBER }}'; + const webLink = prNumber && '${{ needs.buildWeb.result }}' === 'success' ? `https://${prNumber}.pr-testing.expensify.com` : ''; + const androidLink = '${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }}' || ''; + const iosLink = '${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }}' || ''; + const mobileExpensifySHA = '${{ steps.getMobileExpensifySHA.outputs.SHA }}' || ''; + + const webStatus = webLink ? '✅ Success' : '${{ needs.buildWeb.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + const androidStatus = androidLink ? '✅ Success' : '${{ needs.buildAndroid.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + const iosStatus = iosLink ? '✅ Success' : '${{ needs.buildIOS.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + + const summary = core.summary + .addTable([ + [{data: 'Platform', header: true}, {data: 'Status', header: true}, {data: 'Download', header: true}], + [ + 'Web', + webStatus, + webLink ? `${webLink}` : '-' + ], + [ + 'Android', + androidStatus, + androidLink ? `${androidLink}` : '-' + ], + [ + 'iOS', + iosStatus, + iosLink ? `${iosLink}` : '-' + ], + ]); + + if (mobileExpensifySHA) { + const mobileExpensifyUrl = `https://github.com/Expensify/Mobile-Expensify/commit/${mobileExpensifySHA}`; + const label = '${{ inputs.MOBILE_EXPENSIFY_REF }}' ? 'Mobile-Expensify SHA (Custom)' : 'Mobile-Expensify Submodule SHA'; + summary.addRaw(`\n**${label}:** [${mobileExpensifySHA}](${mobileExpensifyUrl})`); + } + + if (prNumber) { + const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`; + summary.addRaw(`\n**PR Link:** ${prUrl}`); + } else { + summary.addRaw(`\n**PR Link:** No PR associated with this commit.`); + } + + summary.write(); diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 3c514083acc8..65e190a78977 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -150,194 +150,16 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - postGitHubCommentBuildStarted: - name: Post build started comment - if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' || needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR != '' }} - needs: [prep, getMobileExpensifyPR] - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Add build start comment to Expensify/App PR - if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' }} - # v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - with: - github-token: ${{ github.token }} - script: | - const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: ${{ needs.prep.outputs.APP_PR_NUMBER }}, - body: `🚧 @${{ github.actor }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).` - }); - - - name: Add build start comment to Expensify/Mobile-Expensify PR - if: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR != '' }} - # v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - with: - github-token: ${{ secrets.OS_BOTIFY_TOKEN }} - script: | - const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: 'Mobile-Expensify', - issue_number: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }}, - body: `🚧 @${{ github.actor }} has triggered a test Expensify/Mobile-Expensify build. You can view the [workflow run here](${workflowURL}).` - }); - - buildAndroid: - name: Build Android - needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef] - if: ${{ inputs.ANDROID }} - uses: ./.github/workflows/buildAndroid.yml - with: - ref: ${{ needs.prep.outputs.APP_REF }} - variant: Adhoc - mobile-expensify-ref: ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }} - pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} - force-native-build: ${{ inputs.FORCE_NATIVE_BUILD && 'true' || 'false' }} - secrets: inherit - - buildIOS: - name: Build iOS + buildApps: needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef] - if: ${{ inputs.IOS }} - uses: ./.github/workflows/buildIOS.yml + uses: ./.github/workflows/buildAdHoc.yml with: - ref: ${{ needs.prep.outputs.APP_REF }} - variant: Adhoc - mobile-expensify-ref: ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }} - pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} - force-native-build: ${{ inputs.FORCE_NATIVE_BUILD && 'true' || 'false' }} + APP_REF: ${{ needs.prep.outputs.APP_REF }} + APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }} + MOBILE_EXPENSIFY_PR: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }} + MOBILE_EXPENSIFY_REF: ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }} + BUILD_ANDROID: ${{ inputs.ANDROID && 'true' || 'false' }} + BUILD_IOS: ${{ inputs.IOS && 'true' || 'false' }} + BUILD_WEB: ${{ inputs.WEB && 'true' || 'false' }} + FORCE_NATIVE_BUILD: ${{ inputs.FORCE_NATIVE_BUILD && 'true' || 'false' }} secrets: inherit - - buildWeb: - name: Build Web - needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef] - if: ${{ inputs.WEB && needs.prep.outputs.APP_PR_NUMBER != '' }} - uses: ./.github/workflows/buildWeb.yml - with: - ref: ${{ needs.prep.outputs.APP_REF }} - environment: adhoc - pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} - upload-artifacts: false - deploy-to-s3: true - build-storybook: false - secrets: inherit - - postGithubComment: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: ${{ always() && (needs.prep.outputs.APP_PR_NUMBER != '' || needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR != '') }} - name: Post a GitHub comment with app download links for testing - needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef, buildWeb, buildAndroid, buildIOS] - steps: - - name: Checkout - # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ needs.prep.outputs.APP_REF }} - - - name: Download Artifact - # v7 - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 - - - name: Publish links to apps for download on Expensify/App PR - if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' }} - uses: ./.github/actions/javascript/postTestBuildComment - with: - REPO: App - APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }} - MOBILE_EXPENSIFY_PR_NUMBER: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }} - GITHUB_TOKEN: ${{ github.token }} - ANDROID: ${{ needs.buildAndroid.result }} - IOS: ${{ needs.buildIOS.result }} - WEB: ${{ needs.buildWeb.result }} - ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }} - IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }} - WEB_LINK: https://${{ needs.prep.outputs.APP_PR_NUMBER }}.pr-testing.expensify.com - - - name: Publish links to apps for download on Expensify/Mobile-Expensify PR - if: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR != '' }} - uses: ./.github/actions/javascript/postTestBuildComment - with: - REPO: Mobile-Expensify - MOBILE_EXPENSIFY_PR_NUMBER: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }} - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - ANDROID: ${{ needs.buildAndroid.result }} - IOS: ${{ needs.buildIOS.result }} - ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }} - IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }} - - buildSummary: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: ${{ always() && (needs.prep.outputs.APP_PR_NUMBER != '' || needs.buildAndroid.outputs.ROCK_ARTIFACT_URL != '' || needs.buildIOS.outputs.ROCK_ARTIFACT_URL != '') }} - name: Build Summary - needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef, buildWeb, buildAndroid, buildIOS] - steps: - - name: Checkout - # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ needs.prep.outputs.APP_REF }} - - - name: Get Mobile-Expensify ref - id: getMobileExpensifySHA - run: | - if [ -n "${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }}" ]; then - echo "SHA=${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }}" >> "$GITHUB_OUTPUT" - elif [ -d "Mobile-Expensify" ]; then - SUBMODULE_SHA=$(git ls-tree HEAD Mobile-Expensify | awk '{print $3}') - echo "SHA=$SUBMODULE_SHA" >> "$GITHUB_OUTPUT" - fi - - - name: Create build summary - # v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - with: - github-token: ${{ github.token }} - script: | - const prNumber = '${{ needs.prep.outputs.APP_PR_NUMBER }}'; - const webLink = prNumber && '${{ needs.buildWeb.result }}' === 'success' ? `https://${prNumber}.pr-testing.expensify.com` : ''; - const androidLink = '${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }}' || ''; - const iosLink = '${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }}' || ''; - const mobileExpensifySHA = '${{ steps.getMobileExpensifySHA.outputs.SHA }}' || ''; - - const webStatus = webLink ? '✅ Success' : '${{ needs.buildWeb.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; - const androidStatus = androidLink ? '✅ Success' : '${{ needs.buildAndroid.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; - const iosStatus = iosLink ? '✅ Success' : '${{ needs.buildIOS.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; - - const summary = core.summary - .addTable([ - [{data: 'Platform', header: true}, {data: 'Status', header: true}, {data: 'Download', header: true}], - [ - 'Web', - webStatus, - webLink ? `${webLink}` : '-' - ], - [ - 'Android', - androidStatus, - androidLink ? `${androidLink}` : '-' - ], - [ - 'iOS', - iosStatus, - iosLink ? `${iosLink}` : '-' - ], - ]); - - if (mobileExpensifySHA) { - const mobileExpensifyUrl = `https://github.com/Expensify/Mobile-Expensify/commit/${mobileExpensifySHA}`; - const label = '${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }}' ? 'Mobile-Expensify SHA (Custom)' : 'Mobile-Expensify Submodule SHA'; - summary.addRaw(`\n**${label}:** [${mobileExpensifySHA}](${mobileExpensifyUrl})`); - } - - if (prNumber) { - const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`; - summary.addRaw(`\n**PR Link:** ${prUrl}`); - } else { - summary.addRaw(`\n**PR Link:** No PR associated with this commit.`); - } - - summary.write(); diff --git a/.github/workflows/testBuildOnPush.yml b/.github/workflows/testBuildOnPush.yml index 1b714a4ea48c..7e03d9c2d9ef 100644 --- a/.github/workflows/testBuildOnPush.yml +++ b/.github/workflows/testBuildOnPush.yml @@ -80,144 +80,13 @@ jobs: core.setFailed(`Commit pushed by non-OSBotify actor. No merged pull request found for commit ${context.sha}`); } - postGitHubCommentBuildStarted: - name: Post build started comment - if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' }} + buildApps: needs: [prep] - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Add build start comment to Expensify/App PR - # v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - with: - github-token: ${{ github.token }} - script: | - const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: ${{ needs.prep.outputs.APP_PR_NUMBER }}, - body: `🚧 @${{ github.actor }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).` - }); - - buildAndroid: - name: Build Android - needs: [prep] - if: ${{ fromJSON(needs.prep.outputs.BUILD_MOBILE) }} - uses: ./.github/workflows/buildAndroid.yml - with: - ref: ${{ needs.prep.outputs.APP_REF }} - variant: Adhoc - pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} - secrets: inherit - - buildIOS: - name: Build iOS - needs: [prep] - if: ${{ fromJSON(needs.prep.outputs.BUILD_MOBILE) }} - uses: ./.github/workflows/buildIOS.yml - with: - ref: ${{ needs.prep.outputs.APP_REF }} - variant: Adhoc - pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} - secrets: inherit - - buildWeb: - name: Build Web - needs: [prep] - if: ${{ fromJSON(needs.prep.outputs.BUILD_WEB) && needs.prep.outputs.APP_PR_NUMBER != '' }} - uses: ./.github/workflows/buildWeb.yml + uses: ./.github/workflows/buildAdHoc.yml with: - ref: ${{ needs.prep.outputs.APP_REF }} - environment: adhoc - pull-request-number: ${{ needs.prep.outputs.APP_PR_NUMBER }} - upload-artifacts: false - deploy-to-s3: true - build-storybook: false + APP_REF: ${{ needs.prep.outputs.APP_REF }} + APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }} + BUILD_ANDROID: ${{ needs.prep.outputs.BUILD_MOBILE }} + BUILD_IOS: ${{ needs.prep.outputs.BUILD_MOBILE }} + BUILD_WEB: ${{ needs.prep.outputs.BUILD_WEB }} secrets: inherit - - postGithubComment: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: ${{ always() && needs.prep.outputs.APP_PR_NUMBER != '' }} - name: Post a GitHub comment with app download links for testing - needs: [prep, buildWeb, buildAndroid, buildIOS] - steps: - - name: Checkout - # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ needs.prep.outputs.APP_REF }} - - - name: Download Artifact - # v7 - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 - - - name: Publish links to apps for download on Expensify/App PR - if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' }} - uses: ./.github/actions/javascript/postTestBuildComment - with: - REPO: App - APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }} - GITHUB_TOKEN: ${{ github.token }} - ANDROID: ${{ needs.buildAndroid.result }} - IOS: ${{ needs.buildIOS.result }} - WEB: ${{ needs.buildWeb.result }} - ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }} - IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }} - WEB_LINK: https://${{ needs.prep.outputs.APP_PR_NUMBER }}.pr-testing.expensify.com - - buildSummary: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: ${{ always() && (needs.prep.outputs.APP_PR_NUMBER != '' || needs.buildAndroid.outputs.ROCK_ARTIFACT_URL != '' || needs.buildIOS.outputs.ROCK_ARTIFACT_URL != '') }} - name: Build Summary - needs: [prep, buildWeb, buildAndroid, buildIOS] - steps: - - name: Checkout - # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ needs.prep.outputs.APP_REF }} - - - name: Create build summary - # v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - with: - github-token: ${{ github.token }} - script: | - const prNumber = '${{ needs.prep.outputs.APP_PR_NUMBER }}'; - const webLink = prNumber && '${{ needs.buildWeb.result }}' === 'success' ? `https://${prNumber}.pr-testing.expensify.com` : ''; - const androidLink = '${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }}' || ''; - const iosLink = '${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }}' || ''; - - const webStatus = webLink ? '✅ Success' : '${{ needs.buildWeb.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; - const androidStatus = androidLink ? '✅ Success' : '${{ needs.buildAndroid.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; - const iosStatus = iosLink ? '✅ Success' : '${{ needs.buildIOS.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; - - const summary = core.summary - .addTable([ - [{data: 'Platform', header: true}, {data: 'Status', header: true}, {data: 'Download', header: true}], - [ - 'Web', - webStatus, - webLink ? `${webLink}` : '-' - ], - [ - 'Android', - androidStatus, - androidLink ? `${androidLink}` : '-' - ], - [ - 'iOS', - iosStatus, - iosLink ? `${iosLink}` : '-' - ], - ]); - - if (prNumber) { - const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`; - summary.addRaw(`\n**PR Link:** ${prUrl}`); - } else { - summary.addRaw(`\n**PR Link:** No PR associated with this commit.`); - } - - summary.write(); From 72e9004c458821a778ae9189592835b6d6322c04 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 14:50:05 -0800 Subject: [PATCH 40/52] Minimize diff by reverting cosmetic changes Revert unnecessary workflow name changes, input description edits, input reordering, indentation reformatting, and checkout action version bumps in pre-existing jobs. Co-authored-by: Cursor --- .github/workflows/buildAdHoc.yml | 36 +++++++++++++-------------- .github/workflows/buildAndroid.yml | 6 ++--- .github/workflows/deploy.yml | 8 +++--- .github/workflows/testBuild.yml | 6 ++--- .github/workflows/testBuildOnPush.yml | 4 +-- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/buildAdHoc.yml b/.github/workflows/buildAdHoc.yml index 36910297b265..4dae01f639e8 100644 --- a/.github/workflows/buildAdHoc.yml +++ b/.github/workflows/buildAdHoc.yml @@ -1,34 +1,34 @@ -name: Build and deploy ad-hoc apps for testing +name: Build and deploy ad-hoc apps on: workflow_call: inputs: APP_REF: - description: Git ref to checkout and build + description: Git ref to checkout for building type: string required: true APP_PR_NUMBER: - description: App PR number (empty if none) - type: string - default: '' - MOBILE_EXPENSIFY_PR: - description: Mobile-Expensify PR number (empty if none) + description: Expensify/App pull request number (empty to skip comments and web deploy) type: string default: '' MOBILE_EXPENSIFY_REF: description: Mobile-Expensify ref to checkout (empty to use submodule at HEAD) type: string default: '' - BUILD_ANDROID: - description: Whether to build Android ('true' or 'false') + MOBILE_EXPENSIFY_PR: + description: Mobile-Expensify pull request number (empty to skip ME PR comments) + type: string + default: '' + BUILD_WEB: + description: Whether to build the web app type: string default: 'true' BUILD_IOS: - description: Whether to build iOS ('true' or 'false') + description: Whether to build the iOS app type: string default: 'true' - BUILD_WEB: - description: Whether to build web ('true' or 'false') + BUILD_ANDROID: + description: Whether to build the Android app type: string default: 'true' FORCE_NATIVE_BUILD: @@ -49,13 +49,13 @@ jobs: with: github-token: ${{ github.token }} script: | - const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - github.rest.issues.createComment({ + const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: ${{ inputs.APP_PR_NUMBER || 0 }}, body: `🚧 @${{ github.actor }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).` - }); + }); - name: Add build start comment to Expensify/Mobile-Expensify PR if: ${{ inputs.MOBILE_EXPENSIFY_PR != '' }} @@ -64,13 +64,13 @@ jobs: with: github-token: ${{ secrets.OS_BOTIFY_TOKEN }} script: | - const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - github.rest.issues.createComment({ + const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + github.rest.issues.createComment({ owner: context.repo.owner, repo: 'Mobile-Expensify', issue_number: ${{ inputs.MOBILE_EXPENSIFY_PR || 0 }}, body: `🚧 @${{ github.actor }} has triggered a test Expensify/Mobile-Expensify build. You can view the [workflow run here](${workflowURL}).` - }); + }); buildAndroid: name: Build Android diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml index 2d7bb476a8c6..bf8e123cf078 100644 --- a/.github/workflows/buildAndroid.yml +++ b/.github/workflows/buildAndroid.yml @@ -1,4 +1,4 @@ -name: Build Android HybridApp +name: Build Android app on: workflow_call: @@ -20,7 +20,7 @@ on: type: string default: '' artifact-prefix: - description: Prefix for build artifact names + description: 'The prefix for build artifact names. This is useful if you need to call multiple builds from the same workflow' type: string default: '' upload-sourcemaps: @@ -42,7 +42,7 @@ on: jobs: build: - name: Build Android HybridApp + name: Build Android app runs-on: blacksmith-32vcpu-ubuntu-2404 env: PULL_REQUEST_NUMBER: ${{ inputs.pull-request-number }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 43a947e1cc40..b77dbcf97e9f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,8 +25,8 @@ jobs: IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} steps: - name: Checkout - # v6 - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 + # v4 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 with: token: ${{ secrets.OS_BOTIFY_TOKEN }} submodules: true @@ -531,8 +531,8 @@ jobs: needs: [androidBuild, androidUploadGooglePlay, androidUploadBrowserStack, androidUploadApplause, androidSubmit, iosBuild, iosUploadTestflight, iosUploadBrowserStack, iosUploadApplause, iosSubmit, web] steps: - name: Checkout - # v6 - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 + # v4 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - name: Post Slack message on failure uses: ./.github/actions/composite/announceFailedWorkflowInSlack diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 65e190a78977..5b69f769785f 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -156,10 +156,10 @@ jobs: with: APP_REF: ${{ needs.prep.outputs.APP_REF }} APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }} - MOBILE_EXPENSIFY_PR: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }} MOBILE_EXPENSIFY_REF: ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }} - BUILD_ANDROID: ${{ inputs.ANDROID && 'true' || 'false' }} - BUILD_IOS: ${{ inputs.IOS && 'true' || 'false' }} + MOBILE_EXPENSIFY_PR: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }} BUILD_WEB: ${{ inputs.WEB && 'true' || 'false' }} + BUILD_IOS: ${{ inputs.IOS && 'true' || 'false' }} + BUILD_ANDROID: ${{ inputs.ANDROID && 'true' || 'false' }} FORCE_NATIVE_BUILD: ${{ inputs.FORCE_NATIVE_BUILD && 'true' || 'false' }} secrets: inherit diff --git a/.github/workflows/testBuildOnPush.yml b/.github/workflows/testBuildOnPush.yml index 7e03d9c2d9ef..556c67476b4e 100644 --- a/.github/workflows/testBuildOnPush.yml +++ b/.github/workflows/testBuildOnPush.yml @@ -86,7 +86,7 @@ jobs: with: APP_REF: ${{ needs.prep.outputs.APP_REF }} APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }} - BUILD_ANDROID: ${{ needs.prep.outputs.BUILD_MOBILE }} - BUILD_IOS: ${{ needs.prep.outputs.BUILD_MOBILE }} BUILD_WEB: ${{ needs.prep.outputs.BUILD_WEB }} + BUILD_IOS: ${{ needs.prep.outputs.BUILD_MOBILE }} + BUILD_ANDROID: ${{ needs.prep.outputs.BUILD_MOBILE }} secrets: inherit From c915d1b08a9f839cfb60a8332eebb3209182480c Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 14:55:24 -0800 Subject: [PATCH 41/52] Add custom-identifier to Rock builds for cache correctness When the same App ref is built with different Mobile-Expensify refs, Rock can serve a cached artifact from the wrong Mobile commit. Pass a custom-identifier derived from both App and Mobile-Expensify SHAs to ensure cache keys are unique per combination. Co-authored-by: Cursor --- .github/workflows/buildAndroid.yml | 9 +++++++++ .github/workflows/buildIOS.yml | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml index bf8e123cf078..83e3ca27859e 100644 --- a/.github/workflows/buildAndroid.yml +++ b/.github/workflows/buildAndroid.yml @@ -65,6 +65,13 @@ jobs: git fetch origin ${{ inputs.mobile-expensify-ref }} git checkout ${{ inputs.mobile-expensify-ref }} + - name: Compute custom build identifier + id: computeIdentifier + run: | + APP_SHORT_SHA=$(git rev-parse --short HEAD) + MOBILE_EXPENSIFY_SHORT_SHA=$(cd Mobile-Expensify && git rev-parse --short HEAD) + echo "IDENTIFIER=${APP_SHORT_SHA}-${MOBILE_EXPENSIFY_SHORT_SHA}" >> "$GITHUB_OUTPUT" + - name: Configure MapBox SDK run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} @@ -165,6 +172,7 @@ jobs: keystore-path: '../tools/buildtools/upload-key.keystore' comment-bot: false rock-build-extra-params: ${{ inputs.variant == 'Adhoc' && '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' || '--extra-params "--profile"' }} + custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }} validate-elf-alignment: false - name: Clear Gradle cache @@ -194,6 +202,7 @@ jobs: keystore-path: '../tools/buildtools/upload-key.keystore' comment-bot: false rock-build-extra-params: ${{ inputs.variant == 'Adhoc' && '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' || '--extra-params "--profile"' }} + custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }} validate-elf-alignment: false - name: Upload Gradle profile report diff --git a/.github/workflows/buildIOS.yml b/.github/workflows/buildIOS.yml index 60ed349f3641..edb87fa8287b 100644 --- a/.github/workflows/buildIOS.yml +++ b/.github/workflows/buildIOS.yml @@ -66,6 +66,13 @@ jobs: git fetch origin ${{ inputs.mobile-expensify-ref }} git checkout ${{ inputs.mobile-expensify-ref }} + - name: Compute custom build identifier + id: computeIdentifier + run: | + APP_SHORT_SHA=$(git rev-parse --short HEAD) + MOBILE_EXPENSIFY_SHORT_SHA=$(cd Mobile-Expensify && git rev-parse --short HEAD) + echo "IDENTIFIER=${APP_SHORT_SHA}-${MOBILE_EXPENSIFY_SHORT_SHA}" >> "$GITHUB_OUTPUT" + - name: Configure MapBox SDK run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} @@ -225,6 +232,7 @@ jobs: certificate-file: './Certificates.p12' provisioning-profiles: ${{ steps.prepare-profiles.outputs.PROFILES }} comment-bot: false + custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }} - name: Set artifact URL output id: set-artifact-url From 0b9ee22e2d6a7c2fdd711fe732e1a9f1dc76c97b Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 15:17:38 -0800 Subject: [PATCH 42/52] Make dSYM upload and download non-fatal for iOS deploys buildIOS.yml already uses if-no-files-found: warn for dSYM upload, but if the artifact isn't created, download-artifact in the upload job would hard-fail and block TestFlight upload. Add continue-on-error to both the dSYM upload and download steps, with ::error:: annotations for visibility when either fails. Co-authored-by: Cursor --- .github/workflows/buildIOS.yml | 6 ++++++ .github/workflows/deploy.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/buildIOS.yml b/.github/workflows/buildIOS.yml index edb87fa8287b..be4083475564 100644 --- a/.github/workflows/buildIOS.yml +++ b/.github/workflows/buildIOS.yml @@ -247,6 +247,8 @@ jobs: path: .rock/cache/ios/export/*.ipa - name: Find and upload dSYM artifact + id: upload-dsym + continue-on-error: true # v6 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: @@ -254,6 +256,10 @@ jobs: path: .rock/cache/ios/export/*.dSYM.zip if-no-files-found: warn + - name: Log dSYM upload failure + if: steps.upload-dsym.outcome == 'failure' + run: echo "::error::Failed to upload dSYM artifact – symbolication data may be missing for this build" + - name: Upload iOS sourcemap artifact if: ${{ inputs.upload-sourcemaps }} # v6 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b77dbcf97e9f..4f74b7f24152 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -317,12 +317,18 @@ jobs: path: ./ - name: Download iOS dSYM artifact + id: download-dsym + continue-on-error: true # v7 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: name: ios-dsym-artifact path: ./ + - name: Log dSYM download failure + if: steps.download-dsym.outcome == 'failure' + run: echo "::error::Failed to download dSYM artifact – symbolication data may be missing for this build" + - name: Set artifact paths for Fastlane run: | echo "ipaPath=$(find "$(pwd)" -name '*.ipa' | head -1)" >> "$GITHUB_ENV" From cece0fe2d6c14b3e58ac21eeb39e9c86c7fddaca Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 15:44:42 -0800 Subject: [PATCH 43/52] Remove unused upload-sourcemaps input from build workflows Sourcemaps are always uploaded unconditionally now since no caller ever set this to false. Co-authored-by: Cursor --- .github/workflows/buildAndroid.yml | 5 ----- .github/workflows/buildIOS.yml | 5 ----- 2 files changed, 10 deletions(-) diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml index 83e3ca27859e..98964564793c 100644 --- a/.github/workflows/buildAndroid.yml +++ b/.github/workflows/buildAndroid.yml @@ -23,10 +23,6 @@ on: description: 'The prefix for build artifact names. This is useful if you need to call multiple builds from the same workflow' type: string default: '' - upload-sourcemaps: - description: Whether to upload sourcemap artifacts - type: boolean - default: true force-native-build: description: Force a full native build, bypassing Rock remote cache type: string @@ -227,7 +223,6 @@ jobs: path: Mobile-Expensify/Android/app/build/outputs/bundle/release/*.aab - name: Upload Android sourcemap artifact - if: ${{ inputs.upload-sourcemaps }} # v6 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: diff --git a/.github/workflows/buildIOS.yml b/.github/workflows/buildIOS.yml index be4083475564..6ff71e78cebb 100644 --- a/.github/workflows/buildIOS.yml +++ b/.github/workflows/buildIOS.yml @@ -23,10 +23,6 @@ on: description: Prefix for build artifact names type: string default: '' - upload-sourcemaps: - description: Whether to upload sourcemap artifacts - type: boolean - default: true force-native-build: description: Force a full native build, bypassing Rock remote cache type: string @@ -261,7 +257,6 @@ jobs: run: echo "::error::Failed to upload dSYM artifact – symbolication data may be missing for this build" - name: Upload iOS sourcemap artifact - if: ${{ inputs.upload-sourcemaps }} # v6 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: From 2525ec12a42d65f15178e64f69edbc27bffbc748 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 16:32:56 -0800 Subject: [PATCH 44/52] Use callable build workflows in verifyHybridApp.yml This ensures the HybridApp verification builds exercise the exact same build pipeline as deploy and ad-hoc builds. Optimizations or fixes to the central buildAndroid/buildIOS workflows automatically apply here. Co-authored-by: Cursor --- .github/workflows/verifyHybridApp.yml | 104 +++----------------------- 1 file changed, 11 insertions(+), 93 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index 4e4c7376a5a6..665d10a57550 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -72,105 +72,23 @@ jobs: fi env: GITHUB_TOKEN: ${{ github.token }} + verify_android: name: Verify Android HybridApp builds on main - runs-on: blacksmith-16vcpu-ubuntu-2404 # Only run on pull requests that are *not* on a fork if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' }} - steps: - - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - with: - submodules: true - ref: ${{ github.event.pull_request.head.sha }} - token: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - - name: Setup Node - id: setup-node - uses: ./.github/actions/composite/setupNode - with: - IS_HYBRID_BUILD: 'true' - - - name: Setup Ruby - # v1.229.0 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 - with: - bundler-cache: true - - - name: Build Android Debug - run: | - if ! npm run android-hybrid-build - then - echo "❌ Android HybridApp failed to build: Please reach out to Contributor+ and/or Expensify engineers for help in #expensify-open-source to resolve." - exit 1 - fi + uses: ./.github/workflows/buildAndroid.yml + with: + ref: ${{ github.event.pull_request.head.sha }} + variant: Release + secrets: inherit verify_ios: name: Verify iOS HybridApp builds on main - runs-on: macos-15-xlarge - env: - DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer # Only run on pull requests that are *not* on a fork if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' }} - steps: - - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - with: - submodules: true - ref: ${{ github.event.pull_request.head.sha }} - token: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - - name: Setup Node - id: setup-node - uses: ./.github/actions/composite/setupNode - with: - IS_HYBRID_BUILD: 'true' - - - name: Setup Ruby - # v1.229.0 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 - with: - bundler-cache: true - - - name: Cache Pod dependencies - # v5.0.1 - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb - id: pods-cache - with: - path: Mobile-Expensify/iOS/Pods - key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') }} - - - name: Compare Podfile.lock and Manifest.lock - id: compare-podfile-and-manifest - run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') == hashFiles('Mobile-Expensify/iOS/Manifest.lock') }}" >> "$GITHUB_OUTPUT" - - - name: Install cocoapods - uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 - if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true' - with: - timeout_minutes: 10 - max_attempts: 5 - command: npm run pod-install - - - name: Build iOS HybridApp - run: | - # Let us know if the builds fails - set -o pipefail - - # Do not start metro - export RCT_NO_LAUNCH_PACKAGER=1 - - # Build iOS using xcodebuild - if ! npm run ios-hybrid-build - then - echo "❌ iOS HybridApp failed to build: Please reach out to Contributor+ and/or Expensify engineers for help in #expensify-open-source to resolve." - exit 1 - fi + uses: ./.github/workflows/buildIOS.yml + with: + ref: ${{ github.event.pull_request.head.sha }} + variant: Release + secrets: inherit From 5c91fafef3a8c7d06c95ad6af3482adae6b631f2 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 16:38:06 -0800 Subject: [PATCH 45/52] Move storybook build to dedicated job in deploy.yml Storybook is only built during deploys, not ad-hoc builds. Giving it its own job on a smaller runner keeps buildWeb.yml focused on the web app and removes an input that only one caller used. Co-authored-by: Cursor --- .github/workflows/buildAdHoc.yml | 1 - .github/workflows/buildWeb.yml | 14 -------------- .github/workflows/deploy.yml | 24 +++++++++++++++++++++++- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.github/workflows/buildAdHoc.yml b/.github/workflows/buildAdHoc.yml index 4dae01f639e8..4678f0589876 100644 --- a/.github/workflows/buildAdHoc.yml +++ b/.github/workflows/buildAdHoc.yml @@ -106,7 +106,6 @@ jobs: pull-request-number: ${{ inputs.APP_PR_NUMBER }} upload-artifacts: false deploy-to-s3: true - build-storybook: false secrets: inherit postGithubComment: diff --git a/.github/workflows/buildWeb.yml b/.github/workflows/buildWeb.yml index 6bd3444b36d0..0721987f0b0e 100644 --- a/.github/workflows/buildWeb.yml +++ b/.github/workflows/buildWeb.yml @@ -23,10 +23,6 @@ on: description: Whether to deploy to S3 type: boolean default: false - build-storybook: - description: Whether to build storybook docs - type: boolean - default: false outputs: S3_URL: @@ -86,16 +82,6 @@ jobs: env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: Build storybook docs - if: ${{ inputs.build-storybook }} - continue-on-error: true - run: | - if [ "${{ inputs.environment }}" == "production" ]; then - npm run storybook-build - else - npm run storybook-build-staging - fi - - name: Deploy to S3 (production/staging) if: ${{ inputs.deploy-to-s3 && inputs.environment != 'adhoc' }} run: | diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4f74b7f24152..ea108585d8fd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -526,10 +526,32 @@ jobs: ref: ${{ github.ref }} environment: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} deploy-to-s3: true - build-storybook: true upload-artifacts: true secrets: inherit + buildStorybook: + name: Build storybook docs + needs: [prep] + runs-on: blacksmith-4vcpu-ubuntu-2404 + continue-on-error: true + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.ref }} + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + + - name: Build storybook docs + run: | + if [ "${{ github.ref }}" == "refs/heads/production" ]; then + npm run storybook-build + else + npm run storybook-build-staging + fi + postSlackMessageOnFailure: name: Post a Slack message when any platform fails to build or deploy runs-on: blacksmith-2vcpu-ubuntu-2404 From faaa96f175922c647a710b1dcd1809625d6ecfed Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 16:42:48 -0800 Subject: [PATCH 46/52] Separate web build from deploy in buildWeb.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildWeb.yml is now a pure build workflow — it builds the web app and uploads artifacts (sourcemaps, tar.gz, zip) but does not deploy. S3 deployment, Cloudflare cache purge, and deploy verification are now handled by dedicated jobs in the callers: - deploy.yml: new webDeploy job - buildAdHoc.yml: new deployWebAdHoc job Co-authored-by: Cursor --- .github/workflows/buildAdHoc.yml | 39 ++++++++++++++---- .github/workflows/buildWeb.yml | 69 +------------------------------- .github/workflows/deploy.yml | 68 +++++++++++++++++++++++++++---- 3 files changed, 94 insertions(+), 82 deletions(-) diff --git a/.github/workflows/buildAdHoc.yml b/.github/workflows/buildAdHoc.yml index 4678f0589876..4f2471c731b7 100644 --- a/.github/workflows/buildAdHoc.yml +++ b/.github/workflows/buildAdHoc.yml @@ -104,15 +104,40 @@ jobs: ref: ${{ inputs.APP_REF }} environment: adhoc pull-request-number: ${{ inputs.APP_PR_NUMBER }} - upload-artifacts: false - deploy-to-s3: true secrets: inherit + deployWebAdHoc: + name: Deploy Web to S3 (adhoc) + needs: [buildWeb] + if: ${{ fromJSON(inputs.BUILD_WEB) && inputs.APP_PR_NUMBER != '' }} + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - name: Download web build artifact + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + with: + name: web-build-tar-gz-artifact + path: ./ + + - name: Extract web build + run: tar -xzvf webBuild.tar.gz + + - name: Configure AWS Credentials + # v6 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Deploy to S3 + run: aws s3 cp --recursive --acl public-read dist s3://ad-hoc-expensify-cash/web/${{ inputs.APP_PR_NUMBER }} + postGithubComment: runs-on: blacksmith-4vcpu-ubuntu-2404 if: ${{ always() && (inputs.APP_PR_NUMBER != '' || inputs.MOBILE_EXPENSIFY_PR != '') }} name: Post a GitHub comment with app download links for testing - needs: [buildWeb, buildAndroid, buildIOS] + needs: [deployWebAdHoc, buildAndroid, buildIOS] steps: - name: Checkout # v6 @@ -134,7 +159,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} ANDROID: ${{ needs.buildAndroid.result }} IOS: ${{ needs.buildIOS.result }} - WEB: ${{ needs.buildWeb.result }} + WEB: ${{ needs.deployWebAdHoc.result }} ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }} IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }} WEB_LINK: https://${{ inputs.APP_PR_NUMBER }}.pr-testing.expensify.com @@ -155,7 +180,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 if: ${{ always() && (inputs.APP_PR_NUMBER != '' || needs.buildAndroid.outputs.ROCK_ARTIFACT_URL != '' || needs.buildIOS.outputs.ROCK_ARTIFACT_URL != '') }} name: Build Summary - needs: [buildWeb, buildAndroid, buildIOS] + needs: [deployWebAdHoc, buildAndroid, buildIOS] steps: - name: Checkout # v6 @@ -180,12 +205,12 @@ jobs: github-token: ${{ github.token }} script: | const prNumber = '${{ inputs.APP_PR_NUMBER }}'; - const webLink = prNumber && '${{ needs.buildWeb.result }}' === 'success' ? `https://${prNumber}.pr-testing.expensify.com` : ''; + const webLink = prNumber && '${{ needs.deployWebAdHoc.result }}' === 'success' ? `https://${prNumber}.pr-testing.expensify.com` : ''; const androidLink = '${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }}' || ''; const iosLink = '${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }}' || ''; const mobileExpensifySHA = '${{ steps.getMobileExpensifySHA.outputs.SHA }}' || ''; - const webStatus = webLink ? '✅ Success' : '${{ needs.buildWeb.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + const webStatus = webLink ? '✅ Success' : '${{ needs.deployWebAdHoc.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; const androidStatus = androidLink ? '✅ Success' : '${{ needs.buildAndroid.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; const iosStatus = iosLink ? '✅ Success' : '${{ needs.buildIOS.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; diff --git a/.github/workflows/buildWeb.yml b/.github/workflows/buildWeb.yml index 0721987f0b0e..482a43e8d64c 100644 --- a/.github/workflows/buildWeb.yml +++ b/.github/workflows/buildWeb.yml @@ -1,4 +1,4 @@ -name: Build and deploy Web +name: Build Web on: workflow_call: @@ -15,28 +15,13 @@ on: description: Pull request number (used for adhoc builds) type: string default: '' - upload-artifacts: - description: Whether to upload compressed build artifacts (.tar.gz, .zip) - type: boolean - default: true - deploy-to-s3: - description: Whether to deploy to S3 - type: boolean - default: false - - outputs: - S3_URL: - description: The S3 URL of the deployed web app (adhoc only) - value: ${{ jobs.build.outputs.S3_URL }} jobs: build: - name: Build and deploy Web + name: Build Web runs-on: blacksmith-32vcpu-ubuntu-2404 env: PULL_REQUEST_NUMBER: ${{ inputs.pull-request-number }} - outputs: - S3_URL: ${{ steps.set-s3-url.outputs.S3_URL }} steps: - name: Checkout # v6 @@ -57,19 +42,6 @@ jobs: - name: Setup Node uses: ./.github/actions/composite/setupNode - - name: Setup Cloudflare CLI - if: ${{ inputs.environment != 'adhoc' }} - run: pip3 install cloudflare==2.19.0 - - - name: Configure AWS Credentials - if: ${{ inputs.deploy-to-s3 }} - # v6 - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - name: Build web run: | if [ "${{ inputs.environment }}" == "production" ]; then @@ -82,40 +54,6 @@ jobs: env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: Deploy to S3 (production/staging) - if: ${{ inputs.deploy-to-s3 && inputs.environment != 'adhoc' }} - run: | - aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist ${{ env.S3_BUCKET }}/ - aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association - aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association ${{ env.S3_BUCKET }}/apple-app-site-association - env: - S3_BUCKET: s3://${{ inputs.environment == 'staging' && 'staging-' || '' }}${{ vars.PRODUCTION_S3_BUCKET }} - - - name: Deploy to S3 (adhoc) - if: ${{ inputs.deploy-to-s3 && inputs.environment == 'adhoc' && inputs.pull-request-number != '' }} - run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://ad-hoc-expensify-cash/web/"$PULL_REQUEST_NUMBER" - - - name: Set S3 URL output - id: set-s3-url - if: ${{ inputs.environment == 'adhoc' && inputs.pull-request-number != '' }} - run: echo "S3_URL=https://${{ inputs.pull-request-number }}.pr-testing.expensify.com" >> "$GITHUB_OUTPUT" - - - name: Purge Cloudflare cache - if: ${{ inputs.environment != 'adhoc' }} - run: | - /home/runner/.local/bin/cli4 --verbose --delete hosts=["$HOST"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache - env: - CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} - HOST: ${{ inputs.environment == 'production' && vars.WEB_PRODUCTION_HOST || vars.WEB_STAGING_HOST }} - - - name: Verify deploy - if: ${{ inputs.environment != 'adhoc' }} - run: | - APP_VERSION=$(jq -r .version < package.json) - ./.github/scripts/verifyDeploy.sh "$HOST" "$APP_VERSION" - env: - HOST: ${{ inputs.environment == 'production' && vars.WEB_PRODUCTION_HOST || vars.WEB_STAGING_HOST }} - - name: Upload web sourcemaps artifact # v6 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f @@ -124,13 +62,11 @@ jobs: path: ./dist/merged-source-map.js.map - name: Compress web build .tar.gz and .zip - if: ${{ inputs.upload-artifacts }} run: | tar -czvf webBuild.tar.gz dist zip -r webBuild.zip dist - name: Upload .tar.gz web build artifact - if: ${{ inputs.upload-artifacts }} # v6 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: @@ -138,7 +74,6 @@ jobs: path: ./webBuild.tar.gz - name: Upload .zip web build artifact - if: ${{ inputs.upload-artifacts }} # v6 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ea108585d8fd..c7e45925d4be 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -518,17 +518,69 @@ jobs: "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36005" \ -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}" - web: - name: Build and deploy Web + webBuild: + name: Build Web needs: [prep] uses: ./.github/workflows/buildWeb.yml with: ref: ${{ github.ref }} environment: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} - deploy-to-s3: true - upload-artifacts: true secrets: inherit + webDeploy: + name: Deploy Web to S3 + needs: [prep, webBuild] + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.ref }} + + - name: Download web build artifact + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + with: + name: web-build-tar-gz-artifact + path: ./ + + - name: Extract web build + run: tar -xzvf webBuild.tar.gz + + - name: Setup Cloudflare CLI + run: pip3 install cloudflare==2.19.0 + + - name: Configure AWS Credentials + # v6 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Deploy to S3 + run: | + aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist ${{ env.S3_BUCKET }}/ + aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association + aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association ${{ env.S3_BUCKET }}/apple-app-site-association + env: + S3_BUCKET: s3://${{ github.ref == 'refs/heads/staging' && 'staging-' || '' }}${{ vars.PRODUCTION_S3_BUCKET }} + + - name: Purge Cloudflare cache + run: | + /home/runner/.local/bin/cli4 --verbose --delete hosts=["$HOST"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache + env: + CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} + HOST: ${{ github.ref == 'refs/heads/production' && vars.WEB_PRODUCTION_HOST || vars.WEB_STAGING_HOST }} + + - name: Verify deploy + run: | + APP_VERSION=$(jq -r .version < package.json) + ./.github/scripts/verifyDeploy.sh "$HOST" "$APP_VERSION" + env: + HOST: ${{ github.ref == 'refs/heads/production' && vars.WEB_PRODUCTION_HOST || vars.WEB_STAGING_HOST }} + buildStorybook: name: Build storybook docs needs: [prep] @@ -556,7 +608,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: blacksmith-2vcpu-ubuntu-2404 if: ${{ failure() }} - needs: [androidBuild, androidUploadGooglePlay, androidUploadBrowserStack, androidUploadApplause, androidSubmit, iosBuild, iosUploadTestflight, iosUploadBrowserStack, iosUploadApplause, iosSubmit, web] + needs: [androidBuild, androidUploadGooglePlay, androidUploadBrowserStack, androidUploadApplause, androidSubmit, iosBuild, iosUploadTestflight, iosUploadBrowserStack, iosUploadApplause, iosSubmit, webDeploy] steps: - name: Checkout # v4 @@ -575,7 +627,7 @@ jobs: ANDROID_RESULT: ${{ steps.platformResults.outputs.ANDROID_RESULT }} IOS_RESULT: ${{ steps.platformResults.outputs.IOS_RESULT }} WEB_RESULT: ${{ steps.platformResults.outputs.WEB_RESULT }} - needs: [androidBuild, androidUploadGooglePlay, androidSubmit, iosBuild, iosUploadTestflight, iosSubmit, web] + needs: [androidBuild, androidUploadGooglePlay, androidSubmit, iosBuild, iosUploadTestflight, iosSubmit, webDeploy] if: ${{ always() }} steps: # Determine effective result for each platform @@ -610,7 +662,7 @@ jobs: echo "IOS_RESULT=${{ needs.iosUploadTestflight.result }}" >> "$GITHUB_OUTPUT" fi - echo "WEB_RESULT=${{ needs.web.result }}" >> "$GITHUB_OUTPUT" + echo "WEB_RESULT=${{ needs.webDeploy.result }}" >> "$GITHUB_OUTPUT" - name: Check deployment success on at least one platform id: checkDeploymentSuccessOnAtLeastOnePlatform @@ -768,7 +820,7 @@ jobs: name: Post a Slack message when all platforms deploy successfully runs-on: blacksmith-2vcpu-ubuntu-2404 if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }} - needs: [prep, androidUploadGooglePlay, androidSubmit, iosUploadTestflight, iosSubmit, web, checkDeploymentSuccess, createRelease] + needs: [prep, androidUploadGooglePlay, androidSubmit, iosUploadTestflight, iosSubmit, webDeploy, checkDeploymentSuccess, createRelease] steps: - name: 'Announces the deploy in the #announce Slack room' # v3 From e1f4c2213f7420465a240eaa4ded364fe4ff68d9 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 16:48:33 -0800 Subject: [PATCH 47/52] Consolidate iOS provisioning profile and ExportOptions steps Merge the variant-specific 1Password and ExportOptions.plist steps into single steps with conditionals, reducing duplication. Co-authored-by: Cursor --- .github/workflows/buildIOS.yml | 41 ++++++++++++++-------------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/.github/workflows/buildIOS.yml b/.github/workflows/buildIOS.yml index 6ff71e78cebb..4b20a2fdacbd 100644 --- a/.github/workflows/buildIOS.yml +++ b/.github/workflows/buildIOS.yml @@ -123,31 +123,26 @@ jobs: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} SHOULD_LOAD_SSL_CERTIFICATES: 'false' - - name: Load Release provisioning profiles from 1Password - if: ${{ inputs.variant == 'Release' }} + - name: Load provisioning profiles from 1Password env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} run: | - op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json - op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore/${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }} - op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Share_Extension/${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }} - op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Notification_Service/${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }} - op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 - - - name: Load AdHoc provisioning profiles from 1Password - if: ${{ inputs.variant == 'Adhoc' }} - env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - run: | - op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc/OldApp_AdHoc.mobileprovision" --force --out-file ./OldApp_AdHoc.mobileprovision - op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Share_Extension/OldApp_AdHoc_Share_Extension.mobileprovision" --force --out-file ./OldApp_AdHoc_Share_Extension.mobileprovision - op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Notification_Service/OldApp_AdHoc_Notification_Service.mobileprovision" --force --out-file ./OldApp_AdHoc_Notification_Service.mobileprovision + if [ "${{ inputs.variant }}" == "Release" ]; then + op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json + op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore/${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }} + op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Share_Extension/${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }} + op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Notification_Service/${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }} + else + op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc/OldApp_AdHoc.mobileprovision" --force --out-file ./OldApp_AdHoc.mobileprovision + op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Share_Extension/OldApp_AdHoc_Share_Extension.mobileprovision" --force --out-file ./OldApp_AdHoc_Share_Extension.mobileprovision + op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Notification_Service/OldApp_AdHoc_Notification_Service.mobileprovision" --force --out-file ./OldApp_AdHoc_Notification_Service.mobileprovision + fi op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 - - name: Create ExportOptions.plist for Release - if: ${{ inputs.variant == 'Release' }} + - name: Create ExportOptions.plist run: | - cat > Mobile-Expensify/iOS/ExportOptions.plist << EOF + if [ "${{ inputs.variant }}" == "Release" ]; then + cat > Mobile-Expensify/iOS/ExportOptions.plist << EOF @@ -166,11 +161,8 @@ jobs: EOF - - - name: Create ExportOptions.plist for AdHoc - if: ${{ inputs.variant == 'Adhoc' }} - run: | - cat > Mobile-Expensify/iOS/ExportOptions.plist << 'EOF' + else + cat > Mobile-Expensify/iOS/ExportOptions.plist << 'EOF' @@ -189,6 +181,7 @@ jobs: EOF + fi - name: Get iOS native version id: getIOSVersion From c7dd51a9230e1da5fd2d874560f303d3c7602072 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 17:03:27 -0800 Subject: [PATCH 48/52] Include storybook docs in web deploy via artifact handoff buildStorybook uploads its output (dist/docs) as an artifact. webDeploy now depends on buildStorybook and downloads the docs into dist/docs before deploying to S3, preserving the original behavior. The download uses continue-on-error since buildStorybook itself is non-fatal. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c7e45925d4be..e5ae07d51e4f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -529,7 +529,8 @@ jobs: webDeploy: name: Deploy Web to S3 - needs: [prep, webBuild] + needs: [prep, webBuild, buildStorybook] + if: ${{ always() && needs.webBuild.result == 'success' }} runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout @@ -548,6 +549,14 @@ jobs: - name: Extract web build run: tar -xzvf webBuild.tar.gz + - name: Download storybook docs artifact + continue-on-error: true + # v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + with: + name: storybook-docs-artifact + path: ./dist/docs + - name: Setup Cloudflare CLI run: pip3 install cloudflare==2.19.0 @@ -604,6 +613,13 @@ jobs: npm run storybook-build-staging fi + - name: Upload storybook docs artifact + # v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: storybook-docs-artifact + path: ./dist/docs + postSlackMessageOnFailure: name: Post a Slack message when any platform fails to build or deploy runs-on: blacksmith-2vcpu-ubuntu-2404 From ab6ed8e909c033b135dda8de71b36a77e95bbfb4 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 17:08:24 -0800 Subject: [PATCH 49/52] Address review comments: pin builds to SHA, strip TestFlight job - Pin reusable build workflow refs to github.sha instead of github.ref to prevent artifact/version mismatches if the branch moves between prep and build execution. - Remove Node, MapBox, and CocoaPods setup from iosUploadTestflight since it only uploads a pre-built IPA via Fastlane. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e5ae07d51e4f..d5da2073f63b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -99,7 +99,7 @@ jobs: if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} uses: ./.github/workflows/buildAndroid.yml with: - ref: ${{ github.ref }} + ref: ${{ github.sha }} variant: Release secrets: inherit @@ -290,7 +290,7 @@ jobs: if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} uses: ./.github/workflows/buildIOS.yml with: - ref: ${{ github.ref }} + ref: ${{ github.sha }} variant: Release secrets: inherit @@ -356,31 +356,6 @@ jobs: op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json - - name: Setup Node - id: setup-node - uses: ./.github/actions/composite/setupNode - with: - IS_HYBRID_BUILD: 'true' - - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - - name: Cache Pod dependencies - # v5.0.1 - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb - id: pods-cache - with: - path: Mobile-Expensify/iOS/Pods - key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') }} - - - name: Install cocoapods - if: ${{ steps.pods-cache.outputs.cache-hit != 'true' }} - uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 - with: - timeout_minutes: 10 - max_attempts: 5 - command: npm run pod-install - - name: Upload release build to TestFlight run: bundle exec fastlane ios upload_testflight_hybrid env: @@ -523,7 +498,7 @@ jobs: needs: [prep] uses: ./.github/workflows/buildWeb.yml with: - ref: ${{ github.ref }} + ref: ${{ github.sha }} environment: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} secrets: inherit From e6a588b3b4364f1e468ea13811636722706ebc74 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 20 Feb 2026 17:31:15 -0800 Subject: [PATCH 50/52] Propagate webBuild failure into WEB_RESULT When webBuild fails, webDeploy is skipped, so WEB_RESULT was reported as 'skipped' instead of 'failure'. Now checkDeploymentSuccess and postSlackMessageOnFailure depend on webBuild directly and propagate its failure into WEB_RESULT. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d5da2073f63b..08d48b96cdd8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -599,7 +599,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: blacksmith-2vcpu-ubuntu-2404 if: ${{ failure() }} - needs: [androidBuild, androidUploadGooglePlay, androidUploadBrowserStack, androidUploadApplause, androidSubmit, iosBuild, iosUploadTestflight, iosUploadBrowserStack, iosUploadApplause, iosSubmit, webDeploy] + needs: [androidBuild, androidUploadGooglePlay, androidUploadBrowserStack, androidUploadApplause, androidSubmit, iosBuild, iosUploadTestflight, iosUploadBrowserStack, iosUploadApplause, iosSubmit, webBuild, webDeploy] steps: - name: Checkout # v4 @@ -618,7 +618,7 @@ jobs: ANDROID_RESULT: ${{ steps.platformResults.outputs.ANDROID_RESULT }} IOS_RESULT: ${{ steps.platformResults.outputs.IOS_RESULT }} WEB_RESULT: ${{ steps.platformResults.outputs.WEB_RESULT }} - needs: [androidBuild, androidUploadGooglePlay, androidSubmit, iosBuild, iosUploadTestflight, iosSubmit, webDeploy] + needs: [androidBuild, androidUploadGooglePlay, androidSubmit, iosBuild, iosUploadTestflight, iosSubmit, webBuild, webDeploy] if: ${{ always() }} steps: # Determine effective result for each platform @@ -653,7 +653,12 @@ jobs: echo "IOS_RESULT=${{ needs.iosUploadTestflight.result }}" >> "$GITHUB_OUTPUT" fi - echo "WEB_RESULT=${{ needs.webDeploy.result }}" >> "$GITHUB_OUTPUT" + # Web: propagate build failure even when deploy is skipped + if [ "${{ needs.webBuild.result }}" == "failure" ]; then + echo "WEB_RESULT=failure" >> "$GITHUB_OUTPUT" + else + echo "WEB_RESULT=${{ needs.webDeploy.result }}" >> "$GITHUB_OUTPUT" + fi - name: Check deployment success on at least one platform id: checkDeploymentSuccessOnAtLeastOnePlatform From dd839f3a17566aec909b936a6c0a5c8af68e0b88 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 21 Feb 2026 13:27:24 -0800 Subject: [PATCH 51/52] Pin all deploy checkouts to github.sha, skip Dependabot verify builds - Pin webDeploy and buildStorybook checkouts to github.sha so deploy verification reads the same commit that was built, not a newer branch tip that may have landed in the interim. - Skip verifyHybridApp build jobs for Dependabot PRs since they lack the repository secrets required by 1Password/AWS setup steps. Co-authored-by: Cursor --- .github/workflows/deploy.yml | 4 ++-- .github/workflows/verifyHybridApp.yml | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 08d48b96cdd8..b02d795c91c8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -512,7 +512,7 @@ jobs: # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - ref: ${{ github.ref }} + ref: ${{ github.sha }} - name: Download web build artifact # v7 @@ -575,7 +575,7 @@ jobs: # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - ref: ${{ github.ref }} + ref: ${{ github.sha }} - name: Setup Node uses: ./.github/actions/composite/setupNode diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index 665d10a57550..39fb5f608114 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -75,8 +75,7 @@ jobs: verify_android: name: Verify Android HybridApp builds on main - # Only run on pull requests that are *not* on a fork - if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' }} + if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' }} uses: ./.github/workflows/buildAndroid.yml with: ref: ${{ github.event.pull_request.head.sha }} @@ -85,8 +84,7 @@ jobs: verify_ios: name: Verify iOS HybridApp builds on main - # Only run on pull requests that are *not* on a fork - if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' }} + if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' }} uses: ./.github/workflows/buildIOS.yml with: ref: ${{ github.event.pull_request.head.sha }} From 1b459bdfddd708f31247b58ea271912408335bf0 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 21 Feb 2026 14:19:54 -0800 Subject: [PATCH 52/52] Propagate buildWeb failure into ad-hoc web status reporting When buildWeb fails, deployWebAdHoc is skipped, so PR comments and build summaries reported Web as 'skipped' instead of 'failure'. Add buildWeb to the needs of postGithubComment and buildSummary, and propagate its failure into the WEB status. Co-authored-by: Cursor --- .github/workflows/buildAdHoc.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/buildAdHoc.yml b/.github/workflows/buildAdHoc.yml index 4f2471c731b7..03f5450e17c4 100644 --- a/.github/workflows/buildAdHoc.yml +++ b/.github/workflows/buildAdHoc.yml @@ -137,7 +137,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 if: ${{ always() && (inputs.APP_PR_NUMBER != '' || inputs.MOBILE_EXPENSIFY_PR != '') }} name: Post a GitHub comment with app download links for testing - needs: [deployWebAdHoc, buildAndroid, buildIOS] + needs: [buildWeb, deployWebAdHoc, buildAndroid, buildIOS] steps: - name: Checkout # v6 @@ -159,7 +159,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} ANDROID: ${{ needs.buildAndroid.result }} IOS: ${{ needs.buildIOS.result }} - WEB: ${{ needs.deployWebAdHoc.result }} + WEB: ${{ needs.buildWeb.result == 'failure' && 'failure' || needs.deployWebAdHoc.result }} ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }} IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }} WEB_LINK: https://${{ inputs.APP_PR_NUMBER }}.pr-testing.expensify.com @@ -180,7 +180,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 if: ${{ always() && (inputs.APP_PR_NUMBER != '' || needs.buildAndroid.outputs.ROCK_ARTIFACT_URL != '' || needs.buildIOS.outputs.ROCK_ARTIFACT_URL != '') }} name: Build Summary - needs: [deployWebAdHoc, buildAndroid, buildIOS] + needs: [buildWeb, deployWebAdHoc, buildAndroid, buildIOS] steps: - name: Checkout # v6 @@ -210,7 +210,8 @@ jobs: const iosLink = '${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }}' || ''; const mobileExpensifySHA = '${{ steps.getMobileExpensifySHA.outputs.SHA }}' || ''; - const webStatus = webLink ? '✅ Success' : '${{ needs.deployWebAdHoc.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; + const webFailed = '${{ needs.buildWeb.result }}' === 'failure' || '${{ needs.deployWebAdHoc.result }}' === 'failure'; + const webStatus = webLink ? '✅ Success' : webFailed ? '❌ Failed' : '⏭️ Skipped'; const androidStatus = androidLink ? '✅ Success' : '${{ needs.buildAndroid.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped'; const iosStatus = iosLink ? '✅ Success' : '${{ needs.buildIOS.result }}' === 'failure' ? '❌ Failed' : '⏭️ Skipped';