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' }} | |
concurrency: | |
group: ${{ github.workflow }}-${{ github.event_name }} | |
cancel-in-progress: true | |
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 | |
uses: ./.github/workflows/createDeployChecklist.yml | |
if: ${{ github.event_name != 'release' }} | |
needs: validateActor | |
secrets: inherit | |
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@v4 | |
- name: Configure MapBox SDK | |
run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} | |
- name: Setup Node | |
uses: ./.github/actions/composite/setupNode | |
- name: Setup Java | |
uses: actions/setup-java@v3 | |
with: | |
distribution: 'oracle' | |
java-version: '17' | |
- name: Setup Ruby | |
uses: ruby/setup-ruby@v1.187.0 | |
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 | |
run: bundle exec fastlane android ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'production' || 'beta' }} | |
env: | |
RUBYOPT: '-rostruct' | |
MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }} | |
MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} | |
VERSION: ${{ env.VERSION_CODE }} | |
- name: Archive Android sourcemaps | |
uses: actions/upload-artifact@v4 | |
with: | |
name: android-sourcemap-${{ github.ref_name }} | |
path: android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map | |
- name: Upload Android build to GitHub artifacts | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
uses: actions/upload-artifact@v4 | |
with: | |
name: app-production-release.aab | |
path: android/app/build/outputs/bundle/productionRelease/app-production-release.aab | |
- name: Upload Android build 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: Upload Android build to GitHub Release | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: | | |
RUN_ID="$(gh run list --workflow platformDeploy.yml --event push --branch ${{ github.event.release.tag_name }} --json databaseId --jq '.[0].databaseId')" | |
gh run download "$RUN_ID" --name app-production-release.aab | |
gh release upload ${{ github.event.release.tag_name }} app-production-release.aab | |
env: | |
GITHUB_TOKEN: ${{ github.token }} | |
- 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-14-large | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Setup Node | |
uses: ./.github/actions/composite/setupNode | |
- 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 desktop app | |
run: | | |
if [[ ${{ env.SHOULD_DEPLOY_PRODUCTION }} == 'true' ]]; then | |
npm run desktop-build | |
else | |
npm run desktop-build-staging | |
fi | |
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 }} | |
GCP_GEOLOCATION_API_KEY: $${{ secrets.GCP_GEOLOCATION_API_KEY_PRODUCTION }} | |
- name: Upload desktop build to GitHub Workflow | |
uses: actions/upload-artifact@v4 | |
with: | |
name: NewExpensify.dmg | |
path: desktop-build/NewExpensify.dmg | |
- name: Upload desktop build to GitHub Release | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: gh release upload ${{ github.event.release.tag_name }} desktop-build/NewExpensify.dmg | |
env: | |
GITHUB_TOKEN: ${{ github.token }} | |
iOS: | |
name: Build and deploy iOS | |
needs: validateActor | |
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} | |
env: | |
DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer | |
runs-on: macos-13-xlarge | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- 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 | |
- name: Setup Ruby | |
uses: ruby/setup-ruby@v1.187.0 | |
with: | |
ruby-version: '2.7' | |
bundler-cache: true | |
- name: Cache Pod dependencies | |
uses: actions/cache@v4 | |
id: pods-cache | |
with: | |
path: ios/Pods | |
key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }} | |
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' || steps.setup-node.outputs.cache-hit != 'true' | |
with: | |
timeout_minutes: 10 | |
max_attempts: 5 | |
command: cd ios && bundle exec pod install | |
- name: Decrypt AppStore profile | |
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg | |
env: | |
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} | |
- name: Decrypt AppStore Notification Service profile | |
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore_Notification_Service.mobileprovision NewApp_AppStore_Notification_Service.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: Set iOS version in ENV | |
run: echo "IOS_VERSION=$(echo '${{ github.event.release.tag_name }}' | tr '-' '.')" >> "$GITHUB_ENV" | |
- name: Run Fastlane | |
run: bundle exec fastlane ios ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'production' || '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 }} | |
VERSION: ${{ env.IOS_VERSION }} | |
- name: Archive iOS sourcemaps | |
uses: actions/upload-artifact@v4 | |
with: | |
name: ios-sourcemap-${{ github.ref_name }} | |
path: main.jsbundle.map | |
- name: Upload iOS build to GitHub artifacts | |
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
uses: actions/upload-artifact@v4 | |
with: | |
name: New Expensify.ipa | |
path: /Users/runner/work/App/App/New Expensify.ipa | |
- name: Upload iOS build 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: Upload iOS build to GitHub Release | |
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
run: | | |
RUN_ID="$(gh run list --workflow platformDeploy.yml --event push --branch ${{ github.event.release.tag_name }} --json databaseId --jq '.[0].databaseId')" | |
gh run download "$RUN_ID" --name 'New Expensify.ipa' | |
gh release upload ${{ github.event.release.tag_name }} 'New Expensify.ipa' | |
env: | |
GITHUB_TOKEN: ${{ github.token }} | |
- 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@v4 | |
- name: Setup Node | |
uses: ./.github/actions/composite/setupNode | |
- name: Setup Cloudflare CLI | |
run: pip3 install cloudflare==2.19.0 | |
- name: Configure AWS Credentials | |
uses: aws-actions/configure-aws-credentials@v4 | |
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 [[ ${{ env.SHOULD_DEPLOY_PRODUCTION }} == 'true' ]]; then | |
npm run build | |
else | |
npm run build-staging | |
fi | |
- name: Build storybook docs | |
continue-on-error: true | |
run: | | |
if [[ ${{ env.SHOULD_DEPLOY_PRODUCTION }} == 'true' ]]; 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://${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && '' || 'staging-' }}expensify-cash | |
- name: Purge Cloudflare cache | |
run: /home/runner/.local/bin/cli4 --verbose --delete hosts=["${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && '' || '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: Checkout | |
uses: actions/checkout@v4 | |
- name: Post Slack message on failure | |
uses: ./.github/actions/composite/announceFailedWorkflowInSlack | |
with: | |
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} | |
# Build a version of iOS and Android HybridApp if we are deploying to staging | |
hybridApp: | |
runs-on: ubuntu-latest | |
needs: validateActor | |
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) && github.event_name == 'push' }} | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: 'Deploy HybridApp' | |
run: gh workflow run --repo Expensify/Mobile-Deploy deploy.yml -f force_build=true -f build_version="$(npm run print-version --silent)" | |
env: | |
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} | |
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@v4 | |
- 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: ${{ !cancelled() }} | |
needs: [android, desktop, iOS, web] | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Setup Node | |
uses: ./.github/actions/composite/setupNode | |
- name: Set version | |
run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" | |
- name: Get Release Pull Request List | |
id: getReleasePRList | |
uses: ./.github/actions/javascript/getDeployPullRequestList | |
with: | |
TAG: ${{ env.VERSION }} | |
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} | |
IS_PRODUCTION_DEPLOY: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} | |
- name: Comment on issues | |
uses: ./.github/actions/javascript/markPullRequestsAsDeployed | |
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 }} |