diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml deleted file mode 100644 index b86b68cc7d7d..000000000000 --- a/.github/actions/composite/buildAndroidE2EAPK/action.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Build an Android apk for e2e tests -description: Build an Android apk for an E2E test build and upload it as an artifact - -inputs: - ARTIFACT_NAME: - description: The name of the workflow artifact where the APK should be uploaded - required: true - ARTIFACT_RETENTION_DAYS: - description: The number of days to retain the artifact - required: false - # Thats github default: - default: "90" - PACKAGE_SCRIPT_NAME: - description: The name of the npm script to run to build the APK - required: true - APP_OUTPUT_PATH: - description: The path to the built APK - required: true - MAPBOX_SDK_DOWNLOAD_TOKEN: - description: The token to use to download the MapBox SDK - required: true - PATH_ENV_FILE: - description: The path to the .env file to use for the build - required: true - EXPENSIFY_PARTNER_NAME: - description: The name of the Expensify partner to use for the build - required: true - EXPENSIFY_PARTNER_PASSWORD: - description: The password of the Expensify partner to use for the build - required: true - EXPENSIFY_PARTNER_USER_ID: - description: The user ID of the Expensify partner to use for the build - required: true - EXPENSIFY_PARTNER_USER_SECRET: - description: The user secret of the Expensify partner to use for the build - required: true - EXPENSIFY_PARTNER_PASSWORD_EMAIL: - description: The email address of the Expensify partner to use for the build - required: true - SLACK_WEBHOOK_URL: - description: 'URL of the slack webhook' - required: true - -runs: - using: composite - steps: - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ inputs.MAPBOX_SDK_DOWNLOAD_TOKEN }} - shell: bash - - - uses: Expensify/App/.github/actions/composite/setupNode@main - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "oracle" - java-version: "17" - - - uses: ruby/setup-ruby@v1.190.0 - with: - bundler-cache: true - - - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef - - - name: Append environment variables to env file - shell: bash - run: | - echo "EXPENSIFY_PARTNER_NAME=${{ inputs.EXPENSIFY_PARTNER_NAME }}" >> ${{ inputs.PATH_ENV_FILE }} - echo "EXPENSIFY_PARTNER_PASSWORD=${{ inputs.EXPENSIFY_PARTNER_PASSWORD }}" >> ${{ inputs.PATH_ENV_FILE }} - echo "EXPENSIFY_PARTNER_USER_ID=${{ inputs.EXPENSIFY_PARTNER_USER_ID }}" >> ${{ inputs.PATH_ENV_FILE }} - echo "EXPENSIFY_PARTNER_USER_SECRET=${{ inputs.EXPENSIFY_PARTNER_USER_SECRET }}" >> ${{ inputs.PATH_ENV_FILE }} - echo "EXPENSIFY_PARTNER_PASSWORD_EMAIL=${{ inputs.EXPENSIFY_PARTNER_PASSWORD_EMAIL }}" >> ${{ inputs.PATH_ENV_FILE }} - - - name: Build APK - run: npm run ${{ inputs.PACKAGE_SCRIPT_NAME }} - shell: bash - env: - RUBYOPT: '-rostruct' - - - name: Announce failed workflow in Slack - if: failure() - uses: 8398a7/action-slack@v3 - with: - status: custom - custom_payload: | - { - channel: '#e2e-announce', - attachments: [{ - color: 'danger', - text: `🚧 ${process.env.AS_REPO} E2E APK build run failed on workflow 🚧`, - }] - } - env: - GITHUB_TOKEN: ${{ github.token }} - SLACK_WEBHOOK_URL: ${{ inputs.SLACK_WEBHOOK_URL }} - - - name: Upload APK - uses: actions/upload-artifact@v4 - with: - name: ${{ inputs.ARTIFACT_NAME }} - path: ${{ inputs.APP_OUTPUT_PATH }} - retention-days: ${{ inputs.ARTIFACT_RETENTION_DAYS }} diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml new file mode 100644 index 000000000000..a8023aebd359 --- /dev/null +++ b/.github/workflows/buildAndroid.yml @@ -0,0 +1,188 @@ +name: Build Android app + +on: + workflow_call: + inputs: + type: + description: 'What type of build to run. Must be one of ["release", "adhoc", "e2e", "e2eDelta"]' + type: string + required: true + ref: + description: Git ref to checkout and build + type: string + required: true + 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. + type: string + required: false + outputs: + AAB_FILE_NAME: + value: ${{ jobs.build.outputs.AAB_FILE_NAME }} + APK_FILE_NAME: + value: ${{ jobs.build.outputs.APK_FILE_NAME }} + + workflow_dispatch: + inputs: + type: + description: What type of build do you want to run? + required: true + type: choice + options: + - release + - adhoc + - e2e + - e2eDelta + 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 + +jobs: + build: + name: Build Android app + runs-on: ubuntu-latest-xl + env: + RUBYOPT: '-rostruct' + outputs: + AAB_FILE_NAME: ${{ steps.build.outputs.AAB_FILE_NAME }} + APK_FILE_NAME: ${{ steps.build.outputs.APK_FILE_NAME }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - name: Configure MapBox SDK + run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: oracle + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1.190.0 + with: + bundler-cache: true + + - name: Decrypt keystore and json key + run: | + cd android/app + gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output my-upload-key.keystore my-upload-key.keystore.gpg + gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg + + - name: Get package version + id: getPackageVersion + run: echo "VERSION=$(jq -r .version < package.json)" >> "$GITHUB_OUTPUT" + + - 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" + + - name: Setup DotEnv + if: ${{ inputs.type != 'release' }} + run: | + if [ '${{ inputs.type }}' == 'adhoc' ]; then + cp .env.staging .env.adhoc + sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + echo "PULL_REQUEST_NUMBER=${{ inputs.pull_request_number }}" >> .env.adhoc + else + envFile='' + if [ '${{ inputs.type }}' == 'e2e' ]; then + envFile='tests/e2e/.env.e2e' + else + envFile=tests/e2e/.env.e2edelta + fi + { + echo "EXPENSIFY_PARTNER_NAME=${{ secrets.EXPENSIFY_PARTNER_NAME }}" + echo "EXPENSIFY_PARTNER_PASSWORD=${{ secrets.EXPENSIFY_PARTNER_PASSWORD }}" + echo "EXPENSIFY_PARTNER_USER_ID=${{ secrets.EXPENSIFY_PARTNER_USER_ID }}" + echo "EXPENSIFY_PARTNER_USER_SECRET=${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }}" + echo "EXPENSIFY_PARTNER_PASSWORD_EMAIL=${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }}" + } >> "$envFile" + fi + + - name: Build Android app + id: build + run: | + lane='' + case '${{ inputs.type }}' in + 'release') + lane='build';; + 'adhoc') + lane='build_adhoc';; + 'e2e') + lane='build_e2e';; + 'e2eDelta') + lane='build_e2eDelta';; + 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" + } >> "$GITHUB_OUTPUT" + env: + MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }} + MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} + + - name: Upload Android AAB artifact + if: ${{ steps.build.outputs.AAB_PATH != '' }} + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}android-artifact-aab + path: ${{ steps.build.outputs.AAB_PATH }} + + - name: Upload Android APK artifact + if: ${{ steps.build.outputs.APK_PATH != '' }} + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}android-artifact-apk + path: ${{ steps.build.outputs.APK_PATH }} + + - name: Upload Android sourcemaps artifact + if: ${{ steps.build.outputs.SHOULD_UPLOAD_SOURCEMAPS == 'true' }} + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}android-artifact-sourcemaps + path: ./android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map + + - name: Announce failure in slack + if: failure() + uses: ./.github/actions/composite/announceFailedWorkflowInSlack + with: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 99cd0c1dabc5..60bb97b0c2e7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -67,84 +67,78 @@ jobs: needs: prep secrets: inherit - android: - name: Build and deploy Android + buildAndroid: + name: Build Android app + uses: ./.github/workflows/buildAndroid.yml + if: ${{ github.ref == 'refs/heads/staging' }} needs: prep - runs-on: ubuntu-latest-xl + secrets: inherit + with: + type: release + ref: staging + + uploadAndroid: + name: Upload Android build to Google Play Store + needs: buildAndroid + runs-on: ubuntu-latest env: RUBYOPT: '-rostruct' steps: - name: Checkout uses: actions/checkout@v4 - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - - name: Setup Node - uses: ./.github/actions/composite/setupNode - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'oracle' - java-version: '17' - - name: Setup Ruby uses: ruby/setup-ruby@v1.190.0 with: bundler-cache: true - - name: Decrypt keystore - run: cd android/app && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output my-upload-key.keystore my-upload-key.keystore.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - - name: Decrypt json key - run: cd android/app && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - - 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" + - name: Download Android build artifacts + uses: actions/download-artifact@v4 + with: + path: /tmp/artifacts + pattern: android-artifact-* + merge-multiple: true - - name: Build Android app - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane android build - env: - MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }} - MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} + - name: Log downloaded artifact paths + run: ls -R /tmp/artifacts - name: Upload Android app to Google Play - run: bundle exec fastlane android ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'upload_google_play_production' || 'upload_google_play_internal' }} + run: bundle exec fastlane android upload_google_play_internal env: - VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} + aabPath: /tmp/artifacts/${{ needs.buildAndroid.outputs.AAB_FILE_NAME }} - name: Upload Android build to Browser Stack - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@./android/app/build/outputs/bundle/productionRelease/app-production-release.aab" + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@/tmp/artifacts/${{ needs.buildAndroid.outputs.AAB_FILE_NAME }}" env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - - name: Upload Android sourcemaps artifact - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - uses: actions/upload-artifact@v4 - with: - name: android-sourcemaps-artifact - path: ./android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map + submitAndroid: + name: Submit Android app for production review + needs: prep + if: ${{ github.ref == 'refs/heads/production' }} + runs-on: ubuntu-latest + env: + RUBYOPT: '-rostruct' + steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Upload Android build artifact - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - uses: actions/upload-artifact@v4 + - name: Setup Ruby + uses: ruby/setup-ruby@v1.190.0 with: - name: android-build-artifact - path: ./android/app/build/outputs/bundle/productionRelease/app-production-release.aab + bundler-cache: true + + - 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" - - name: Set current App version in Env - run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" + - name: Submit Android build for review + run: bundle exec fastlane android upload_google_play_production + env: + VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} - name: Warn deployers if Android production deploy failed - if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + if: ${{ failure() }} uses: 8398a7/action-slack@v3 with: status: custom @@ -266,9 +260,6 @@ jobs: env: LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - 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" @@ -381,9 +372,6 @@ jobs: env: CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} - - name: Set current App version in Env - run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" - - name: Verify staging deploy if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} run: ./.github/scripts/verifyDeploy.sh staging ${{ needs.prep.outputs.APP_VERSION }} @@ -419,7 +407,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: ubuntu-latest if: ${{ failure() }} - needs: [android, desktop, iOS, web] + needs: [buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web] steps: - name: Checkout uses: actions/checkout@v4 @@ -444,16 +432,25 @@ jobs: runs-on: ubuntu-latest outputs: IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} - IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastAllPlatform.outputs.IS_ALL_PLATFORMS_DEPLOYED }} - needs: [android, desktop, iOS, web] + IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }} + needs: [buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web] if: ${{ always() }} steps: - name: Check deployment success on at least one platform id: checkDeploymentSuccessOnAtLeastOnePlatform run: | isAtLeastOnePlatformDeployed="false" - if [ "${{ needs.android.result }}" == "success" ] || \ - [ "${{ needs.iOS.result }}" == "success" ] || \ + if [ ${{ github.ref }} == 'refs/heads/production' ]; then + if [ "${{ needs.submitAndroid.result }}" == "success" ]; then + isAtLeastOnePlatformDeployed="true" + fi + else + if [ "${{ needs.uploadAndroid.result }}" == "success" ]; then + isAtLeastOnePlatformDeployed="true" + fi + fi + + if [ "${{ needs.iOS.result }}" == "success" ] || \ [ "${{ needs.desktop.result }}" == "success" ] || \ [ "${{ needs.web.result }}" == "success" ]; then isAtLeastOnePlatformDeployed="true" @@ -462,15 +459,25 @@ jobs: echo "IS_AT_LEAST_ONE_PLATFORM_DEPLOYED is $isAtLeastOnePlatformDeployed" - name: Check deployment success on all platforms - id: checkDeploymentSuccessOnAtLeastAllPlatform + id: checkDeploymentSuccessOnAllPlatforms run: | isAllPlatformsDeployed="false" - if [ "${{ needs.android.result }}" == "success" ] && \ - [ "${{ needs.iOS.result }}" == "success" ] && \ + if [ "${{ needs.iOS.result }}" == "success" ] && \ [ "${{ needs.desktop.result }}" == "success" ] && \ [ "${{ needs.web.result }}" == "success" ]; then isAllPlatformsDeployed="true" fi + + if [ ${{ github.ref }} == 'refs/heads/production' ]; then + if [ "${{ needs.submitAndroid.result }}" != "success" ]; then + isAllPlatformsDeployed="false" + fi + else + if [ "${{ needs.uploadAndroid.result }}" != "success" ]; then + isAllPlatformsDeployed="false" + fi + fi + echo "IS_ALL_PLATFORMS_DEPLOYED=$isAllPlatformsDeployed" >> "$GITHUB_OUTPUT" echo "IS_ALL_PLATFORMS_DEPLOYED is $isAllPlatformsDeployed" @@ -590,7 +597,7 @@ jobs: name: Post a Slack message when all platforms deploy successfully runs-on: ubuntu-latest if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }} - needs: [prep, android, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] steps: - name: 'Announces the deploy in the #announce Slack room' uses: 8398a7/action-slack@v3 @@ -644,11 +651,11 @@ jobs: postGithubComments: uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} - needs: [prep, android, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] with: version: ${{ needs.prep.outputs.APP_VERSION }} env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} - android: ${{ needs.android.result }} + android: ${{ github.ref == 'refs/heads/production' && needs.submitAndroid.result || needs.uploadAndroid.result }} ios: ${{ needs.iOS.result }} web: ${{ needs.web.result }} desktop: ${{ needs.desktop.result }} diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index b9352d406feb..f88e841617bb 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -20,13 +20,15 @@ concurrency: cancel-in-progress: true jobs: - buildBaseline: - runs-on: ubuntu-latest-xl - name: Build apk from latest release as a baseline + prep: + runs-on: ubuntu-latest + name: Find the baseline and delta refs, and check for an existing build artifact for that commit outputs: - VERSION: ${{ steps.getMostRecentRelease.outputs.VERSION }} - ARTIFACT_FOUND: ${{ steps.checkForExistingArtifact.outputs.ARTIFACT_FOUND }} - ARTIFACT_WORKFLOW_ID: ${{ steps.checkForExistingArtifact.outputs.ARTIFACT_WORKFLOW_ID }} + BASELINE_ARTIFACT_FOUND: ${{ steps.checkForExistingArtifact.outputs.ARTIFACT_FOUND }} + BASELINE_ARTIFACT_WORKFLOW_ID: ${{ steps.checkForExistingArtifact.outputs.ARTIFACT_WORKFLOW_ID }} + BASELINE_VERSION: ${{ steps.getMostRecentRelease.outputs.VERSION }} + DELTA_REF: ${{ steps.getDeltaRef.outputs.DELTA_REF }} + IS_PR_MERGED: ${{ steps.getPullRequestDetails.outputs.IS_MERGED }} steps: - uses: actions/checkout@v4 with: @@ -44,41 +46,12 @@ jobs: uses: ./.github/actions/javascript/getArtifactInfo with: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - ARTIFACT_NAME: baseline-apk-${{ steps.getMostRecentRelease.outputs.VERSION }} + ARTIFACT_NAME: baseline-${{ steps.getMostRecentRelease.outputs.VERSION }}-android-artifact-apk - name: Skip build if there's already an existing artifact for the baseline if: ${{ fromJSON(steps.checkForExistingArtifact.outputs.ARTIFACT_FOUND) }} run: echo 'APK for baseline ${{ steps.getMostRecentRelease.outputs.VERSION }} already exists, reusing existing build' - - name: Checkout "Baseline" commit (last release) - if: ${{ !fromJSON(steps.checkForExistingArtifact.outputs.ARTIFACT_FOUND) }} - run: | - git fetch origin tag ${{ steps.getMostRecentRelease.outputs.VERSION }} --no-tags --depth=1 - git switch --detach ${{ steps.getMostRecentRelease.outputs.VERSION }} - - - uses: Expensify/App/.github/actions/composite/buildAndroidE2EAPK@main - if: ${{ !fromJSON(steps.checkForExistingArtifact.outputs.ARTIFACT_FOUND) }} - with: - ARTIFACT_NAME: baseline-apk-${{ steps.getMostRecentRelease.outputs.VERSION }} - PACKAGE_SCRIPT_NAME: android-build-e2e - APP_OUTPUT_PATH: android/app/build/outputs/apk/e2e/release/app-e2e-release.apk - MAPBOX_SDK_DOWNLOAD_TOKEN: ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - EXPENSIFY_PARTNER_NAME: ${{ secrets.EXPENSIFY_PARTNER_NAME }} - EXPENSIFY_PARTNER_PASSWORD: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD }} - EXPENSIFY_PARTNER_USER_ID: ${{ secrets.EXPENSIFY_PARTNER_USER_ID }} - EXPENSIFY_PARTNER_USER_SECRET: ${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }} - EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - PATH_ENV_FILE: tests/e2e/.env.e2e - - buildDelta: - runs-on: ubuntu-latest-xl - name: Build apk from delta ref - outputs: - DELTA_REF: ${{ steps.getDeltaRef.outputs.DELTA_REF }} - steps: - - uses: actions/checkout@v4 - - name: Get pull request details id: getPullRequestDetails uses: ./.github/actions/javascript/getPullRequestDetails @@ -87,63 +60,54 @@ jobs: PULL_REQUEST_NUMBER: ${{ inputs.PR_NUMBER }} USER: ${{ github.actor }} - - name: Merged PR - Get merge commit sha for the pull request - if: ${{ fromJSON(steps.getPullRequestDetails.outputs.IS_MERGED) }} - id: getMergeCommitShaIfMergedPR - run: | - MERGE_COMMIT_SHA=${{ steps.getPullRequestDetails.outputs.MERGE_COMMIT_SHA }} - git fetch origin "$MERGE_COMMIT_SHA" --no-tags --depth=1 - echo "MERGE_COMMIT_SHA=$MERGE_COMMIT_SHA" >> "$GITHUB_OUTPUT" - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Unmerged PR - Fetch head ref of unmerged PR - if: ${{ !fromJSON(steps.getPullRequestDetails.outputs.IS_MERGED) }} + - name: Determine "delta ref" + id: getDeltaRef run: | - git fetch origin ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} --no-tags --depth=1 + if [ '${{ steps.getPullRequestDetails.outputs.IS_MERGED }}' == 'true' ]; then + echo "DELTA_REF=${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }}" >> "$GITHUB_OUTPUT" + else + # Set dummy git credentials + git config --global user.email "test@test.com" + git config --global user.name "Test" - - name: Unmerged PR - Set dummy git credentials before merging - if: ${{ !fromJSON(steps.getPullRequestDetails.outputs.IS_MERGED) }} - run: | - git config --global user.email "test@test.com" - git config --global user.name "Test" + # Fetch head_ref of unmerged PR + git fetch origin ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} --no-tags --depth=1 - - name: Unmerged PR - Merge pull request locally and get merge commit sha - if: ${{ !fromJSON(steps.getPullRequestDetails.outputs.IS_MERGED) }} - id: getMergeCommitShaIfUnmergedPR - run: | - git merge --allow-unrelated-histories -X ours --no-commit ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - git checkout ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - env: - GITHUB_TOKEN: ${{ github.token }} + # Merge pull request locally and get merge commit sha + git merge --allow-unrelated-histories -X ours --no-commit ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} + git checkout ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - - name: Determine "delta ref" - id: getDeltaRef - run: echo "DELTA_REF=${{ steps.getMergeCommitShaIfMergedPR.outputs.MERGE_COMMIT_SHA || steps.getMergeCommitShaIfUnmergedPR.outputs.MERGE_COMMIT_SHA }}" >> "$GITHUB_OUTPUT" - env: - GITHUB_TOKEN: ${{ github.token }} + # Create and push a branch so it can be checked out in another runner + git checkout -b e2eDelta-${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} + git push origin e2eDelta-${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} + echo "DELTA_REF=e2eDelta-${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }}" >> "$GITHUB_OUTPUT" + fi - - name: Checkout "delta ref" - run: git checkout ${{ steps.getDeltaRef.outputs.DELTA_REF }} + buildBaseline: + name: Build apk from latest release as a baseline + uses: ./.github/workflows/buildAndroid.yml + needs: prep + if: ${{ !fromJSON(needs.prep.outputs.BASELINE_ARTIFACT_FOUND) }} + secrets: inherit + with: + type: e2e + ref: ${{ needs.prep.outputs.BASELINE_VERSION }} + artifact-prefix: baseline-${{ needs.prep.outputs.BASELINE_VERSION }} - - uses: Expensify/App/.github/actions/composite/buildAndroidE2EAPK@main - with: - ARTIFACT_NAME: delta-apk-${{ steps.getDeltaRef.outputs.DELTA_REF }} - ARTIFACT_RETENTION_DAYS: 3 # We don't need to store the delta apk for long, its only really needed for the next job in this workflow - PACKAGE_SCRIPT_NAME: android-build-e2edelta - APP_OUTPUT_PATH: android/app/build/outputs/apk/e2edelta/release/app-e2edelta-release.apk - MAPBOX_SDK_DOWNLOAD_TOKEN: ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - EXPENSIFY_PARTNER_NAME: ${{ secrets.EXPENSIFY_PARTNER_NAME }} - EXPENSIFY_PARTNER_PASSWORD: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD }} - EXPENSIFY_PARTNER_USER_ID: ${{ secrets.EXPENSIFY_PARTNER_USER_ID }} - EXPENSIFY_PARTNER_USER_SECRET: ${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }} - EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - PATH_ENV_FILE: tests/e2e/.env.e2edelta + buildDelta: + name: Build apk from delta ref + uses: ./.github/workflows/buildAndroid.yml + needs: prep + secrets: inherit + with: + type: e2eDelta + ref: ${{ needs.prep.outputs.DELTA_REF }} + artifact-prefix: delta-${{ needs.prep.outputs.DELTA_REF }} runTestsInAWS: runs-on: ubuntu-latest - needs: [buildBaseline, buildDelta] + needs: [prep, buildBaseline, buildDelta] + if: ${{ always() }} name: Run E2E tests in AWS device farm steps: - uses: actions/checkout@v4 @@ -161,25 +125,25 @@ jobs: uses: actions/download-artifact@v4 id: downloadBaselineAPK with: - name: baseline-apk-${{ needs.buildBaseline.outputs.VERSION }} + name: baseline-${{ needs.prep.outputs.BASELINE_VERSION }}-android-artifact-apk path: zip # Set github-token only if the baseline was built in this workflow run: - github-token: ${{ needs.buildBaseline.outputs.ARTIFACT_WORKFLOW_ID && github.token }} - run-id: ${{ needs.buildBaseline.outputs.ARTIFACT_WORKFLOW_ID }} + github-token: ${{ needs.prep.outputs.BASELINE_ARTIFACT_WORKFLOW_ID && github.token }} + run-id: ${{ needs.prep.outputs.BASELINE_ARTIFACT_WORKFLOW_ID }} # The downloaded artifact will be a file named "app-e2e-release.apk" so we have to rename it - name: Rename baseline APK - run: mv "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2e-release.apk" "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease.apk" + run: mv "${{ steps.downloadBaselineAPK.outputs.download-path }}/app-e2e-release.apk" "${{ steps.downloadBaselineAPK.outputs.download-path }}/app-e2eRelease.apk" - name: Download delta APK uses: actions/download-artifact@v4 id: downloadDeltaAPK with: - name: delta-apk-${{ needs.buildDelta.outputs.DELTA_REF }} + name: delta-${{ needs.prep.outputs.DELTA_REF }}-android-artifact-apk path: zip - name: Rename delta APK - run: mv "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2edelta-release.apk" "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2edeltaRelease.apk" + run: mv "${{ steps.downloadDeltaAPK.outputs.download-path }}/app-e2edelta-release.apk" "${{ steps.downloadDeltaAPK.outputs.download-path }}/app-e2edeltaRelease.apk" - name: Compile test runner to be executable in a nodeJS environment run: npm run e2e-test-runner-build @@ -289,3 +253,13 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + cleanupDeltaRef: + needs: [prep, runTestsInAWS] + if: ${{ always() && needs.prep.outputs.IS_PR_MERGED != 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Delete temporary merge branch created for delta ref + run: git push -d origin ${{ needs.prep.outputs.DELTA_REF }} diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index f523faf785c0..672d468ed3b1 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -45,7 +45,7 @@ jobs: needs: validateActor if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} outputs: - REF: ${{steps.getHeadRef.outputs.REF}} + REF: ${{ steps.getHeadRef.outputs.REF }} steps: - name: Checkout if: ${{ github.event_name == 'workflow_dispatch' }} @@ -60,48 +60,43 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - android: - name: Build and deploy Android for testing - needs: [validateActor, getBranchRef] + buildAndroid: + name: Build Android app for testing + uses: ./.github/workflows/buildAndroid.yml if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} - runs-on: ubuntu-latest-xl + needs: [validateActor, getBranchRef] + secrets: inherit + with: + type: adhoc + ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} + pull_request_number: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }} + + uploadAndroid: + name: Upload Android app to S3 + needs: [buildAndroid] + runs-on: ubuntu-latest env: RUBYOPT: '-rostruct' + outputs: + S3_APK_PATH: ${{ steps.exportS3Path.outputs.S3_APK_PATH }} steps: - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} - - - name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it - run: | - cp .env.staging .env.adhoc - sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc - echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc - - - name: Setup Node - uses: ./.github/actions/composite/setupNode - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'oracle' - java-version: '17' - name: Setup Ruby uses: ruby/setup-ruby@v1.190.0 with: bundler-cache: true - - name: Decrypt keystore - run: cd android/app && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output my-upload-key.keystore my-upload-key.keystore.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + - name: Download Android build artifacts + uses: actions/download-artifact@v4 + with: + path: /tmp/artifacts + pattern: android-artifact-* + merge-multiple: true - - name: Decrypt json key - run: cd android/app && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + - name: Log downloaded artifact paths + run: ls -R /tmp/artifacts - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 @@ -110,28 +105,20 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-1 - - name: Configure MapBox SDK - run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - - name: Run AdHoc build - run: bundle exec fastlane android build_adhoc - env: - MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }} - MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} - - name: Upload AdHoc build to S3 run: bundle exec fastlane android upload_s3 env: + apkPath: /tmp/artifacts/${{ needs.buildAndroid.outputs.APK_FILE_NAME }} S3_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }} S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} S3_BUCKET: ad-hoc-expensify-cash S3_REGION: us-east-1 - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: android - path: ./android_paths.json + - name: Export S3 paths + id: exportS3Path + run: | + # $s3APKPath is set from within the Fastfile, android upload_s3 lane + echo "S3_APK_PATH=$s3APKPath" >> "$GITHUB_OUTPUT" iOS: name: Build and deploy iOS for testing @@ -304,7 +291,7 @@ jobs: postGithubComment: runs-on: ubuntu-latest name: Post a GitHub comment with app download links for testing - needs: [validateActor, getBranchRef, android, iOS, desktop, web] + needs: [validateActor, getBranchRef, uploadAndroid, iOS, desktop, web] if: ${{ always() }} steps: - name: Checkout @@ -317,17 +304,6 @@ jobs: uses: actions/download-artifact@v4 if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} - - name: Read JSONs with android paths - id: get_android_path - if: ${{ needs.android.result == 'success' }} - run: | - content_android="$(cat ./android/android_paths.json)" - content_android="${content_android//'%'/'%25'}" - content_android="${content_android//$'\n'/'%0A'}" - content_android="${content_android//$'\r'/'%0D'}" - android_path=$(echo "$content_android" | jq -r '.html_path') - echo "android_path=$android_path" >> "$GITHUB_OUTPUT" - - name: Read JSONs with iOS paths id: get_ios_path if: ${{ needs.iOS.result == 'success' }} @@ -345,11 +321,11 @@ jobs: with: PR_NUMBER: ${{ env.PULL_REQUEST_NUMBER }} GITHUB_TOKEN: ${{ github.token }} - ANDROID: ${{ needs.android.result }} + ANDROID: ${{ needs.uploadAndroid.result }} DESKTOP: ${{ needs.desktop.result }} IOS: ${{ needs.iOS.result }} WEB: ${{ needs.web.result }} - ANDROID_LINK: ${{steps.get_android_path.outputs.android_path}} + ANDROID_LINK: ${{ needs.uploadAndroid.outputs.S3_APK_PATH }} DESKTOP_LINK: https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/${{ env.PULL_REQUEST_NUMBER }}/NewExpensify.dmg - IOS_LINK: ${{steps.get_ios_path.outputs.ios_path}} + IOS_LINK: ${{ steps.get_ios_path.outputs.ios_path }} WEB_LINK: https://${{ env.PULL_REQUEST_NUMBER }}.pr-testing.expensify.com diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 15eb36c819b5..eed84acdc916 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -15,14 +15,14 @@ require 'ostruct' skip_docs opt_out_usage -KEY_GRADLE_APK_PATH = "gradleAPKOutputPath" -KEY_GRADLE_AAB_PATH = "gradleAABOutputPath" +KEY_GRADLE_APK_PATH = "apkPath" +KEY_S3_APK_PATH = "s3APKPath" +KEY_GRADLE_AAB_PATH = "aabPath" KEY_IPA_PATH = "ipaPath" KEY_DSYM_PATH = "dsymPath" -# Export environment variables in the parent shell. -# In a GitHub Actions environment, it will save the environment variables in the GITHUB_ENV file. -# In any other environment, it will save them to the current shell environment using the `export` command. +# Export environment variables to GITHUB_ENV +# If there's no GITHUB_ENV file set in the env, then this is a no-op def exportEnvVars(env_vars) github_env_path = ENV['GITHUB_ENV'] if github_env_path && File.exist?(github_env_path) @@ -33,13 +33,6 @@ def exportEnvVars(env_vars) file.puts "#{key}=#{value}" end end - else - puts "Saving environment variables in parent shell..." - env_vars.each do |key, value| - puts "#{key}=#{value}" - command = "export #{key}=#{value}" - system(command) - end end end @@ -102,7 +95,7 @@ platform :android do setGradleOutputsInEnv() end - lane :build_e2edelta do + lane :build_e2eDelta do ENV["ENVFILE"]="tests/e2e/.env.e2edelta" ENV["ENTRY_FILE"]="src/libs/E2E/reactNativeLaunchingTest.ts" ENV["E2E_TESTING"]="true" @@ -139,7 +132,10 @@ platform :android do apk: ENV[KEY_GRADLE_APK_PATH], app_directory: "android/#{ENV['PULL_REQUEST_NUMBER']}", ) - sh("echo '{\"apk_path\": \"#{lane_context[SharedValues::S3_APK_OUTPUT_PATH]}\",\"html_path\": \"#{lane_context[SharedValues::S3_HTML_OUTPUT_PATH]}\"}' > ../android_paths.json") + puts "Saving S3 outputs in env..." + exportEnvVars({ + KEY_S3_APK_PATH => lane_context[SharedValues::S3_HTML_OUTPUT_PATH], + }) end desc "Upload app to Google Play for internal testing" diff --git a/package.json b/package.json index a5a0ae7c7cb1..647271ad1424 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,8 @@ "createDocsRoutes": "ts-node .github/scripts/createDocsRoutes.ts", "detectRedirectCycle": "ts-node .github/scripts/detectRedirectCycle.ts", "desktop-build-adhoc": "./scripts/build-desktop.sh adhoc", - "ios-build": "fastlane ios build_unsigned", - "android-build": "fastlane android build_local", - "android-build-e2e": "bundle exec fastlane android build_e2e", - "android-build-e2edelta": "bundle exec fastlane android build_e2edelta", + "ios-build": "bundle exec fastlane ios build_unsigned", + "android-build": "bundle exec fastlane android build_local", "test": "TZ=utc NODE_OPTIONS=--experimental-vm-modules jest", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint",