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@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-${{ github.ref_name }} | |
path: android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.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-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 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 }} | |
GCP_GEOLOCATION_API_KEY: $${{ secrets.GCP_GEOLOCATION_API_KEY_PRODUCTION }} | |
- 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 }} | |
GCP_GEOLOCATION_API_KEY: $${{ secrets.GCP_GEOLOCATION_API_KEY_STAGING }} | |
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@a05e47355e80e57b9a67566a813648fa67d92011 | |
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: 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-${{ github.ref_name }} | |
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@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 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 --verbose --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 --verbose --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: 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 }} |