Skip to content

Cherry-pick a pull request #3132

Cherry-pick a pull request

Cherry-pick a pull request #3132

Workflow file for this run

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 }}