Merge pull request #30961 from dukenv0307/fix/30933 #2190
Workflow file for this run
This file contains 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: Build and deploy android, desktop, iOS, and web clients | |
# This workflow is run when any tag is published | |
on: | |
push: | |
tags: | |
- '*' | |
release: | |
types: [created] | |
env: | |
SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} | |
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer | |
jobs: | |
validateActor: | |
runs-on: ubuntu-latest | |
outputs: | |
IS_DEPLOYER: ${{ fromJSON(steps.isUserDeployer.outputs.IS_DEPLOYER) || github.actor == 'OSBotify' || github.actor == 'os-botify[bot]' }} | |
steps: | |
- name: Check if user is deployer | |
id: isUserDeployer | |
run: | | |
if gh api /orgs/Expensify/teams/mobile-deployers/memberships/${{ github.actor }} --silent; then | |
echo "IS_DEPLOYER=true" >> "$GITHUB_OUTPUT" | |
else | |
echo "IS_DEPLOYER=false" >> "$GITHUB_OUTPUT" | |
fi | |
env: | |
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} | |
# 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 | |
runs-on: ubuntu-latest | |
if: ${{ github.event_name != 'release' }} | |
needs: validateActor | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- name: Setup Node | |
uses: Expensify/App/.github/actions/composite/setupNode@main | |
- name: Set version | |
id: getVersion | |
run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_OUTPUT" | |
- name: Create or update staging deploy | |
uses: Expensify/App/.github/actions/javascript/createOrUpdateStagingDeploy@main | |
with: | |
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} | |
NPM_VERSION: ${{ steps.getVersion.outputs.VERSION }} | |
android: | |
name: Build and deploy Android | |
needs: validateActor | |
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} | |
runs-on: ubuntu-latest-xl | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- name: Configure MapBox SDK | |
run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} | |
- name: Setup Node | |
uses: Expensify/App/.github/actions/composite/setupNode@main | |
- name: Setup Ruby | |
uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011 | |
with: | |
ruby-version: '2.7' | |
bundler-cache: true | |
- name: Decrypt keystore | |
run: cd android/app && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output my-upload-key.keystore my-upload-key.keystore.gpg | |
env: | |
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} | |
- name: Decrypt json key | |
run: cd android/app && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg | |
env: | |
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} | |
- name: Set version in ENV | |
run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_ENV" | |
- name: Run Fastlane beta | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: bundle exec fastlane android beta | |
env: | |
MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }} | |
MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} | |
- name: Run Fastlane production | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: bundle exec fastlane android production | |
env: | |
VERSION: ${{ env.VERSION_CODE }} | |
- name: Archive Android sourcemaps | |
uses: actions/upload-artifact@v3 | |
with: | |
name: android-sourcemap | |
path: android/app/build/generated/sourcemaps/react/release/*.map | |
- name: Upload Android version to GitHub artifacts | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
uses: actions/upload-artifact@v3 | |
with: | |
name: app-production-release.aab | |
path: android/app/build/outputs/bundle/productionRelease/app-production-release.aab | |
- name: Upload Android version to Browser Stack | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@./android/app/build/outputs/bundle/productionRelease/app-production-release.aab" | |
env: | |
BROWSERSTACK: ${{ secrets.BROWSERSTACK }} | |
- name: Warn deployers if Android production deploy failed | |
if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
uses: 8398a7/action-slack@v3 | |
with: | |
status: custom | |
custom_payload: | | |
{ | |
channel: '#deployer', | |
attachments: [{ | |
color: "#DB4545", | |
pretext: `<!subteam^S4TJJ3PSL>`, | |
text: `💥 Android production deploy failed. Please manually submit ${{ github.event.release.tag_name }} in the <https://play.google.com/console/u/0/developers/8765590895836334604/app/4973041797096886180/releases/overview|Google Play Store>. 💥`, | |
}] | |
} | |
env: | |
GITHUB_TOKEN: ${{ github.token }} | |
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
desktop: | |
name: Build and deploy Desktop | |
needs: validateActor | |
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} | |
runs-on: macos-12-xl | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- name: Setup Node | |
uses: Expensify/App/.github/actions/composite/setupNode@main | |
- name: Decrypt Developer ID Certificate | |
run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg | |
env: | |
DEVELOPER_ID_SECRET_PASSPHRASE: ${{ secrets.DEVELOPER_ID_SECRET_PASSPHRASE }} | |
- name: Build production desktop app | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: npm run desktop-build | |
env: | |
CSC_LINK: ${{ secrets.CSC_LINK }} | |
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} | |
APPLE_ID: ${{ secrets.APPLE_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 }} | |
- name: Build staging desktop app | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: npm run desktop-build-staging | |
env: | |
CSC_LINK: ${{ secrets.CSC_LINK }} | |
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} | |
APPLE_ID: ${{ secrets.APPLE_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 }} | |
iOS: | |
name: Build and deploy iOS | |
needs: validateActor | |
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} | |
runs-on: macos-13-xlarge | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- name: Configure MapBox SDK | |
run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} | |
- name: Setup Node | |
uses: Expensify/App/.github/actions/composite/setupNode@main | |
- name: Setup Ruby | |
uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011 | |
with: | |
ruby-version: '2.7' | |
bundler-cache: true | |
- name: Cache Pod dependencies | |
uses: actions/cache@v3 | |
id: pods-cache | |
with: | |
path: ios/Pods | |
key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock') }} | |
restore-keys: ${{ runner.os }}-pods-cache- | |
- name: Compare Podfile.lock and Manifest.lock | |
id: compare-podfile-and-manifest | |
run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('ios/Podfile.lock') == hashFiles('ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT" | |
- name: Install cocoapods | |
uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 | |
if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' | |
with: | |
timeout_minutes: 10 | |
max_attempts: 5 | |
command: cd ios && bundle exec pod install | |
- name: Decrypt profile | |
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output chat_expensify_appstore.mobileprovision chat_expensify_appstore.mobileprovision.gpg | |
env: | |
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} | |
- name: Decrypt certificate | |
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg | |
env: | |
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} | |
- name: Decrypt App Store Connect API key | |
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output ios-fastlane-json-key.json ios-fastlane-json-key.json.gpg | |
env: | |
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} | |
- name: Run Fastlane | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: bundle exec fastlane ios beta | |
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 }} | |
- name: Archive iOS sourcemaps | |
uses: actions/upload-artifact@v3 | |
with: | |
name: ios-sourcemap | |
path: main.jsbundle.map | |
- name: Upload iOS version to GitHub artifacts | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
uses: actions/upload-artifact@v3 | |
with: | |
name: New Expensify.ipa | |
path: /Users/runner/work/App/App/New Expensify.ipa | |
- name: Upload iOS version to Browser Stack | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@/Users/runner/work/App/App/New Expensify.ipa" | |
env: | |
BROWSERSTACK: ${{ secrets.BROWSERSTACK }} | |
- name: Set iOS version in ENV | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: echo "IOS_VERSION=$(echo '${{ github.event.release.tag_name }}' | tr '-' '.')" >> "$GITHUB_ENV" | |
- name: Run Fastlane for App Store release | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: bundle exec fastlane ios production | |
env: | |
VERSION: ${{ env.IOS_VERSION }} | |
- name: Warn deployers if iOS production deploy failed | |
if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
uses: 8398a7/action-slack@v3 | |
with: | |
status: custom | |
custom_payload: | | |
{ | |
channel: '#deployer', | |
attachments: [{ | |
color: "#DB4545", | |
pretext: `<!subteam^S4TJJ3PSL>`, | |
text: `💥 iOS production deploy failed. Please manually submit ${{ env.IOS_VERSION }} in the <https://appstoreconnect.apple.com/apps/1530278510/appstore|App Store>. 💥`, | |
}] | |
} | |
env: | |
GITHUB_TOKEN: ${{ github.token }} | |
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
web: | |
name: Build and deploy Web | |
needs: validateActor | |
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} | |
runs-on: ubuntu-latest-xl | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- name: Setup Node | |
uses: Expensify/App/.github/actions/composite/setupNode@main | |
- name: Setup Cloudflare CLI | |
run: pip3 install cloudflare | |
- name: Configure AWS Credentials | |
uses: Expensify/App/.github/actions/composite/configureAwsCredentials@main | |
with: | |
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
- name: Build web for production | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: npm run build | |
- name: Build web for staging | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: npm run build-staging | |
- name: Build storybook docs for production | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: npm run storybook-build | |
continue-on-error: true | |
- name: Build storybook docs for staging | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: npm run storybook-build-staging | |
continue-on-error: true | |
- name: Deploy production to S3 | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://expensify-cash/ && aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE s3://expensify-cash/.well-known/apple-app-site-association s3://expensify-cash/.well-known/apple-app-site-association && aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE s3://expensify-cash/.well-known/apple-app-site-association s3://expensify-cash/apple-app-site-association | |
- name: Deploy staging to S3 | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://staging-expensify-cash/ && aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE s3://staging-expensify-cash/.well-known/apple-app-site-association s3://staging-expensify-cash/.well-known/apple-app-site-association && aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE s3://staging-expensify-cash/.well-known/apple-app-site-association s3://staging-expensify-cash/apple-app-site-association | |
- name: Purge production Cloudflare cache | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: /home/runner/.local/bin/cli4 --delete hosts=["new.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache | |
env: | |
CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} | |
- name: Purge staging Cloudflare cache | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: /home/runner/.local/bin/cli4 --delete hosts=["staging.new.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache | |
env: | |
CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} | |
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: Post Slack message on failure | |
uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main | |
with: | |
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} | |
postSlackMessageOnSuccess: | |
name: Post a Slack message when all platforms deploy successfully | |
runs-on: ubuntu-latest | |
if: ${{ success() }} | |
needs: [android, desktop, iOS, web] | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- name: Set version | |
run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" | |
- name: 'Announces the deploy in the #announce Slack room' | |
uses: 8398a7/action-slack@v3 | |
with: | |
status: custom | |
custom_payload: | | |
{ | |
channel: '#announce', | |
attachments: [{ | |
color: 'good', | |
text: `🎉️ Successfully deployed ${process.env.AS_REPO} <https://github.com/Expensify/App/releases/tag/${{ env.VERSION }}|${{ env.VERSION }}> to ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'production' || 'staging' }} 🎉️`, | |
}] | |
} | |
env: | |
GITHUB_TOKEN: ${{ github.token }} | |
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
- name: 'Announces the deploy in the #deployer Slack room' | |
uses: 8398a7/action-slack@v3 | |
with: | |
status: custom | |
custom_payload: | | |
{ | |
channel: '#deployer', | |
attachments: [{ | |
color: 'good', | |
text: `🎉️ Successfully deployed ${process.env.AS_REPO} <https://github.com/Expensify/App/releases/tag/${{ env.VERSION }}|${{ env.VERSION }}> to ${{ fromJSON(env.SHOULD_DEPLOY_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' | |
uses: 8398a7/action-slack@v3 | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_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/${{ env.VERSION }}|${{ env.VERSION }}> to production 🎉️`, | |
}] | |
} | |
env: | |
GITHUB_TOKEN: ${{ github.token }} | |
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
postGithubComment: | |
name: Post a GitHub comment when platforms are done building and deploying | |
runs-on: ubuntu-latest | |
if: ${{ always() }} | |
needs: [android, desktop, iOS, web] | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- name: Setup Node | |
uses: Expensify/App/.github/actions/composite/setupNode@main | |
- name: Set version | |
run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" | |
- name: Get Release Pull Request List | |
id: getReleasePRList | |
uses: Expensify/App/.github/actions/javascript/getDeployPullRequestList@main | |
with: | |
TAG: ${{ env.VERSION }} | |
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} | |
IS_PRODUCTION_DEPLOY: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
- name: Comment on issues | |
uses: Expensify/App/.github/actions/javascript/markPullRequestsAsDeployed@main | |
with: | |
PR_LIST: ${{ steps.getReleasePRList.outputs.PR_LIST }} | |
IS_PRODUCTION_DEPLOY: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
DEPLOY_VERSION: ${{ env.VERSION }} | |
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} | |
ANDROID: ${{ needs.android.result }} | |
DESKTOP: ${{ needs.desktop.result }} | |
IOS: ${{ needs.iOS.result }} | |
WEB: ${{ needs.web.result }} |