Skip to content

Commit 21da0ab

Browse files
authored
Add release branch backport workflow (#386)
Closes #372 This new workflow will attempt to automatically cherry-pick marked contributions to a development branch to its corresponding release branch. If a merge conflict occurs, the commit is committed to a new branch with merge markers and then a PR is created into the target branch with those markers. The PR is labeled with `type:release-merge-conflict` to indicate that it needs manual resolution. The PR (if created) is expected to fail compilation and status checks of course due to the merge conflict markers. A human should then checkout the PR branch, resolve the conflicts, and push the changes back to the PR branch. --- - To mark a PR going into the development branch so that it should be cherry-picked to the release branch, add the `type:backport` label to the PR. --- Notes: - The workflow is synced to all repos that currently have a `dev` branch. - Each repo that `backport-to-release-branch.yml` is synced to must have a `CHERRY_PICK_TOKEN` defined with repo write permission. Signed-off-by: Michael Kubacki <michael.kubacki@microsoft.com>
1 parent 6c29981 commit 21da0ab

4 files changed

Lines changed: 275 additions & 9 deletions

File tree

.sync/Files.yml

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,20 @@ group:
403403
microsoft/mu_tiano_platforms
404404
microsoft/mu_tiano_plus
405405
406+
# Leaf Workflow - Backport Dev Branch Changes to Release Branch
407+
- files:
408+
- source: .sync/workflows/leaf/backport-to-release-branch.yml
409+
dest: .github/workflows/backport-to-release-branch.yml
410+
template: true
411+
repos: |
412+
microsoft/mu_basecore
413+
microsoft/mu_common_intel_min_platform
414+
microsoft/mu_oem_sample
415+
microsoft/mu_plus
416+
microsoft/mu_silicon_arm_tiano
417+
microsoft/mu_silicon_intel_tiano
418+
microsoft/mu_tiano_plus
419+
406420
# Leaf Workflow - CodeQL
407421
# Note: This workflow should be used in repos that build firmware
408422
# packages from a CI builder (i.e. a CISettings.py file).
@@ -658,29 +672,40 @@ group:
658672
repos: |
659673
microsoft/mu_tiano_platforms
660674
661-
# Pull Request Template - Common Template
675+
# Pull Request Template - Common Template - Backport Option
662676
- files:
663677
- source: .sync/github_templates/pull_requests/pull_request_template.md
664678
dest: .github/pull_request_template.md
679+
template:
680+
additional_checkboxes:
681+
- Backport to release branch?
665682
repos: |
666683
microsoft/mu_basecore
667684
microsoft/mu_common_intel_min_platform
685+
microsoft/mu_oem_sample
686+
microsoft/mu_plus
687+
microsoft/mu_silicon_arm_tiano
688+
microsoft/mu_silicon_intel_tiano
689+
microsoft/mu_tiano_plus
690+
691+
# Pull Request Template - Common Template
692+
- files:
693+
- source: .sync/github_templates/pull_requests/pull_request_template.md
694+
dest: .github/pull_request_template.md
695+
template:
696+
additional_checkboxes: []
697+
repos: |
668698
microsoft/mu_crypto_release
669699
microsoft/mu_feature_config
670700
microsoft/mu_feature_debugger
671701
microsoft/mu_feature_dfci
672702
microsoft/mu_feature_ipmi
673703
microsoft/mu_feature_mm_supv
674704
microsoft/mu_feature_uefi_variable
675-
microsoft/mu_oem_sample
676-
microsoft/mu_plus
677705
microsoft/mu_rust_helpers
678706
microsoft/mu_rust_hid
679707
microsoft/mu_rust_pi
680-
microsoft/mu_silicon_arm_tiano
681-
microsoft/mu_silicon_intel_tiano
682708
microsoft/mu_tiano_platforms
683-
microsoft/mu_tiano_plus
684709
685710
# Rust - Pipeline Files
686711
- files:

.sync/github_templates/pull_requests/pull_request_template.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ For details on how to complete these options and their meaning refer to [CONTRIB
99
- [ ] Breaking change?
1010
- [ ] Includes tests?
1111
- [ ] Includes documentation?
12+
{% for additional_checkbox in additional_checkboxes %}
13+
- [ ] {{ additional_checkbox }}
14+
{% endfor %}
1215

1316
## How This Was Tested
1417

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

0 commit comments

Comments
 (0)