Skip to content

Commit 0757fba

Browse files
Add cherry-picking workflow (#177)
* Add automated cherry-picking workflow from https://github.com/ACCESS-NRI/access-om3-configs/blob/ec3759d080f6da4d22fab9d5235c300d1b49d0a8/.github/workflows/automatic-cherry-pick.yaml Co-authored-by: Micael Oliveira <micael.oliveira@anu.edu.au>
1 parent d5ff6ee commit 0757fba

2 files changed

Lines changed: 266 additions & 0 deletions

File tree

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
name: cherry-pick
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
jobs:
8+
process_comment:
9+
# This job will parse the cherry-pick command and perform several sanity checks.
10+
# Ideally we want all pull requests opened from a given command to be correct and successfull, so that
11+
# developers don't have to reissue a cherry-pick command for a subset of the commits/branches.
12+
# Therefore, we do all possible sanity checks in this job, before any pull request is opened in subsequent jobs.
13+
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '!cherry-pick')
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: write
17+
pull-requests: write
18+
outputs:
19+
commits: ${{ steps.command.outputs.commits }}
20+
branches: ${{ steps.command.outputs.branches }}
21+
branch_matrix: ${{ steps.command.outputs.branch_matrix }}
22+
status: ${{ steps.report.outputs.status }}
23+
steps:
24+
- name: Checkout repository
25+
uses: actions/checkout@v4
26+
with:
27+
# We need the history of all branches for the sanity checks
28+
fetch-depth: 0
29+
30+
- name: Parse Command
31+
# Parse a command of the form:
32+
#
33+
# cherry-pick <hash_1> <hash_2> ... <hash_n> into <branch_1> <branch_2> ... <branch_n>
34+
#
35+
# and generate lists of commits and branches for further processing.
36+
#
37+
# TODO: make this step into a separate job, so that we can handle more than one command per comment
38+
id: command
39+
shell: bash
40+
run: |
41+
set -f
42+
command_string=$(echo "${{ github.event.comment.body }}" | grep -m 1 cherry-pick)
43+
echo "command=$command_string" >> $GITHUB_OUTPUT
44+
command=($command_string)
45+
command=("${command[@]:1}")
46+
47+
has_separator=false
48+
arg_type=hash
49+
commits=()
50+
branches=()
51+
for token in ${command[@]}; do
52+
if [[ "$token" == "into" ]]; then
53+
arg_type=branch
54+
has_separator=true
55+
else
56+
if [[ "$arg_type" == "hash" ]]; then
57+
commits+=( $token )
58+
elif [[ "$arg_type" == "branch" ]]; then
59+
branches+=( $token )
60+
fi
61+
fi
62+
done
63+
64+
# Check command correctness
65+
if [[ "$has_separator" = false ]]; then
66+
errors="
67+
- the command is missing the \\\`into\\\` separator."
68+
else
69+
if [[ -z "$commits" ]]; then
70+
errors+="
71+
- no list of commits to cherry-pick was provided"
72+
fi
73+
if [[ -z "$branches" ]]; then
74+
errors+="
75+
- no list of target branches was provided"
76+
fi
77+
fi
78+
if [[ -n "$errors" ]]; then
79+
errors="Incorrect cherry-pick command:$errors"
80+
printf "ERROR_MSG<<EOF\n%s\nEOF" "$errors" >> $GITHUB_ENV
81+
else
82+
# Output lists of commits and branches
83+
echo "commits=${commits[@]}" >> $GITHUB_OUTPUT
84+
echo "branches=${branches[@]}" >> $GITHUB_OUTPUT
85+
86+
# We also output the list of branches as json, so they can be used to generate a matrix for the next job
87+
echo "branch_matrix=$(jq -cn '$ARGS.positional' --args -- "${branches[@]}")" >> $GITHUB_OUTPUT
88+
fi
89+
90+
- name: Check PR Status
91+
if: env.ERROR_MSG == ''
92+
run: |
93+
# Check if the PR has been merged
94+
if [[ -z "${{ github.event.issue.pull_request.merged_at }}" ]]; then
95+
echo "ERROR_MSG=Pull request has not been merged yet. Cannot cherry-pick commits." >> $GITHUB_ENV
96+
fi
97+
98+
- name: Check commits
99+
if: env.ERROR_MSG == ''
100+
env:
101+
GH_TOKEN: ${{ github.token }}
102+
run: |
103+
# Check that the commits to cherry-pick are actually part of this PRs target branch
104+
target=$(gh api repos/{owner}/{repo}/pulls/${{ github.event.issue.number }} -q .base.ref)
105+
for commit in ${{ steps.command.outputs.commits }}; do
106+
if git merge-base --is-ancestor ${commit} origin/$target; then
107+
echo "Commit $commit found in $target branch."
108+
else
109+
missing_commits+=" \\\`$commit\\\`"
110+
fi
111+
done
112+
if [[ -n "$missing_commits" ]]; then
113+
echo "ERROR_MSG=Could not find commit(s) $missing_commits in [$target](${{ github.repositoryUrl }}/tree/$target)." >> $GITHUB_ENV
114+
fi
115+
116+
- name: Check Target Branches
117+
if: env.ERROR_MSG == ''
118+
run: |
119+
# Check that cherry-pick target branches actually exist
120+
for branch in ${{ steps.command.outputs.branches }}; do
121+
if [[ -n "$(git ls-remote --heads origin ${branch})" ]]; then
122+
echo "Found branch $branch in repository."
123+
else
124+
missing_branches+=" \\\`$branch\\\`"
125+
fi
126+
done
127+
if [[ -n "$missing_branches" ]]; then
128+
echo "ERROR_MSG=Could not find branch(es) $missing_branches in repository." >> $GITHUB_ENV
129+
fi
130+
131+
- name: Check Previous Cherry-picks
132+
if: env.ERROR_MSG == ''
133+
run: |
134+
# Check that branches with the cherry-picked commits have not been pushed to the remote yet
135+
for branch in ${{ steps.command.outputs.branches }}; do
136+
new_branch=cherry_pick_from_pr${{ github.event.issue.number }}_into_$branch
137+
if [[ -z "$(git ls-remote --heads origin $new_branch)" ]]; then
138+
echo "No previous attempt to cherry-pick commits from this PR into branch $branch found."
139+
else
140+
duplicated_branches+=" $branch"
141+
fi
142+
done
143+
if [[ -n "$duplicated_branches" ]]; then
144+
errors="It seems there are previous unfinished attempts to cherry-pick commits from this PR to the following branch(es):"
145+
for branch in $duplicated_branches; do
146+
errors+="
147+
- [$branch](https://${{ github.repository }}/tree/$branch)"
148+
done
149+
errors+="
150+
151+
If the current cherry-pick attempt is for a different set of commits, make sure that the previous attempts are fully merged and that the corresponding branches have been deleted."
152+
printf "ERROR_MSG<<EOF\n%s\nEOF" "$errors" >> $GITHUB_ENV
153+
fi
154+
155+
- name: Status Report
156+
id: report
157+
env:
158+
GH_TOKEN: ${{ github.token }}
159+
run: |
160+
if [[ -n '${{ env.ERROR_MSG }}' ]]; then
161+
body="> ${{ steps.command.outputs.command }}
162+
163+
Automatic cherry-pick failed. ${{ env.ERROR_MSG }}"
164+
gh pr comment ${{ github.event.issue.number }} --body "$body"
165+
echo "status=failure" >> $GITHUB_OUTPUT
166+
else
167+
echo "status=success" >> $GITHUB_OUTPUT
168+
fi
169+
170+
create_pr:
171+
runs-on: ubuntu-latest
172+
needs: process_comment
173+
if: needs.process_comment.outputs.status == 'success'
174+
permissions:
175+
contents: write
176+
pull-requests: write
177+
strategy:
178+
matrix:
179+
branch: ${{ fromJson(needs.process_comment.outputs.branch_matrix) }}
180+
steps:
181+
- name: Checkout repository
182+
uses: actions/checkout@v4
183+
with:
184+
# We need the history of all branches
185+
fetch-depth: 0
186+
187+
- name: Determine branch information
188+
id: info
189+
run: |
190+
echo "new_branch=cherry_pick_from_pr${{ github.event.issue.number }}_into_${{ matrix.branch }}" >> $GITHUB_OUTPUT
191+
echo "target_branch_url=[${{ matrix.branch }}](https://github.com/${{ github.repository }}/tree/${{ matrix.branch }})" >> $GITHUB_OUTPUT
192+
193+
- name: Cherry-pick commits
194+
id: cherry-pick
195+
continue-on-error: true
196+
run: |
197+
# We use the github-actions bot account for creating the commits. Note that this will not work if the repository requires signed commits.
198+
git config user.name "github-actions[bot]"
199+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
200+
201+
git checkout -b ${{ steps.info.outputs.new_branch }} origin/${{ matrix.branch }}
202+
git cherry-pick ${{ needs.process_comment.outputs.commits }}
203+
204+
- name: Open pull request
205+
if: steps.cherry-pick.outcome == 'success'
206+
id: open_pr
207+
env:
208+
GH_TOKEN: ${{ github.token }}
209+
run: |
210+
git push --set-upstream origin ${{ steps.info.outputs.new_branch }}
211+
url=$(gh pr create -B ${{ matrix.branch }} -t "Cherry-pick commits from #${{ github.event.issue.number }}" \
212+
-b "Cherry-picking commit(s) ${{ needs.process_comment.outputs.commits }} from #${{ github.event.issue.number }} into ${{ steps.info.outputs.target_branch_url }}.")
213+
echo "pr_url=$url" >> $GITHUB_OUTPUT
214+
215+
- name: Report success
216+
if: steps.cherry-pick.outcome == 'success'
217+
shell: bash
218+
env:
219+
GH_TOKEN: ${{ github.token }}
220+
BODY: |
221+
Automatic Git cherry-picking of commit(s) ${{ needs.process_comment.outputs.commits }} into ${{ steps.info.outputs.target_branch_url }} was successful.
222+
223+
The new pull request can be reviewed and approved [here](${{ steps.open_pr.outputs.pr_url }}).
224+
run: |
225+
gh pr comment ${{ github.event.issue.number }} --body '${{ env.BODY }}'
226+
227+
- name: Manual cherry-pick instructions
228+
if: steps.cherry-pick.outcome == 'failure'
229+
shell: bash
230+
env:
231+
GH_TOKEN: ${{ github.token }}
232+
BODY: |
233+
Automatic Git cherry-picking of commit(s) ${{ needs.process_comment.outputs.commits }} into ${{ steps.info.outputs.target_branch_url }} failed. This usually happens when cherry-picking results in a conflic or an empty commit. To manually cherry-pick the commits and open a pull request, please follow these instructions:
234+
1. Create new branch from target branch:
235+
```console
236+
git checkout ${{ matrix.branch }}
237+
git pull
238+
git checkout -b ${{ steps.info.outputs.new_branch }}
239+
```
240+
2. Cherry-pick commits:
241+
```console
242+
git cherry-pick ${{ needs.process_comment.outputs.commits }}
243+
```
244+
3. Fix any conflicts and/or empty commits by following the instructions provided by Git.
245+
4. Push the new branch:
246+
```console
247+
git push --set-upstream origin ${{ steps.info.outputs.new_branch }}
248+
```
249+
5. Open a new pull request on github making sure the target branch is set to ${{ matrix.branch }}.
250+
run: |
251+
gh pr comment ${{ github.event.issue.number }} --body '${{ env.BODY }}'

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,21 @@ More information on submitting a Pull Request and on the specifics of this pipel
105105
For more information on the manually running the pytests that are run as part of the reproducibility CI checks, see
106106
[model-config-tests](https://github.com/ACCESS-NRI/model-config-tests/).
107107

108+
## Automated Cherry Picking
109+
110+
There is a workflow which enables semi-automated cherry-picking from one branch into another, using the !cherry-pick keyword in a pull-request. This is useful when a change needs to be applied across multiple branches.
111+
112+
For example, if a pull-request changes `dev-preindustrial+concentrations`, to apply the change to `dev-4xCO2+concentrations`:
113+
- First finalise and merge the pull-request into `dev-preindustrial+concentrations`
114+
- Second, as a standalone comment in the pull-request, use the keyword as follows:
115+
` !cherry-pick <commit> into <branch> `
116+
117+
<commit> must exist in `dev-preindustrial+concentrations`. This can be one or multiple commit hashes seperated by spaces.
118+
<branch> would be `dev-4xCO2+concentrations` in this example
119+
120+
This will attempt to make a new pull-request with the request commit(s), and leave a comment on the initial-pull request with the outcome.
121+
122+
108123
## Conditions of use
109124

110125
`<TO DO>`

0 commit comments

Comments
 (0)