Cherry-pick a pull request #3132
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Cherry-pick a pull request | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| PULL_REQUEST_URL: | |
| description: The full URL of the E/App or E/Mobile-Expensify pull request to cherry-pick | |
| required: true | |
| TARGET: | |
| description: CP to staging or production? | |
| required: true | |
| type: choice | |
| options: | |
| - staging | |
| - production | |
| default: staging | |
| workflow_call: | |
| inputs: | |
| # Note: this is required for manually-run CP's, but we don't require it for the workflow_call trigger. | |
| # The reason is that this workflow is only called programmatically after a CP to Prod, | |
| # and in that case we just want to CP a version bump - no other PR. | |
| PULL_REQUEST_URL: | |
| description: The full URL of the E/App or E/Mobile-Expensify pull request to cherry-pick | |
| type: string | |
| required: false | |
| TARGET: | |
| description: CP to staging or production? | |
| type: string | |
| required: true | |
| concurrency: | |
| group: "cherrypick" | |
| cancel-in-progress: false | |
| jobs: | |
| createNewVersion: | |
| uses: ./.github/workflows/createNewVersion.yml | |
| secrets: inherit | |
| with: | |
| # In order to submit a new build for production review, it must have a higher PATCH version than the previously-submitted build. | |
| # The typical case is that with each staging deploy, we bump the BUILD version, and with each prod deploy we bump the PATCH version. | |
| # However, if PULL_REQUEST_URL is empty, we assume we want to do a PATCH version bump. | |
| # The reason we assume that is because the use-case for a no-PR CP is after a CP to Production. | |
| # In that case, we need to bump the staging version to be higher than prod. | |
| # We use a patch version so that there's a diff in both `CFBundleVersion` and `CFBundleShortVersion` on iOS, | |
| # so that when we later CP that version bump to staging, the `CFBundleShortVersion` diff is picked up as well. | |
| SEMVER_LEVEL: ${{ (inputs.TARGET == 'production' || inputs.PULL_REQUEST_URL == '') && 'PATCH' || 'BUILD' }} | |
| cherryPick: | |
| needs: createNewVersion | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Extract PR information | |
| # Note: this step is only skipped when there's no PULL_REQUEST_URL, which is only ever be the case when we're CPing just a version bump. | |
| if: ${{ inputs.PULL_REQUEST_URL != '' }} | |
| id: getPRInfo | |
| run: | | |
| echo "REPO_FULL_NAME=$(echo '${{ inputs.PULL_REQUEST_URL }}' | sed -E 's|https?://github.com/([^/]+/[^/]+)/pull/.*|\1|')" >> "$GITHUB_OUTPUT" | |
| echo "PR_NUMBER=$(echo '${{ inputs.PULL_REQUEST_URL }}' | sed -E 's|.*/pull/([0-9]+).*|\1|')" >> "$GITHUB_OUTPUT" | |
| - name: Verify pull request URL | |
| if: ${{ inputs.PULL_REQUEST_URL != '' }} | |
| run: | | |
| if [[ "${{ steps.getPRInfo.outputs.REPO_FULL_NAME }}" != ${{ github.repository }} ]] && [[ ! "${{ steps.getPRInfo.outputs.REPO_FULL_NAME }}" =~ Expensify/Mobile-Expensify* ]]; then | |
| echo "::error::❌ Cherry picks are only supported for the Expensify/App and Expensify/Mobile-Expensify repositories. Found: ${{ steps.getPRInfo.outputs.REPO_FULL_NAME }}" | |
| exit 1 | |
| fi | |
| - name: Set conflict branch name | |
| if: ${{ inputs.PULL_REQUEST_URL != '' }} | |
| id: getBranchName | |
| run: echo "CONFLICT_BRANCH_NAME=cherry-pick-${{ inputs.TARGET }}-${{ steps.getPRInfo.outputs.PR_NUMBER }}-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT" | |
| # v4 | |
| - name: Checkout target branch | |
| uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 | |
| with: | |
| ref: ${{ inputs.TARGET }} | |
| token: ${{ secrets.OS_BOTIFY_TOKEN }} | |
| submodules: true | |
| # This command is necessary to fetch any branch other than main in the submodule. | |
| # See https://github.com/actions/checkout/issues/1815#issuecomment-2777836442 for further context. | |
| - name: Enable branch-switching in submodules | |
| run: | | |
| git submodule foreach '\ | |
| git config --add remote.origin.fetch "+refs/heads/staging:refs/remotes/origin/staging" && \ | |
| git config --add remote.origin.fetch "+refs/heads/production:refs/remotes/origin/production"' | |
| - name: Set up git for OSBotify | |
| id: setupGitForOSBotify | |
| uses: Expensify/GitHub-Actions/setupGitForOSBotify@main | |
| with: | |
| OP_VAULT: ${{ vars.OP_VAULT }} | |
| OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} | |
| OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} | |
| OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} | |
| - name: Get previous app version | |
| id: getPreviousVersion | |
| uses: ./.github/actions/javascript/getPreviousVersion | |
| with: | |
| SEMVER_LEVEL: ${{ inputs.TARGET == 'staging' && 'PATCH' || 'MINOR' }} | |
| - name: Fetch history of relevant refs | |
| run: | | |
| git fetch origin main ${{ inputs.TARGET }} --no-recurse-submodules --no-tags --shallow-exclude ${{ steps.getPreviousVersion.outputs.PREVIOUS_VERSION }} | |
| cd Mobile-Expensify | |
| git fetch origin main ${{ inputs.TARGET }} --no-recurse-submodules --no-tags --shallow-exclude ${{ steps.getPreviousVersion.outputs.PREVIOUS_VERSION }} | |
| - name: Get E/App version bump commit | |
| id: getVersionBumpCommit | |
| run: | | |
| git switch main | |
| VERSION_BUMP_COMMIT="$(git log -1 --format='%H' --author='OSBotify' --grep 'Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}')" | |
| if [ -z "$VERSION_BUMP_COMMIT" ]; then | |
| echo "::error::❌ Could not find E/App version bump commit for ${{ needs.createNewVersion.outputs.NEW_VERSION }}" | |
| git log --oneline | |
| else | |
| echo "::notice::👀 Found E/App version bump commit $VERSION_BUMP_COMMIT" | |
| fi | |
| echo "VERSION_BUMP_SHA=$VERSION_BUMP_COMMIT" >> "$GITHUB_OUTPUT" | |
| - name: Get Mobile-Expensify version bump commit | |
| id: getMobileExpensifyVersionBumpCommit | |
| working-directory: Mobile-Expensify | |
| run: | | |
| git switch main | |
| VERSION_BUMP_COMMIT="$(git log -1 --format='%H' --author='OSBotify' --grep 'Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}')" | |
| if [ -z "$VERSION_BUMP_COMMIT" ]; then | |
| echo "::error::❌ Could not find Mobile-Expensify version bump commit for ${{ needs.createNewVersion.outputs.NEW_VERSION }}" | |
| git log --oneline | |
| else | |
| echo "::notice::👀 Found Mobile-Expensify version bump commit $VERSION_BUMP_COMMIT" | |
| fi | |
| echo "VERSION_BUMP_SHA=$VERSION_BUMP_COMMIT" >> "$GITHUB_OUTPUT" | |
| - name: Get merge commit for pull request to CP | |
| if: ${{ inputs.PULL_REQUEST_URL != '' }} | |
| id: getCPMergeCommit | |
| run: | | |
| read -r MERGE_COMMIT_SHA MERGE_ACTOR <<< "$(gh pr view ${{ inputs.PULL_REQUEST_URL }} --json mergeCommit,author --jq '"\(.mergeCommit.oid) \(.author.login)"')" | |
| echo "MERGE_COMMIT_SHA=$MERGE_COMMIT_SHA" >> "$GITHUB_OUTPUT" | |
| echo "MERGE_ACTOR=$MERGE_ACTOR" >> "$GITHUB_OUTPUT" | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} | |
| - name: Cherry-pick the Mobile-Expensify version bump to Mobile-Expensify target branch | |
| working-directory: Mobile-Expensify | |
| run: | | |
| git switch ${{ inputs.TARGET }} | |
| git cherry-pick -S -x --mainline 1 --strategy=recursive -Xtheirs ${{ steps.getMobileExpensifyVersionBumpCommit.outputs.VERSION_BUMP_SHA }} | |
| git commit --amend -m "$(git log -1 --pretty=%B)" -m "(cherry-picked to ${{ inputs.TARGET }} by ${{ github.actor }})" | |
| git push origin ${{ inputs.TARGET }} | |
| - name: Cherry-pick the E/App version-bump to target branch | |
| run: | | |
| git switch ${{ inputs.TARGET }} | |
| git cherry-pick -S -x --mainline 1 --strategy=recursive -Xtheirs ${{ steps.getVersionBumpCommit.outputs.VERSION_BUMP_SHA }} | |
| git commit --amend -m "$(git log -1 --pretty=%B)" -m "(cherry-picked to ${{ inputs.TARGET }} by ${{ github.actor }})" | |
| - name: Update the Mobile-Expensify submodule on E/App target branch | |
| run: | | |
| git add Mobile-Expensify | |
| git commit -m "Update Mobile-Expensify submodule version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}" | |
| - name: Cherry-pick the merge commit of target PR | |
| if: ${{ inputs.PULL_REQUEST_URL != '' }} | |
| id: cherryPick | |
| # If cherry picking a Mobile-Expensify change, we need to run the cherry pick in the Mobile-Expensify directory | |
| working-directory: ${{ startsWith(steps.getPRInfo.outputs.REPO_FULL_NAME, 'Expensify/Mobile-Expensify') && 'Mobile-Expensify' || '.' }} | |
| 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 "HAS_CONFLICTS=false" >> "$GITHUB_OUTPUT" | |
| git commit --amend -m "$(git log -1 --pretty=%B)" -m "(cherry-picked to ${{ inputs.TARGET }} by ${{ github.actor }})" | |
| 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 cherry-pick --abort | |
| echo "HAS_CONFLICTS=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Push changes | |
| run: | | |
| if [[ '${{ steps.cherryPick.outputs.HAS_CONFLICTS }}' == 'true' ]]; then | |
| git checkout -b ${{ steps.getBranchName.outputs.CONFLICT_BRANCH_NAME }} | |
| git push --set-upstream origin ${{ steps.getBranchName.outputs.CONFLICT_BRANCH_NAME }} | |
| else | |
| if [[ "${{ steps.getPRInfo.outputs.REPO_FULL_NAME }}" =~ Expensify/Mobile-Expensify* ]]; then | |
| # Push Mobile-Expensify changes first | |
| cd Mobile-Expensify | |
| git push origin ${{ inputs.TARGET }} | |
| cd .. | |
| # Update and commit the submodule reference in E/App | |
| git add Mobile-Expensify | |
| git commit -m "Update Mobile-Expensify submodule to include cherry-picked PR #${{ steps.getPRInfo.outputs.PR_NUMBER }}" | |
| fi | |
| # Push E/App changes | |
| git push origin ${{ inputs.TARGET }} | |
| fi | |
| - name: Create Pull Request to manually finish CP | |
| if: steps.cherryPick.outputs.HAS_CONFLICTS == 'true' | |
| id: createPullRequest | |
| run: | | |
| AUTHOR_CHECKLIST=$(sed -n '/### PR Author Checklist/,$p' .github/PULL_REQUEST_TEMPLATE.md) | |
| PR_DESCRIPTION=$(cat <<EOF | |
| 🍒 Cherry pick ${{ inputs.PULL_REQUEST_URL }} to ${{ inputs.TARGET }} 🍒 | |
| This PR had conflicts when we tried to cherry-pick it to ${{ inputs.TARGET }}. You'll need to manually perform the cherry-pick, using the following steps: | |
| \`\`\`bash | |
| git fetch | |
| git checkout ${{ steps.getBranchName.outputs.CONFLICT_BRANCH_NAME }} | |
| git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }} | |
| \`\`\` | |
| Then manually resolve conflicts, and commit the change with \`git cherry-pick --continue\`. Lastly, please run: | |
| \`\`\`bash | |
| git commit --amend -m "\$(git log -1 --pretty=%B)" -m "(cherry-picked to ${{ inputs.TARGET }} by ${{ github.actor }})" | |
| \`\`\` | |
| This last part is important. It will help us keep track of who triggered this CP, and will ensure that version bumps are tracked correctly. Once all that's done, push your changes with \`git push origin ${{ steps.getBranchName.outputs.CONFLICT_BRANCH_NAME }}\`, and then open this PR for review. | |
| Note that you **must** test this PR, and both the author and reviewer checklist should be completed, just as if you were merging the PR to main. | |
| _Pro-tip:_ If this PR appears to have conflicts against the _${{ inputs.TARGET }}_ base, it means that the version on ${{ inputs.TARGET }} has been updated. The easiest thing to do if you see this is to close the PR and re-run the CP. | |
| $AUTHOR_CHECKLIST | |
| EOF | |
| ) | |
| # Create PR | |
| gh pr create \ | |
| --title "🍒 Cherry pick PR #${{ steps.getPRInfo.outputs.PR_NUMBER }} to ${{ inputs.TARGET }} 🍒" \ | |
| --body "$PR_DESCRIPTION" \ | |
| --label "Engineering,Hourly,${{ inputs.TARGET == 'staging' && 'CP Staging' || 'CP Production' }}" \ | |
| --base "${{ inputs.TARGET }}" | |
| sleep 5 | |
| # Save the Cherry-pick PR URL for Slack notification | |
| PR_URL="$(gh pr view --json url --jq .url)" | |
| echo "PR_URL=$PR_URL" >> "$GITHUB_OUTPUT" | |
| env: | |
| GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} | |
| - name: Announce CP conflict in #deployer | |
| if: steps.cherryPick.outputs.HAS_CONFLICTS == 'true' | |
| uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e | |
| with: | |
| status: custom | |
| custom_payload: | | |
| { | |
| channel: '#deployer', | |
| attachments: [{ | |
| color: "#DB4545", | |
| pretext: `<!subteam^S4TJJ3PSL>`, | |
| text: "🚨 Cherry-pick to ${{ inputs.TARGET }} has conflicts and requires manual resolution.\nOriginal PR: ${{ inputs.PULL_REQUEST_URL }}\nConflict PR: ${{ steps.createPullRequest.outputs.PR_URL }}" | |
| }] | |
| } | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} | |
| - name: Add assignees to conflict PRs | |
| if: steps.cherryPick.outputs.HAS_CONFLICTS == 'true' | |
| run: | | |
| gh pr edit --add-assignee "${{ github.actor }},${{ steps.getCPMergeCommit.outputs.MERGE_ACTOR }}" | |
| ORIGINAL_PR_AUTHOR="$(gh pr view ${{ inputs.PULL_REQUEST_URL }} --json author --jq .author.login)" | |
| gh pr edit --add-assignee "$ORIGINAL_PR_AUTHOR" | |
| env: | |
| GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} | |
| # In cases when the original PR author is outside the org, the `gh pr edit` command will fail. But we don't want to fail the workflow in that case. | |
| continue-on-error: true | |
| - name: Label original PR with CP label | |
| if: ${{ inputs.PULL_REQUEST_URL != '' }} | |
| run: gh pr edit ${{ inputs.PULL_REQUEST_URL }} --add-label '${{ inputs.TARGET == 'staging' && 'CP Staging' || 'CP Production' }}' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} | |
| - name: "Announces a CP failure in the #announce Slack room" | |
| # v3 | |
| uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e | |
| if: ${{ failure() }} | |
| with: | |
| status: custom | |
| custom_payload: | | |
| { | |
| channel: '#announce', | |
| attachments: [{ | |
| color: "#DB4545", | |
| pretext: `<!subteam^S4TJJ3PSL>`, | |
| text: `💥 Failed to CP ${{ inputs.PULL_REQUEST_URL }} to ${{ inputs.TARGET }} 💥`, | |
| }] | |
| } | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} |