ci(BA-434): Graceful exit if committed directly to main (#3351) #548
This file contains 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: backport | |
on: | |
push: | |
branches: | |
- 'main' | |
workflow_dispatch: | |
inputs: | |
commit_hash: | |
description: 'The commit hash to backport' | |
required: true | |
concurrency: | |
group: ${{ github.workflow }}-${{ github.ref }} | |
jobs: | |
get-backport-commits: | |
runs-on: ubuntu-latest | |
outputs: | |
matrix: ${{ steps.push.outputs.commits || steps.dispatch.outputs.commits }} | |
steps: | |
- if: ${{ github.event_name == 'push' }} | |
name: Get all commits in the push event | |
id: push | |
env: | |
COMMITS: ${{ toJson(github.event.commits) }} | |
run: | | |
commits_json=$(echo "$COMMITS" | jq -c '[.[].id]') | |
echo "commits=${commits_json}" >> $GITHUB_OUTPUT | |
- if: ${{ github.event_name == 'workflow_dispatch' }} | |
name: Get the commit from the PR | |
id: dispatch | |
run: | | |
commits=$(echo '${{ github.event.inputs.commit_hash }}' | jq -Rc '[.]') | |
echo "commits=$commits" >> $GITHUB_OUTPUT | |
get-backport-target-branch: | |
runs-on: ubuntu-latest | |
needs: get-backport-commits | |
strategy: | |
matrix: | |
commits: ${{ fromJson(needs.get-backport-commits.outputs.matrix) }} | |
outputs: | |
matrix: ${{ steps.milestones.outputs.matrix }} | |
latest_commit: ${{ steps.commit.outputs.latest_commit }} | |
commit_message: ${{ steps.commit.outputs.commit_message }} | |
pr_number: ${{ steps.commit.outputs.pr_number }} | |
latest_release: ${{ steps.commit.outputs.latest_release }} | |
author: ${{ steps.commit.outputs.author }} | |
author_email: ${{ steps.commit.outputs.author_email }} | |
labels: ${{ steps.commit.outputs.labels }} | |
steps: | |
- name: Checkout the revision | |
uses: actions/checkout@v4 | |
with: | |
lfs: false | |
ref: ${{ matrix.commits }} | |
- name: Extract pr_number from commit message | |
id: commit | |
run: | | |
latest_commit=$(git rev-parse HEAD) # Get the recent commit hash of the current repository | |
echo "latest_commit=$latest_commit" >> $GITHUB_OUTPUT | |
commit_message=$(git show -s --format=%B $latest_commit) # Get messages from recent commit | |
echo "commit_message<<EOF" >> $GITHUB_OUTPUT | |
echo "$commit_message" >> $GITHUB_OUTPUT | |
echo "EOF" >> $GITHUB_OUTPUT | |
pr_number=$(echo $commit_message | (grep -oP "\(#\d+\)" || true) | (grep -oP "\d+" || true)) # Get pr number from commit message | |
echo "pr_number=$pr_number" >> $GITHUB_OUTPUT | |
latest_release=$(cat VERSION | grep -oP "\d+\.\d+") | |
echo "latest_release=$latest_release" >> $GITHUB_OUTPUT | |
author=$(git show -s --format=%an $latest_commit) | |
echo "author=$author" >> $GITHUB_OUTPUT | |
author_email=$(git show -s --format=%ae $latest_commit) | |
echo "author_email=$author_email" >> $GITHUB_OUTPUT | |
labels=$(gh pr view $pr_number --json labels | jq -r '.labels[].name') | |
echo "labels<<EOF" >> $GITHUB_OUTPUT | |
echo "$labels" >> $GITHUB_OUTPUT | |
echo "EOF" >> $GITHUB_OUTPUT | |
env: | |
GH_TOKEN: ${{ github.token }} | |
- name: Get target branches | |
id: milestones | |
if: ${{ steps.commit.outputs.pr_number }} | |
run: | | |
target_milestone=$(gh pr view ${{ steps.commit.outputs.pr_number }} --json milestone --jq .milestone.title) | |
milestones=$(gh api /repos/:owner/:repo/milestones --jq '.[].title') | |
echo "Milestones configured in the repo: $milestones" | |
# Remove Backlog from the backport target branch | |
milestones=($milestones) | |
for i in "${!milestones[@]}"; do | |
if [[ "${milestones[$i]}" == "Backlog" ]]; then | |
unset 'milestones[$i]' | |
fi | |
done | |
for i in "${!milestones[@]}"; do | |
if ! git ls-remote --heads | grep -q "refs/heads/${milestones[$i]}\$"; then | |
unset 'milestones[$i]' | |
fi | |
done | |
echo "Milestones with the corresponding release branch: ${milestones[@]}" | |
sort_milestones=($(printf "%s\n" "${milestones[@]}" | sort -r)) | |
for i in "${!sort_milestones[@]}"; do | |
if [[ "${sort_milestones[$i]}" == "$target_milestone" ]]; then | |
target_milestones=("${sort_milestones[@]:0:$((i+1))}") | |
break | |
fi | |
done | |
pr_head=$(gh pr view ${{ steps.commit.outputs.pr_number }} --json headRepositoryOwner,headRefName --jq '.headRepositoryOwner.login + ":" + .headRefName') | |
STATUS_CODE=$(curl -s -o response.json -w "%{http_code}" -X GET "${{ secrets.KVSTORE_URL }}/?key=head_$pr_head" \ | |
-H "Authorization: Bearer ${{ secrets.KVSTORE_TOKEN }}") | |
if [ $STATUS_CODE == "404" ]; then | |
target_branches=("${target_milestones[@]}") | |
elif [ $STATUS_CODE == "200" ]; then | |
pr_base=$(jq -r '.value' response.json) | |
target_branches=("backport/${pr_base}-to-${target_milestones[@]}") | |
else | |
echo "Failed to get the target branches" | |
echo "response=$(cat response.json)" | |
exit 1 | |
fi | |
curl -X DELETE "${{ secrets.KVSTORE_URL }}/?key=head_$pr_head" \ | |
-H "Authorization: Bearer ${{ secrets.KVSTORE_TOKEN }}" | |
curl -X DELETE "${{ secrets.KVSTORE_URL }}/?key=base_$pr_base" \ | |
-H "Authorization: Bearer ${{ secrets.KVSTORE_TOKEN }}" | |
matrix=$(printf '%s\n' "${target_branches[@]}" | grep -v '^$' | jq -R . | jq -sc .) | |
echo "matrix=$matrix" >> $GITHUB_OUTPUT | |
env: | |
GH_TOKEN: ${{ github.token }} | |
backport: | |
if: ${{ needs.get-backport-target-branch.outputs.matrix != '[]' && needs.get-backport-target-branch.outputs.matrix != '' }} | |
runs-on: ubuntu-latest | |
needs: get-backport-target-branch | |
strategy: | |
matrix: | |
target_branch: ${{ fromJson(needs.get-backport-target-branch.outputs.matrix) }} | |
fail-fast: false | |
permissions: | |
pull-requests: write | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
ref: ${{ matrix.target_branch }} | |
- name: Cherry-pick | |
env: | |
COMMIT_MESSAGE: ${{ needs.get-backport-target-branch.outputs.commit_message }} | |
run: | | |
git config user.name "${{ needs.get-backport-target-branch.outputs.author }}" | |
git config user.email "${{ needs.get-backport-target-branch.outputs.author_email }}" | |
latest_commit="${{ needs.get-backport-target-branch.outputs.latest_commit }}" | |
git fetch origin main "$latest_commit" | |
git cherry-pick --strategy=recursive --strategy-option=theirs "$latest_commit" | |
git commit \ | |
--amend -m "${COMMIT_MESSAGE}" \ | |
--trailer "Backported-from=main (${{ needs.get-backport-target-branch.outputs.latest_release }})" \ | |
--trailer "Backported-to=${{ matrix.target_branch }}" \ | |
--trailer "Backport-of=${{ needs.get-backport-target-branch.outputs.pr_number }}" | |
- name: When cherry-pick is failed | |
if: failure() | |
run: | | |
gh pr comment ${{ needs.get-backport-target-branch.outputs.pr_number }} -b "Backport to ${{ matrix.target_branch }} is failed. Please backport manually." | |
env: | |
GH_TOKEN: ${{ github.token }} | |
- id: commit_message | |
env: | |
COMMIT_MESSAGE: ${{ needs.get-backport-target-branch.outputs.commit_message }} | |
run: | | |
commit_header=$(echo "${COMMIT_MESSAGE}" | head -n 1) | |
echo "commit_header=$commit_header" >> $GITHUB_OUTPUT | |
commit_body=$(echo "${COMMIT_MESSAGE}" | awk '/^$/{p++;next} p==1') | |
echo "commit_body<<EOF" >> $GITHUB_OUTPUT | |
echo "$commit_body" >> $GITHUB_OUTPUT | |
echo "EOF" >> $GITHUB_OUTPUT | |
commit_footer=$(echo "${COMMIT_MESSAGE}" | awk '/^$/{p++;next} p==2') | |
echo "commit_footer<<EOF" >> $GITHUB_OUTPUT | |
echo "$commit_footer" >> $GITHUB_OUTPUT | |
echo "EOF" >> $GITHUB_OUTPUT | |
- name: Create Backport PR | |
id: pr | |
uses: peter-evans/create-pull-request@v6 | |
with: | |
token: ${{ secrets.OCTODOG }} | |
author: "${{ needs.get-backport-target-branch.outputs.author }} <${{ needs.get-backport-target-branch.outputs.author_email }}>" | |
title: "${{ steps.commit_message.outputs.commit_header }}" | |
body: "This is an auto-generated backport PR of #${{ needs.get-backport-target-branch.outputs.pr_number }} to the ${{ matrix.target_branch }} release." | |
branch: "backport/${{ needs.get-backport-target-branch.outputs.pr_number }}-to-${{ matrix.target_branch }}" | |
base: ${{ matrix.target_branch }} | |
labels: | | |
backport | |
${{ needs.get-backport-target-branch.outputs.labels }} | |
assignees: ${{ needs.get-backport-target-branch.outputs.author }} | |
- id: pr_id | |
run: | | |
pr_id=$(gh api graphql -f query=' | |
query ($pr_number: Int!, $owner: String!, $name: String!) { | |
repository(owner: $owner, name: $name) { | |
pullRequest(number: $pr_number) { | |
id | |
} | |
} | |
} | |
' -F pr_number=${{ steps.pr.outputs.pull-request-number }} -f owner=${{ github.repository_owner }} -f name=${{ github.event.repository.name }} | jq -r '.data.repository.pullRequest.id') | |
echo "pr_id=$pr_id" >> $GITHUB_OUTPUT | |
env: | |
GH_TOKEN: ${{ secrets.OCTODOG }} | |
- id: commit_footer | |
run: | | |
commit_footer="Co-authored-by: ${{ needs.get-backport-target-branch.outputs.author }} <${{ needs.get-backport-target-branch.outputs.author_email }}> | |
${{ steps.commit_message.outputs.commit_footer }} | |
Backported-from: main (${{ needs.get-backport-target-branch.outputs.latest_release }}) | |
Backported-to: ${{ matrix.target_branch }} | |
Backport-of: ${{ needs.get-backport-target-branch.outputs.pr_number }}" | |
commit_footer=$(echo "$commit_footer" | grep '.') # remove empty lines | |
echo "commit_footer<<EOF" >> $GITHUB_OUTPUT | |
echo "$commit_footer" >> $GITHUB_OUTPUT | |
echo "EOF" >> $GITHUB_OUTPUT | |
- name: Enable auto-merge | |
if: ${{ steps.pr.outputs.pull-request-number }} | |
run: | | |
gh api graphql -f query=' | |
mutation ($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) { | |
enablePullRequestAutoMerge(input: { | |
pullRequestId: $pullRequestId, | |
mergeMethod: $mergeMethod, | |
commitBody: """ | |
${{ steps.commit_message.outputs.commit_body }} | |
${{ steps.commit_footer.outputs.commit_footer }} | |
""", | |
commitHeadline: "${{ steps.commit_message.outputs.commit_header }} (#${{ steps.pr.outputs.pull-request-number }})" | |
}) { | |
pullRequest { | |
autoMergeRequest { | |
enabledAt | |
} | |
} | |
} | |
} | |
' -F pullRequestId=${{ steps.pr_id.outputs.pr_id }} -f mergeMethod="SQUASH" | |
env: | |
GH_TOKEN: ${{ secrets.OCTODOG }} |