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,