Skip to content

Build and deploy apps for testing #5439

Build and deploy apps for testing

Build and deploy apps for testing #5439

Workflow file for this run

name: Build and deploy apps for testing
on:
workflow_dispatch:
inputs:
# If not specified, only build iOS and Android apps from the main branch of Expensify/App
APP_PULL_REQUEST_URL:
description: The Expensify/App pull request URL (e.g., https://github.com/Expensify/App/pull/12345). Defaults to main.
required: false
default: ''
# Pull Request URL from Mobile-Expensify repo for correct placement of OD app. It will take precedence over MOBILE-EXPENSIFY from App's PR description if both are specified. If nothing is specified defaults to Mobile-Expensify's main
MOBILE_EXPENSIFY_PULL_REQUEST_URL:
description: The Expensify/Mobile-Expensify pull request URL. Defaults to main. Overrides MOBILE-EXPENSIFY set in App's PR description.
required: false
default: ''
REVIEWED_CODE:
description: I reviewed this pull request and verified that it does not contain any malicious code.
type: boolean
required: true
default: false
WEB:
description: Should build web app?
type: boolean
default: true
IOS:
description: Should build iOS app?
type: boolean
default: true
ANDROID:
description: Should build android app?
type: boolean
default: true
jobs:
prep:
runs-on: ubuntu-latest
outputs:
APP_REF: ${{ steps.getHeadRef.outputs.REF || 'main' }}
APP_PR_NUMBER: ${{ steps.extractAppPRNumber.outputs.PR_NUMBER }}
MOBILE_PR_NUMBER: ${{ steps.extractMobilePRNumber.outputs.PR_NUMBER }}
steps:
- name: Checkout
# v4
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
- name: Validate that user is an Expensify employee
uses: ./.github/actions/composite/validateActor
with:
REQUIRE_APP_DEPLOYER: false
OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }}
- name: Validate that the user reviewed the pull request before running a test build
if: ${{ !inputs.REVIEWED_CODE }}
run: |
echo "::error::🕵️‍♀️ Please carefully review the pull request before running a test build to ensure it does not contain any malicious code"
exit 1
- name: Extract App PR number from URL
id: extractAppPRNumber
if: ${{ inputs.APP_PULL_REQUEST_URL != '' }}
run: |
PR_NUMBER=$(echo '${{ inputs.APP_PULL_REQUEST_URL }}' | sed -E 's|.*/pull/([0-9]+).*|\1|')
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::❌ Could not extract PR number from URL. Please provide a valid GitHub PR URL (e.g., https://github.com/Expensify/App/pull/12345)"
exit 1
fi
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_OUTPUT"
- name: Extract Mobile-Expensify PR number from URL
id: extractMobilePRNumber
if: ${{ inputs.MOBILE_EXPENSIFY_PULL_REQUEST_URL != '' }}
run: |
PR_NUMBER=$(echo '${{ inputs.MOBILE_EXPENSIFY_PULL_REQUEST_URL }}' | sed -E 's|.*/pull/([0-9]+).*|\1|')
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::❌ Could not extract PR number from URL. Please provide a valid GitHub PR URL (e.g., https://github.com/Expensify/Mobile-Expensify/pull/12345)"
exit 1
fi
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_OUTPUT"
- name: Check if App pull request number is correct
if: ${{ github.event_name == 'workflow_dispatch' }}
id: getHeadRef
run: |
set -e
if [ -z "${{ steps.extractAppPRNumber.outputs.PR_NUMBER }}" ]; then
echo "REF=" >> "$GITHUB_OUTPUT"
else
echo "REF=$(gh pr view ${{ steps.extractAppPRNumber.outputs.PR_NUMBER }} --json headRefOid --jq '.headRefOid')" >> "$GITHUB_OUTPUT"
fi
env:
GITHUB_TOKEN: ${{ github.token }}
getMobileExpensifyPR:
runs-on: ubuntu-latest
needs: [prep]
outputs:
MOBILE_EXPENSIFY_PR: ${{ steps.mobileExpensifyPR.outputs.result }}
steps:
- name: Check if author specified Expensify/Mobile-Expensify PR
id: mobileExpensifyPR
# v7
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
with:
github-token: ${{ github.token }}
result-encoding: string
script: |
if ('${{ needs.prep.outputs.MOBILE_PR_NUMBER }}') return '${{ needs.prep.outputs.MOBILE_PR_NUMBER }}';
if (!'${{ needs.prep.outputs.APP_PR_NUMBER }}') return '';
const pullRequest = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: '${{ needs.prep.outputs.APP_PR_NUMBER }}',
});
const body = pullRequest.data.body;
const regex = /MOBILE-EXPENSIFY:\s*https:\/\/github.com\/Expensify\/Mobile-Expensify\/pull\/(?<prNumber>\d+)/;
const found = body.match(regex)?.groups?.prNumber || "";
return found.trim();
getMobileExpensifyRef:
runs-on: ubuntu-latest
needs: [prep, getMobileExpensifyPR]
outputs:
MOBILE_EXPENSIFY_REF: ${{ steps.getHeadRef.outputs.REF || 'main' }}
steps:
- name: Checkout
# v4
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
- name: Check if Expensify/Mobile-Expensify pull request number is correct
id: getHeadRef
run: |
set -e
if [[ -z "${{ needs.prep.outputs.MOBILE_PR_NUMBER }}" && -z "${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }}" ]]; then
echo "REF=" >> "$GITHUB_OUTPUT"
else
echo "PR=${{ needs.prep.outputs.MOBILE_PR_NUMBER || needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }}" >> "$GITHUB_OUTPUT"
echo "REF=$(gh pr view ${{ needs.prep.outputs.MOBILE_PR_NUMBER || needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }} -R Expensify/Mobile-Expensify --json headRefOid --jq '.headRefOid')" >> "$GITHUB_OUTPUT"
fi
env:
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
postGitHubCommentBuildStarted:
name: Post build started comment
runs-on: ubuntu-latest
needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef]
steps:
- name: Add build start comment to Expensify/App PR
if: ${{ needs.prep.outputs.APP_PR_NUMBER != '' }}
# v7
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
with:
github-token: ${{ github.token }}
script: |
const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ needs.prep.outputs.APP_PR_NUMBER }},
body: `🚧 @${{ github.actor }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).`
});
- name: Add build start comment to Expensify/Mobile-Expensify PR
if: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR != '' }}
# v7
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
with:
github-token: ${{ secrets.OS_BOTIFY_TOKEN }}
script: |
const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: 'Mobile-Expensify',
issue_number: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }},
body: `🚧 @${{ github.actor }} has triggered a test Expensify/Mobile-Expensify build. You can view the [workflow run here](${workflowURL}).`
});
web:
name: Build and deploy Web
if: ${{ inputs.WEB && needs.prep.outputs.APP_PR_NUMBER }}
needs: [prep]
runs-on: ubuntu-latest-xl
env:
PULL_REQUEST_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }}
steps:
- name: Checkout
# v4
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
with:
ref: ${{ needs.prep.outputs.APP_REF }}
- name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it
run: |
cp .env.staging .env.adhoc
sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc
- name: Setup Node
uses: ./.github/actions/composite/setupNode
- 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 for testing
run: npm run build-adhoc
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Deploy to S3 for internal testing
run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://ad-hoc-expensify-cash/web/"$PULL_REQUEST_NUMBER"
androidHybrid:
name: Build Android HybridApp
if: ${{ inputs.ANDROID }}
needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef]
runs-on: ubuntu-latest-xl
env:
PULL_REQUEST_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }}
outputs:
ROCK_ANDROID_ADHOC_INDEX_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }}
steps:
- name: Checkout
# v4
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
with:
submodules: true
ref: ${{ needs.prep.outputs.APP_REF }}
token: ${{ secrets.OS_BOTIFY_TOKEN }}
- name: Checkout Mobile-Expensify to specified branch or commit
if: ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF != '' }}
run: |
cd Mobile-Expensify
git fetch origin ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }}
git checkout ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }}
echo "Building from https://github.com/Expensify/Mobile-Expensify/pull/${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }}"
- 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 dotenv
run: |
cp .env.staging .env.adhoc
sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
echo "APP_PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc
- name: Setup 1Password CLI and certificates
uses: Expensify/GitHub-Actions/setup-certificate-1p@main
with:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
SHOULD_LOAD_SSL_CERTIFICATES: 'false'
- name: Load files from 1Password
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
run: |
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: 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: Rock Remote Build - Android
id: rock-remote-build-android
uses: callstackincubator/android@0bbc1b7c2e1a8be1ecb4d6c744c211869823fd65
env:
GITHUB_TOKEN: ${{ github.token }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
IS_HYBRID_APP: true
with:
variant: 'Adhoc'
sign: true
re-sign: true
ad-hoc: true
keystore-file: './upload-key.keystore'
keystore-store-file: 'upload-key.keystore'
keystore-store-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }}
keystore-key-alias: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }}
keystore-key-password: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }}
# Specify the path (relative to the Android source directory) where the keystore should be placed.
keystore-path: '../tools/buildtools/upload-key.keystore'
comment-bot: false
rock-build-extra-params: '--extra-params -PreactNativeArchitectures=arm64-v8a,x86_64'
- name: Set artifact URL output
id: set-artifact-url
run: echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT"
iosHybrid:
name: Build and deploy iOS for testing
if: ${{ inputs.IOS }}
needs: [prep, getMobileExpensifyPR, getMobileExpensifyRef]
env:
DEVELOPER_DIR: /Applications/Xcode_26.0.app/Contents/Developer
PULL_REQUEST_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }}
runs-on: macos-15-xlarge
outputs:
ROCK_IOS_ADHOC_INDEX_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }}
steps:
- name: Checkout
# v4
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
with:
submodules: true
ref: ${{ needs.prep.outputs.APP_REF }}
token: ${{ secrets.OS_BOTIFY_TOKEN }}
- name: Checkout Mobile-Expensify to specified branch or commit
if: ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF != '' }}
run: |
cd Mobile-Expensify
git fetch origin ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }}
git checkout ${{ needs.getMobileExpensifyRef.outputs.MOBILE_EXPENSIFY_REF }}
echo "Building from https://github.com/Expensify/Mobile-Expensify/pull/${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }}"
- 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: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it
run: |
cp .env.staging .env.adhoc
sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc
- name: Setup 1Password CLI and certificates
uses: Expensify/GitHub-Actions/setup-certificate-1p@main
with:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
SHOULD_LOAD_SSL_CERTIFICATES: 'false'
- name: Load files from 1Password
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
run: |
op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc/OldApp_AdHoc.mobileprovision" --force --out-file ./OldApp_AdHoc.mobileprovision
op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Share_Extension/OldApp_AdHoc_Share_Extension.mobileprovision" --force --out-file ./OldApp_AdHoc_Share_Extension.mobileprovision
op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Notification_Service/OldApp_AdHoc_Notification_Service.mobileprovision" --force --out-file ./OldApp_AdHoc_Notification_Service.mobileprovision
op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12
- name: Create ExportOptions.plist
run: |
cat > Mobile-Expensify/iOS/ExportOptions.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>ad-hoc</string>
<key>provisioningProfiles</key>
<dict>
<key>com.expensify.expensifylite.adhoc</key>
<string>(OldApp) AdHoc</string>
<key>com.expensify.expensifylite.adhoc.SmartScanExtension</key>
<string>(OldApp) AdHoc: Share Extension</string>
<key>com.expensify.expensifylite.adhoc.NotificationServiceExtension</key>
<string>(OldApp) AdHoc: Notification Service</string>
</dict>
</dict>
</plist>
EOF
- 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: Rock Remote Build - iOS
id: rock-remote-build-ios
uses: callstackincubator/ios@8dcef6cc275e0cf3299f5a97cde5ebd635c887d7
env:
GITHUB_TOKEN: ${{ github.token }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
IS_HYBRID_APP: true
with:
destination: device
re-sign: true
ad-hoc: true
scheme: 'Expensify AdHoc'
configuration: 'AdHoc'
certificate-file: './Certificates.p12'
provisioning-profiles: |
[
{
"name": "(OldApp) AdHoc",
"file": "./OldApp_AdHoc.mobileprovision"
},
{
"name": "(OldApp) AdHoc: Share Extension",
"file": "./OldApp_AdHoc_Share_Extension.mobileprovision"
},
{
"name": "(OldApp) AdHoc: Notification Service",
"file": "./OldApp_AdHoc_Notification_Service.mobileprovision"
}
]
comment-bot: false
- name: Set artifact URL output
id: set-artifact-url
run: echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT"
postGithubComment:
runs-on: ubuntu-latest
if: always()
name: Post a GitHub comment with app download links for testing
needs: [prep, getMobileExpensifyPR, web, androidHybrid, iosHybrid]
steps:
- name: Checkout
# v4
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
with:
ref: ${{ needs.prep.outputs.APP_REF }}
- name: Download Artifact
# v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
- name: Publish links to apps for download on Expensify/App PR
if: ${{ needs.prep.outputs.APP_PR_NUMBER || (!needs.prep.outputs.APP_PR_NUMBER && !needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR) }}
uses: ./.github/actions/javascript/postTestBuildComment
with:
REPO: App
APP_PR_NUMBER: ${{ needs.prep.outputs.APP_PR_NUMBER }}
MOBILE_EXPENSIFY_PR_NUMBER: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }}
GITHUB_TOKEN: ${{ github.token }}
ANDROID: ${{ needs.androidHybrid.result }}
IOS: ${{ needs.iosHybrid.result }}
WEB: ${{ needs.web.result }}
ANDROID_LINK: ${{ needs.androidHybrid.outputs.ROCK_ANDROID_ADHOC_INDEX_URL }}
IOS_LINK: ${{ needs.iosHybrid.outputs.ROCK_IOS_ADHOC_INDEX_URL }}
WEB_LINK: https://${{ needs.prep.outputs.APP_PR_NUMBER }}.pr-testing.expensify.com
- name: Publish links to apps for download on Expensify/Mobile-Expensify PR
if: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }}
uses: ./.github/actions/javascript/postTestBuildComment
with:
REPO: Mobile-Expensify
MOBILE_EXPENSIFY_PR_NUMBER: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }}
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
ANDROID: ${{ needs.androidHybrid.result }}
IOS: ${{ needs.iosHybrid.result }}
ANDROID_LINK: ${{ needs.androidHybrid.outputs.ROCK_ANDROID_ADHOC_INDEX_URL }}
IOS_LINK: ${{ needs.iosHybrid.outputs.ROCK_IOS_ADHOC_INDEX_URL }}