diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml index b0945f4e0920..bfe3ad21bbe2 100644 --- a/.github/workflows/cherryPick.yml +++ b/.github/workflows/cherryPick.yml @@ -34,23 +34,20 @@ jobs: needs: [validateActor, createNewVersion] if: ${{ always() && fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} runs-on: ubuntu-latest + env: + NEW_VERSION: ${{ github.event.inputs.NEW_VERSION || needs.createNewVersion.outputs.NEW_VERSION }} steps: # Version: 2.3.4 - name: Checkout staging branch uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f with: ref: staging - token: ${{ secrets.OS_BOTIFY_TOKEN }} + fetch-depth: 0 - uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - name: Create branch for new pull request - run: | - git checkout -b ${{ github.actor }}-cherry-pick-staging-${{ github.event.inputs.PULL_REQUEST_NUMBER }} - git push --set-upstream origin ${{ github.actor }}-cherry-pick-staging-${{ github.event.inputs.PULL_REQUEST_NUMBER }} - - name: Get merge commit for CP pull request id: getCPMergeCommit uses: Expensify/App/.github/actions/javascript/getPullRequestDetails@main @@ -59,18 +56,6 @@ jobs: USER: ${{ github.actor }} PULL_REQUEST_NUMBER: ${{ github.event.inputs.PULL_REQUEST_NUMBER }} - - name: Save correct NEW_VERSION to env - env: - NEW_VERSION: ${{ github.event.inputs.NEW_VERSION }} - run: | - if [ -z "$NEW_VERSION" ]; then - echo "NEW_VERSION=${{ needs.createNewVersion.outputs.NEW_VERSION }}" >> "$GITHUB_ENV" - echo "New version is ${{ env.NEW_VERSION }}" - else - echo "NEW_VERSION=${{ github.event.inputs.NEW_VERSION }}" >> "$GITHUB_ENV" - echo "New version is ${{ env.NEW_VERSION }}" - fi; - - name: Get merge commit for version-bump pull request id: getVersionBumpMergeCommit uses: Expensify/App/.github/actions/javascript/getPullRequestDetails@main @@ -79,31 +64,38 @@ jobs: USER: OSBotify TITLE_REGEX: Update version to ${{ env.NEW_VERSION }} - - name: Cherry-pick the version-bump to new branch + - name: Create branch for new pull request run: | - git fetch - git cherry-pick -S -x --mainline 1 --strategy=recursive -Xtheirs ${{ steps.getVersionBumpMergeCommit.outputs.MERGE_COMMIT_SHA }} + SOURCE_MERGE_BASE=$(git merge-base staging ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}) + VERSION_BUMP_MERGE_BASE=$(git merge-base staging ${{ steps.getVersionBumpMergeCommit.outputs.MERGE_COMMIT_SHA }}) + CP_MERGE_BASE=$(git merge-base "$SOURCE_MERGE_BASE" "$VERSION_BUMP_MERGE_BASE") + git checkout -b ${{ github.actor }}-cherry-pick-staging-${{ github.event.inputs.PULL_REQUEST_NUMBER }} "$CP_MERGE_BASE" + git push --set-upstream origin ${{ github.actor }}-cherry-pick-staging-${{ github.event.inputs.PULL_REQUEST_NUMBER }} - - name: Cherry-pick the merge commit of target PR to new branch - id: cherryPick - run: | - echo "Attempting to cherry-pick ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}" - if git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}; then - echo "🎉 No conflicts! CP was a success, PR can be automerged 🎉" - echo "::set-output name=SHOULD_AUTOMERGE::true" - else - echo "😞 PR can't be automerged, there are merge conflicts in the following files:" - git --no-pager diff --name-only --diff-filter=U - git add . - GIT_MERGE_AUTOEDIT=no git cherry-pick --continue - echo "::set-output name=SHOULD_AUTOMERGE::false" - fi + - name: Cherry-pick the version-bump to new branch + run: git cherry-pick -S -x --mainline 1 -Xtheirs ${{ steps.getVersionBumpMergeCommit.outputs.MERGE_COMMIT_SHA }} + + - name: Cherry-pick the merge commit of target PR to the new branch + run: git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }} - name: Push changes to CP branch run: git push - - name: Create Pull Request - id: createPullRequest + - name: Create main pull request + id: createMainPullRequest + run: | + gh pr create \ + --title "🍒 Cherry pick PR #${{ github.event.inputs.PULL_REQUEST_NUMBER }} to main 🍒" \ + --body "This pull request is needed so that this cherry-pick becomes a common ancestor for both main and staging. More details in https://github.com/Expensify/App/pull/10316" \ + --label "automerge" \ + --base "main" + sleep 5 + echo "::set-output name=PR_NUMBER::$(gh pr view --json 'number' --jq '.number')" + env: + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Create staging pull request + id: createStagingPullRequest run: | gh pr create \ --title "🍒 Cherry pick PR #${{ github.event.inputs.PULL_REQUEST_NUMBER }} to staging 🍒" \ @@ -115,66 +107,74 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + - name: Check if main pull request is mergeable + id: isMainPullRequestMergeable + uses: Expensify/App/.github/actions/javascript/isPullRequestMergeable@main + with: + GITHUB_TOKEN: ${{ github.token }} + PULL_REQUEST_NUMBER: ${{ steps.createMainPullRequest.outputs.PR_NUMBER }} + + - name: Check if staging pull request is mergeable + id: isStagingPullRequestMergeable + uses: Expensify/App/.github/actions/javascript/isPullRequestMergeable@main + with: + GITHUB_TOKEN: ${{ github.token }} + PULL_REQUEST_NUMBER: ${{ steps.createStagingPullRequest.outputs.PR_NUMBER }} + - name: Check if ShortVersionString is up to date id: isShortVersionStringUpdated uses: Expensify/App/.github/actions/javascript/checkBundleVersionStringMatch@main - - name: Auto-assign PR if there are merge conflicts or if the bundle versions are mismatched - if: ${{ !fromJSON(steps.cherryPick.outputs.SHOULD_AUTOMERGE) || !fromJSON(steps.isShortVersionStringUpdated.outputs.BUNDLE_VERSIONS_MATCH) }} - run: gh pr edit --add-label "Engineering,Hourly" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Assign the PR to the deployer - if: ${{ !fromJSON(steps.cherryPick.outputs.SHOULD_AUTOMERGE) }} - run: gh pr edit --add-assignee ${{ steps.getCPMergeCommit.outputs.MERGE_ACTOR }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Check if the PRs can be auto-merged + id: checkForAutomerge + run: echo "::set-output name=SHOULD_AUTOMERGE::${{ fromJSON(steps.isMainPullRequestMergeable.outputs.IS_MERGEABLE) && fromJSON(steps.isStagingPullRequestMergeable.outputs.IS_MERGEABLE) && fromJSON(steps.isShortVersionStringUpdated.outputs.BUNDLE_VERSIONS_MATCH) }}" - - name: If PR has merge conflicts, comment with instructions for assignee - if: ${{ !fromJSON(steps.cherryPick.outputs.SHOULD_AUTOMERGE) }} + - name: Auto-assign PRs if either is not mergeable or if the bundle versions are mismatched + if: ${{ !fromJSON(steps.checkForAutomerge.outputs.SHOULD_AUTOMERGE) }} run: | - gh pr comment --body \ - "This pull request has merge conflicts and can not be automatically merged. :disappointed: - Please manually resolve the conflicts, push your changes, and then request another reviewer to review and merge. - **Important:** There may be conflicts that GitHub is not able to detect, so please _carefully_ review this pull request before approving." + gh pr edit ${{ steps.createMainPullRequest.outputs.PR_NUMBER }} --add-label "Engineering,Hourly" + gh pr edit ${{ steps.createStagingPullRequest.outputs.PR_NUMBER }} --add-label "Engineering,Hourly" + gh pr edit ${{ steps.createMainPullRequest.outputs.PR_NUMBER }} --add-assignee ${{ steps.getCPMergeCommit.outputs.MERGE_ACTOR }} + gh pr edit ${{ steps.createStagingPullRequest.outputs.PR_NUMBER }} --add-assignee ${{ steps.getCPMergeCommit.outputs.MERGE_ACTOR }} + gh pr comment ${{ steps.createMainPullRequest.outputs.PR_NUMBER }} --body \ + "This pull request has an [associated mate against the staging branch](#${{ steps.createStagingPullRequest.outputs.PR_NUMBER }}) which had conflicts and could not be automatically merged. :disappointed: + Please manually resolve the conflicts, push your changes, and then request another reviewer to review and merge. _Only after doing that_, merge this PR." + gh pr comment ${{ steps.createStagingPullRequest.outputs.PR_NUMBER }} --body \ + "This pull request has merge conflicts and can not be automatically merged. :disappointed: + Please manually resolve the conflicts, push your changes, and then request another reviewer to review and merge." env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - name: If PR has a bundle version mismatch, comment with the instructions for assignee if: ${{ !fromJSON(steps.isShortVersionStringUpdated.outputs.BUNDLE_VERSIONS_MATCH) }} run: | - gh pr comment --body \ - "The CFBundleShortVersionString value in this PR is not compatible with the CFBundleVersion, so cherry picking it will result in an iOS deploy failure. - Please manually resolve the mismatch, push your changes, and then request another reviewer to review and merge. - **Important:** This mismatch can be caused by a failed Update Protected Branch workflow followed by a manual CP, but please confirm the cause of the mismatch before updating any version numbers." + gh pr comment ${{ steps.createStagingPullRequest.outputs.PR_NUMBER }} --body \ + "The CFBundleShortVersionString value in this PR is not compatible with the CFBundleVersion, so cherry picking it will result in an iOS deploy failure. + Please manually resolve the mismatch, push your changes, and then request another reviewer to review and merge. + **Important:** This mismatch can be caused by a failed Update Protected Branch workflow followed by a manual CP, but please confirm the cause of the mismatch before updating any version numbers." env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - - name: Auto-approve the PR + - name: Auto-approve the PRs # Important: only auto-approve if there was no merge conflict! - if: ${{ fromJSON(steps.cherryPick.outputs.SHOULD_AUTOMERGE) }} - run: gh pr review --approve + if: ${{ fromJSON(steps.checkForAutomerge.outputs.SHOULD_AUTOMERGE) }} + run: | + gh pr review ${{ steps.createMainPullRequest.outputs.PR_NUMBER }} --approve + gh pr review ${{ steps.createStagingPullRequest.outputs.PR_NUMBER }} --approve env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Check if pull request is mergeable - id: isPullRequestMergeable - uses: Expensify/App/.github/actions/javascript/isPullRequestMergeable@main - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PULL_REQUEST_NUMBER: ${{ steps.createPullRequest.outputs.pr_number }} - - name: Auto-merge the PR - # Important: only auto-merge if there was no merge conflict and the PR is mergable (not blocked by a missing status check)! - if: ${{ fromJSON(steps.cherryPick.outputs.SHOULD_AUTOMERGE) && fromJSON(steps.isPullRequestMergeable.outputs.IS_MERGEABLE) }} - run: gh pr merge ${{ steps.createPullRequest.outputs.pr_number }} --merge --delete-branch + if: ${{ fromJSON(steps.checkForAutomerge.outputs.SHOULD_AUTOMERGE) }} + run: | + gh pr merge ${{ steps.createMainPullRequest.outputs.PR_NUMBER }} --merge --delete-branch + gh pr merge ${{ steps.createStagingPullRequest.outputs.PR_NUMBER }} --merge --delete-branch env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - name: 'Announces a CP failure in the #announce Slack room' uses: 8398a7/action-slack@v3 - if: ${{ failure() || !fromJSON(steps.isPullRequestMergeable.outputs.IS_MERGEABLE) }} + if: ${{ failure() || !fromJSON(steps.checkForAutomerge.outputs.SHOULD_AUTOMERGE) }} with: status: custom custom_payload: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 16d090bbd433..564983cf6987 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,5 +28,5 @@ jobs: env: CI: true - - name: Pull Request Tests - run: tests/unit/getPullRequestsMergedBetweenTest.sh + - name: Test CI Git Logic + run: tests/unit/gitTestCI.sh diff --git a/scripts/shellUtils.sh b/scripts/shellUtils.sh index 0eb4f20e82f2..f6bc54d839f8 100644 --- a/scripts/shellUtils.sh +++ b/scripts/shellUtils.sh @@ -30,3 +30,30 @@ function assert_equal { success "Assertion passed: $1 is equal to $1" fi } + +function assert_string_contains_substring { + if [[ "$1" != *"$2"* ]]; then + error "Assertion failed: \"$1\" does not contain substring \"$2\"" + exit 1 + else + success "Assertion passed: \"$1\" contains substring \"$2\"" + fi +} + +function assert_string_doesnt_contain_substring { + if [[ "$1" == *"$2"* ]]; then + error "Assertion failed: \"$1\" contains substring \"$2\"" + exit 1 + else + success "Assertion passed: \"$1\" does not contain substring \"$2\"" + fi +} + +function assert_file_doesnt_exist { + if [[ -f "$1" ]]; then + error "Assertion failed: File $1 exists" + exit 1 + else + success "Assertion passed: File $1 does not exist" + fi +} diff --git a/tests/unit/getPullRequestsMergedBetweenTest.sh b/tests/unit/getPullRequestsMergedBetweenTest.sh deleted file mode 100755 index 538de83700b4..000000000000 --- a/tests/unit/getPullRequestsMergedBetweenTest.sh +++ /dev/null @@ -1,330 +0,0 @@ -#!/bin/bash - -# Fail immediately if there is an error thrown -set -e - -TEST_DIR=$(dirname "$(dirname "$(cd "$(dirname "$0")" || exit 1;pwd)/$(basename "$0")")") -SCRIPTS_DIR="$TEST_DIR/../scripts" -DUMMY_DIR="$HOME/DumDumRepo" -getPullRequestsMergedBetween="$TEST_DIR/utils/getPullRequestsMergedBetween.mjs" - -source "$SCRIPTS_DIR/shellUtils.sh" - -function print_version { - < package.json jq -r .version -} - -### Phase 0: Verify necessary tools are installed (all tools should be pre-installed on all GitHub Actions runners) - -if ! command -v jq &> /dev/null -then - error "command jq could not be found, install it with \`brew install jq\` (macOS) or \`apt-get install jq\` (Linux) and re-run this script" - exit 1 -fi - -if ! command -v npm &> /dev/null -then - error "command npm could not be found, install it and re-run this script" - exit 1 -fi - - -### Setup -title "Starting setup" - -info "Creating new dummy repo at $DUMMY_DIR" -mkdir "$DUMMY_DIR" -cd "$DUMMY_DIR" || exit 1 -success "Successfully created dummy repo at $(pwd)" - -info "Initializing npm project in $DUMMY_DIR" -if [[ $(npm init -y) ]] -then - success "Successfully initialized npm project" -else - error "Failed initializing npm project" - exit 1 -fi - -info "Installing node dependencies..." -if [[ $(npm install underscore && npm install) ]] -then - success "Successfully installed node dependencies" -else - error "Failed installing node dependencies" - exit 1 -fi - -info "Initializing Git repo..." -git init -b main -git config user.email "test@test.com" -git config user.name "test" -git add package.json package-lock.json -git commit -m "Initial commit" - -info "Bumping version to 1.0.1" -npm --no-git-tag-version version 1.0.1 -m "Update version to 1.0.1" -git add package.json -git commit -m "Update version to 1.0.1" - -info "Creating branches..." -git checkout -b staging -git checkout -b production -git checkout main - -success "Initialized Git repo!" - -info "Creating initial tag..." -git checkout staging -git tag "$(print_version)" -git checkout main -success "Created initial tag $(print_version)" - -success "Setup complete!" - - -title "Scenario #1: Merge a pull request while the checklist is unlocked" - -# Create "PR 1", and merge that PR to main. -info "Creating PR #1" -git checkout main -git checkout -b pr-1 -echo "Changes from PR #1" >> PR1.txt -git add PR1.txt -git commit -m "Changes from PR #1" -success "Created PR #1 in branch pr-1" - -info "Merging PR #1 to main" -git checkout main -git merge pr-1 --no-ff -m "Merge pull request #1 from Expensify/pr-1" -git branch -d pr-1 -success "Merged PR #1 to main" - -# Bump the version to 1.0.2 -info "Bumping version to 1.0.2" -git checkout main -git checkout -b version-bump -npm --no-git-tag-version version 1.0.2 -m "Update version to 1.0.2" -git add package.json package-lock.json -git commit -m "Update version to $(print_version)" -git checkout main -git merge version-bump --no-ff -m "Merge pull request #2 from Expensify/version-bump" -info "Merged PR #2 to main" -git branch -d version-bump -success "Version bumped to $(print_version) on main" - -# Merge main into staging -info "Merging main into staging..." -git checkout staging -git checkout -b update-staging-from-main -git merge --no-edit -Xtheirs main || { git diff --name-only --diff-filter=U | xargs git rm; git -c core.editor=true merge --continue; } -git checkout staging -git merge update-staging-from-main --no-ff -m "Merge pull request #3 from Expensify/update-staging-from-main" -info "Merged PR #3 to staging" -git branch -d update-staging-from-main -success "Merged main into staging!" - -# Tag staging -info "Tagging new version..." -git checkout staging -git tag "$(print_version)" -git checkout main -success "Created new tag $(print_version)" - -# Verify output for checklist and deploy comment -info "Checking output of getPullRequestsMergedBetween 1.0.1 1.0.2" -output=$(node "$getPullRequestsMergedBetween" '1.0.1' '1.0.2') -assert_equal "$output" "[ '1' ]" - -success "Scenario #1 completed successfully!" - - -title "Scenario #2: Merge a pull request with the checklist locked, but don't CP it" - -info "Creating PR #4 and merging it into main..." -git checkout main -git checkout -b pr-4 -echo "Changes from PR #4" >> PR4.txt -git add PR4.txt -git commit -m "Changes from PR #4" -git checkout main -git merge pr-4 --no-ff -m "Merge pull request #4 from Expensify/pr-4" -info "Merged PR #4 into main" -git branch -d pr-4 -success "Created PR #4 and merged it to main!" - -success "Scenario #2 completed successfully!" - - -title "Scenario #3: Merge a pull request with the checklist locked and CP it to staging" - -info "Creating PR #5 and merging it into main..." -git checkout main -git checkout -b pr-5 -echo "Changes from PR #5" >> PR5.txt -git add PR5.txt -git commit -m "Changes from PR #5" -git checkout main -git merge pr-5 --no-ff -m "Merge pull request #5 from Expensify/pr-5" -PR_5_MERGE_COMMIT="$(git log -1 --format='%H')" -info "Merged PR #5 into main" -git branch -d pr-5 -success "Created PR #5 and merged it to main!" - -info "Bumping version to 1.0.3 on main..." -git checkout main -git checkout -b version-bump -npm --no-git-tag-version version 1.0.3 -m "Update version to 1.0.3" -git add package.json package-lock.json -git commit -m "Update version to $(print_version)" -git checkout main -git merge version-bump --no-ff -m "Merge pull request #6 from Expensify/version-bump" -VERSION_BUMP_MERGE_COMMIT="$(git log -1 --format='%H')" -info "Merged PR #6 into main" -git branch -d version-bump -success "Bumped version to 1.0.3 on main!" - -info "Cherry picking PR #5 and the version bump to staging..." -git checkout staging -git checkout -b cherry-pick-staging-5 -git cherry-pick -x --mainline 1 --strategy=recursive -Xtheirs "$PR_5_MERGE_COMMIT" -git cherry-pick -x --mainline 1 "$VERSION_BUMP_MERGE_COMMIT" -git checkout staging -git merge cherry-pick-staging-5 --no-ff -m "Merge pull request #7 from Expensify/cherry-pick-staging-5" -git branch -d cherry-pick-staging-5 -info "Merged PR #7 into staging" -success "Successfully cherry-picked PR #5 to staging!" - -info "Tagging the new version on staging..." -git checkout staging -git tag "$(print_version)" -success "Created tag $(print_version)" - -# Verify output for checklist -info "Checking output of getPullRequestsMergedBetween 1.0.1 1.0.3" -output=$(node "$getPullRequestsMergedBetween" '1.0.1' '1.0.3') -assert_equal "$output" "[ '7', '5', '1' ]" - -# Verify output for deploy comment -info "Checking output of getPullRequestsMergedBetween 1.0.2 1.0.3" -output=$(node "$getPullRequestsMergedBetween" '1.0.2' '1.0.3') -assert_equal "$output" "[ '7', '5' ]" - -success "Scenario #3 completed successfully!" - - -title "Scenario #4: Close the checklist" -title "Scenario #4A: Run the production deploy" - -info "Updating production from staging..." -git checkout production -git checkout -b update-production-from-staging -git merge --no-edit -Xtheirs staging || { git diff --name-only --diff-filter=U | xargs git rm; git -c core.editor=true merge --continue; } -git checkout production -git merge update-production-from-staging --no-ff -m "Merge pull request #8 from Expensify/update-production-from-staging" -info "Merged PR #8 into production" -git branch -d update-production-from-staging -success "Updated production from staging!" - -# Verify output for release body and production deploy comments -info "Checking output of getPullRequestsMergedBetween 1.0.1 1.0.3" -output=$(node "$getPullRequestsMergedBetween" '1.0.1' '1.0.3') -assert_equal "$output" "[ '7', '5', '1' ]" - -success "Scenario #4A completed successfully!" - -title "Scenario #4B: Run the staging deploy and create a new checklist" - -info "Bumping version to 1.1.0 on main..." -git checkout main -git checkout -b version-bump -npm --no-git-tag-version version 1.1.0 -m "Update version to 1.1.0" -git add package.json package-lock.json -git commit -m "Update version to $(print_version)" -git checkout main -git merge version-bump --no-ff -m "Merge pull request #9 from Expensify/version-bump" -info "Merged PR #9 into main" -git branch -d version-bump -success "Successfully updated version to 1.1.0 on main!" - -info "Updating staging from main..." -git checkout staging -git checkout -b update-staging-from-main -git merge --no-edit -Xtheirs main || { git diff --name-only --diff-filter=U | xargs git rm; git -c core.editor=true merge --continue; } -git checkout staging -git merge update-staging-from-main --no-ff -m "Merge pull request #10 from Expensify/update-staging-from-main" -info "Merged PR #10 into staging" -git branch -d update-staging-from-main -success "Successfully updated staging from main!" - -info "Tagging new version on staging..." -git checkout staging -git tag "$(print_version)" -success "Successfully tagged version $(print_version) on staging" - -# Verify output for new checklist and staging deploy comments -info "Checking output of getPullRequestsMergedBetween 1.0.3 1.1.0" -output=$(node "$getPullRequestsMergedBetween" '1.0.3' '1.1.0') -assert_equal "$output" "[ '4' ]" - -success "Scenario #4B completed successfully!" - - -title "Scenario #5: Merging another pull request when the checklist is unlocked" - -info "Creating PR #11 and merging it to main..." -git checkout main -git checkout -b pr-11 -echo "Changes from PR #11" >> PR11.txt -git add PR11.txt -git commit -m "Changes from PR #11" -git checkout main -git merge pr-11 --no-ff -m "Merge pull request #11 from Expensify/pr-11" -info "Merged PR #11 into main" -git branch -d pr-11 -success "Created PR #11 and merged it into main!" - -info "Bumping version to 1.1.1 on main..." -git checkout main -git checkout -b version-bump -npm --no-git-tag-version version 1.1.1 -m "Update version to 1.1.1" -git add package.json package-lock.json -git commit -m "Update version to $(cat package.json | jq -r .version)" -git checkout main -git merge version-bump --no-ff -m "Merge pull request #12 from Expensify/version-bump" -info "Merged PR #12 into main" -git branch -d version-bump -success "Bumped version to 1.1.1 on main!" - -info "Merging main into staging..." -git checkout staging -git checkout -b update-staging-from-main -git merge --no-edit -Xtheirs main || { git diff --name-only --diff-filter=U | xargs git rm; git -c core.editor=true merge --continue; } -git checkout staging -git merge update-staging-from-main --no-ff -m "Merge pull request #13 from Expensify/update-staging-from-main" -info "Merged PR #13 into staging" -git branch -d update-staging-from-main -success "Merged main into staging!" - -info "Tagging staging..." -git checkout staging -git tag "$(print_version)" -success "Successfully tagged version $(print_version) on staging" - -# Verify output for checklist -info "Checking output of getPullRequestsMergedBetween 1.0.3 1.1.1" -output=$(node "$getPullRequestsMergedBetween" '1.0.3' '1.1.1') -assert_equal "$output" "[ '11', '4' ]" - -# Verify output for deploy comment -info "Checking output of getPullRequestsMergedBetween 1.1.0 1.1.1" -output=$(node "$getPullRequestsMergedBetween" '1.1.0' '1.1.1') -assert_equal "$output" "[ '11' ]" - -success "Scenario #6 completed successfully!" - -### Cleanup -title "Cleaning up..." -cd "$TEST_DIR" || exit 1 -rm -rf "$DUMMY_DIR" -success "All tests passed! Hooray!" diff --git a/tests/unit/gitTestCI.sh b/tests/unit/gitTestCI.sh new file mode 100755 index 000000000000..35f50492a01b --- /dev/null +++ b/tests/unit/gitTestCI.sh @@ -0,0 +1,446 @@ +#!/bin/bash + +# Fail immediately if there is an error thrown +set -e + +TEST_DIR=$(dirname "$(dirname "$(cd "$(dirname "$0")" || exit 1;pwd)/$(basename "$0")")") +SCRIPTS_DIR="$TEST_DIR/../scripts" +DUMMY_DIR="$HOME/DumDumRepo" +getPullRequestsMergedBetween="$TEST_DIR/utils/getPullRequestsMergedBetween.mjs" + +source "$SCRIPTS_DIR/shellUtils.sh" + +PR_COUNT=0 + +### Utility functions + +function print_version { + < package.json jq -r .version +} + +# Bump the package version on main +# @param $1 – the new package version +# @param $2 – Pass --keep-version-branch to skip deleting the version bump branch +function bump_version { + info "Bumping version to $1" + git checkout main + git checkout -b version-bump + npm --no-git-tag-version version "$1" -m "Update version to $1" + git add package.json package-lock.json + git commit -m "Update version to $(print_version)" + git checkout main + git merge version-bump --no-ff -m "Merge pull request #$((++PR_COUNT)) from Expensify/version-bump" + info "Merged PR #$PR_COUNT to main" + if [[ "$2" != '--keep-version-branch' ]]; then + git branch -d version-bump + fi + success "Version bumped to $(print_version) on main" +} + +function tag_staging { + info "Tagging new version..." + git checkout staging + git tag "$(print_version)" + success "Created new tag $(print_version)" +} + +# Update staging or production branch +# @param $1 – the name of the branch to update +function update_protected_branch { + TARGET_BRANCH="$1" + [[ $TARGET_BRANCH = 'staging' ]] && SOURCE_BRANCH='main' || SOURCE_BRANCH='staging' + UPDATE_BRANCH="update-$TARGET_BRANCH-from-$SOURCE_BRANCH" + info "Merging $SOURCE_BRANCH into $TARGET_BRANCH..." + git checkout "$TARGET_BRANCH" + git checkout -b "$UPDATE_BRANCH" + git merge --no-edit -Xtheirs "$SOURCE_BRANCH" || { git diff --name-only --diff-filter=U | xargs git rm; git -c core.editor=true merge --continue; } + git checkout "$TARGET_BRANCH" + git merge "$UPDATE_BRANCH" --no-ff -m "Merge pull request #$((++PR_COUNT)) from Expensify/$UPDATE_BRANCH" + info "Merged PR #$PR_COUNT to staging" + git branch -d "$UPDATE_BRANCH" + success "Merged $SOURCE_BRANCH into $TARGET_BRANCH" +} + +# CP a PR +# @param $1 – the number of a PR to "cherry-pick" to staging +function cherry_pick { + info "Cherry picking PR #$1 and the version bump to staging..." + + SOURCE_BRANCH="pr-$1" + SOURCE_HEAD=$(git show-ref --verify "refs/heads/$SOURCE_BRANCH" | grep -o '^\S*') + SOURCE_MERGE_COMMIT=$(git log --merges --format="%H %P" | grep "$SOURCE_HEAD" | grep -o '^\S*') + SOURCE_MERGE_BASE=$(git merge-base staging "$SOURCE_MERGE_COMMIT") + + VERSION_BUMP_HEAD=$(git show-ref --verify refs/heads/version-bump | grep -o '^\S*') + VERSION_BUMP_MERGE_COMMIT=$(git log --merges --format="%H %P" | grep "$VERSION_BUMP_HEAD" | grep -o '^\S*') + VERSION_BUMP_MERGE_BASE=$(git merge-base staging "$VERSION_BUMP_MERGE_COMMIT") + + CP_BRANCH="cherry-pick-staging-$1" + CP_MERGE_BASE=$(git merge-base "$SOURCE_MERGE_BASE" "$VERSION_BUMP_MERGE_BASE") + git checkout -b "$CP_BRANCH" "$CP_MERGE_BASE" + + git cherry-pick -x --mainline 1 -Xtheirs "$SOURCE_MERGE_COMMIT" + git cherry-pick -x --mainline 1 -Xtheirs "$VERSION_BUMP_MERGE_COMMIT" + + git checkout main + git merge --no-ff --no-edit "$CP_BRANCH" -m "Merge pull request #$((++PR_COUNT)) from Expensify/$CP_BRANCH" || { git diff --name-only --diff-filter=U | xargs git rm; git -c core.editor=true merge --continue; } + info "Merged PR #$PR_COUNT into main" + + git checkout staging + git merge --no-ff --no-edit -Xtheirs "$CP_BRANCH" -m "Merge pull request #$((++PR_COUNT)) from Expensify/$CP_BRANCH" || { git diff --name-only --diff-filter=U | xargs git rm; git -c core.editor=true merge --continue; } + info "Merged PR #$PR_COUNT into staging" + + git branch -d "$CP_BRANCH" + git checkout main + git branch -d "$SOURCE_BRANCH" + git branch -d version-bump + success "Successfully cherry-picked PR #$1 to staging!" +} + +### Phase 0: Verify necessary tools are installed (all tools should be pre-installed on all GitHub Actions runners) + +if ! command -v jq &> /dev/null +then + error "command jq could not be found, install it with \`brew install jq\` (macOS) or \`apt-get install jq\` (Linux) and re-run this script" + exit 1 +fi + +if ! command -v npm &> /dev/null +then + error "command npm could not be found, install it and re-run this script" + exit 1 +fi + + +### Setup +title "Starting setup" + +info "Creating new dummy repo at $DUMMY_DIR" +mkdir "$DUMMY_DIR" +cd "$DUMMY_DIR" || exit 1 +success "Successfully created dummy repo at $(pwd)" + +info "Initializing npm project in $DUMMY_DIR" +if [[ $(npm init -y) ]] +then + success "Successfully initialized npm project" +else + error "Failed initializing npm project" + exit 1 +fi + +info "Installing node dependencies..." +if [[ $(npm install underscore && npm install) ]] +then + success "Successfully installed node dependencies" +else + error "Failed installing node dependencies" + exit 1 +fi + +info "Initializing Git repo..." +git init -b main +git config user.email "test@test.com" +git config user.name "test" +git add package.json package-lock.json +git commit -m "Initial commit" + +info "Bumping version to 1.0.1" +npm --no-git-tag-version version 1.0.1 -m "Update version to 1.0.1" +git add package.json +git add package-lock.json +git commit -m "Update version to 1.0.1" + +info "Creating branches..." +git checkout -b staging +git checkout -b production +git checkout main +success "Initialized Git repo!" + +tag_staging + +success "Setup complete!" + + +title "Scenario #1: Merge a pull request while the checklist is unlocked" + +# Create PR 1, merge that PR to main. +info "Creating PR #$((++PR_COUNT))" +git checkout main +BRANCH_NAME="pr-$PR_COUNT" +FILE_NAME="PR_$PR_COUNT.txt" +git checkout -b "$BRANCH_NAME" +echo "Changes from PR #$PR_COUNT" >> "$FILE_NAME" +git add "$FILE_NAME" +git commit -m "Changes from PR #$PR_COUNT" +success "Created PR #$PR_COUNT in branch $BRANCH_NAME" + +info "Merging PR #$PR_COUNT to main" +git checkout main +git merge "$BRANCH_NAME" --no-ff -m "Merge pull request #$PR_COUNT from Expensify/$BRANCH_NAME" +git branch -d "$BRANCH_NAME" +success "Merged PR #$PR_COUNT to main" + +assert_string_contains_substring "$(cat $FILE_NAME)" "Changes from PR #$PR_COUNT" +PREVIOUS_PR=$PR_COUNT + +bump_version '1.0.2' +update_protected_branch 'staging' +tag_staging + +assert_string_contains_substring "$(cat $FILE_NAME)" "Changes from PR #$PREVIOUS_PR" + +# Verify output for checklist and deploy comment +info "Checking output of getPullRequestsMergedBetween 1.0.1 1.0.2" +output=$(node "$getPullRequestsMergedBetween" '1.0.1' '1.0.2') +assert_equal "$output" "[ '1' ]" + +success "Scenario #1 completed successfully!" + + +title "Scenario #2: Merge a pull request with the checklist locked, but don't CP it" + +info "Creating PR #$((++PR_COUNT)) and merging it into main..." +git checkout main +BRANCH_NAME="pr-$PR_COUNT" +FILE_NAME="PR_$PR_COUNT.txt" +git checkout -b "$BRANCH_NAME" +echo "Changes from PR #$PR_COUNT" >> "$FILE_NAME" +git add "$FILE_NAME" +git commit -m "Changes from PR #$PR_COUNT" +git checkout main +git merge "$BRANCH_NAME" --no-ff -m "Merge pull request #$PR_COUNT from Expensify/$BRANCH_NAME" +info "Merged PR #$PR_COUNT into main" +git branch -d "$BRANCH_NAME" +success "Created PR #$PR_COUNT and merged it to main!" + +assert_string_contains_substring "$(cat $FILE_NAME)" "Changes from PR #$PR_COUNT" +git checkout staging +assert_string_doesnt_contain_substring "$(cat $FILE_NAME)" "Changes from PR #$PR_COUNT" + +success "Scenario #2 completed successfully!" + + +title "Scenario #3: Merge a pull request with the checklist locked and CP it to staging" + +PREVIOUS_PR=$PR_COUNT +CP_PR=$((++PR_COUNT)) +info "Creating PR #$PR_COUNT and merging it into main..." +git checkout main +BRANCH_NAME="pr-$PR_COUNT" +FILE_NAME="PR_$PR_COUNT.txt" +git checkout -b "$BRANCH_NAME" +echo "Changes from PR #$PR_COUNT" >> "$FILE_NAME" +git add "$FILE_NAME" +git commit -m "Changes from PR #$PR_COUNT" +git checkout main +git merge "$BRANCH_NAME" --no-ff -m "Merge pull request #$PR_COUNT from Expensify/$BRANCH_NAME" +info "Merged PR #$PR_COUNT into main" +success "Created PR #$PR_COUNT and merged it to main!" + +assert_string_contains_substring "$(cat $FILE_NAME)" "Changes from PR #$CP_PR" + +bump_version '1.0.3' --keep-version-branch +cherry_pick 5 +tag_staging + +assert_string_contains_substring "$(cat $FILE_NAME)" "Changes from PR #$CP_PR" +assert_file_doesnt_exist "PR_$PREVIOUS_PR.txt" + +# Verify output for checklist +info "Checking output of getPullRequestsMergedBetween 1.0.1 1.0.3" +output=$(node "$getPullRequestsMergedBetween" '1.0.1' '1.0.3') +assert_equal "$output" "[ '8', '5', '1' ]" + +# Verify output for deploy comment +info "Checking output of getPullRequestsMergedBetween 1.0.2 1.0.3" +output=$(node "$getPullRequestsMergedBetween" '1.0.2' '1.0.3') +assert_equal "$output" "[ '8', '5' ]" + +success "Scenario #3 completed successfully!" + + +title "Scenario #4: Close the checklist" +title "Scenario #4A: Run the production deploy" + +update_protected_branch 'production' + +assert_string_contains_substring "$(cat PR_1.txt)" "Changes from PR #1" +assert_string_contains_substring "$(cat PR_5.txt)" "Changes from PR #5" +assert_file_doesnt_exist "PR_4.txt" + +# Verify output for release body and production deploy comments +info "Checking output of getPullRequestsMergedBetween 1.0.1 1.0.3" +output=$(node "$getPullRequestsMergedBetween" '1.0.1' '1.0.3') +assert_equal "$output" "[ '8', '5', '1' ]" + +success "Scenario #4A completed successfully!" + +title "Scenario #4B: Run the staging deploy and create a new checklist" + +bump_version '1.1.0' +update_protected_branch 'staging' +tag_staging + +assert_string_contains_substring "$(cat PR_4.txt)" "Changes from PR #4" + +# Verify output for new checklist and staging deploy comments +info "Checking output of getPullRequestsMergedBetween 1.0.3 1.1.0" +output=$(node "$getPullRequestsMergedBetween" '1.0.3' '1.1.0') +assert_equal "$output" "[ '7', '4' ]" + +success "Scenario #4B completed successfully!" + + +title "Scenario #5: Merging another pull request when the checklist is unlocked" + +info "Creating PR #$((++PR_COUNT)) and merging it to main..." +git checkout main +BRANCH_NAME="pr-$PR_COUNT" +FILE_NAME="PR_$PR_COUNT.txt" +git checkout -b "$BRANCH_NAME" +echo "Changes from PR #$PR_COUNT" >> "$FILE_NAME" +git add "$FILE_NAME" +git commit -m "Changes from PR #$PR_COUNT" +git checkout main +git merge "$BRANCH_NAME" --no-ff -m "Merge pull request #$PR_COUNT from Expensify/$BRANCH_NAME" +info "Merged PR #$PR_COUNT into main" +git branch -d "$BRANCH_NAME" +success "Created PR #$PR_COUNT and merged it into main!" + +assert_string_contains_substring "$(cat $FILE_NAME)" "Changes from PR #$PR_COUNT" +PREVIOUS_PR=$PR_COUNT + +bump_version '1.1.1' +update_protected_branch 'staging' +tag_staging + +assert_string_contains_substring "$(cat $FILE_NAME)" "Changes from PR #$PREVIOUS_PR" + +# Verify output for checklist +info "Checking output of getPullRequestsMergedBetween 1.0.3 1.1.1" +output=$(node "$getPullRequestsMergedBetween" '1.0.3' '1.1.1') +assert_equal "$output" "[ '12', '7', '4' ]" + +# Verify output for deploy comment +info "Checking output of getPullRequestsMergedBetween 1.1.0 1.1.1" +output=$(node "$getPullRequestsMergedBetween" '1.1.0' '1.1.1') +assert_equal "$output" "[ '12' ]" + +success "Scenario #5 completed successfully!" + + +title "Scenario #6: Cherry-picking a revert" + +info "Creating PR #$((++PR_COUNT)) and merging it to main..." +git checkout main +BRANCH_NAME="pr-$PR_COUNT" +FILE_NAME="PR_$PR_COUNT.txt" +git checkout -b "$BRANCH_NAME" +echo "some content" >> $FILE_NAME +git add $FILE_NAME +git commit -m "Create $FILE_NAME" +git checkout main +git merge "$BRANCH_NAME" --no-ff -m "Merge pull request #$PR_COUNT from Expensify/$BRANCH_NAME" +git branch -d $BRANCH_NAME +success "Merged PR #$PR_COUNT into main!" + +assert_string_contains_substring "$(cat $FILE_NAME)" "some content" + +bump_version '1.1.2' +update_protected_branch 'staging' +tag_staging + +assert_string_contains_substring "$(cat $FILE_NAME)" "some content" + +info "Creating PR #$((++PR_COUNT)) and merging it to main..." +git checkout main +BRANCH_NAME="pr-$PR_COUNT" +git checkout -b "$BRANCH_NAME" +printf "Prepended content\n\n%s" "$(cat "$FILE_NAME")" > $FILE_NAME +printf "\nAppended content\n" >> $FILE_NAME +git add $FILE_NAME +git commit -m "Prepend and append content to $FILE_NAME" +git checkout main +git merge $BRANCH_NAME --no-ff -m "Merge pull request #$PR_COUNT from Expensify/$BRANCH_NAME" +git branch -d $BRANCH_NAME +success "Merged PR #$PR_COUNT into main!" + +info "Asserting that prepended content, original content, and appended content are present on main" +git checkout main +assert_string_contains_substring "$(cat $FILE_NAME)" "Prepended content" +assert_string_contains_substring "$(cat $FILE_NAME)" "some content" +assert_string_contains_substring "$(cat $FILE_NAME)" "Appended content" + +bump_version '1.1.3' +update_protected_branch 'staging' +tag_staging + +info "Asserting that prepended content, original content, and appended content are present on staging" +git checkout staging +assert_string_contains_substring "$(cat $FILE_NAME)" "Prepended content" +assert_string_contains_substring "$(cat $FILE_NAME)" "some content" +assert_string_contains_substring "$(cat $FILE_NAME)" "Appended content" + +info "Creating PR #$((++PR_COUNT)) to revert the append and prepend..." +git checkout main +BRANCH_NAME="pr-$PR_COUNT" +git checkout -b "$BRANCH_NAME" +echo "some content" > $FILE_NAME +git add $FILE_NAME +git commit -m "Revert PR" +git checkout main +git merge $BRANCH_NAME --no-ff -m "Merge pull request #$PR_COUNT from Expensify/$BRANCH_NAME" +info "Merged PR #$PR_COUNT into main" + +info "Asserting that PR is reverted on main" +git checkout main +assert_string_doesnt_contain_substring "$(cat $FILE_NAME)" "Prepended content" +assert_string_contains_substring "$(cat $FILE_NAME)" "some content" +assert_string_doesnt_contain_substring "$(cat $FILE_NAME)" "Appended content" + +PREVIOUS_PR=$PR_COUNT +bump_version '1.1.4' --keep-version-branch +cherry_pick "$PREVIOUS_PR" +tag_staging + +info "Asserting that PR is reverted on staging" +assert_string_doesnt_contain_substring "$(cat $FILE_NAME)" "Prepended content" +assert_string_contains_substring "$(cat $FILE_NAME)" "some content" +assert_string_doesnt_contain_substring "$(cat $FILE_NAME)" "Appended content" + +info "Repeating previously reverted PR..." +git checkout main +BRANCH_NAME="pr-$((++PR_COUNT))" +git checkout -b "$BRANCH_NAME" +printf "Prepended content\n\n%s" "$(cat $FILE_NAME)" > $FILE_NAME +printf "\nAppended content\n" >> $FILE_NAME +git add $FILE_NAME +git commit -m "Prepend and append content to $FILE_NAME" +git checkout main +git merge "$BRANCH_NAME" --no-ff -m "Merge pull request #$PR_COUNT from Expensify/$BRANCH_NAME" +git branch -d "$BRANCH_NAME" +success "Merged PR #22 into main!" + +info "Asserting that prepended content, original content, and appended content are present on main" +git checkout main +assert_string_contains_substring "$(cat $FILE_NAME)" "Prepended content" +assert_string_contains_substring "$(cat $FILE_NAME)" "some content" +assert_string_contains_substring "$(cat $FILE_NAME)" "Appended content" + +bump_version '1.1.5' +update_protected_branch 'staging' +tag_staging + +info "Asserting that prepended content, original content, and appended content are present on staging" +assert_string_contains_substring "$(cat $FILE_NAME)" "Prepended content" +assert_string_contains_substring "$(cat $FILE_NAME)" "some content" +assert_string_contains_substring "$(cat $FILE_NAME)" "Appended content" + +success "Scenario #6 completed successfully!" + +### Cleanup +title "Cleaning up..." +cd "$TEST_DIR" || exit 1 +rm -rf "$DUMMY_DIR" +success "All tests passed! Hooray!"