Skip to content

Build and deploy hybrid apps for testing #159

Build and deploy hybrid apps for testing

Build and deploy hybrid apps for testing #159

name: Build and deploy hybrid apps for testing
on:
workflow_dispatch:
inputs:
APP_PULL_REQUEST_NUMBER:
description: Pull Request number from App repo for correct placement of ND app. If not specified defaults to main branch.
required: false
default: ''
HYBRIDAPP_PULL_REQUEST_NUMBER:
description: Pull Request number from Mobile-Expensify repo for correct placement of OD app. It will take precedence over MOBILE-EXPENSIFY from App's PR description if both specified. If nothing is specified defaults to Mobile-Expensify's main
required: false
default: ''
jobs:
prep:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- 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 input
run: |
if [[ -z "${{ github.event.inputs.APP_PULL_REQUEST_NUMBER }}" && -z "${{ github.event.inputs.HYBRIDAPP_PULL_REQUEST_NUMBER }}" ]]; then
echo "Invalid input. You have to pass at least one PR number"
exit 1
fi
getNewDotRef:
runs-on: ubuntu-latest
needs: [prep]
outputs:
REF: ${{ steps.getHeadRef.outputs.REF }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check if pull request number is correct
id: getHeadRef
run: |
set -e
if [[ -z "${{ github.event.inputs.APP_PULL_REQUEST_NUMBER }}" ]]; then
echo "REF=" >> "$GITHUB_OUTPUT"
else
echo "REF=$(gh pr view ${{ github.event.inputs.APP_PULL_REQUEST_NUMBER }} -R Expensify/App --json headRefOid --jq '.headRefOid')" >> "$GITHUB_OUTPUT"
fi
env:
GITHUB_TOKEN: ${{ github.token }}
getOldDotPR:
runs-on: ubuntu-latest
needs: [prep]
outputs:
OLD_DOT_PR: ${{ steps.old-dot-pr.outputs.result }}
steps:
- name: Check if author specifed Old Dot PR
id: old-dot-pr
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
result-encoding: string
script: |
if (!'${{ github.event.inputs.APP_PULL_REQUEST_NUMBER }}') return '';
const pullRequest = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: '${{ github.event.inputs.APP_PULL_REQUEST_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();
getOldDotRef:
runs-on: ubuntu-latest
needs: [getOldDotPR]
outputs:
OLD_DOT_REF: ${{ steps.getHeadRef.outputs.REF }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check if pull request number is correct
id: getHeadRef
run: |
set -e
if [[ -z "${{ github.event.inputs.HYBRIDAPP_PULL_REQUEST_NUMBER }}" && -z "${{ needs.getOldDotPR.outputs.OLD_DOT_PR }}" ]]; then
echo "REF=" >> "$GITHUB_OUTPUT"
else
echo "REF=$(gh pr view ${{ github.event.inputs.HYBRIDAPP_PULL_REQUEST_NUMBER || needs.getOldDotPR.outputs.OLD_DOT_PR }} -R Expensify/Mobile-Expensify --json headRefOid --jq '.headRefOid')" >> "$GITHUB_OUTPUT"
fi
env:
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
postGitHubCommentBuildStarted:
runs-on: ubuntu-latest
needs: [getNewDotRef, getOldDotPR, getOldDotRef]
steps:
- name: Add build start comment to ND PR
if: ${{ github.event.inputs.APP_PULL_REQUEST_NUMBER != ''}}
uses: actions/github-script@v7
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: ${{ github.event.inputs.APP_PULL_REQUEST_NUMBER }},
body: `🚧 @${{ github.actor }} has triggered a test hybrid app build. You can view the [workflow run here](${workflowURL}).`
});
- name: Add build start comment to OD PR
if: ${{ github.event.inputs.HYBRIDAPP_PULL_REQUEST_NUMBER != '' || needs.getOldDotPR.outputs.OLD_DOT_PR != '' }}
uses: actions/github-script@v7
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: ${{ github.event.inputs.HYBRIDAPP_PULL_REQUEST_NUMBER || needs.getOldDotPR.outputs.OLD_DOT_PR }},
body: `🚧 @${{ github.actor }} has triggered a test hybrid app build. You can view the [workflow run here](${workflowURL}).`
});
androidHybrid:
name: Build Android HybridApp
needs: [getNewDotRef, getOldDotPR, getOldDotRef]
runs-on: ubuntu-latest-xl
outputs:
S3_APK_PATH: ${{ steps.exportAndroidS3Path.outputs.S3_APK_PATH }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
ref: ${{ needs.getNewDotRef.outputs.REF || 'main' }}
token: ${{ secrets.OS_BOTIFY_TOKEN }}
# fetch-depth: 0 is required in order to fetch the correct submodule branch
fetch-depth: 0
- name: Update submodule to match main
run: |
git submodule update --init --remote
if [[ -z "${{ needs.getOldDotRef.outputs.OLD_DOT_REF }}" ]]; then
echo "Building from Mobile-Expensify main branch"
fi
- name: Checkout Old Dot to author specified branch or commit
if: ${{ needs.getOldDotRef.outputs.OLD_DOT_REF != '' }}
run: |
cd Mobile-Expensify
git fetch origin ${{ needs.getOldDotRef.outputs.OLD_DOT_REF }}
git checkout ${{ needs.getOldDotRef.outputs.OLD_DOT_REF }}
echo "Building from https://github.com/Expensify/Mobile-Expensify/pull/${{ github.event.inputs.HYBRIDAPP_PULL_REQUEST_NUMBER || needs.getOldDotPR.outputs.OLD_DOT_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 "PULL_REQUEST_NUMBER=${{ github.event.inputs.APP_PULL_REQUEST_NUMBER }}" >> .env.adhoc
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'oracle'
java-version: '17'
- name: Setup Ruby
uses: ruby/[email protected]
with:
bundler-cache: true
- name: Install 1Password CLI
uses: 1password/install-cli-action@v1
- 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
uses: 1password/load-secrets-action@v2
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: Build Android app
id: build
env:
ANDROID_UPLOAD_KEYSTORE_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }}
ANDROID_UPLOAD_KEYSTORE_ALIAS: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }}
ANDROID_UPLOAD_KEY_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }}
run: bundle exec fastlane android build_adhoc_hybrid
- 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: Upload Android AdHoc build to S3
run: bundle exec fastlane android upload_s3
env:
S3_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_BUCKET: ad-hoc-expensify-cash
S3_REGION: us-east-1
- name: Export S3 path
id: exportAndroidS3Path
run: |
# $s3APKPath is set from within the Fastfile, android upload_s3 lane
echo "S3_APK_PATH=$s3APKPath" >> "$GITHUB_OUTPUT"
iosHybrid:
name: Build and deploy iOS for testing
needs: [getNewDotRef, getOldDotPR, getOldDotRef]
env:
DEVELOPER_DIR: /Applications/Xcode_16.2.0.app/Contents/Developer
runs-on: macos-15-xlarge
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
ref: ${{ needs.getNewDotRef.outputs.REF || 'main'}}
token: ${{ secrets.OS_BOTIFY_TOKEN }}
# fetch-depth: 0 is required in order to fetch the correct submodule branch
fetch-depth: 0
- name: Update submodule to match main
run: |
git submodule update --init --remote
if [[ -z "${{ needs.getOldDotRef.outputs.OLD_DOT_REF }}" ]]; then
echo "Building from Mobile-Expensify main branch"
fi
- name: Checkout Old Dot to author specified branch or commit
if: ${{ needs.getOldDotRef.outputs.OLD_DOT_REF != '' }}
run: |
cd Mobile-Expensify
git fetch origin ${{ needs.getOldDotRef.outputs.OLD_DOT_REF }}
git checkout ${{ needs.getOldDotRef.outputs.OLD_DOT_REF }}
echo "Building from https://github.com/Expensify/Mobile-Expensify/pull/${{ github.event.inputs.HYBRIDAPP_PULL_REQUEST_NUMBER || needs.getOldDotPR.outputs.OLD_DOT_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=${{ github.event.inputs.APP_PULL_REQUEST_NUMBER }}" >> .env.adhoc
- name: Setup Ruby
uses: ruby/[email protected]
with:
bundler-cache: true
- name: Install New Expensify Gems
run: bundle install
- name: Cache Pod dependencies
uses: actions/cache@v4
id: pods-cache
with:
path: Mobile-Expensify/iOS/Pods
key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/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('Mobile-Expensify/iOS/Podfile.lock') == hashFiles('Mobile-Expensify/iOS/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: npm run pod-install
- name: Install 1Password CLI
uses: 1password/install-cli-action@v1
- 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: Build AdHoc app
run: bundle exec fastlane ios build_adhoc_hybrid
- 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: Upload AdHoc build to S3
run: bundle exec fastlane ios upload_s3
env:
S3_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_BUCKET: ad-hoc-expensify-cash
S3_REGION: us-east-1
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: ios
path: ./ios_paths.json
postGithubComment:
runs-on: ubuntu-latest
name: Post a GitHub comment with app download links for testing
needs: [getNewDotRef, androidHybrid, iosHybrid]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.getNewDotRef.outputs.REF }}
- name: Download Artifact
uses: actions/download-artifact@v4
- name: Read JSONs with iOS paths
id: get_ios_path
if: ${{ needs.iosHybrid.result == 'success' }}
run: |
content_ios="$(cat ./ios/ios_paths.json)"
content_ios="${content_ios//'%'/'%25'}"
content_ios="${content_ios//$'\n'/'%0A'}"
content_ios="${content_ios//$'\r'/'%0D'}"
ios_path=$(echo "$content_ios" | jq -r '.html_path')
echo "ios_path=$ios_path" >> "$GITHUB_OUTPUT"
- name: Publish links to apps for download
uses: ./.github/actions/javascript/postTestBuildComment
with:
PR_NUMBER: ${{ github.event.inputs.APP_PULL_REQUEST_NUMBER }}
GITHUB_TOKEN: ${{ github.token }}
ANDROID: ${{ needs.androidHybrid.result }}
IOS: ${{ needs.iosHybrid.result }}
ANDROID_LINK: ${{ needs.androidHybrid.outputs.S3_APK_PATH }}
IOS_LINK: ${{ steps.get_ios_path.outputs.ios_path }}