Skip to content

Commit 35527d3

Browse files
authored
Repo File Sync: Add Release Branch Backport Workflow (#309)
synced local file(s) with [microsoft/mu_devops](https://github.com/microsoft/mu_devops). 🤖: View the [Repo File Sync Configuration File](https://github.com/microsoft/mu_devops/blob/main/.sync/Files.yml) to see how files are synced. --- This PR was created automatically by the [repo-file-sync-action](https://github.com/BetaHuhn/repo-file-sync-action) workflow run [#11631955745](https://github.com/microsoft/mu_devops/actions/runs/11631955745) Signed-off-by: Project Mu UEFI Bot <uefibot@microsoft.com>
1 parent cefd593 commit 35527d3

File tree

3 files changed

+240
-3
lines changed

3 files changed

+240
-3
lines changed

.github/pull_request_template.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ For details on how to complete these options and their meaning refer to [CONTRIB
99
- [ ] Breaking change?
1010
- [ ] Includes tests?
1111
- [ ] Includes documentation?
12+
- [ ] Backport to release branch?
1213

1314
## How This Was Tested
1415

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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+

.github/workflows/label-issues/regex-pull-requests.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,15 @@
1414

1515
# Maintenance: Keep labels organized in ascending alphabetical order - easier to scan, identify duplicates, etc.
1616

17+
type:backport:
18+
- '\s*-\s*\[\s*[x|X]\s*\] Backport to release branch\?'
19+
1720
impact:breaking-change:
1821
- '\s*-\s*\[\s*[x|X]\s*\] Breaking change\?'
1922

23+
type:documentation:
24+
- '\s*-\s*\[\s*[x|X]\s*\] Includes documentation\?'
25+
2026
impact:non-functional:
2127
- '\s*-\s*\[\s*(?![x|X])\s*\] Impacts functionality\?'
2228

@@ -25,6 +31,3 @@ impact:security:
2531

2632
impact:testing:
2733
- '\s*-\s*\[\s*[x|X]\s*\] Includes tests\?'
28-
29-
type:documentation:
30-
- '\s*-\s*\[\s*[x|X]\s*\] Includes documentation\?'

0 commit comments

Comments
 (0)