diff --git a/.github/workflows/buildAdHoc.yml b/.github/workflows/buildAdHoc.yml
index 91f36c5460660..03f5450e17c4b 100644
--- a/.github/workflows/buildAdHoc.yml
+++ b/.github/workflows/buildAdHoc.yml
@@ -31,10 +31,6 @@ on:
description: Whether to build the Android app
type: string
default: 'true'
- TRIGGER_ACTOR:
- description: GitHub username who triggered the build
- type: string
- required: true
FORCE_NATIVE_BUILD:
description: Force a full native build, bypassing Rock remote cache
type: string
@@ -57,8 +53,8 @@ jobs:
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
- issue_number: ${{ inputs.APP_PR_NUMBER }},
- body: `π§ @${{ inputs.TRIGGER_ACTOR }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).`
+ issue_number: ${{ inputs.APP_PR_NUMBER || 0 }},
+ 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
@@ -72,268 +68,59 @@ jobs:
github.rest.issues.createComment({
owner: context.repo.owner,
repo: 'Mobile-Expensify',
- issue_number: ${{ inputs.MOBILE_EXPENSIFY_PR }},
- body: `π§ @${{ inputs.TRIGGER_ACTOR }} has triggered a test Expensify/Mobile-Expensify build. You can view the [workflow run here](${workflowURL}).`
+ issue_number: ${{ inputs.MOBILE_EXPENSIFY_PR || 0 }},
+ 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.BUILD_WEB == 'true' && inputs.APP_PR_NUMBER != '' }}
- runs-on: blacksmith-16vcpu-ubuntu-2404
- env:
- PULL_REQUEST_NUMBER: ${{ inputs.APP_PR_NUMBER }}
- steps:
- - name: Checkout
- # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- with:
- ref: ${{ inputs.APP_REF }}
-
- - name: Create .env.adhoc file based on staging
- run: |
- cp .env.staging .env.adhoc
- sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
-
- - name: Inject CI data into JS bundle
- run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER"
-
- - name: Setup Node
- uses: ./.github/actions/composite/setupNode
-
- - name: Configure AWS Credentials
- # v6
- uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
- 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.BUILD_ANDROID == 'true' }}
- runs-on: blacksmith-32vcpu-ubuntu-2404
- env:
- PULL_REQUEST_NUMBER: ${{ inputs.APP_PR_NUMBER }}
- outputs:
- ROCK_ANDROID_ADHOC_INDEX_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }}
- steps:
- - name: Checkout
- # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- with:
- submodules: true
- ref: ${{ inputs.APP_REF }}
- token: ${{ secrets.OS_BOTIFY_TOKEN }}
-
- - name: Checkout Mobile-Expensify to specified branch or commit
- if: ${{ inputs.MOBILE_EXPENSIFY_REF != '' }}
- run: |
- cd Mobile-Expensify
- git fetch origin ${{ inputs.MOBILE_EXPENSIFY_REF }}
- git checkout ${{ inputs.MOBILE_EXPENSIFY_REF }}
- echo "Building from https://github.com/Expensify/Mobile-Expensify/pull/${{ inputs.MOBILE_EXPENSIFY_PR }}"
-
- - name: Compute custom build identifier
- id: computeIdentifier
- run: |
- APP_SHORT_SHA=$(git rev-parse --short HEAD)
- MOBILE_EXPENSIFY_SHORT_SHA=$(cd Mobile-Expensify && git rev-parse --short HEAD)
- echo "IDENTIFIER=${APP_SHORT_SHA}-${MOBILE_EXPENSIFY_SHORT_SHA}" >> "$GITHUB_OUTPUT"
-
- - 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: Setup Gradle
- # v4
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244
-
- - name: Run grunt build
- run: |
- cd Mobile-Expensify
- npm run grunt:build:shared
-
- - name: Create .env.adhoc file based on staging
- run: |
- cp .env.staging .env.adhoc
- sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
-
- - name: Inject CI data into JS bundle
- run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER"
-
- - 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
- # v3
- uses: 1password/load-secrets-action@8d0d610af187e78a2772c2d18d627f4c52d3fbfb
- 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
- # v6
- uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
- 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@4cedf4d9b5c167452c96fe67233577e0fde9a025
- env:
- GITHUB_TOKEN: ${{ github.token }}
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- IS_HYBRID_APP: true
- FORCE_NATIVE_BUILD: ${{ inputs.FORCE_NATIVE_BUILD == 'true' && github.run_id || '' }}
- 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 --profile"'
- custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }}
- validate-elf-alignment: false
-
- - name: Upload Gradle profile report
- if: always()
- # v6
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
- with:
- name: gradle-profile-report
- path: Mobile-Expensify/Android/build/reports/profile/
- if-no-files-found: ignore
-
- - 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.BUILD_IOS == 'true' }}
- env:
- DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer
- PULL_REQUEST_NUMBER: ${{ inputs.APP_PR_NUMBER }}
- runs-on: macos-15-xlarge
- outputs:
- ROCK_IOS_ADHOC_INDEX_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }}
+ buildAndroid:
+ name: Build Android
+ if: ${{ fromJSON(inputs.BUILD_ANDROID) }}
+ uses: ./.github/workflows/buildAndroid.yml
+ with:
+ ref: ${{ inputs.APP_REF }}
+ variant: Adhoc
+ mobile-expensify-ref: ${{ inputs.MOBILE_EXPENSIFY_REF }}
+ pull-request-number: ${{ inputs.APP_PR_NUMBER }}
+ force-native-build: ${{ inputs.FORCE_NATIVE_BUILD }}
+ secrets: inherit
+
+ buildIOS:
+ name: Build iOS
+ if: ${{ fromJSON(inputs.BUILD_IOS) }}
+ uses: ./.github/workflows/buildIOS.yml
+ with:
+ ref: ${{ inputs.APP_REF }}
+ variant: Adhoc
+ mobile-expensify-ref: ${{ inputs.MOBILE_EXPENSIFY_REF }}
+ pull-request-number: ${{ inputs.APP_PR_NUMBER }}
+ force-native-build: ${{ inputs.FORCE_NATIVE_BUILD }}
+ secrets: inherit
+
+ buildWeb:
+ name: Build Web
+ if: ${{ fromJSON(inputs.BUILD_WEB) && inputs.APP_PR_NUMBER != '' }}
+ uses: ./.github/workflows/buildWeb.yml
+ with:
+ ref: ${{ inputs.APP_REF }}
+ environment: adhoc
+ pull-request-number: ${{ inputs.APP_PR_NUMBER }}
+ secrets: inherit
+
+ deployWebAdHoc:
+ name: Deploy Web to S3 (adhoc)
+ needs: [buildWeb]
+ if: ${{ fromJSON(inputs.BUILD_WEB) && inputs.APP_PR_NUMBER != '' }}
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- - name: Checkout
- # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- with:
- submodules: true
- ref: ${{ inputs.APP_REF }}
- token: ${{ secrets.OS_BOTIFY_TOKEN }}
-
- - name: Checkout Mobile-Expensify to specified branch or commit
- if: ${{ inputs.MOBILE_EXPENSIFY_REF != '' }}
- run: |
- cd Mobile-Expensify
- git fetch origin ${{ inputs.MOBILE_EXPENSIFY_REF }}
- git checkout ${{ inputs.MOBILE_EXPENSIFY_REF }}
- echo "Building from https://github.com/Expensify/Mobile-Expensify/pull/${{ inputs.MOBILE_EXPENSIFY_PR }}"
-
- - name: Compute custom build identifier
- id: computeIdentifier
- run: |
- APP_SHORT_SHA=$(git rev-parse --short HEAD)
- MOBILE_EXPENSIFY_SHORT_SHA=$(cd Mobile-Expensify && git rev-parse --short HEAD)
- echo "IDENTIFIER=${APP_SHORT_SHA}-${MOBILE_EXPENSIFY_SHORT_SHA}" >> "$GITHUB_OUTPUT"
-
- - 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
- run: |
- cp .env.staging .env.adhoc
- sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
-
- - name: Inject CI data into JS bundle
- run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER"
-
- - name: Setup 1Password CLI and certificates
- uses: Expensify/GitHub-Actions/setup-certificate-1p@main
+ - name: Download web build artifact
+ # v7
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
with:
- OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- SHOULD_LOAD_SSL_CERTIFICATES: 'false'
+ name: web-build-tar-gz-artifact
+ path: ./
- - 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'
-
-
-
-
- method
- ad-hoc
- provisioningProfiles
-
- com.expensify.expensifylite.adhoc
- (OldApp) AdHoc
- com.expensify.expensifylite.adhoc.SmartScanExtension
- (OldApp) AdHoc: Share Extension
- com.expensify.expensifylite.adhoc.NotificationServiceExtension
- (OldApp) AdHoc: Notification Service
-
-
-
- EOF
+ - name: Extract web build
+ run: tar -xzvf webBuild.tar.gz
- name: Configure AWS Credentials
# v6
@@ -343,48 +130,14 @@ jobs:
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@dd30f7e53eee2ea6a59509793d0a30fbb5c91216
- env:
- GITHUB_TOKEN: ${{ github.token }}
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- IS_HYBRID_APP: true
- FORCE_NATIVE_BUILD: ${{ inputs.FORCE_NATIVE_BUILD == 'true' && github.run_id || '' }}
- 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
- custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }}
-
- - name: Set artifact URL output
- id: set-artifact-url
- run: echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT"
+ - name: Deploy to S3
+ run: aws s3 cp --recursive --acl public-read dist s3://ad-hoc-expensify-cash/web/${{ inputs.APP_PR_NUMBER }}
postGithubComment:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: ${{ always() && (inputs.APP_PR_NUMBER != '' || inputs.MOBILE_EXPENSIFY_PR != '') }}
name: Post a GitHub comment with app download links for testing
- needs: [web, androidHybrid, iosHybrid]
+ needs: [buildWeb, deployWebAdHoc, buildAndroid, buildIOS]
steps:
- name: Checkout
# v6
@@ -404,11 +157,11 @@ jobs:
APP_PR_NUMBER: ${{ inputs.APP_PR_NUMBER }}
MOBILE_EXPENSIFY_PR_NUMBER: ${{ inputs.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 }}
+ ANDROID: ${{ needs.buildAndroid.result }}
+ IOS: ${{ needs.buildIOS.result }}
+ WEB: ${{ needs.buildWeb.result == 'failure' && 'failure' || needs.deployWebAdHoc.result }}
+ ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }}
+ IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }}
WEB_LINK: https://${{ inputs.APP_PR_NUMBER }}.pr-testing.expensify.com
- name: Publish links to apps for download on Expensify/Mobile-Expensify PR
@@ -418,16 +171,16 @@ jobs:
REPO: Mobile-Expensify
MOBILE_EXPENSIFY_PR_NUMBER: ${{ inputs.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 }}
+ ANDROID: ${{ needs.buildAndroid.result }}
+ IOS: ${{ needs.buildIOS.result }}
+ ANDROID_LINK: ${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }}
+ IOS_LINK: ${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }}
buildSummary:
runs-on: blacksmith-4vcpu-ubuntu-2404
- if: ${{ always() && (inputs.APP_PR_NUMBER != '' || needs.androidHybrid.outputs.ROCK_ANDROID_ADHOC_INDEX_URL != '' || needs.iosHybrid.outputs.ROCK_IOS_ADHOC_INDEX_URL != '') }}
+ if: ${{ always() && (inputs.APP_PR_NUMBER != '' || needs.buildAndroid.outputs.ROCK_ARTIFACT_URL != '' || needs.buildIOS.outputs.ROCK_ARTIFACT_URL != '') }}
name: Build Summary
- needs: [web, androidHybrid, iosHybrid]
+ needs: [buildWeb, deployWebAdHoc, buildAndroid, buildIOS]
steps:
- name: Checkout
# v6
@@ -452,14 +205,15 @@ jobs:
github-token: ${{ github.token }}
script: |
const prNumber = '${{ inputs.APP_PR_NUMBER }}';
- const webLink = prNumber && '${{ needs.web.result }}' === 'success' ? `https://${prNumber}.pr-testing.expensify.com` : '';
- const androidLink = '${{ needs.androidHybrid.outputs.ROCK_ANDROID_ADHOC_INDEX_URL }}' || '';
- const iosLink = '${{ needs.iosHybrid.outputs.ROCK_IOS_ADHOC_INDEX_URL }}' || '';
+ const webLink = prNumber && '${{ needs.deployWebAdHoc.result }}' === 'success' ? `https://${prNumber}.pr-testing.expensify.com` : '';
+ const androidLink = '${{ needs.buildAndroid.outputs.ROCK_ARTIFACT_URL }}' || '';
+ const iosLink = '${{ needs.buildIOS.outputs.ROCK_ARTIFACT_URL }}' || '';
const mobileExpensifySHA = '${{ steps.getMobileExpensifySHA.outputs.SHA }}' || '';
- const webStatus = webLink ? 'β
Success' : '${{ needs.web.result }}' === 'failure' ? 'β Failed' : 'βοΈ Skipped';
- const androidStatus = androidLink ? 'β
Success' : '${{ needs.androidHybrid.result }}' === 'failure' ? 'β Failed' : 'βοΈ Skipped';
- const iosStatus = iosLink ? 'β
Success' : '${{ needs.iosHybrid.result }}' === 'failure' ? 'β Failed' : 'βοΈ Skipped';
+ const webFailed = '${{ needs.buildWeb.result }}' === 'failure' || '${{ needs.deployWebAdHoc.result }}' === 'failure';
+ const webStatus = webLink ? 'β
Success' : webFailed ? 'β Failed' : 'βοΈ Skipped';
+ const androidStatus = androidLink ? 'β
Success' : '${{ needs.buildAndroid.result }}' === 'failure' ? 'β Failed' : 'βοΈ Skipped';
+ const iosStatus = iosLink ? 'β
Success' : '${{ needs.buildIOS.result }}' === 'failure' ? 'β Failed' : 'βοΈ Skipped';
const summary = core.summary
.addTable([
diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml
index acf097f89a1f6..f9b22b3849081 100644
--- a/.github/workflows/buildAndroid.yml
+++ b/.github/workflows/buildAndroid.yml
@@ -3,75 +3,94 @@ name: Build Android app
on:
workflow_call:
inputs:
- type:
- description: 'What type of build to run. Must be one of ["release", "adhoc"]'
- type: string
- required: true
ref:
description: Git ref to checkout and build
type: string
required: true
+ variant:
+ description: "'Release' or 'Adhoc'"
+ type: string
+ required: true
+ mobile-expensify-ref:
+ description: Mobile-Expensify ref to checkout (empty to use submodule at HEAD)
+ type: string
+ default: ''
+ pull-request-number:
+ description: Pull request number associated with this build
+ type: string
+ default: ''
artifact-prefix:
description: 'The prefix for build artifact names. This is useful if you need to call multiple builds from the same workflow'
type: string
- required: false
default: ''
- pull_request_number:
- description: The pull request number associated with this build, if relevant.
+ force-native-build:
+ description: Force a full native build, bypassing Rock remote cache
type: string
- required: false
+ default: 'false'
outputs:
- AAB_FILE_NAME:
- description: Name of the AAB file produced by this workflow.
- value: ${{ jobs.build.outputs.AAB_FILE_NAME }}
- APK_FILE_NAME:
- description: Name of the APK file produced by this workflow.
- value: ${{ jobs.build.outputs.APK_FILE_NAME }}
- APK_ARTIFACT_NAME:
- description: Name of the APK artifact.
- value: ${{ jobs.build.outputs.APK_ARTIFACT_NAME }}
-
- workflow_dispatch:
- inputs:
- type:
- description: What type of build do you want to run?
- required: true
- type: choice
- options:
- - release
- - adhoc
- ref:
- description: Git ref to checkout and build
- required: true
- type: string
-
- pull_request_number:
- description: The pull request number associated with this build, if relevant.
- type: number
- required: false
+ VERSION_CODE:
+ description: Android version code from the build
+ value: ${{ jobs.build.outputs.VERSION_CODE }}
+ ROCK_ARTIFACT_URL:
+ description: URL to download the ad-hoc build artifact (adhoc only)
+ value: ${{ jobs.build.outputs.ROCK_ARTIFACT_URL }}
jobs:
build:
name: Build Android app
- runs-on: blacksmith-16vcpu-ubuntu-2404
+ runs-on: blacksmith-32vcpu-ubuntu-2404
+ env:
+ PULL_REQUEST_NUMBER: ${{ inputs.pull-request-number }}
outputs:
- AAB_FILE_NAME: ${{ steps.build.outputs.AAB_FILE_NAME }}
- APK_FILE_NAME: ${{ steps.build.outputs.APK_FILE_NAME }}
- APK_ARTIFACT_NAME: ${{ steps.build.outputs.APK_ARTIFACT_NAME }}
-
+ VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }}
+ ROCK_ARTIFACT_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }}
steps:
- name: Checkout
- # v4
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
+ # v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
+ submodules: true
ref: ${{ inputs.ref }}
+ token: ${{ secrets.OS_BOTIFY_TOKEN }}
+
+ - name: Checkout Mobile-Expensify to specified branch or commit
+ if: ${{ inputs.mobile-expensify-ref != '' }}
+ run: |
+ cd Mobile-Expensify
+ git fetch origin ${{ inputs.mobile-expensify-ref }}
+ git checkout ${{ inputs.mobile-expensify-ref }}
+
+ - name: Compute custom build identifier
+ id: computeIdentifier
+ run: |
+ APP_SHORT_SHA=$(git rev-parse --short HEAD)
+ MOBILE_EXPENSIFY_SHORT_SHA=$(cd Mobile-Expensify && git rev-parse --short HEAD)
+ echo "IDENTIFIER=${APP_SHORT_SHA}-${MOBILE_EXPENSIFY_SHORT_SHA}" >> "$GITHUB_OUTPUT"
- 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: Create .env.adhoc file based on staging
+ if: ${{ inputs.variant == 'Adhoc' }}
+ run: |
+ cp .env.staging .env.adhoc
+ sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
+
+ - name: Inject CI data into JS bundle
+ if: ${{ inputs.variant == 'Adhoc' && inputs.pull-request-number != '' }}
+ run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER"
- name: Get Java version
id: get-java-version
@@ -88,12 +107,6 @@ jobs:
# v4
uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244
- - name: Setup Ruby
- # v1.229.0
- uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
- with:
- bundler-cache: true
-
- name: Setup 1Password CLI and certificates
uses: Expensify/GitHub-Actions/setup-certificate-1p@main
with:
@@ -101,90 +114,146 @@ jobs:
SHOULD_LOAD_SSL_CERTIFICATES: 'false'
- name: Load files from 1Password
- working-directory: android/app
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- run: op read "op://${{ vars.OP_VAULT }}/New Expensify my-upload-key.keystore/my-upload-key.keystore" --force --out-file ./my-upload-key.keystore
+ 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
+ cp ./upload-key.keystore Mobile-Expensify/Android
- - name: Get package version
- id: getPackageVersion
- run: echo "VERSION=$(jq -r .version < package.json)" >> "$GITHUB_OUTPUT"
+ - name: Load Android upload keystore credentials from 1Password
+ id: load-credentials
+ # v3
+ uses: 1password/load-secrets-action@8d0d610af187e78a2772c2d18d627f4c52d3fbfb
+ 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: Get Android native version
id: getAndroidVersion
- run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT"
+ run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT"
+
+ - name: Configure AWS Credentials
+ # v6
+ uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
+ 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: Setup DotEnv
- if: ${{ inputs.type == 'adhoc' }}
+ - name: Rock Remote Build - Android
+ id: rock-remote-build-android
+ continue-on-error: true
+ uses: callstackincubator/android@4cedf4d9b5c167452c96fe67233577e0fde9a025
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ IS_HYBRID_APP: true
+ FORCE_NATIVE_BUILD: ${{ inputs.force-native-build == 'true' && github.run_id || '' }}
+ with:
+ variant: ${{ inputs.variant }}
+ sign: true
+ re-sign: true
+ ad-hoc: ${{ inputs.variant == 'Adhoc' }}
+ 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 }}
+ keystore-path: '../tools/buildtools/upload-key.keystore'
+ comment-bot: false
+ rock-build-extra-params: ${{ inputs.variant == 'Adhoc' && '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' || '--extra-params "--profile"' }}
+ custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }}
+ validate-elf-alignment: false
+
+ - name: Clear Gradle cache
+ if: steps.rock-remote-build-android.outcome == 'failure'
run: |
- cp .env.staging .env.adhoc
- sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
- echo "PULL_REQUEST_NUMBER=${{ inputs.pull_request_number }}" >> .env.adhoc
+ echo "::warning::Android build failed, clearing Gradle caches and retryingβ¦"
+ rm -rf ~/.gradle/caches
- - name: Build Android app (retryable)
- # v3
- uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08
- id: build
+ - name: Rock Remote Build - Android (retry)
+ if: steps.rock-remote-build-android.outcome == 'failure'
+ uses: callstackincubator/android@4cedf4d9b5c167452c96fe67233577e0fde9a025
env:
- MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }}
- MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }}
+ GITHUB_TOKEN: ${{ github.token }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ IS_HYBRID_APP: true
+ FORCE_NATIVE_BUILD: ${{ inputs.force-native-build == 'true' && github.run_id || '' }}
with:
- retry_on: error
- retry_wait_seconds: 60
- timeout_minutes: 60
- max_attempts: 3
- command: |
- lane=''
- case '${{ inputs.type }}' in
- 'release')
- lane='build';;
- 'adhoc')
- lane='build_adhoc';;
- esac
- bundle exec fastlane android "$lane"
-
- # Refresh environment variables from GITHUB_ENV that are updated when running fastlane
- # shellcheck disable=SC1090
- source "$GITHUB_ENV"
-
- SHOULD_UPLOAD_SOURCEMAPS='false'
- if [ -f ./android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map ]; then
- SHOULD_UPLOAD_SOURCEMAPS='true'
- fi
-
- {
- # aabPath and apkPath are environment varibles set within the Fastfile
- echo "AAB_PATH=$aabPath"
- echo "AAB_FILE_NAME=$(basename "$aabPath")"
- echo "APK_PATH=$apkPath"
- echo "APK_FILE_NAME=$(basename "$apkPath")"
- echo "SHOULD_UPLOAD_SOURCEMAPS=$SHOULD_UPLOAD_SOURCEMAPS"
- echo "APK_ARTIFACT_NAME=${{ inputs.artifact-prefix }}android-apk-artifact" >> "$GITHUB_OUTPUT"
- } >> "$GITHUB_OUTPUT"
-
- - name: Upload Android AAB artifact
- if: ${{ steps.build.outputs.AAB_PATH != '' }}
- continue-on-error: true
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ variant: ${{ inputs.variant }}
+ sign: true
+ re-sign: true
+ ad-hoc: ${{ inputs.variant == 'Adhoc' }}
+ 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 }}
+ keystore-path: '../tools/buildtools/upload-key.keystore'
+ comment-bot: false
+ rock-build-extra-params: ${{ inputs.variant == 'Adhoc' && '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"' || '--extra-params "--profile"' }}
+ custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }}
+ validate-elf-alignment: false
+
+ - name: Upload Gradle profile report
+ if: always()
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
- name: ${{ inputs.artifact-prefix }}android-aab-artifact
- path: ${{ steps.build.outputs.AAB_PATH }}
+ name: ${{ inputs.artifact-prefix }}gradle-profile-report
+ path: Mobile-Expensify/Android/build/reports/profile/
+ if-no-files-found: ignore
- - name: Upload Android APK artifact
- if: ${{ steps.build.outputs.APK_PATH != '' }}
- continue-on-error: true
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ - name: Set artifact URL output
+ id: set-artifact-url
+ if: ${{ inputs.variant == 'Adhoc' }}
+ run: echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT"
+
+ - name: Find and upload AAB artifact
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
- name: ${{ steps.build.outputs.APK_ARTIFACT_NAME }}
- path: ${{ steps.build.outputs.APK_PATH }}
+ name: ${{ inputs.artifact-prefix }}androidBuild-artifact
+ path: Mobile-Expensify/Android/app/build/outputs/bundle/release/*.aab
- - name: Upload Android sourcemaps artifact
- if: ${{ steps.build.outputs.SHOULD_UPLOAD_SOURCEMAPS == 'true' }}
- continue-on-error: true
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ - name: Upload Android sourcemap artifact
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
+ with:
+ name: ${{ inputs.artifact-prefix }}android-sourcemap-artifact
+ path: Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map
+
+ - name: Install bundletool
+ run: |
+ readonly BUNDLETOOL_VERSION="1.18.1"
+ readonly BUNDLETOOL_URL="https://github.com/google/bundletool/releases/download/${BUNDLETOOL_VERSION}/bundletool-all-${BUNDLETOOL_VERSION}.jar"
+ curl -L -o bundletool.jar "$BUNDLETOOL_URL"
+ readonly EXPECTED_SHA="675786493983787ffa11550bdb7c0715679a44e1643f3ff980a529e9c822595c"
+ SHA="$(sha256sum bundletool.jar | cut -d ' ' -f1)"
+ if [[ "$SHA" != "$EXPECTED_SHA" ]]; then
+ echo "SHA mismatch: expected $EXPECTED_SHA but got $SHA"
+ exit 1
+ fi
+
+ - name: Generate APK from AAB
+ run: |
+ AAB_PATH=$(find Mobile-Expensify/Android/app/build/outputs/bundle -name '*.aab' | head -1)
+ java -jar bundletool.jar build-apks --bundle="$AAB_PATH" --output=Expensify.apks \
+ --mode=universal \
+ --ks=upload-key.keystore \
+ --ks-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} \
+ --ks-key-alias=${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} \
+ --key-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }}
+ unzip -p Expensify.apks universal.apk > Expensify.apk
+
+ - name: Upload Android APK build artifact
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
- name: ${{ inputs.artifact-prefix }}android-sourcemaps-artifact
- path: ./android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map
+ name: ${{ inputs.artifact-prefix }}android-apk-artifact
+ path: Expensify.apk
diff --git a/.github/workflows/buildIOS.yml b/.github/workflows/buildIOS.yml
new file mode 100644
index 0000000000000..4b20a2fdacbd9
--- /dev/null
+++ b/.github/workflows/buildIOS.yml
@@ -0,0 +1,257 @@
+name: Build iOS HybridApp
+
+on:
+ workflow_call:
+ inputs:
+ ref:
+ description: Git ref to checkout and build
+ type: string
+ required: true
+ variant:
+ description: "'Release' or 'Adhoc'"
+ type: string
+ required: true
+ mobile-expensify-ref:
+ description: Mobile-Expensify ref to checkout (empty to use submodule at HEAD)
+ type: string
+ default: ''
+ pull-request-number:
+ description: Pull request number associated with this build
+ type: string
+ default: ''
+ artifact-prefix:
+ description: Prefix for build artifact names
+ type: string
+ default: ''
+ force-native-build:
+ description: Force a full native build, bypassing Rock remote cache
+ type: string
+ default: 'false'
+
+ outputs:
+ IOS_VERSION:
+ description: iOS version string from the build
+ value: ${{ jobs.build.outputs.IOS_VERSION }}
+ ROCK_ARTIFACT_URL:
+ description: URL to download the ad-hoc build artifact (adhoc only)
+ value: ${{ jobs.build.outputs.ROCK_ARTIFACT_URL }}
+
+jobs:
+ build:
+ name: Build iOS HybridApp
+ runs-on: macos-15-xlarge
+ env:
+ DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer
+ PULL_REQUEST_NUMBER: ${{ inputs.pull-request-number }}
+ outputs:
+ IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }}
+ ROCK_ARTIFACT_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }}
+ steps:
+ - name: Checkout
+ # v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ with:
+ submodules: true
+ ref: ${{ inputs.ref }}
+ token: ${{ secrets.OS_BOTIFY_TOKEN }}
+
+ - name: Checkout Mobile-Expensify to specified branch or commit
+ if: ${{ inputs.mobile-expensify-ref != '' }}
+ run: |
+ cd Mobile-Expensify
+ git fetch origin ${{ inputs.mobile-expensify-ref }}
+ git checkout ${{ inputs.mobile-expensify-ref }}
+
+ - name: Compute custom build identifier
+ id: computeIdentifier
+ run: |
+ APP_SHORT_SHA=$(git rev-parse --short HEAD)
+ MOBILE_EXPENSIFY_SHORT_SHA=$(cd Mobile-Expensify && git rev-parse --short HEAD)
+ echo "IDENTIFIER=${APP_SHORT_SHA}-${MOBILE_EXPENSIFY_SHORT_SHA}" >> "$GITHUB_OUTPUT"
+
+ - 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
+ if: ${{ inputs.variant == 'Adhoc' }}
+ run: |
+ cp .env.staging .env.adhoc
+ sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
+
+ - name: Inject CI data into JS bundle
+ if: ${{ inputs.variant == 'Adhoc' && inputs.pull-request-number != '' }}
+ run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER"
+
+ - name: Setup Ruby
+ # v1.229.0
+ uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
+ with:
+ bundler-cache: true
+
+ - name: Install New Expensify Gems
+ run: bundle install
+
+ - name: Cache Pod dependencies
+ # v5.0.1
+ uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb
+ 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/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: npm run pod-install
+
+ - 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 provisioning profiles from 1Password
+ env:
+ OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
+ run: |
+ if [ "${{ inputs.variant }}" == "Release" ]; then
+ op read "op://${{ vars.OP_VAULT }}/firebase.json/firebase.json" --force --out-file ./firebase.json
+ op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore/${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}
+ op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Share_Extension/${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}
+ op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Notification_Service/${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}
+ else
+ 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
+ fi
+ op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12
+
+ - name: Create ExportOptions.plist
+ run: |
+ if [ "${{ inputs.variant }}" == "Release" ]; then
+ cat > Mobile-Expensify/iOS/ExportOptions.plist << EOF
+
+
+
+
+ method
+ app-store
+ provisioningProfiles
+
+ ${{ vars.APPLE_ID }}
+ ${{ vars.APPLE_STORE_PROVISIONING_PROFILE_NAME }}
+ ${{ vars.APPLE_ID }}.SmartScanExtension
+ ${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_NAME }}
+ ${{ vars.APPLE_ID }}.NotificationServiceExtension
+ ${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME }}
+
+
+
+ EOF
+ else
+ cat > Mobile-Expensify/iOS/ExportOptions.plist << 'EOF'
+
+
+
+
+ method
+ ad-hoc
+ provisioningProfiles
+
+ com.expensify.expensifylite.adhoc
+ (OldApp) AdHoc
+ com.expensify.expensifylite.adhoc.SmartScanExtension
+ (OldApp) AdHoc: Share Extension
+ com.expensify.expensifylite.adhoc.NotificationServiceExtension
+ (OldApp) AdHoc: Notification Service
+
+
+
+ EOF
+ fi
+
+ - name: Get iOS native version
+ id: getIOSVersion
+ run: echo "IOS_VERSION=$(jq -r .version < package.json | tr '-' '.')" >> "$GITHUB_OUTPUT"
+
+ - name: Configure AWS Credentials
+ # v6
+ uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
+ 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: Prepare provisioning profiles JSON
+ id: prepare-profiles
+ run: |
+ if [ "${{ inputs.variant }}" == "Release" ]; then
+ echo 'PROFILES=[{"name":"${{ vars.APPLE_STORE_PROVISIONING_PROFILE_NAME }}","file":"./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}"},{"name":"${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_NAME }}","file":"./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}"},{"name":"${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME }}","file":"./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}"}]' >> "$GITHUB_OUTPUT"
+ else
+ echo '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"}]' >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Rock Remote Build - iOS
+ id: rock-remote-build-ios
+ uses: callstackincubator/ios@dd30f7e53eee2ea6a59509793d0a30fbb5c91216
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ IS_HYBRID_APP: true
+ FORCE_NATIVE_BUILD: ${{ inputs.force-native-build == 'true' && github.run_id || '' }}
+ with:
+ destination: device
+ re-sign: true
+ ad-hoc: ${{ inputs.variant == 'Adhoc' }}
+ scheme: ${{ inputs.variant == 'Release' && 'Expensify' || 'Expensify AdHoc' }}
+ configuration: ${{ inputs.variant == 'Release' && 'Release' || 'AdHoc' }}
+ certificate-file: './Certificates.p12'
+ provisioning-profiles: ${{ steps.prepare-profiles.outputs.PROFILES }}
+ comment-bot: false
+ custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }}
+
+ - name: Set artifact URL output
+ id: set-artifact-url
+ if: ${{ inputs.variant == 'Adhoc' }}
+ run: echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT"
+
+ - name: Find and upload IPA artifact
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
+ with:
+ name: ${{ inputs.artifact-prefix }}iosBuild-artifact
+ path: .rock/cache/ios/export/*.ipa
+
+ - name: Find and upload dSYM artifact
+ id: upload-dsym
+ continue-on-error: true
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
+ with:
+ name: ${{ inputs.artifact-prefix }}ios-dsym-artifact
+ path: .rock/cache/ios/export/*.dSYM.zip
+ if-no-files-found: warn
+
+ - name: Log dSYM upload failure
+ if: steps.upload-dsym.outcome == 'failure'
+ run: echo "::error::Failed to upload dSYM artifact β symbolication data may be missing for this build"
+
+ - name: Upload iOS sourcemap artifact
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
+ with:
+ name: ${{ inputs.artifact-prefix }}ios-sourcemap-artifact
+ path: Mobile-Expensify/main.jsbundle.map
diff --git a/.github/workflows/buildWeb.yml b/.github/workflows/buildWeb.yml
new file mode 100644
index 0000000000000..482a43e8d64cc
--- /dev/null
+++ b/.github/workflows/buildWeb.yml
@@ -0,0 +1,81 @@
+name: Build Web
+
+on:
+ workflow_call:
+ inputs:
+ ref:
+ description: Git ref to checkout and build
+ type: string
+ required: true
+ environment:
+ description: "'production', 'staging', or 'adhoc'"
+ type: string
+ required: true
+ pull-request-number:
+ description: Pull request number (used for adhoc builds)
+ type: string
+ default: ''
+
+jobs:
+ build:
+ name: Build Web
+ runs-on: blacksmith-32vcpu-ubuntu-2404
+ env:
+ PULL_REQUEST_NUMBER: ${{ inputs.pull-request-number }}
+ steps:
+ - name: Checkout
+ # v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ with:
+ ref: ${{ inputs.ref }}
+
+ - name: Create .env.adhoc file based on staging
+ if: ${{ inputs.environment == 'adhoc' }}
+ run: |
+ cp .env.staging .env.adhoc
+ sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
+
+ - name: Inject CI data into JS bundle
+ if: ${{ inputs.environment == 'adhoc' && inputs.pull-request-number != '' }}
+ run: ./.github/scripts/inject-ci-data.sh PULL_REQUEST_NUMBER="$PULL_REQUEST_NUMBER"
+
+ - name: Setup Node
+ uses: ./.github/actions/composite/setupNode
+
+ - name: Build web
+ run: |
+ if [ "${{ inputs.environment }}" == "production" ]; then
+ npm run build
+ elif [ "${{ inputs.environment }}" == "staging" ]; then
+ npm run build-staging
+ else
+ npm run build-adhoc
+ fi
+ env:
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+
+ - name: Upload web sourcemaps artifact
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
+ with:
+ name: web-sourcemaps-artifact
+ path: ./dist/merged-source-map.js.map
+
+ - name: Compress web build .tar.gz and .zip
+ run: |
+ tar -czvf webBuild.tar.gz dist
+ zip -r webBuild.zip dist
+
+ - name: Upload .tar.gz web build artifact
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
+ with:
+ name: web-build-tar-gz-artifact
+ path: ./webBuild.tar.gz
+
+ - name: Upload .zip web build artifact
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
+ with:
+ name: web-build-zip-artifact
+ path: ./webBuild.zip
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 7e3820ce524e6..ceaee7ac4b8de 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -19,6 +19,10 @@ jobs:
TAG: ${{ steps.getTagName.outputs.TAG }}
# Is this deploy for a cherry-pick?
IS_CHERRY_PICK: ${{ steps.isCherryPick.outputs.IS_CHERRY_PICK }}
+ # Should we build native apps? (only on staging or cherry-pick, not production)
+ SHOULD_BUILD_NATIVE: ${{ github.ref == 'refs/heads/staging' || fromJSON(steps.isCherryPick.outputs.IS_CHERRY_PICK) }}
+ VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }}
+ IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }}
steps:
- name: Checkout
# v4
@@ -73,6 +77,14 @@ jobs:
isCherryPick,
);
+ - name: Get Android native version
+ id: getAndroidVersion
+ run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT"
+
+ - name: Get iOS native version
+ id: getIOSVersion
+ run: echo "IOS_VERSION=$(echo '${{ steps.getAppVersion.outputs.VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT"
+
# 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
@@ -81,44 +93,35 @@ jobs:
needs: prep
secrets: inherit
- android:
- name: Build and deploy Android HybridApp
- needs: prep
- runs-on: blacksmith-16vcpu-ubuntu-2404
- env:
- SHOULD_BUILD_APP: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }}
- steps:
- - name: Checkout App and Mobile-Expensify repo
- # v4
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
- with:
- submodules: true
- token: ${{ secrets.OS_BOTIFY_TOKEN }}
+ androidBuild:
+ name: Build Android HybridApp
+ needs: [prep]
+ if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }}
+ uses: ./.github/workflows/buildAndroid.yml
+ with:
+ ref: ${{ github.sha }}
+ variant: Release
+ secrets: inherit
- - name: Configure MapBox SDK
- run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }}
+ androidUploadGooglePlay:
+ name: Upload Android to Google Play
+ needs: [prep, androidBuild]
+ runs-on: blacksmith-2vcpu-ubuntu-2404
+ if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }}
+ steps:
+ - name: Checkout
+ # v6
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
- - name: Setup Node
- id: setup-node
- uses: ./.github/actions/composite/setupNode
+ - name: Download Android build artifact
+ # v7
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
with:
- IS_HYBRID_BUILD: 'true'
-
- - name: Run grunt build
- run: |
- cd Mobile-Expensify
- npm run grunt:build:shared
+ name: androidBuild-artifact
+ path: ./
- - name: Get Java version
- id: get-java-version
- uses: ./.github/actions/composite/getJavaVersion
-
- - name: Setup Java
- # v4
- uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12
- with:
- distribution: oracle
- java-version: ${{ steps.get-java-version.outputs.version }}
+ - name: Set aabPath for Fastlane
+ run: echo "aabPath=$(find "$(pwd)" -name '*.aab' | head -1)" >> "$GITHUB_ENV"
- name: Setup Ruby
# v1.229.0
@@ -129,60 +132,56 @@ jobs:
- name: Install New Expensify Gems
run: bundle install
- - name: Setup 1Password CLI and certificates
+ - name: Setup 1Password CLI
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
+ - name: Load Google Play credentials 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: Upload Android app to Google Play
+ run: bundle exec fastlane android ${{ vars.ANDROID_UPLOAD_COMMAND }}
+ env:
+ VERSION: ${{ needs.prep.outputs.VERSION_CODE }}
+ ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME }}
- - name: Load Android upload keystore credentials from 1Password
- id: load-credentials
- # v2
- uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0
+ androidSubmit:
+ name: Submit Android for production rollout
+ needs: [prep, androidBuild, androidUploadGooglePlay]
+ runs-on: blacksmith-2vcpu-ubuntu-2404
+ if: ${{ always() && !cancelled() && github.ref == 'refs/heads/production' && needs.androidBuild.result != 'failure' && needs.androidUploadGooglePlay.result != 'failure' }}
+ steps:
+ - name: Checkout
+ # v6
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+
+ - name: Setup Ruby
+ # v1.229.0
+ uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
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
- APPLAUSE_API_KEY: op://${{ vars.OP_VAULT }}/Applause-API-Key/password
+ bundler-cache: true
- - name: Get Android native version
- id: getAndroidVersion
- run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT"
+ - name: Install New Expensify Gems
+ run: bundle install
- - name: Build Android app
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
- run: bundle exec fastlane android build_hybrid
- 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 }}
- ANDROID_BUILD_TYPE: ${{ vars.ANDROID_BUILD_TYPE }}
- GITHUB_ACTOR: ${{ github.actor }}
- GITHUB_TOKEN: ${{ github.token }}
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ - name: Setup 1Password CLI
+ uses: Expensify/GitHub-Actions/setup-certificate-1p@main
+ with:
+ OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
+ SHOULD_LOAD_SSL_CERTIFICATES: 'false'
- - name: Upload Android app to Google Play
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
- run: bundle exec fastlane android ${{ vars.ANDROID_UPLOAD_COMMAND }}
+ - name: Load Google Play credentials from 1Password
env:
- VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }}
- ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME }}
+ OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
+ run: |
+ op read "op://${{ vars.OP_VAULT }}/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json
- name: Get current Android rollout percentage
- if: ${{ github.ref == 'refs/heads/production' }}
id: getAndroidRolloutPercentage
uses: ./.github/actions/javascript/getAndroidRolloutPercentage
with:
@@ -191,90 +190,17 @@ jobs:
# Complete the previous version rollout if the current rollout percentage is not -1 (no rollout in progress) or 1 (fully rolled out)
- name: Submit previous production build to 100%
- if: ${{ github.ref == 'refs/heads/production' && !contains(fromJSON('["1", "-1"]'), steps.getAndroidRolloutPercentage.outputs.CURRENT_ROLLOUT_PERCENTAGE) }}
+ if: ${{ !contains(fromJSON('["1", "-1"]'), steps.getAndroidRolloutPercentage.outputs.CURRENT_ROLLOUT_PERCENTAGE) }}
run: bundle exec fastlane android complete_hybrid_rollout
continue-on-error: true
- name: Submit production build for Google Play review and a slow rollout
- if: ${{ github.ref == 'refs/heads/production' }}
run: bundle exec fastlane android upload_google_play_production_hybrid_rollout
env:
- VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }}
-
- - name: Upload Android build to Browser Stack
- if: ${{ fromJSON(env.IS_APP_REPO) && (github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK)) }}
- run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ env.aabPath }}"
- env:
- BROWSERSTACK: ${{ secrets.BROWSERSTACK }}
-
- - name: Install bundletool
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
- run: |
- readonly BUNDLETOOL_VERSION="1.18.1"
- readonly BUNDLETOOL_URL="https://github.com/google/bundletool/releases/download/${BUNDLETOOL_VERSION}/bundletool-all-${BUNDLETOOL_VERSION}.jar"
-
- # Download jar from GitHub Release
- curl -L -o bundletool.jar "$BUNDLETOOL_URL"
-
- # Validate checksum
- readonly EXPECTED_SHA="675786493983787ffa11550bdb7c0715679a44e1643f3ff980a529e9c822595c"
- SHA="$(sha256sum bundletool.jar | cut -d ' ' -f1)"
- if [[ "$SHA" != "$EXPECTED_SHA" ]]; then
- echo "SHA mismatch: expected $EXPECTED_SHA but got $ACTUAL_SHA"
- exit 1
- fi
-
- - name: Generate APK from AAB
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
- run: |
- # Generate apks using bundletool
- java -jar bundletool.jar build-apks --bundle=${{ env.aabPath }} --output=Expensify.apks \
- --mode=universal \
- --ks=upload-key.keystore \
- --ks-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} \
- --ks-key-alias=${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} \
- --key-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }}
-
- # Unzip just the universal apk
- unzip -p Expensify.apks universal.apk > Expensify.apk
-
- - name: Upload Android build to Applause
- if: ${{ fromJSON(env.IS_APP_REPO) && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }}
- run: |
- APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')
- curl -F "file=@Expensify.apk" \
- "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36008" \
- -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}"
-
- - name: Upload Android APK build artifact
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
- with:
- name: android-apk-artifact
- path: Expensify.apk
-
- - name: Upload Android build artifact
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
- with:
- name: android-build-artifact
- path: ${{ env.aabPath }}
-
- - name: Upload Android sourcemap artifact
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
- with:
- name: android-sourcemap-artifact
- path: /home/runner/work/App/App/Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map
-
- - name: Set current App version in Env
- run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV"
+ VERSION: ${{ needs.prep.outputs.VERSION_CODE }}
- name: Warn deployers if Android production deploy failed
- if: ${{ failure() && github.ref == 'refs/heads/production' }}
+ if: ${{ failure() }}
# v3
uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e
with:
@@ -292,29 +218,121 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
- ios:
- name: Build and deploy iOS HybridApp
- needs: prep
+ androidUploadBrowserStack:
+ name: Upload Android to BrowserStack
+ needs: [prep, androidBuild]
+ runs-on: blacksmith-2vcpu-ubuntu-2404
+ if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }}
+ continue-on-error: true
+ steps:
+ - name: Download Android APK artifact
+ # v7
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
+ with:
+ name: android-apk-artifact
+ path: ./
+
+ - name: Find APK path
+ id: find-apk
+ run: echo "APK_PATH=$(find "$(pwd)" -name '*.apk' | head -1)" >> "$GITHUB_OUTPUT"
+
+ - name: Upload Android build to BrowserStack
+ if: ${{ fromJSON(env.IS_APP_REPO) }}
+ run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ steps.find-apk.outputs.APK_PATH }}"
+ env:
+ BROWSERSTACK: ${{ secrets.BROWSERSTACK }}
+
+ androidUploadApplause:
+ name: Upload Android to Applause
+ needs: [prep, androidBuild]
+ runs-on: blacksmith-2vcpu-ubuntu-2404
+ if: ${{ github.repository == 'Expensify/App' && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }}
+ continue-on-error: true
+ steps:
+ - name: Download Android APK artifact
+ # v7
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
+ with:
+ name: android-apk-artifact
+ path: ./
+
+ - name: Setup 1Password CLI
+ 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 Applause API key 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 }}
+ APPLAUSE_API_KEY: op://${{ vars.OP_VAULT }}/Applause-API-Key/password
+
+ - name: Find APK path
+ id: find-apk
+ run: echo "APK_PATH=$(find "$(pwd)" -name '*.apk' | head -1)" >> "$GITHUB_OUTPUT"
+
+ - name: Upload Android build to Applause
+ if: ${{ fromJSON(env.IS_APP_REPO) }}
+ run: |
+ APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')
+ curl -F "file=@${{ steps.find-apk.outputs.APK_PATH }}" \
+ "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36008" \
+ -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}"
+
+ iosBuild:
+ name: Build iOS HybridApp
+ needs: [prep]
+ if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }}
+ uses: ./.github/workflows/buildIOS.yml
+ with:
+ ref: ${{ github.sha }}
+ variant: Release
+ secrets: inherit
+
+ iosUploadTestflight:
+ name: Upload iOS to TestFlight
+ needs: [prep, iosBuild]
runs-on: macos-15-xlarge
+ if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }}
env:
DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer
- SHOULD_BUILD_APP: ${{ github.ref == 'refs/heads/staging' || fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }}
steps:
- name: Checkout
- # v4
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
+ # v6
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
with:
- submodules: true
token: ${{ secrets.OS_BOTIFY_TOKEN }}
+ submodules: true
- - name: Configure MapBox SDK
- run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }}
+ - name: Download iOS build artifact
+ # v7
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
+ with:
+ name: iosBuild-artifact
+ path: ./
- - name: Setup Node
- id: setup-node
- uses: ./.github/actions/composite/setupNode
+ - name: Download iOS dSYM artifact
+ id: download-dsym
+ continue-on-error: true
+ # v7
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
with:
- IS_HYBRID_BUILD: 'true'
+ name: ios-dsym-artifact
+ path: ./
+
+ - name: Log dSYM download failure
+ if: steps.download-dsym.outcome == 'failure'
+ run: echo "::error::Failed to download dSYM artifact β symbolication data may be missing for this build"
+
+ - name: Set artifact paths for Fastlane
+ run: |
+ echo "ipaPath=$(find "$(pwd)" -name '*.ipa' | head -1)" >> "$GITHUB_ENV"
+ echo "dsymPath=$(find "$(pwd)" -name '*.dSYM.zip' | head -1)" >> "$GITHUB_ENV"
- name: Setup Ruby
# v1.229.0
@@ -325,74 +343,19 @@ jobs:
- name: Install New Expensify Gems
run: bundle install
- - name: Cache Pod dependencies
- # v5.0.1
- uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb
- id: pods-cache
- with:
- path: Mobile-Expensify/iOS/Pods
- key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') }}
-
- - 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/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: npm run pod-install
-
- - name: Setup 1Password CLI and certificates
+ - name: Setup 1Password CLI
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
+ - name: Load iOS credentials from 1Password
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
run: |
- op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore/${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}
- op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Share_Extension/${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}
- op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Notification_Service/${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}
op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json
- op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12
-
- - name: Load iOS 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 }}
- APPLAUSE_API_KEY: op://${{ vars.OP_VAULT }}/Applause-API-Key/password
-
- - name: Set current App version in Env
- run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV"
-
- - name: Get iOS native version
- id: getIOSVersion
- run: echo "IOS_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT"
-
- - name: Build iOS HybridApp
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
- run: bundle exec fastlane ios build_hybrid
- env:
- APPLE_ID: ${{ vars.APPLE_ID }}
- APPLE_STORE_PROVISIONING_PROFILE_FILE: ${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}
- APPLE_STORE_PROVISIONING_PROFILE_NAME: ${{ vars.APPLE_STORE_PROVISIONING_PROFILE_NAME }}
- APPLE_SHARE_PROVISIONING_PROFILE_FILE: ${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}
- APPLE_SHARE_PROVISIONING_PROFILE_NAME: ${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_NAME }}
- APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE: ${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}
- APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME: ${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME }}
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Upload release build to TestFlight
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
run: bundle exec fastlane ios upload_testflight_hybrid
env:
APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }}
@@ -401,50 +364,51 @@ jobs:
APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }}
APPLE_ID: ${{ vars.APPLE_ID }}
+ iosSubmit:
+ name: Submit iOS for production rollout
+ needs: [prep, iosBuild, iosUploadTestflight]
+ runs-on: macos-15-xlarge
+ if: ${{ always() && !cancelled() && github.ref == 'refs/heads/production' && needs.iosBuild.result != 'failure' && needs.iosUploadTestflight.result != 'failure' }}
+ env:
+ DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer
+ steps:
+ - name: Checkout
+ # v6
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+
+ - name: Setup Ruby
+ # v1.229.0
+ uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
+ with:
+ bundler-cache: true
+
+ - name: Install New Expensify Gems
+ run: bundle install
+
+ - name: Setup 1Password CLI
+ 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 iOS credentials from 1Password
+ env:
+ OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
+ run: |
+ op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json
+
- name: Submit previous production build to 100%
- if: ${{ github.ref == 'refs/heads/production' }}
run: bundle exec fastlane ios complete_hybrid_rollout
continue-on-error: true
- name: Submit production build for App Store review and a slow rollout
- if: ${{ github.ref == 'refs/heads/production' }}
run: bundle exec fastlane ios submit_hybrid_for_rollout
env:
- VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }}
+ VERSION: ${{ needs.prep.outputs.IOS_VERSION }}
APPLE_ID: ${{ vars.APPLE_ID }}
- - name: Upload iOS build to Browser Stack
- if: ${{ fromJSON(env.IS_APP_REPO) && fromJSON(env.SHOULD_BUILD_APP) }}
- run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ env.ipaPath }}"
- env:
- BROWSERSTACK: ${{ secrets.BROWSERSTACK }}
-
- - name: Upload iOS build to Applause
- if: ${{ fromJSON(env.IS_APP_REPO) && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }}
- run: |
- APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')
- curl -F "file=@${{ env.ipaPath }}" \
- "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36005" \
- -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}"
-
- - name: Upload iOS build artifact
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
- with:
- name: ios-build-artifact
- path: ${{ env.ipaPath }}
-
- - name: Upload iOS sourcemap artifact
- if: ${{ fromJSON(env.SHOULD_BUILD_APP) }}
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
- with:
- name: ios-sourcemap-artifact
- path: /Users/runner/work/App/App/Mobile-Expensify/main.jsbundle.map
-
- name: Warn deployers if iOS production deploy failed
- if: ${{ failure() && github.ref == 'refs/heads/production' }}
+ if: ${{ failure() }}
# v3
uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e
with:
@@ -455,62 +419,136 @@ jobs:
attachments: [{
color: "#DB4545",
pretext: ``,
- text: `π₯ iOS HybridApp production deploy failed. Please ${{ steps.getIOSVersion.outputs.IOS_VERSION }} in the . π₯`,
+ text: `π₯ iOS HybridApp production deploy failed. Please ${{ needs.prep.outputs.IOS_VERSION }} in the . π₯`,
}]
}
env:
GITHUB_TOKEN: ${{ github.token }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
- web:
- name: Build and deploy Web
- needs: prep
- runs-on: blacksmith-16vcpu-ubuntu-2404
+ iosUploadBrowserStack:
+ name: Upload iOS to BrowserStack
+ needs: [prep, iosBuild]
+ runs-on: blacksmith-2vcpu-ubuntu-2404
+ if: ${{ fromJSON(needs.prep.outputs.SHOULD_BUILD_NATIVE) }}
+ continue-on-error: true
+ steps:
+ - name: Download iOS build artifact
+ # v7
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
+ with:
+ name: iosBuild-artifact
+ path: ./
+
+ - name: Find IPA path
+ id: find-ipa
+ run: echo "IPA_PATH=$(find "$(pwd)" -name '*.ipa' | head -1)" >> "$GITHUB_OUTPUT"
+
+ - name: Upload iOS build to BrowserStack
+ if: ${{ fromJSON(env.IS_APP_REPO) }}
+ run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ steps.find-ipa.outputs.IPA_PATH }}"
+ env:
+ BROWSERSTACK: ${{ secrets.BROWSERSTACK }}
+
+ iosUploadApplause:
+ name: Upload iOS to Applause
+ needs: [prep, iosBuild]
+ runs-on: blacksmith-2vcpu-ubuntu-2404
+ if: ${{ github.repository == 'Expensify/App' && github.ref == 'refs/heads/staging' && !fromJSON(needs.prep.outputs.IS_CHERRY_PICK) }}
+ continue-on-error: true
+ steps:
+ - name: Download iOS build artifact
+ # v7
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
+ with:
+ name: iosBuild-artifact
+ path: ./
+
+ - name: Setup 1Password CLI
+ 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 Applause API key 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 }}
+ APPLAUSE_API_KEY: op://${{ vars.OP_VAULT }}/Applause-API-Key/password
+
+ - name: Find IPA path
+ id: find-ipa
+ run: echo "IPA_PATH=$(find "$(pwd)" -name '*.ipa' | head -1)" >> "$GITHUB_OUTPUT"
+
+ - name: Upload iOS build to Applause
+ if: ${{ fromJSON(env.IS_APP_REPO) }}
+ run: |
+ APPLAUSE_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')
+ curl -F "file=@${{ steps.find-ipa.outputs.IPA_PATH }}" \
+ "https://api.applause.com/v2/builds?name=Expensify_$APPLAUSE_VERSION&productId=36005" \
+ -H "X-Api-Key: ${{ steps.load-credentials.outputs.APPLAUSE_API_KEY }}"
+
+ webBuild:
+ name: Build Web
+ needs: [prep]
+ uses: ./.github/workflows/buildWeb.yml
+ with:
+ ref: ${{ github.sha }}
+ environment: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }}
+ secrets: inherit
+
+ webDeploy:
+ name: Deploy Web to S3
+ needs: [prep, webBuild, buildStorybook]
+ if: ${{ always() && needs.webBuild.result == 'success' }}
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout
- # v4
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
+ # v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ with:
+ ref: ${{ github.sha }}
- - name: Setup Node
- uses: ./.github/actions/composite/setupNode
+ - name: Download web build artifact
+ # v7
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
+ with:
+ name: web-build-tar-gz-artifact
+ path: ./
+
+ - name: Extract web build
+ run: tar -xzvf webBuild.tar.gz
+
+ - name: Download storybook docs artifact
+ continue-on-error: true
+ # v7
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
+ with:
+ name: storybook-docs-artifact
+ path: ./dist/docs
- name: Setup Cloudflare CLI
run: pip3 install cloudflare==2.19.0
- name: Configure AWS Credentials
- # v4
- uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722
+ # v6
+ uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
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 [[ '${{ github.ref }}' == 'refs/heads/production' ]]; then
- npm run build
- else
- npm run build-staging
- fi
- env:
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
-
- - name: Build storybook docs
- continue-on-error: true
- run: |
- if [[ ${{ github.ref }} == 'refs/heads/production' ]]; 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
+ aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist ${{ env.S3_BUCKET }}/
+ aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association
+ aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_BUCKET }}/.well-known/apple-app-site-association ${{ env.S3_BUCKET }}/apple-app-site-association
env:
- S3_URL: s3://${{ github.ref == 'refs/heads/staging' && 'staging-' || '' }}${{ vars.PRODUCTION_S3_BUCKET }}
+ S3_BUCKET: s3://${{ github.ref == 'refs/heads/staging' && 'staging-' || '' }}${{ vars.PRODUCTION_S3_BUCKET }}
- name: Purge Cloudflare cache
run: |
@@ -521,41 +559,46 @@ jobs:
- name: Verify deploy
run: |
- ./.github/scripts/verifyDeploy.sh "$HOST" "${{ needs.prep.outputs.APP_VERSION }}"
+ APP_VERSION=$(jq -r .version < package.json)
+ ./.github/scripts/verifyDeploy.sh "$HOST" "$APP_VERSION"
env:
HOST: ${{ github.ref == 'refs/heads/production' && vars.WEB_PRODUCTION_HOST || vars.WEB_STAGING_HOST }}
- - name: Upload web sourcemaps artifact
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ buildStorybook:
+ name: Build storybook docs
+ needs: [prep]
+ runs-on: blacksmith-4vcpu-ubuntu-2404
+ continue-on-error: true
+ steps:
+ - name: Checkout
+ # v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
- name: web-sourcemaps-artifact
- path: ./dist/merged-source-map.js.map
+ ref: ${{ github.sha }}
- - name: Compress web build .tar.gz and .zip
- run: |
- tar -czvf webBuild.tar.gz dist
- zip -r webBuild.zip dist
+ - name: Setup Node
+ uses: ./.github/actions/composite/setupNode
- - name: Upload .tar.gz web build artifact
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
- with:
- name: web-build-tar-gz-artifact
- path: ./webBuild.tar.gz
+ - name: Build storybook docs
+ run: |
+ if [ "${{ github.ref }}" == "refs/heads/production" ]; then
+ npm run storybook-build
+ else
+ npm run storybook-build-staging
+ fi
- - name: Upload .zip web build artifact
- # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ - name: Upload storybook docs artifact
+ # v6
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
- name: web-build-zip-artifact
- path: ./webBuild.zip
+ name: storybook-docs-artifact
+ path: ./dist/docs
postSlackMessageOnFailure:
name: Post a Slack message when any platform fails to build or deploy
runs-on: blacksmith-2vcpu-ubuntu-2404
if: ${{ failure() }}
- needs: [android, ios, web]
+ needs: [androidBuild, androidUploadGooglePlay, androidUploadBrowserStack, androidUploadApplause, androidSubmit, iosBuild, iosUploadTestflight, iosUploadBrowserStack, iosUploadApplause, iosSubmit, webBuild, webDeploy]
steps:
- name: Checkout
# v4
@@ -571,16 +614,58 @@ jobs:
outputs:
IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }}
IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }}
- needs: [android, ios, web]
+ ANDROID_RESULT: ${{ steps.platformResults.outputs.ANDROID_RESULT }}
+ IOS_RESULT: ${{ steps.platformResults.outputs.IOS_RESULT }}
+ WEB_RESULT: ${{ steps.platformResults.outputs.WEB_RESULT }}
+ needs: [androidBuild, androidUploadGooglePlay, androidSubmit, iosBuild, iosUploadTestflight, iosSubmit, webBuild, webDeploy]
if: ${{ always() }}
steps:
+ # Determine effective result for each platform
+ - name: Determine platform results
+ id: platformResults
+ run: |
+ # Android: use submit result for production, upload result for staging.
+ # On production cherry-picks, propagate build/upload failures that caused submit to be skipped.
+ if [ "${{ github.ref }}" == "refs/heads/production" ]; then
+ if [ "${{ needs.androidBuild.result }}" == "failure" ] || [ "${{ needs.androidUploadGooglePlay.result }}" == "failure" ]; then
+ echo "ANDROID_RESULT=failure" >> "$GITHUB_OUTPUT"
+ else
+ echo "ANDROID_RESULT=${{ needs.androidSubmit.result }}" >> "$GITHUB_OUTPUT"
+ fi
+ elif [ "${{ needs.androidBuild.result }}" == "failure" ]; then
+ echo "ANDROID_RESULT=failure" >> "$GITHUB_OUTPUT"
+ else
+ echo "ANDROID_RESULT=${{ needs.androidUploadGooglePlay.result }}" >> "$GITHUB_OUTPUT"
+ fi
+
+ # iOS: use submit result for production, upload result for staging.
+ # On production cherry-picks, propagate build/upload failures that caused submit to be skipped.
+ if [ "${{ github.ref }}" == "refs/heads/production" ]; then
+ if [ "${{ needs.iosBuild.result }}" == "failure" ] || [ "${{ needs.iosUploadTestflight.result }}" == "failure" ]; then
+ echo "IOS_RESULT=failure" >> "$GITHUB_OUTPUT"
+ else
+ echo "IOS_RESULT=${{ needs.iosSubmit.result }}" >> "$GITHUB_OUTPUT"
+ fi
+ elif [ "${{ needs.iosBuild.result }}" == "failure" ]; then
+ echo "IOS_RESULT=failure" >> "$GITHUB_OUTPUT"
+ else
+ echo "IOS_RESULT=${{ needs.iosUploadTestflight.result }}" >> "$GITHUB_OUTPUT"
+ fi
+
+ # Web: propagate build failure even when deploy is skipped
+ if [ "${{ needs.webBuild.result }}" == "failure" ]; then
+ echo "WEB_RESULT=failure" >> "$GITHUB_OUTPUT"
+ else
+ echo "WEB_RESULT=${{ needs.webDeploy.result }}" >> "$GITHUB_OUTPUT"
+ fi
+
- name: Check deployment success on at least one platform
id: checkDeploymentSuccessOnAtLeastOnePlatform
run: |
isAtLeastOnePlatformDeployed="false"
- if [ "${{ needs.iOS.result }}" == "success" ] || \
- [ "${{ needs.android.result }}" == "success" ] || \
- [ "${{ needs.web.result }}" == "success" ]; then
+ if [ "${{ steps.platformResults.outputs.IOS_RESULT }}" == "success" ] || \
+ [ "${{ steps.platformResults.outputs.ANDROID_RESULT }}" == "success" ] || \
+ [ "${{ steps.platformResults.outputs.WEB_RESULT }}" == "success" ]; then
isAtLeastOnePlatformDeployed="true"
fi
echo "IS_AT_LEAST_ONE_PLATFORM_DEPLOYED=$isAtLeastOnePlatformDeployed" >> "$GITHUB_OUTPUT"
@@ -590,9 +675,9 @@ jobs:
id: checkDeploymentSuccessOnAllPlatforms
run: |
isAllPlatformsDeployed="false"
- if [ "${{ needs.iOS.result }}" == "success" ] && \
- [ "${{ needs.android.result }}" == "success" ] && \
- [ "${{ needs.web.result }}" == "success" ]; then
+ if [ "${{ steps.platformResults.outputs.IOS_RESULT }}" == "success" ] && \
+ [ "${{ steps.platformResults.outputs.ANDROID_RESULT }}" == "success" ] && \
+ [ "${{ steps.platformResults.outputs.WEB_RESULT }}" == "success" ]; then
isAllPlatformsDeployed="true"
fi
@@ -668,10 +753,10 @@ jobs:
run: |
# Release asset name should follow the template: fileNameOnRunner#fileNameInRelease
files="
- ./android-build-artifact/Expensify-release.aab#android.aab
+ ./androidBuild-artifact/Expensify-release.aab#android.aab
./android-apk-artifact/Expensify.apk#android.apk
./android-sourcemap-artifact/index.android.bundle.map#android-sourcemap.js.map
- ./ios-build-artifact/Expensify.ipa#ios.ipa
+ ./iosBuild-artifact/Expensify.ipa#ios.ipa
./ios-sourcemap-artifact/main.jsbundle.map#ios-sourcemap.js.map
./web-sourcemaps-artifact/web-merged-source-map.js.map#web-sourcemap.js.map
./web-build-tar-gz-artifact/webBuild.tar.gz#web.tar.gz
@@ -730,7 +815,7 @@ jobs:
name: Post a Slack message when all platforms deploy successfully
runs-on: blacksmith-2vcpu-ubuntu-2404
if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }}
- needs: [prep, android, ios, web, checkDeploymentSuccess, createRelease]
+ needs: [prep, androidUploadGooglePlay, androidSubmit, iosUploadTestflight, iosSubmit, webDeploy, checkDeploymentSuccess, createRelease]
steps:
- name: 'Announces the deploy in the #announce Slack room'
# v3
@@ -787,11 +872,11 @@ jobs:
postGithubComments:
uses: ./.github/workflows/postDeployComments.yml
if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }}
- needs: [prep, android, ios, web, checkDeploymentSuccess, createRelease]
+ needs: [prep, checkDeploymentSuccess, createRelease]
secrets: inherit
with:
version: ${{ needs.prep.outputs.APP_VERSION }}
env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }}
- android: ${{ needs.android.result }}
- ios: ${{ needs.ios.result }}
- web: ${{ needs.web.result }}
+ android: ${{ needs.checkDeploymentSuccess.outputs.ANDROID_RESULT }}
+ ios: ${{ needs.checkDeploymentSuccess.outputs.IOS_RESULT }}
+ web: ${{ needs.checkDeploymentSuccess.outputs.WEB_RESULT }}
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 7d0db6f4dee08..5b69f769785fb 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -161,6 +161,5 @@ jobs:
BUILD_WEB: ${{ inputs.WEB && 'true' || 'false' }}
BUILD_IOS: ${{ inputs.IOS && 'true' || 'false' }}
BUILD_ANDROID: ${{ inputs.ANDROID && 'true' || 'false' }}
- TRIGGER_ACTOR: ${{ github.actor }}
FORCE_NATIVE_BUILD: ${{ inputs.FORCE_NATIVE_BUILD && 'true' || 'false' }}
secrets: inherit
diff --git a/.github/workflows/testBuildOnPush.yml b/.github/workflows/testBuildOnPush.yml
index 5bc2bbe150632..556c67476b4e7 100644
--- a/.github/workflows/testBuildOnPush.yml
+++ b/.github/workflows/testBuildOnPush.yml
@@ -89,5 +89,4 @@ jobs:
BUILD_WEB: ${{ needs.prep.outputs.BUILD_WEB }}
BUILD_IOS: ${{ needs.prep.outputs.BUILD_MOBILE }}
BUILD_ANDROID: ${{ needs.prep.outputs.BUILD_MOBILE }}
- TRIGGER_ACTOR: ${{ github.actor }}
secrets: inherit
diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml
index 4e4c7376a5a61..39fb5f608114c 100644
--- a/.github/workflows/verifyHybridApp.yml
+++ b/.github/workflows/verifyHybridApp.yml
@@ -72,105 +72,21 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ github.token }}
+
verify_android:
name: Verify Android HybridApp builds on main
- runs-on: blacksmith-16vcpu-ubuntu-2404
- # Only run on pull requests that are *not* on a fork
- if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' }}
- steps:
- - name: Checkout
- # v4
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
- with:
- submodules: true
- ref: ${{ github.event.pull_request.head.sha }}
- token: ${{ secrets.OS_BOTIFY_TOKEN }}
-
- - 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: Setup Ruby
- # v1.229.0
- uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
- with:
- bundler-cache: true
-
- - name: Build Android Debug
- run: |
- if ! npm run android-hybrid-build
- then
- echo "β Android HybridApp failed to build: Please reach out to Contributor+ and/or Expensify engineers for help in #expensify-open-source to resolve."
- exit 1
- fi
+ if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' }}
+ uses: ./.github/workflows/buildAndroid.yml
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ variant: Release
+ secrets: inherit
verify_ios:
name: Verify iOS HybridApp builds on main
- runs-on: macos-15-xlarge
- env:
- DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer
- # Only run on pull requests that are *not* on a fork
- if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' }}
- steps:
- - name: Checkout
- # v4
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
- with:
- submodules: true
- ref: ${{ github.event.pull_request.head.sha }}
- token: ${{ secrets.OS_BOTIFY_TOKEN }}
-
- - 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: Setup Ruby
- # v1.229.0
- uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
- with:
- bundler-cache: true
-
- - name: Cache Pod dependencies
- # v5.0.1
- uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb
- id: pods-cache
- with:
- path: Mobile-Expensify/iOS/Pods
- key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') }}
-
- - 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: Build iOS HybridApp
- run: |
- # Let us know if the builds fails
- set -o pipefail
-
- # Do not start metro
- export RCT_NO_LAUNCH_PACKAGER=1
-
- # Build iOS using xcodebuild
- if ! npm run ios-hybrid-build
- then
- echo "β iOS HybridApp failed to build: Please reach out to Contributor+ and/or Expensify engineers for help in #expensify-open-source to resolve."
- exit 1
- fi
+ if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' }}
+ uses: ./.github/workflows/buildIOS.yml
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ variant: Release
+ secrets: inherit
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 2dc1d82f29279..afe929f5a3b38 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -490,7 +490,10 @@ platform :ios do
desc "Upload HybridApp to TestFlight"
lane :upload_testflight_hybrid do
+ # Use environment variable if set (for separate upload jobs), otherwise fall back to lane context (for local runs)
+ ipa_path = ENV[KEY_IPA_PATH] || lane_context[SharedValues::IPA_OUTPUT_PATH]
upload_to_testflight(
+ ipa: ipa_path,
app_identifier: ENV["APPLE_ID"],
api_key_path: "./ios-fastlane-json-key.json",
distribute_external: true,