1name: Backport to release/v1
2
3# Cherry-picks a merged PR's commit straight onto release/v1 and pushes it —
4# no intermediate PR. Fires when:
5# - a PR labeled `backport/v1` is merged into master, or
6# - a maintainer comments `/backport v1` on an already-merged PR.
7# On a clean cherry-pick the commit is pushed directly. On conflict nothing is
8# pushed; the source PR is labeled `backport-failed` and a comment asks for a
9# manual cherry-pick.
10
11on:
12 pull_request_target:
13 types: [closed]
14 issue_comment:
15 types: [created]
16
17permissions:
18 contents: write
19 pull-requests: write
20 issues: write
21
22jobs:
23 backport:
24 name: Backport
25 runs-on: ubuntu-latest
26 if: >
27 (github.event_name == 'pull_request_target'
28 && github.event.pull_request.merged == true
29 && contains(github.event.pull_request.labels.*.name, 'backport/v1'))
30 || (github.event_name == 'issue_comment'
31 && github.event.issue.pull_request
32 && github.event.issue.state == 'closed'
33 && startsWith(github.event.comment.body, '/backport v1')
34 && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association))
35 steps:
36 - name: Resolve PR and merge commit
37 id: pr
38 uses: actions/github-script@v9
39 with:
40 github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
41 script: |
42 const { owner, repo } = context.repo;
43 const num = context.eventName === 'issue_comment'
44 ? context.payload.issue.number
45 : context.payload.pull_request.number;
46
47 const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: num });
48
49 if (!pr.merged || !pr.merge_commit_sha) {
50 core.notice(`PR #${num} is not merged; nothing to backport.`);
51 core.setOutput('run', 'false');
52 return;
53 }
54 core.setOutput('run', 'true');
55 core.setOutput('number', String(num));
56 core.setOutput('sha', pr.merge_commit_sha);
57 core.setOutput('title', pr.title);
58
59 - name: Checkout
60 if: steps.pr.outputs.run == 'true'
61 uses: actions/checkout@v7
62 with:
63 fetch-depth: 0
64 ref: release/v1
65 token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
66
67 - name: Cherry-pick onto release/v1
68 if: steps.pr.outputs.run == 'true'
69 id: pick
70 run: |
71 set -euo pipefail
72 SHA="${{ steps.pr.outputs.sha }}"
73 git config user.name "Floatpane Bot"
74 git config user.email "us@floatpane.com"
75 git fetch --no-tags origin "$SHA"
76
77 # A true merge commit has >1 parent and needs a mainline (-m 1);
78 # squash/rebase merges are a single commit cherry-picked as-is.
79 PARENTS=$(git rev-list --parents -n1 "$SHA" | wc -w)
80 PICK_ARGS="-x"
81 if [ "$PARENTS" -gt 2 ]; then PICK_ARGS="-x -m 1"; fi
82
83 if git cherry-pick $PICK_ARGS "$SHA"; then
84 git push origin HEAD:release/v1
85 echo "status=ok" >> "$GITHUB_OUTPUT"
86 else
87 git cherry-pick --abort || true
88 echo "status=conflict" >> "$GITHUB_OUTPUT"
89 fi
90
91 - name: Report result
92 if: steps.pr.outputs.run == 'true'
93 uses: actions/github-script@v9
94 with:
95 github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
96 script: |
97 const { owner, repo } = context.repo;
98 const issue_number = Number('${{ steps.pr.outputs.number }}');
99 const status = '${{ steps.pick.outputs.status }}';
100 const sha = '${{ steps.pr.outputs.sha }}';
101
102 if (status === 'ok') {
103 await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['backported'] });
104 try {
105 await github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'backport-failed' });
106 } catch (e) { if (e.status !== 404) throw e; }
107 await github.rest.issues.createComment({
108 owner, repo, issue_number,
109 body: `Cherry-picked \`${sha.substring(0, 7)}\` onto \`release/v1\`.`,
110 });
111 } else {
112 await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['backport-failed'] });
113 await github.rest.issues.createComment({
114 owner, repo, issue_number,
115 body: [
116 `Backport to \`release/v1\` hit a conflict — cherry-pick it manually:`,
117 '```bash',
118 'git fetch origin',
119 'git checkout release/v1',
120 `git cherry-pick -x ${sha}`,
121 '# resolve conflicts, then:',
122 'git push origin release/v1',
123 '```',
124 ].join('\n'),
125 });
126 core.setFailed('Cherry-pick conflict.');
127 }