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 }}'
0 commit comments