Merge pull request #87651 from Krishna2323/krishna2323/issue/87644 #5417
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy code to staging or production | |
| on: | |
| push: | |
| branches: [staging, production] | |
| env: | |
| IS_APP_REPO: ${{ github.repository == 'Expensify/App' }} | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| prep: | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| outputs: | |
| APP_VERSION: ${{ steps.getAppVersion.outputs.VERSION }} | |
| TAG: ${{ steps.getTagName.outputs.TAG }} | |
| # Is this deploy for a cherry-pick? | |
| 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 | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| token: ${{ secrets.OS_BOTIFY_TOKEN }} | |
| submodules: true | |
| - name: Validate actor | |
| id: validateActor | |
| uses: ./.github/actions/composite/validateActor | |
| with: | |
| OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} | |
| - name: Setup git for OSBotify | |
| uses: Expensify/GitHub-Actions/setupGitForOSBotify@main | |
| id: setupGitForOSBotify | |
| with: | |
| OP_VAULT: ${{ vars.OP_VAULT }} | |
| OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} | |
| OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} | |
| OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} | |
| - name: Get app version | |
| id: getAppVersion | |
| run: echo "VERSION=$(jq -r .version < package.json)" >> "$GITHUB_OUTPUT" | |
| - name: Get tag | |
| id: getTagName | |
| run: echo "TAG=${{ github.ref == 'refs/heads/production' && steps.getAppVersion.outputs.VERSION || format('{0}-staging', steps.getAppVersion.outputs.VERSION) }}" >> "$GITHUB_OUTPUT" | |
| - name: Create and push tag | |
| run: | | |
| git tag ${{ steps.getTagName.outputs.TAG }} | |
| git push origin --tags | |
| cd Mobile-Expensify | |
| git tag ${{ steps.getTagName.outputs.TAG }} | |
| git push origin --tags | |
| # We use JS here instead of bash/jq because inlining potentially large json into a bash command is non-trivial. | |
| # JS is better at handling JSON: https://stackoverflow.com/questions/72953526/github-actions-how-to-pass-tojson-result-to-shell-commands | |
| - name: Check if this deploy was triggered by a cherry-pick | |
| id: isCherryPick | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| with: | |
| script: | | |
| const commitMessages = context.payload.commits.map((commit) => commit.message); | |
| const isCherryPick = commitMessages.some((message) => /.*\(cherry-picked to .* by .*\)$/.test(message)); | |
| console.log('Is cherry pick?', isCherryPick); | |
| core.setOutput( | |
| 'IS_CHERRY_PICK', | |
| 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 | |
| uses: ./.github/workflows/createDeployChecklist.yml | |
| if: ${{ github.ref == 'refs/heads/staging' }} | |
| needs: prep | |
| secrets: inherit | |
| androidBuild: | |
| name: Build Android HybridApp | |
| needs: [prep] | |
| if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} | |
| uses: ./.github/workflows/buildAndroid.yml | |
| with: | |
| ref: ${{ github.sha }} | |
| variant: Release | |
| secrets: inherit | |
| androidUploadGooglePlay: | |
| name: Upload Android to Google Play | |
| needs: [prep, androidBuild] | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} | |
| steps: | |
| - name: Checkout | |
| # v6 | |
| uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 | |
| - name: Download Android build artifact | |
| # v7 | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 | |
| with: | |
| name: androidBuild-artifact | |
| path: ./ | |
| - name: Set aabPath for Fastlane | |
| run: | | |
| AAB_PATH="$(pwd)/${{ needs.androidBuild.outputs.AAB_FILENAME }}" | |
| if [ ! -f "$AAB_PATH" ]; then | |
| echo "::error::Expected AAB not found at $AAB_PATH" | |
| exit 1 | |
| fi | |
| echo "aabPath=$AAB_PATH" >> "$GITHUB_ENV" | |
| - 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.prep.outputs.VERSION_CODE }} | |
| ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME }} | |
| androidSubmit: | |
| name: Submit Android for production rollout | |
| needs: [prep, androidBuild, androidUploadGooglePlay] | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| if: ${{ always() && !cancelled() && github.ref == 'refs/heads/production' && needs.androidBuild.result != 'failure' && needs.androidUploadGooglePlay.result != 'failure' }} | |
| 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 | |
| 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: ${{ !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 | |
| run: bundle exec fastlane android upload_google_play_production_hybrid_rollout | |
| env: | |
| VERSION: ${{ needs.prep.outputs.VERSION_CODE }} | |
| - name: Warn deployers if Android production deploy failed | |
| if: ${{ failure() }} | |
| # v3 | |
| uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e | |
| with: | |
| status: custom | |
| custom_payload: | | |
| { | |
| channel: '#deployer', | |
| attachments: [{ | |
| color: "#DB4545", | |
| pretext: `<!subteam^S4TJJ3PSL>`, | |
| text: `💥 Android HybridApp production <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|deploy run> failed. Please <https://stackoverflowteams.com/c/expensify/questions/5738|manually submit> ${{ needs.prep.outputs.APP_VERSION }} in the <https://play.google.com/console/u/0/developers/8765590895836334604/app/4974129597497161901/releases/overview|Google Play Store> 💥`, | |
| }] | |
| } | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
| androidUploadBrowserStack: | |
| name: Upload Android to BrowserStack | |
| needs: [prep, androidBuild] | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} | |
| continue-on-error: true | |
| steps: | |
| - 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: | | |
| APK_PATH="$(pwd)/${{ needs.androidBuild.outputs.APK_FILENAME }}" | |
| if [ ! -f "$APK_PATH" ]; then | |
| echo "::error::Expected APK not found at $APK_PATH" | |
| exit 1 | |
| fi | |
| echo "APK_PATH=$APK_PATH" >> "$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: blacksmith-2vcpu-ubuntu-2404 | |
| 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 | |
| # v7 | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 | |
| 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 Applause API key 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: Find APK path | |
| id: find-apk | |
| run: | | |
| APK_PATH="$(pwd)/${{ needs.androidBuild.outputs.APK_FILENAME }}" | |
| if [ ! -f "$APK_PATH" ]; then | |
| echo "::error::Expected APK not found at $APK_PATH" | |
| exit 1 | |
| fi | |
| echo "APK_PATH=$APK_PATH" >> "$GITHUB_OUTPUT" | |
| - name: Upload Android build to Applause | |
| 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 }}" \ | |
| "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36008" \ | |
| -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}" | |
| iosBuild: | |
| name: Build iOS HybridApp | |
| needs: [prep] | |
| if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} | |
| uses: ./.github/workflows/buildIOS.yml | |
| with: | |
| ref: ${{ github.sha }} | |
| variant: Release | |
| secrets: inherit | |
| iosUploadTestflight: | |
| 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: | |
| - name: Checkout | |
| # v6 | |
| uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 | |
| with: | |
| token: ${{ secrets.OS_BOTIFY_TOKEN }} | |
| submodules: true | |
| - name: Download iOS build artifact | |
| # v7 | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 | |
| with: | |
| name: iosBuild-artifact | |
| 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: | | |
| IPA_PATH="$(pwd)/${{ needs.iosBuild.outputs.IPA_FILENAME }}" | |
| if [ ! -f "$IPA_PATH" ]; then | |
| echo "::error::Expected IPA not found at $IPA_PATH" | |
| exit 1 | |
| fi | |
| echo "ipaPath=$IPA_PATH" >> "$GITHUB_ENV" | |
| DSYM_PATH="$(pwd)/${{ needs.iosBuild.outputs.DSYM_FILENAME }}" | |
| if [ ! -f "$DSYM_PATH" ]; then | |
| echo "::warning::Expected dSYM not found at $DSYM_PATH" | |
| fi | |
| echo "dsymPath=$DSYM_PATH" >> "$GITHUB_ENV" | |
| - 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: Upload release build to TestFlight | |
| run: bundle exec fastlane ios upload_testflight_hybrid | |
| env: | |
| APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} | |
| APPLE_CONTACT_PHONE: ${{ secrets.APPLE_CONTACT_PHONE }} | |
| APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }} | |
| APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }} | |
| APPLE_ID: ${{ vars.APPLE_ID }} | |
| iosSubmit: | |
| name: Submit iOS for production rollout | |
| needs: [prep, iosBuild, iosUploadTestflight] | |
| runs-on: macos-15-xlarge | |
| 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: | |
| - 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% | |
| run: bundle exec fastlane ios complete_hybrid_rollout | |
| continue-on-error: true | |
| - name: Submit production build for App Store review and a slow rollout | |
| run: bundle exec fastlane ios submit_hybrid_for_rollout | |
| env: | |
| VERSION: ${{ needs.prep.outputs.IOS_VERSION }} | |
| APPLE_ID: ${{ vars.APPLE_ID }} | |
| - name: Warn deployers if iOS production deploy failed | |
| if: ${{ failure() }} | |
| # v3 | |
| uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e | |
| with: | |
| status: custom | |
| custom_payload: | | |
| { | |
| channel: '#deployer', | |
| attachments: [{ | |
| color: "#DB4545", | |
| pretext: `<!subteam^S4TJJ3PSL>`, | |
| text: `💥 iOS HybridApp production deploy failed. Please <https://stackoverflowteams.com/c/expensify/questions/5740|manually submit> ${{ needs.prep.outputs.IOS_VERSION }} in the <https://appstoreconnect.apple.com/apps/471713959/appstore|App Store>. 💥`, | |
| }] | |
| } | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
| iosUploadBrowserStack: | |
| name: Upload iOS to BrowserStack | |
| needs: [prep, iosBuild] | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }} | |
| continue-on-error: true | |
| steps: | |
| - name: Download iOS build artifact | |
| # v7 | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 | |
| with: | |
| name: iosBuild-artifact | |
| path: ./ | |
| - name: Find IPA path | |
| id: find-ipa | |
| run: | | |
| IPA_PATH="$(pwd)/${{ needs.iosBuild.outputs.IPA_FILENAME }}" | |
| if [ ! -f "$IPA_PATH" ]; then | |
| echo "::error::Expected IPA not found at $IPA_PATH" | |
| exit 1 | |
| fi | |
| echo "IPA_PATH=$IPA_PATH" >> "$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: blacksmith-2vcpu-ubuntu-2404 | |
| 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 | |
| # v7 | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 | |
| with: | |
| name: iosBuild-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 Applause API key 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: Find IPA path | |
| id: find-ipa | |
| run: | | |
| IPA_PATH="$(pwd)/${{ needs.iosBuild.outputs.IPA_FILENAME }}" | |
| if [ ! -f "$IPA_PATH" ]; then | |
| echo "::error::Expected IPA not found at $IPA_PATH" | |
| exit 1 | |
| fi | |
| echo "IPA_PATH=$IPA_PATH" >> "$GITHUB_OUTPUT" | |
| - name: Upload iOS build to Applause | |
| 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 }}" \ | |
| "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36005" \ | |
| -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}" | |
| webBuild: | |
| name: Build Web | |
| needs: [prep] | |
| uses: ./.github/workflows/buildWeb.yml | |
| with: | |
| ref: ${{ github.sha }} | |
| environment: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} | |
| secrets: inherit | |
| webDeploy: | |
| name: Deploy Web to S3 | |
| needs: [prep, webBuild, buildStorybook] | |
| if: ${{ always() && needs.webBuild.result == 'success' }} | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| steps: | |
| - name: Checkout | |
| # v6 | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| ref: ${{ github.sha }} | |
| - 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 "${{ needs.webBuild.outputs.TAR_FILENAME }}" | |
| - 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 | |
| - 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] | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| continue-on-error: true | |
| steps: | |
| - name: Checkout | |
| # v6 | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| ref: ${{ github.sha }} | |
| - 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 | |
| - 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 | |
| if: ${{ failure() }} | |
| needs: [androidBuild, androidUploadGooglePlay, androidUploadBrowserStack, androidUploadApplause, androidSubmit, iosBuild, iosUploadTestflight, iosUploadBrowserStack, iosUploadApplause, iosSubmit, webBuild, webDeploy] | |
| steps: | |
| - name: Checkout | |
| # v6 | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| - name: Post Slack message on failure | |
| uses: ./.github/actions/composite/announceFailedWorkflowInSlack | |
| with: | |
| SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} | |
| checkDeploymentSuccess: | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| 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 }} | |
| 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, webBuild, webDeploy] | |
| if: ${{ always() }} | |
| steps: | |
| # Determine effective result for each platform | |
| - name: Determine platform results | |
| id: platformResults | |
| run: | | |
| # 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 | |
| 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. | |
| # On production cherry-picks, propagate build/upload failures that caused submit to be skipped. | |
| if [ "${{ github.ref }}" == "refs/heads/production" ]; then | |
| 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 | |
| echo "IOS_RESULT=${{ needs.iosUploadTestflight.result }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| # 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 | |
| run: | | |
| isAtLeastOnePlatformDeployed="false" | |
| 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" | |
| echo "IS_AT_LEAST_ONE_PLATFORM_DEPLOYED is $isAtLeastOnePlatformDeployed" | |
| - name: Check deployment success on all platforms | |
| id: checkDeploymentSuccessOnAllPlatforms | |
| run: | | |
| isAllPlatformsDeployed="false" | |
| if [ "${{ steps.platformResults.outputs.IOS_RESULT }}" == "success" ] && \ | |
| [ "${{ steps.platformResults.outputs.ANDROID_RESULT }}" == "success" ] && \ | |
| [ "${{ steps.platformResults.outputs.WEB_RESULT }}" == "success" ]; then | |
| isAllPlatformsDeployed="true" | |
| fi | |
| echo "IS_ALL_PLATFORMS_DEPLOYED=$isAllPlatformsDeployed" >> "$GITHUB_OUTPUT" | |
| echo "IS_ALL_PLATFORMS_DEPLOYED is $isAllPlatformsDeployed" | |
| createRelease: | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} | |
| needs: [prep, checkDeploymentSuccess] | |
| permissions: | |
| contents: write | |
| steps: | |
| # v4.2.1 | |
| - name: Download all workflow run artifacts | |
| uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e | |
| - name: Get last production release | |
| id: get_last_prod_version | |
| run: echo "LAST_PROD_VERSION=$(gh release list --repo ${{ github.repository }} --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName')" >> "$GITHUB_OUTPUT" | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| - name: 🚀 Create release 🚀 | |
| run: | | |
| # Check if the release already exists | |
| if gh release view "${{ needs.prep.outputs.TAG }}" --repo "${{ github.repository }}" &> /dev/null; then | |
| echo "Release ${{ needs.prep.outputs.TAG }} already exists, skipping creating it again." | |
| else | |
| echo "Release ${{ needs.prep.outputs.TAG }} does not exist, creating it now." | |
| readonly CREATE_MAX_RETRIES=5 | |
| for ((i = 0; i <= CREATE_MAX_RETRIES; i++)); do | |
| if gh release create "${{ needs.prep.outputs.TAG }}" ${{ github.ref == 'refs/heads/staging' && '--prerelease' || '' }} \ | |
| --repo "${{ github.repository }}" \ | |
| --title "${{ needs.prep.outputs.TAG }}" \ | |
| ${{ github.ref == 'refs/heads/production' && format('--notes-start-tag {0}', steps.get_last_prod_version.outputs.LAST_PROD_VERSION) || '' }} \ | |
| --generate-notes \ | |
| --verify-tag \ | |
| --target "${{ github.ref }}"; then | |
| break | |
| fi | |
| if [[ $i -lt $CREATE_MAX_RETRIES ]]; then | |
| echo "Failed to create release. Retrying in 3 seconds... ($((CREATE_MAX_RETRIES - i)) attempts left)" | |
| sleep 3 | |
| else | |
| echo "Failed to create release after $((CREATE_MAX_RETRIES + 1)) attempts" | |
| exit 1 | |
| fi | |
| done | |
| readonly VIEW_MAX_RETRIES=10 | |
| for ((i = 0; i <= VIEW_MAX_RETRIES; i++)); do | |
| if gh release view "${{ needs.prep.outputs.TAG }}" --repo "${{ github.repository }}" &> /dev/null; then | |
| break | |
| fi | |
| if [[ $i -lt $VIEW_MAX_RETRIES ]]; then | |
| echo "Release ${{ needs.prep.outputs.TAG }} not yet visible after creation. Retrying... ($((VIEW_MAX_RETRIES - i)) attempts left)" | |
| sleep 1 | |
| else | |
| echo "Release ${{ needs.prep.outputs.TAG }} never became visible after $((VIEW_MAX_RETRIES + 1)) checks" | |
| exit 1 | |
| fi | |
| done | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| - name: Rename web sourcemaps artifacts before assets upload in order to have unique ReleaseAsset.name | |
| continue-on-error: true | |
| run: | | |
| mv ./web-sourcemaps-artifact/merged-source-map.js.map ./web-sourcemaps-artifact/web-merged-source-map.js.map | |
| - name: Upload artifacts to GitHub Release | |
| continue-on-error: true | |
| run: | | |
| # Release asset name should follow the template: fileNameOnRunner#fileNameInRelease | |
| files=" | |
| ./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 | |
| ./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 | |
| ./web-build-zip-artifact/webBuild.zip#web.zip | |
| " | |
| # Loop through each file and upload individually (so if one fails, we still have other platforms uploaded) | |
| # Note: Not all of these files are present for production releases, because we don't build the native apps for prod deploys. That's expected. | |
| echo -e "$files" | xargs -I {} --max-procs=4 bash -c ' | |
| if gh release upload ${{ needs.prep.outputs.TAG }} --repo ${{ github.repository }} --clobber {}; then | |
| echo "✅ Successfully uploaded {}" | |
| else | |
| echo "❌ Failed to upload {}" | |
| fi | |
| ' | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| - name: Warn deployers if deploy failed | |
| if: ${{ failure() }} | |
| # v3 | |
| uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e | |
| with: | |
| status: custom | |
| custom_payload: | | |
| { | |
| channel: '#deployer', | |
| attachments: [{ | |
| color: "#DB4545", | |
| pretext: `<!subteam^S4TJJ3PSL>`, | |
| text: `💥 NewDot ${{ github.ref == 'refs/heads/staging' && 'staging' || 'production' }} deploy failed. 💥`, | |
| }] | |
| } | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
| # Why is this necessary for CP-to-prod? Consider this scenario: | |
| # 1. You close a checklist and we create a new staging version `9.0.34-0` and a new checklist | |
| # 2. You then CP a PR to production, and in the process create `9.0.35-0` | |
| # 3. You close the new checklist, and we try to ship `9.0.34-0` to production. This won't work, because we already submitted a higher version `9.0-35-0` | |
| # | |
| # To address this, we'll: | |
| # 1. Bump the version on main again | |
| # 2. CP that version bump to staging | |
| cherryPickExtraVersionBump: | |
| needs: [prep, checkDeploymentSuccess] | |
| if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) && github.ref == 'refs/heads/production' && fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} | |
| uses: ./.github/workflows/cherryPick.yml | |
| secrets: inherit | |
| with: | |
| # Note: by omitting PULL_REQUEST_URL, we are just doing a version bump and CP'ing it to staging | |
| TARGET: staging | |
| postSlackMessageOnSuccess: | |
| 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, webDeploy, checkDeploymentSuccess, createRelease] | |
| steps: | |
| - name: 'Announces the deploy in the #announce Slack room' | |
| # v3 | |
| uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e | |
| with: | |
| status: custom | |
| custom_payload: | | |
| { | |
| channel: '#announce', | |
| attachments: [{ | |
| color: 'good', | |
| text: `🎉️ Successfully deployed ${process.env.AS_REPO} <https://github.com/Expensify/App/releases/tag/${{ needs.prep.outputs.TAG }}|${{ needs.prep.outputs.TAG }}> to ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} 🎉️`, | |
| }] | |
| } | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
| - name: 'Announces the deploy in the #deployer Slack room' | |
| # v3 | |
| uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e | |
| with: | |
| status: custom | |
| custom_payload: | | |
| { | |
| channel: '#deployer', | |
| attachments: [{ | |
| color: 'good', | |
| text: `🎉️ Successfully deployed ${process.env.AS_REPO} <https://github.com/Expensify/App/releases/tag/${{ needs.prep.outputs.TAG }}|${{ needs.prep.outputs.TAG }}> to ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} 🎉️`, | |
| }] | |
| } | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
| - name: 'Announces a production deploy in the #expensify-open-source Slack room' | |
| # v3 | |
| uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e | |
| if: ${{ github.ref == 'refs/heads/production' }} | |
| with: | |
| status: custom | |
| custom_payload: | | |
| { | |
| channel: '#expensify-open-source', | |
| attachments: [{ | |
| color: 'good', | |
| text: `🎉️ Successfully deployed ${process.env.AS_REPO} <https://github.com/Expensify/App/releases/tag/${{ needs.prep.outputs.TAG }}|${{ needs.prep.outputs.TAG }}> to production 🎉️`, | |
| }] | |
| } | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
| postGithubComments: | |
| uses: ./.github/workflows/postDeployComments.yml | |
| if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} | |
| needs: [prep, checkDeploymentSuccess, createRelease, androidBuild, iosBuild] | |
| secrets: inherit | |
| with: | |
| version: ${{ needs.prep.outputs.APP_VERSION }} | |
| env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} | |
| android: ${{ needs.checkDeploymentSuccess.outputs.ANDROID_RESULT }} | |
| ios: ${{ needs.checkDeploymentSuccess.outputs.IOS_RESULT }} | |
| web: ${{ needs.checkDeploymentSuccess.outputs.WEB_RESULT }} | |
| android_sentry_url: ${{ needs.androidBuild.outputs.SENTRY_URL }} | |
| ios_sentry_url: ${{ needs.iosBuild.outputs.SENTRY_URL }} |