Skip to content

Workflow file for this run

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') }}
- 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-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: 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://${{ env.SHOULD_DEPLOY_PRODUCTION != 'true' && 'staging-' || '' }}expensify-cash
- name: Purge Cloudflare cache
run: /home/runner/.local/bin/cli4 --verbose --delete hosts=["${{ env.SHOULD_DEPLOY_PRODUCTION != 'true' && 'staging.' || '' }}new.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache
env:
CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }}
- name: Verify staging deploy
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
run: |
DOWNLOADED_VERSION="$(wget -q -O /dev/stdout https://staging.new.expensify.com/version.json | jq -r '.version')"
if [[ '${{ github.ref_name }}' != "$DOWNLOADED_VERSION" ]]; then
echo "Error: deployed version does not match local version. Something went wrong..."
exit 1
fi
- name: Verify production deploy
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
run: |
DOWNLOADED_VERSION="$(wget -q -O /dev/stdout https://new.expensify.com/version.json | jq -r '.version')"
if [[ '${{ github.event.release.tag_name }}' != "$DOWNLOADED_VERSION" ]]; then
echo "Error: deployed version does not match local version. Something went wrong..."
exit 1
fi
- name: Upload web build to GitHub artifacts
uses: actions/upload-artifact@v4
with:
name: web-build
path: dist
- name: Upload web build to GitHub Release
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
run: |
tar -czvf webBuild.tar.gz dist
zip -r webBuild.zip dist
gh release upload ${{ github.event.release.tag_name }} webBuild.tar.gz webBuild.zip
env:
GITHUB_TOKEN: ${{ github.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 }}