Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
d7d8686
Separate build and upload jobs in deploy workflow
roryabraham Feb 3, 2026
f3251cb
Update upload-artifact action to v6
roryabraham Feb 3, 2026
cfddcf8
Update download-artifact action to v7
roryabraham Feb 3, 2026
5582497
Update checkout action to v6
roryabraham Feb 3, 2026
926fe68
Set artifact paths for Fastlane after downloading artifacts
roryabraham Feb 3, 2026
8252550
Skip artifact-dependent steps on production deploys
roryabraham Feb 3, 2026
2bc8888
DRY up staging/cherry-pick condition with SHOULD_DEPLOY_NATIVE output
roryabraham Feb 3, 2026
a947ec1
Rename SHOULD_DEPLOY_NATIVE to SHOULD_BUILD_NATIVE
roryabraham Feb 3, 2026
67fa88b
Add submodules to iOS upload job checkout
roryabraham Feb 3, 2026
da387f8
Add Node setup and pod install to iOS upload job
roryabraham Feb 3, 2026
ac13408
Use bracket notation for hyphenated job IDs
roryabraham Feb 3, 2026
4727e54
Add build jobs to failure notification needs
roryabraham Feb 3, 2026
afc91ea
Read IPA path from environment in upload_testflight_hybrid
roryabraham Feb 3, 2026
8d2c83e
Use dynamic artifact paths instead of hardcoded filenames
roryabraham Feb 3, 2026
01b44ba
Add MapBox SDK setup before pod install in iOS upload job
roryabraham Feb 3, 2026
93169af
Fall back to lane context for IPA path in upload_testflight_hybrid
roryabraham Feb 3, 2026
faa6181
Use recursive find for artifact paths
roryabraham Feb 3, 2026
5506e75
Use dynamic paths for APK/IPA in testing upload jobs
roryabraham Feb 3, 2026
b48ce10
Use camelCase for job names instead of kebab-case
roryabraham Feb 3, 2026
a93aba2
Propagate build failures to deployment status checks
roryabraham Feb 3, 2026
433d098
Refactor deploy workflow to separate build, upload, and submit jobs
roryabraham Feb 3, 2026
d96a434
Merge remote-tracking branch 'origin/main' into Rory-SeparateBuildAnd…
roryabraham Feb 19, 2026
09bb630
Use 32vcpu runners for heavy build jobs
roryabraham Feb 19, 2026
6abb732
Move version outputs to prep job so submit jobs run on production
roryabraham Feb 19, 2026
6f03e63
Gate submit jobs on upload completion for cherry-pick deploys
roryabraham Feb 19, 2026
e6400f2
Propagate build/upload failures in production deploy status
roryabraham Feb 19, 2026
d71dd9d
Quote $(pwd) in find commands to fix shellcheck SC2046
roryabraham Feb 19, 2026
4bd09e5
Block submit jobs when build fails on cherry-pick deploys
roryabraham Feb 20, 2026
0b43e7f
Upload AAB artifact before APK generation steps
roryabraham Feb 20, 2026
591bdbd
Restrict Applause uploads to staging non-cherry-pick deploys
roryabraham Feb 20, 2026
6e936f8
Add setup-gradle to androidBuild job in deploy workflow
roryabraham Feb 20, 2026
a82e7cc
Retry Android build after clearing Gradle cache on failure
roryabraham Feb 20, 2026
bccb127
Remove stale buildAndroid.yml and disabled e2ePerformanceTests.yml
roryabraham Feb 20, 2026
fedd763
Add buildAndroid.yml callable workflow using Rock Remote Build
roryabraham Feb 20, 2026
14424d2
Add buildIOS.yml callable workflow using Rock Remote Build
roryabraham Feb 20, 2026
f8b11e6
Add buildWeb.yml callable workflow
roryabraham Feb 20, 2026
c332d3f
Update deploy.yml to use callable build workflows
roryabraham Feb 20, 2026
fbe534d
Update testBuild.yml and testBuildOnPush.yml to use callable build wo…
roryabraham Feb 20, 2026
1aa3dad
Remove buildAdHoc.yml
roryabraham Feb 20, 2026
92eceb6
Merge origin/main into Rory-SeparateBuildAndUpload
roryabraham Feb 20, 2026
c8dcefb
Merge origin/main into Rory-SeparateBuildAndUpload
roryabraham Feb 20, 2026
2b9719a
DRY up test build workflows with buildAdHoc.yml wrapper
roryabraham Feb 20, 2026
72e9004
Minimize diff by reverting cosmetic changes
roryabraham Feb 20, 2026
c915d1b
Add custom-identifier to Rock builds for cache correctness
roryabraham Feb 20, 2026
0b9ee22
Make dSYM upload and download non-fatal for iOS deploys
roryabraham Feb 20, 2026
cece0fe
Remove unused upload-sourcemaps input from build workflows
roryabraham Feb 20, 2026
2525ec1
Use callable build workflows in verifyHybridApp.yml
roryabraham Feb 21, 2026
5c91faf
Move storybook build to dedicated job in deploy.yml
roryabraham Feb 21, 2026
faaa96f
Separate web build from deploy in buildWeb.yml
roryabraham Feb 21, 2026
e1f4c22
Consolidate iOS provisioning profile and ExportOptions steps
roryabraham Feb 21, 2026
c7dd51a
Include storybook docs in web deploy via artifact handoff
roryabraham Feb 21, 2026
ab6ed8e
Address review comments: pin builds to SHA, strip TestFlight job
roryabraham Feb 21, 2026
e6a588b
Propagate webBuild failure into WEB_RESULT
roryabraham Feb 21, 2026
dd839f3
Pin all deploy checkouts to github.sha, skip Dependabot verify builds
roryabraham Feb 21, 2026
1b459bd
Propagate buildWeb failure into ad-hoc web status reporting
roryabraham Feb 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
388 changes: 71 additions & 317 deletions .github/workflows/buildAdHoc.yml

Large diffs are not rendered by default.

310 changes: 190 additions & 120 deletions .github/workflows/buildAndroid.yml

Large diffs are not rendered by default.

257 changes: 257 additions & 0 deletions .github/workflows/buildIOS.yml
Original file line number Diff line number Diff line change
@@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>${{ vars.APPLE_ID }}</key>
<string>${{ vars.APPLE_STORE_PROVISIONING_PROFILE_NAME }}</string>
<key>${{ vars.APPLE_ID }}.SmartScanExtension</key>
<string>${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_NAME }}</string>
<key>${{ vars.APPLE_ID }}.NotificationServiceExtension</key>
<string>${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME }}</string>
</dict>
</dict>
</plist>
EOF
else
cat > Mobile-Expensify/iOS/ExportOptions.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>ad-hoc</string>
<key>provisioningProfiles</key>
<dict>
<key>com.expensify.expensifylite.adhoc</key>
<string>(OldApp) AdHoc</string>
<key>com.expensify.expensifylite.adhoc.SmartScanExtension</key>
<string>(OldApp) AdHoc: Share Extension</string>
<key>com.expensify.expensifylite.adhoc.NotificationServiceExtension</key>
<string>(OldApp) AdHoc: Notification Service</string>
</dict>
</dict>
</plist>
EOF
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
81 changes: 81 additions & 0 deletions .github/workflows/buildWeb.yml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading