Merge pull request #63826 from Expensify/revert-63242-perf/navigation… #4155
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 | |
env: | |
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 | |
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: ${{ 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 }} | |
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: ${{ 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 | |
- 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: 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" | |
# 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 | |
if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} | |
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 | |
if: ${{ fromJSON(env.SHOULD_BUILD_APP) }} | |
# v4 | |
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
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 | |
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 | |
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 <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>. 💥 But first check the workflow, it's likely that the release was successful and <https://github.com/Expensify/Expensify/issues/488492|this error> is occuring)`, | |
}] | |
} | |
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 | |
SHOULD_BUILD_APP: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }} | |
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: ${{ fromJSON(env.SHOULD_BUILD_APP) }} | |
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: ${{ fromJSON(env.SHOULD_BUILD_APP) }} | |
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) && fromJSON(env.SHOULD_BUILD_APP) }} | |
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) && 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 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 | |
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: | | |
# 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." | |
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 }}" | |
RETRIES=0 | |
readonly MAX_RETRIES=10 | |
until [[ $(gh release view "${{ needs.prep.outputs.TAG }}" --repo "${{ github.repository }}") || $RETRIES -ge $MAX_RETRIES ]]; do | |
echo "Release ${{ needs.prep.outputs.TAG }} not yet visible after creation. Retrying... ($((MAX_RETRIES - RETRIES++)) attempts left)" | |
sleep 1 | |
done | |
fi | |
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 | |
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: 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.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, 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 }} |