|
| 1 | +# This workflow moves marked commits from a development branch to a release branch. |
| 2 | +# |
| 3 | +# Each commit in the development branch is cherry-picked to the release branch if the commit originates from a merged |
| 4 | +# PR that is marked for backport. |
| 5 | +# |
| 6 | +# Merge conflicts should be rare. Should one occur, the changes are committed to a new branch with merge markers and |
| 7 | +# then a PR is created into the target branch with those markers. The PR is labeled with "type:release-merge-conflict" |
| 8 | +# to indicate that it needs manual resolution. |
| 9 | +# |
| 10 | +# The PR is expected to fail compilation and status checks (of course) due to the merge conflict markers. A human |
| 11 | +# should then checkout the PR branch, resolve the conflicts, and push the changes back to the PR branch. |
| 12 | +# |
| 13 | +# NOTE: This file is automatically synchronized from Mu DevOps. Update the original file there |
| 14 | +# instead of the file in this repo. |
| 15 | +# |
| 16 | +# - Mu DevOps Repo: https://github.com/microsoft/mu_devops |
| 17 | +# - File Sync Settings: https://github.com/microsoft/mu_devops/blob/main/.sync/Files.yml |
| 18 | +# |
| 19 | +# Copyright (c) Microsoft Corporation. |
| 20 | +# SPDX-License-Identifier: BSD-2-Clause-Patent |
| 21 | +# |
| 22 | + |
| 23 | +name: Backport Commits to Release Branch |
| 24 | + |
| 25 | +on: |
| 26 | + push: |
| 27 | + branches: |
| 28 | + - dev/202405 |
| 29 | + |
| 30 | +jobs: |
| 31 | + backport: |
| 32 | + name: Backport Dev Branch Commits to Release Branch |
| 33 | + runs-on: ubuntu-latest |
| 34 | + |
| 35 | + steps: |
| 36 | + - name: Checkout code |
| 37 | + uses: actions/checkout@v4 |
| 38 | + with: |
| 39 | + fetch-depth: 0 |
| 40 | + token: ${{ secrets.CHERRY_PICK_TOKEN }} |
| 41 | + |
| 42 | + - name: Determine Contribution Info |
| 43 | + id: backport_info |
| 44 | + uses: actions/github-script@v7 |
| 45 | + with: |
| 46 | + script: | |
| 47 | + const BOLD = "\u001b[1m"; |
| 48 | + const GREEN = "\u001b[32m"; |
| 49 | +
|
| 50 | + const ref = process.env.GITHUB_REF; |
| 51 | + const sourceBranchName = ref.replace('refs/heads/', ''); |
| 52 | + const targetBranchName = sourceBranchName.replace('dev', 'release'); |
| 53 | +
|
| 54 | + const commits = context.payload.commits; |
| 55 | + const commitCount = commits.length; |
| 56 | +
|
| 57 | + if (commits.length === 0) { |
| 58 | + console.log(GREEN + "No commits found. Exiting workflow."); |
| 59 | + core.setOutput('backport_needed', 'false'); |
| 60 | + process.exit(0); |
| 61 | + } |
| 62 | +
|
| 63 | + console.log(`Source branch name is ${sourceBranchName}`); |
| 64 | + console.log(`Target branch name is ${targetBranchName}\n`); |
| 65 | +
|
| 66 | + core.startGroup(`${commitCount} Commit(s) in this Contribution`); |
| 67 | + commits.forEach((commit, index) => { |
| 68 | + console.log(BOLD + `Commit #${index + 1}: ${commit.id}`); |
| 69 | + console.log(`${commit.message}\n`); |
| 70 | + }); |
| 71 | + core.endGroup(); |
| 72 | +
|
| 73 | + core.setOutput('backport_needed', 'true'); |
| 74 | + core.setOutput('source_branch_name', sourceBranchName); |
| 75 | + core.setOutput('target_branch_name', targetBranchName); |
| 76 | + core.setOutput('first_commit_id', commits[0].id); |
| 77 | + core.setOutput('commits', JSON.stringify(commits)); |
| 78 | + core.setOutput('commit_by_id', commits.map(commit => commit.id).join(' ')); |
| 79 | + core.setOutput('commit_messages', commits.map(commit => `${commit.message.split('\n')[0]}\n${commit.message.split('\n').slice(1).join('\n')}\n---`).join('\n')); |
| 80 | + core.setOutput('commit_count', commitCount); |
| 81 | +
|
| 82 | + - name: Check if Backport is Requested |
| 83 | + id: backport_check |
| 84 | + uses: actions/github-script@v7 |
| 85 | + with: |
| 86 | + script: | |
| 87 | + if (${{ steps.backport_info.outputs.backport_needed }} === 'false') { |
| 88 | + core.setOutput('backport_needed', 'false'); |
| 89 | + process.exit(0); |
| 90 | + } |
| 91 | +
|
| 92 | + const BOLD = "\u001b[1m"; |
| 93 | + const GREEN = "\u001b[32m"; |
| 94 | + const MAGENTA = "\u001b[35m"; |
| 95 | +
|
| 96 | + const response = await github.request("GET /repos/${{ github.repository }}/commits/${{ steps.backport_info.outputs.first_commit_id }}/pulls", { |
| 97 | + headers: { |
| 98 | + authorization: `token ${process.env.GITHUB_TOKEN}` |
| 99 | + } |
| 100 | + }); |
| 101 | +
|
| 102 | + const prNumber = response.data.length > 0 ? response.data[0].number : null; |
| 103 | +
|
| 104 | + console.log(`Associated Pull Request Number: ${prNumber}\n`); |
| 105 | +
|
| 106 | + if (!prNumber) { |
| 107 | + console.log(GREEN + "No associated pull request found. Nothing to backport! Exiting."); |
| 108 | + core.setOutput('backport_needed', 'false'); |
| 109 | + process.exit(0); |
| 110 | + } |
| 111 | +
|
| 112 | + const { data: pull } = await github.rest.pulls.get({ |
| 113 | + owner: context.repo.owner, |
| 114 | + repo: context.repo.repo, |
| 115 | + pull_number: prNumber |
| 116 | + }); |
| 117 | +
|
| 118 | + core.startGroup(`${pull.labels.length} Label(s) in the PR`); |
| 119 | + pull.labels.forEach((label, index) => { |
| 120 | + console.log(BOLD + `Label #${index + 1}: \"${label.name}\"`); |
| 121 | + }); |
| 122 | + core.endGroup(); |
| 123 | +
|
| 124 | + const label = pull.labels.find(l => l.name === 'type:backport'); |
| 125 | + if (!label) { |
| 126 | + console.log(GREEN + "Changes are not requested for backport. Exiting."); |
| 127 | + core.setOutput('backport_needed', 'false'); |
| 128 | + process.exit(0); |
| 129 | + } |
| 130 | +
|
| 131 | + console.log(MAGENTA + "The changes are requested for backport. Proceeding with backport.\n"); |
| 132 | +
|
| 133 | + core.setOutput('pr_number', prNumber); |
| 134 | + core.setOutput('backport_needed', 'true'); |
| 135 | + env: |
| 136 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 137 | + |
| 138 | + - name: Checkout a Local ${{ steps.backport_info.outputs.target_branch_name }} Branch (Destination Branch) |
| 139 | + if: steps.backport_check.outputs.backport_needed == 'true' |
| 140 | + run: | |
| 141 | + git config --global user.email "mubot@microsoft.com" |
| 142 | + git config --global user.name "Project Mu Bot" |
| 143 | + git checkout -b ${{ steps.backport_info.outputs.target_branch_name }} origin/${{ steps.backport_info.outputs.target_branch_name }} |
| 144 | +
|
| 145 | + - name: Check for Merge Conflicts |
| 146 | + if: steps.backport_check.outputs.backport_needed == 'true' |
| 147 | + id: merge_conflicts |
| 148 | + run: | |
| 149 | + conflict=false |
| 150 | +
|
| 151 | + for commit in ${{ steps.backport_info.outputs.commit_by_id }}; do |
| 152 | + echo -e "\nAttempting to cherry-pick commit $commit..." |
| 153 | +
|
| 154 | + set +e |
| 155 | + cherry_pick_output=$( { git cherry-pick $commit; } 2>&1 ) |
| 156 | + set -e |
| 157 | +
|
| 158 | + if echo "$cherry_pick_output" | grep -q "The previous cherry-pick is now empty"; then |
| 159 | + echo "Cherry-picking $commit resulted in an empty commit. Skipping it."; |
| 160 | + git cherry-pick --skip; |
| 161 | + elif echo "$cherry_pick_output" | grep -q "Merge conflict in"; then |
| 162 | + echo "Merge conflict detected for commit $commit! Committing it with conflict markers."; |
| 163 | + original_author=$(git log -1 --pretty=format:'%an <%ae>' $commit) |
| 164 | + original_date=$(git log -1 --pretty=format:'%ad' --date=iso-strict $commit) |
| 165 | + original_message=$(git log -1 --pretty=%B $commit) |
| 166 | + git add -A |
| 167 | + GIT_COMMITTER_DATE="$original_date" GIT_AUTHOR_DATE="$original_date" git commit --author="$original_author" -m "[CONFLICT] $original_message" |
| 168 | + conflict=true; |
| 169 | + else |
| 170 | + echo "$commit was cherry-picked successfully."; |
| 171 | + fi |
| 172 | + done |
| 173 | +
|
| 174 | + echo "merge_conflict=$conflict" >> $GITHUB_ENV |
| 175 | + continue-on-error: true |
| 176 | +
|
| 177 | + - name: Push to ${{ steps.backport_info.outputs.target_branch_name }} if No Conflicts |
| 178 | + if: steps.backport_check.outputs.backport_needed == 'true' && env.merge_conflict == 'false' |
| 179 | + run: | |
| 180 | + git push origin ${{ steps.backport_info.outputs.target_branch_name }}:${{ steps.backport_info.outputs.target_branch_name }} |
| 181 | +
|
| 182 | + - name: Generate a Unique PR Branch Name (On Merge Conflict) |
| 183 | + if: steps.backport_check.outputs.backport_needed == 'true' && env.merge_conflict == 'true' |
| 184 | + id: merge_conflict_branch_info |
| 185 | + run: | |
| 186 | + TIMESTAMP=$(date +%Y%m%d%H%M%S) |
| 187 | + branch_name="merge-conflict/${{ steps.backport_info.outputs.target_branch_name }}/$TIMESTAMP" |
| 188 | +
|
| 189 | + echo -e "\nMerge conflict branch name generated: $branch_name" |
| 190 | +
|
| 191 | + git branch -m $branch_name |
| 192 | + git push origin refs/heads/$branch_name:refs/heads/$branch_name |
| 193 | +
|
| 194 | + echo "branch_name=$branch_name" >> $GITHUB_OUTPUT |
| 195 | +
|
| 196 | + - name: Create Pull Request (On Merge Conflict) |
| 197 | + if: steps.backport_check.outputs.backport_needed == 'true' && env.merge_conflict == 'true' |
| 198 | + run: | |
| 199 | + PR_BRANCH="${{ steps.merge_conflict_branch_info.outputs.branch_name }}" |
| 200 | + BASE_BRANCH="${{ steps.backport_info.outputs.target_branch_name }}" |
| 201 | + PR_TITLE="Manual Merge Conflict Resolution for ${{ steps.backport_info.outputs.commit_count }} Commits into ${{ steps.backport_info.outputs.target_branch_name }}" |
| 202 | + PR_BODY="This pull request is created to resolve the merge conflict that occurred while backporting the commits |
| 203 | + from ${{ steps.backport_info.outputs.source_branch_name }} to ${{ steps.backport_info.outputs.target_branch_name }}. |
| 204 | +
|
| 205 | + **Commits in this PR:** |
| 206 | +
|
| 207 | + ${{ steps.backport_info.outputs.commit_messages }} |
| 208 | +
|
| 209 | + **Instructions:** |
| 210 | +
|
| 211 | + 1. Checkout this PR branch locally. |
| 212 | + 2. Verify all commits that are being backported are present in the branch. |
| 213 | + 3. Resolve the merge conflict markers in the files. |
| 214 | + 4. Commit the changes. |
| 215 | + 5. Push the changes back to this PR branch. |
| 216 | +
|
| 217 | + **Note:** |
| 218 | +
|
| 219 | + If it is too complicated to use this branch as-is, then simply attempt to merge the same set of commits into |
| 220 | + the release branch locally, resolve the conflicts, and force push the changes to the PR branch." |
| 221 | +
|
| 222 | + echo "PR Title: $PR_TITLE" |
| 223 | + echo "PR Body: $PR_BODY" |
| 224 | + echo "PR Branch: $PR_BRANCH" |
| 225 | + echo "Base Branch: $BASE_BRANCH" |
| 226 | +
|
| 227 | + curl -s -X POST https://api.github.com/repos/${{ github.repository }}/pulls \ |
| 228 | + -H "Authorization: token $CHERRY_PICK_TOKEN" \ |
| 229 | + -H "Content-Type: application/json" \ |
| 230 | + -d "{\"title\":\"$PR_TITLE\",\"body\":\"$PR_BODY\",\"head\":\"$PR_BRANCH\",\"base\":\"$BASE_BRANCH\",\"labels\":[\"type:release-merge-conflict\"]}" |
| 231 | + env: |
| 232 | + CHERRY_PICK_TOKEN: ${{ secrets.CHERRY_PICK_TOKEN }} |
| 233 | +
|
0 commit comments