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