Merge pull request #61286 from ChavdaSachin/fix/login-email-auto-unfocus #3988
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: ubuntu-latest | |
| 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 }} | |
| steps: | |
| - name: Checkout | |
| # v4 | |
| uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 | |
| 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@e7aeb8c663f696059ebb5f9ab1425ed2ef511bdb | |
| 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, | |
| ); | |
| # 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 | |
| android: | |
| name: Build and deploy Android HybridApp | |
| needs: prep | |
| runs-on: ubuntu-latest-xl | |
| steps: | |
| - name: Checkout App and Mobile-Expensify repo | |
| # v4 | |
| uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 | |
| 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: Setup Java | |
| # v4 | |
| uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 | |
| with: | |
| distribution: 'oracle' | |
| java-version: '17' | |
| - name: Setup Ruby | |
| # v1.229.0 | |
| uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 | |
| with: | |
| bundler-cache: true | |
| - name: Install New Expensify Gems | |
| run: bundle install | |
| - name: Install 1Password CLI | |
| # v1 | |
| uses: 1password/install-cli-action@143a85f84a90555d121cde2ff5872e393a47ab9f | |
| - 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 | |
| # 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 | |
| if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} | |
| run: bundle exec fastlane android build_hybrid | |
| 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 }} | |
| - 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: ${{ 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 | |
| - name: Submit production build for Google Play review and a slow rollout | |
| if: ${{ github.ref == 'refs/heads/production' }} | |
| run: | | |
| # Complete the previous version rollout if the current rollout percentage is not -1 (no rollout in progress) or 1 (fully rolled out) | |
| echo "Current rollout percentage: ${{ steps.getAndroidRolloutPercentage.outputs.CURRENT_ROLLOUT_PERCENTAGE }}" | |
| if [[ ${{ steps.getAndroidRolloutPercentage.outputs.CURRENT_ROLLOUT_PERCENTAGE }} != '-1' && ${{ steps.getAndroidRolloutPercentage.outputs.CURRENT_ROLLOUT_PERCENTAGE }} != '1' ]]; then | |
| echo "Completing the previous version rollout" | |
| bundle exec fastlane android complete_hybrid_rollout | |
| else | |
| echo "Skipping the completion of the previous version rollout" | |
| fi | |
| # Submit the new version for review and slow rollout when it's approved | |
| 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: Generate APK from AAB | |
| run: | | |
| json=$(curl -s https://api.github.com/repos/google/bundletool/releases/latest) | |
| downloadUrl=$(echo "$json" | jq -r ".assets | .[].browser_download_url") | |
| curl "$downloadUrl" -4 -sL -o 'bundletool.jar' | |
| 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 -p Expensify.apks universal.apk > Expensify.apk | |
| - name: Upload Android APK build artifact | |
| if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} | |
| # v4 | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: android-apk-artifact | |
| path: Expensify.apk | |
| - name: Upload Android build artifact | |
| if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} | |
| # v4 | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: android-build-artifact | |
| path: ${{ env.aabPath }} | |
| - name: Upload Android sourcemap artifact | |
| if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} | |
| # v4 | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| 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" | |
| - name: Warn deployers if Android production deploy failed | |
| if: ${{ failure() && github.ref == 'refs/heads/production' }} | |
| # v3 | |
| uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e | |
| with: | |
| status: custom | |
| custom_payload: | | |
| { | |
| channel: '#deployer', | |
| attachments: [{ | |
| color: "#DB4545", | |
| pretext: `<!subteam^S4TJJ3PSL>`, | |
| text: `💥 Android HybridApp production deploy failed. No need to do anything, Jules will <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 }} | |
| desktop: | |
| name: Build and deploy Desktop | |
| needs: prep | |
| runs-on: macos-14-large | |
| steps: | |
| - name: Checkout | |
| # v4 | |
| uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 | |
| - name: Setup Node | |
| uses: ./.github/actions/composite/setupNode | |
| with: | |
| IS_DESKTOP_BUILD: true | |
| - name: Load Desktop 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 }} | |
| DESKTOP_CERTIFICATE_BASE64: "op://${{ vars.OP_VAULT }}/Desktop Certificates.p12/CSC_LINK" | |
| DESKTOP_CERTIFICATE_PASSWORD: "op://${{ vars.OP_VAULT }}/Desktop Certificates.p12/CSC_KEY_PASSWORD" | |
| - name: Build desktop app | |
| run: ${{ github.ref == 'refs/heads/production' && 'npm run desktop-build' || 'npm run desktop-build-staging' }} | |
| env: | |
| CSC_LINK: ${{ steps.load-credentials.outputs.DESKTOP_CERTIFICATE_BASE64 }} | |
| CSC_KEY_PASSWORD: ${{ steps.load-credentials.outputs.DESKTOP_CERTIFICATE_PASSWORD }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| GCP_GEOLOCATION_API_KEY: ${{ secrets.GCP_GEOLOCATION_API_KEY_PRODUCTION }} | |
| S3_BUCKET: ${{ github.ref == 'refs/heads/production' && vars.PRODUCTION_S3_BUCKET || vars.STAGING_S3_BUCKET }} | |
| - name: Upload desktop sourcemaps artifact | |
| # v4 | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: desktop-sourcemaps-artifact | |
| path: ./desktop/dist/www/merged-source-map.js.map | |
| - name: Upload desktop build artifact | |
| # v4 | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: desktop-build-artifact | |
| path: ./desktop-build/NewExpensify.dmg | |
| ios: | |
| name: Build and deploy iOS HybridApp | |
| needs: prep | |
| runs-on: macos-15-xlarge | |
| env: | |
| DEVELOPER_DIR: /Applications/Xcode_16.2.0.app/Contents/Developer | |
| steps: | |
| - name: Checkout | |
| # v4 | |
| uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 | |
| 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 | |
| # v4 | |
| uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 | |
| 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: Install 1Password CLI | |
| # v1 | |
| uses: 1password/install-cli-action@143a85f84a90555d121cde2ff5872e393a47ab9f | |
| - 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 }}/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: 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" | |
| - name: Build iOS HybridApp | |
| if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} | |
| 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 }} | |
| - 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 }} | |
| 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 }} | |
| - 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)) }} | |
| run: bundle exec fastlane ios upload_dsyms_hybrid | |
| - name: Submit production build for App Store review and a slow rollout | |
| if: ${{ github.ref == 'refs/heads/production' }} | |
| run: | | |
| # Complete the previous version rollout | |
| bundle exec fastlane ios complete_hybrid_rollout | |
| # Submit the new version for review and phased rollout when it's approved | |
| bundle exec fastlane ios submit_hybrid_for_rollout | |
| env: | |
| VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} | |
| APPLE_ID: ${{ vars.APPLE_ID }} | |
| - name: Upload iOS 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.ipaPath }}" | |
| env: | |
| BROWSERSTACK: ${{ secrets.BROWSERSTACK }} | |
| - name: Upload iOS build artifact | |
| if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} | |
| # v4 | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: ios-build-artifact | |
| path: ${{ env.ipaPath }} | |
| - name: Upload iOS sourcemap artifact | |
| if: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} | |
| # 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 | |
| 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> ${{ steps.getIOSVersion.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 }} | |
| web: | |
| name: Build and deploy Web | |
| needs: prep | |
| runs-on: ubuntu-latest-xl | |
| steps: | |
| - name: Checkout | |
| # v4 | |
| uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 | |
| - 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 | |
| - 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 | |
| # v4 | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| 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 | |
| # v4 | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: web-build-tar-gz-artifact | |
| path: ./webBuild.tar.gz | |
| - name: Upload .zip web build artifact | |
| # v4 | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: web-build-zip-artifact | |
| path: ./webBuild.zip | |
| postSlackMessageOnFailure: | |
| name: Post a Slack message when any platform fails to build or deploy | |
| runs-on: ubuntu-latest | |
| if: ${{ failure() }} | |
| needs: [android, desktop, ios, web] | |
| steps: | |
| - name: Checkout | |
| # v4 | |
| uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 | |
| - name: Post Slack message on failure | |
| uses: ./.github/actions/composite/announceFailedWorkflowInSlack | |
| with: | |
| SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} | |
| checkDeploymentSuccess: | |
| runs-on: ubuntu-latest | |
| 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, desktop, ios, 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" ] || \ | |
| [ "${{ needs.desktop.result }}" == "success" ] || \ | |
| [ "${{ needs.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 [ "${{ needs.iOS.result }}" == "success" ] && \ | |
| [ "${{ needs.android.result }}" == "success" ] && \ | |
| [ "${{ needs.desktop.result }}" == "success" ] && \ | |
| [ "${{ needs.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: ubuntu-latest | |
| if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} | |
| needs: [prep, checkDeploymentSuccess] | |
| 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: | | |
| gh release create ${{ needs.prep.outputs.TAG }} ${{ github.ref == 'refs/heads/staging' && '--prerelease' || '' }} \ | |
| --repo ${{ github.repository }} \ | |
| --title ${{ needs.prep.outputs.TAG }} \ | |
| --notes-start-tag ${{ steps.get_last_prod_version.outputs.LAST_PROD_VERSION }} \ | |
| --generate-notes \ | |
| --verify-tag \ | |
| --target ${{ github.ref }} | |
| RETRIES=0 | |
| readonly MAX_RETRIES=10 | |
| until [[ $(gh release view ${{ needs.prep.outputs.TAG }} --repo ${{ github.repository }}) || $RETRIES -ge $MAX_RETRIES ]]; do | |
| echo "Release not found, retrying $((MAX_RETRIES - RETRIES++)) times" | |
| sleep 1 | |
| done | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| - name: Rename web and desktop sourcemaps artifacts before assets upload in order to have unique ReleaseAsset.name | |
| continue-on-error: true | |
| run: | | |
| mv ./desktop-sourcemaps-artifact/merged-source-map.js.map ./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map | |
| 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=" | |
| ./android-build-artifact/Expensify-release.aab#android.aab | |
| ./android-apk-artifact/Expensify.apk#android.apk | |
| ./android-sourcemap-artifact/index.android.bundle.map#android-sourcemap.js.map | |
| ./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map#desktop-sourcemap.js.map | |
| ./desktop-build-artifact/NewExpensify.dmg#desktop.dmg | |
| ./ios-build-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 | |
| createNewVersion: | |
| 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/createNewVersion.yml | |
| secrets: inherit | |
| with: | |
| SEMVER_LEVEL: BUILD | |
| cherryPickVersionBump: | |
| needs: [createNewVersion, checkDeploymentSuccess, prep] | |
| if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) && github.ref == 'refs/heads/production' && fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| # v4.2.2 | |
| - name: Checkout | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 | |
| with: | |
| ref: staging | |
| token: ${{ secrets.OS_BOTIFY_TOKEN }} | |
| submodules: true | |
| # This command is necessary to fetch any branch other than main in the submodule. | |
| # See https://github.com/actions/checkout/issues/1815#issuecomment-2777836442 for further context. | |
| - name: Enable branch-switching in submodules | |
| run: | | |
| git submodule foreach '\ | |
| git config --add remote.origin.fetch "+refs/heads/staging:refs/remotes/origin/staging" && \ | |
| git config --add remote.origin.fetch "+refs/heads/production:refs/remotes/origin/production"' | |
| - name: Set up git for OSBotify | |
| id: setupGitForOSBotify | |
| uses: Expensify/GitHub-Actions/setupGitForOSBotify@main | |
| 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 previous app version | |
| id: getPreviousVersion | |
| uses: ./.github/actions/javascript/getPreviousVersion | |
| with: | |
| SEMVER_LEVEL: 'PATCH' | |
| - name: Fetch history of relevant refs | |
| run: | | |
| git fetch origin main staging --no-recurse-submodules --no-tags --shallow-exclude ${{ steps.getPreviousVersion.outputs.PREVIOUS_VERSION }} | |
| cd Mobile-Expensify | |
| git fetch origin main staging --no-recurse-submodules --no-tags --shallow-exclude ${{ steps.getPreviousVersion.outputs.PREVIOUS_VERSION }} | |
| - name: Get E/App version bump commit | |
| id: getVersionBumpCommit | |
| run: | | |
| git switch main | |
| VERSION_BUMP_COMMIT="$(git log -1 --format='%H' --author='OSBotify' --grep 'Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}')" | |
| if [ -z "$VERSION_BUMP_COMMIT" ]; then | |
| echo "::error::❌ Could not find E/App version bump commit for ${{ needs.createNewVersion.outputs.NEW_VERSION }}" | |
| git log --oneline | |
| else | |
| echo "::notice::👀 Found E/App version bump commit $VERSION_BUMP_COMMIT" | |
| fi | |
| echo "VERSION_BUMP_SHA=$VERSION_BUMP_COMMIT" >> "$GITHUB_OUTPUT" | |
| - name: Get Mobile-Expensify version bump commit | |
| id: getMobileExpensifyVersionBumpCommit | |
| working-directory: Mobile-Expensify | |
| run: | | |
| git switch main | |
| VERSION_BUMP_COMMIT="$(git log -1 --format='%H' --author='OSBotify' --grep 'Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}')" | |
| if [ -z "$VERSION_BUMP_COMMIT" ]; then | |
| echo "::error::❌ Could not find Mobile-Expensify version bump commit for ${{ needs.createNewVersion.outputs.NEW_VERSION }}" | |
| git log --oneline | |
| else | |
| echo "::notice::👀 Found Mobile-Expensify version bump commit $VERSION_BUMP_COMMIT" | |
| fi | |
| echo "VERSION_BUMP_SHA=$VERSION_BUMP_COMMIT" >> "$GITHUB_OUTPUT" | |
| - name: Cherry-pick the Mobile-Expensify version bump to Mobile-Expensify staging | |
| working-directory: Mobile-Expensify | |
| run: | | |
| git switch staging | |
| git cherry-pick -S -x --mainline 1 --strategy=recursive -Xtheirs ${{ steps.getMobileExpensifyVersionBumpCommit.outputs.VERSION_BUMP_SHA }} | |
| git commit --amend -m "$(git log -1 --pretty=%B)" -m "(cherry-picked to staging by ${{ github.actor }})" | |
| git push origin staging | |
| - name: Cherry-pick the E/App version-bump to target branch | |
| run: | | |
| git switch staging | |
| git cherry-pick -S -x --mainline 1 --strategy=recursive -Xtheirs ${{ steps.getVersionBumpCommit.outputs.VERSION_BUMP_SHA }} | |
| git commit --amend -m "$(git log -1 --pretty=%B)" -m "(cherry-picked to staging by ${{ github.actor }})" | |
| - name: Update the Mobile-Expensify submodule on E/App staging | |
| run: | | |
| git add Mobile-Expensify | |
| git commit -m "Update Mobile-Expensify submodule version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}" | |
| git push origin staging | |
| updateChecklistWithVersionBump: | |
| name: Update deploy checklist with the new version bump | |
| needs: [prep, checkDeploymentSuccess, cherryPickVersionBump] | |
| 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/createDeployChecklist.yml | |
| secrets: inherit | |
| postSlackMessageOnSuccess: | |
| 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, desktop, ios, web, 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.APP_VERSION }}|${{ needs.prep.outputs.APP_VERSION }}> 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.APP_VERSION }}|${{ needs.prep.outputs.APP_VERSION }}> 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.APP_VERSION }}|${{ needs.prep.outputs.APP_VERSION }}> 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, android, desktop, ios, web, checkDeploymentSuccess, createRelease] | |
| with: | |
| version: ${{ needs.prep.outputs.APP_VERSION }} | |
| env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} | |
| android: ${{ needs.android.result }} | |
| ios: ${{ needs.ios.result }} | |
| web: ${{ needs.web.result }} | |
| desktop: ${{ needs.desktop.result }} |