Skip to content

Commit 77797b0

Browse files
vdemeesterclaude
andcommitted
feat: Add GitHub Actions cherry-pick slash command
- Replace Prow cherry-picker with native GitHub Actions workflow - Enable fork-safe automated cherry-picking via /cherry-pick command - Use SHA-pinned actions for enhanced security and reproducibility Signed-off-by: Vincent Demeester <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 5fb1bf0 commit 77797b0

File tree

2 files changed

+253
-2
lines changed

2 files changed

+253
-2
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# Cherry Pick Command Workflow
2+
#
3+
# This workflow is triggered by the /cherry-pick slash command from the slash.yml workflow.
4+
# It automatically cherry-picks merged PRs to the specified target branches.
5+
#
6+
# Usage: Comment `/cherry-pick <target-branch> [<target-branch> ...]` on a merged pull request
7+
# Example: `/cherry-pick release-v0.47.x`
8+
# Example: `/cherry-pick release-v0.47.x release-v1.3.x`
9+
#
10+
# Security Notes:
11+
# - Only users with "write" permission can trigger this command (enforced in slash.yml)
12+
# - Works safely with PRs from forks because it only cherry-picks already-merged commits
13+
# - Uses CHATOPS_TOKEN to create PRs and push to branches
14+
# - The action creates a new branch from the target branch, not from the fork
15+
16+
name: Cherry Pick Command
17+
18+
on:
19+
repository_dispatch:
20+
types: [cherry-pick-command]
21+
22+
permissions:
23+
contents: write
24+
pull-requests: write
25+
issues: write
26+
27+
jobs:
28+
prepare:
29+
runs-on: ubuntu-latest
30+
outputs:
31+
branches: ${{ steps.parse-args.outputs.branches }}
32+
error: ${{ steps.parse-args.outputs.error }}
33+
message: ${{ steps.parse-args.outputs.message }}
34+
steps:
35+
- name: Add reaction to trigger comment
36+
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
37+
with:
38+
token: ${{ secrets.CHATOPS_TOKEN }}
39+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
40+
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
41+
reactions: "+1"
42+
43+
- name: Get target branches from command args
44+
id: parse-args
45+
run: |
46+
# Parse all unnamed arguments from the slash command
47+
ARGS_JSON='${{ toJson(github.event.client_payload.slash_command.args.unnamed) }}'
48+
49+
# Debug: show what we received
50+
echo "Raw args JSON: $ARGS_JSON"
51+
52+
# Extract branch names from the JSON object, filtering out "all" field if present
53+
# The unnamed args come as {arg1: "branch1", arg2: "branch2", ...}
54+
BRANCHES=$(echo "$ARGS_JSON" | jq -r '[to_entries[] | select(.key | startswith("arg")) | .value | select(. != null and . != "")] | @json')
55+
56+
echo "Parsed branches: $BRANCHES"
57+
58+
if [ "$BRANCHES" = "[]" ] || [ -z "$BRANCHES" ]; then
59+
echo "error=true" >> $GITHUB_OUTPUT
60+
echo "message=Missing target branch(es). Usage: /cherry-pick <target-branch> [<target-branch2> ...]" >> $GITHUB_OUTPUT
61+
else
62+
echo "branches=$BRANCHES" >> $GITHUB_OUTPUT
63+
echo "error=false" >> $GITHUB_OUTPUT
64+
fi
65+
66+
- name: Comment on error
67+
if: steps.parse-args.outputs.error == 'true'
68+
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
69+
with:
70+
token: ${{ secrets.CHATOPS_TOKEN }}
71+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
72+
issue-number: ${{ github.event.client_payload.github.payload.issue.number }}
73+
body: |
74+
❌ **Cherry-pick failed**: ${{ steps.parse-args.outputs.message }}
75+
76+
**Usage**: `/cherry-pick <target-branch> [<target-branch2> ...]`
77+
**Examples**:
78+
- `/cherry-pick release-v1.0`
79+
- `/cherry-pick release-v1.0 release-v1.1 release-v2.0`
80+
81+
cherry-pick:
82+
needs: prepare
83+
if: needs.prepare.outputs.error == 'false'
84+
runs-on: ubuntu-latest
85+
strategy:
86+
fail-fast: false
87+
matrix:
88+
branch: ${{ fromJson(needs.prepare.outputs.branches) }}
89+
steps:
90+
- name: Checkout repository
91+
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
92+
with:
93+
token: ${{ secrets.CHATOPS_TOKEN }}
94+
fetch-depth: 0
95+
96+
- name: Perform cherry-pick
97+
id: cherry-pick
98+
continue-on-error: true
99+
env:
100+
GH_TOKEN: ${{ secrets.CHATOPS_TOKEN }}
101+
TARGET_BRANCH: ${{ matrix.branch }}
102+
PR_NUMBER: ${{ github.event.client_payload.pull_request.number }}
103+
run: |
104+
set +e # Don't exit on error, we want to capture it
105+
106+
# Capture all output
107+
OUTPUT_FILE=$(mktemp)
108+
109+
{
110+
echo "🤖 Starting cherry-pick process..."
111+
112+
git config user.name "Shortbrain bot"
113+
git config user.email "[email protected]"
114+
115+
# Get PR information
116+
echo "Fetching PR #$PR_NUMBER information..."
117+
PR_DATA=$(gh pr view $PR_NUMBER --repo ${{ github.repository }} --json state,mergeCommit,mergedAt)
118+
119+
# Check if PR is merged
120+
PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
121+
MERGED_AT=$(echo "$PR_DATA" | jq -r '.mergedAt')
122+
if [ "$PR_STATE" != "MERGED" ] || [ "$MERGED_AT" = "null" ]; then
123+
echo "❌ ERROR: PR #$PR_NUMBER is not merged yet (state: $PR_STATE). Cherry-pick requires merged PRs."
124+
exit 1
125+
fi
126+
127+
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
128+
echo "Found merge commit: $MERGE_COMMIT"
129+
130+
# Fetch target branch
131+
echo "Fetching target branch: $TARGET_BRANCH..."
132+
if ! git fetch origin "$TARGET_BRANCH" 2>&1; then
133+
echo "❌ ERROR: Target branch '$TARGET_BRANCH' does not exist or cannot be fetched."
134+
exit 1
135+
fi
136+
137+
# Check if a cherry-pick PR already exists
138+
CHERRY_PICK_BRANCH="cherry-pick-$PR_NUMBER-to-$TARGET_BRANCH"
139+
echo "Checking for existing cherry-pick PR..."
140+
EXISTING_PR=$(gh pr list \
141+
--repo ${{ github.repository }} \
142+
--head "$CHERRY_PICK_BRANCH" \
143+
--base "$TARGET_BRANCH" \
144+
--json number,url \
145+
--jq '.[0] | select(. != null)')
146+
147+
if [ -n "$EXISTING_PR" ]; then
148+
PR_URL=$(echo "$EXISTING_PR" | jq -r '.url')
149+
PR_NUM=$(echo "$EXISTING_PR" | jq -r '.number')
150+
echo "ℹ️ Cherry-pick PR already exists: #$PR_NUM"
151+
echo "URL: $PR_URL"
152+
echo "existing_pr_url=$PR_URL" >> $GITHUB_OUTPUT
153+
echo "existing_pr_number=$PR_NUM" >> $GITHUB_OUTPUT
154+
exit 0
155+
fi
156+
157+
# Create new branch for cherry-pick
158+
echo "Creating cherry-pick branch: $CHERRY_PICK_BRANCH..."
159+
git checkout -b "$CHERRY_PICK_BRANCH" "origin/$TARGET_BRANCH"
160+
161+
# Perform cherry-pick
162+
echo "Cherry-picking commit $MERGE_COMMIT..."
163+
if ! git cherry-pick -m 1 "$MERGE_COMMIT" 2>&1; then
164+
echo "❌ ERROR: Cherry-pick failed due to conflicts or other errors."
165+
git cherry-pick --abort 2>/dev/null || true
166+
exit 1
167+
fi
168+
169+
# Push the new branch
170+
echo "Pushing cherry-pick branch..."
171+
git push origin "$CHERRY_PICK_BRANCH"
172+
173+
# Create pull request
174+
echo "Creating pull request..."
175+
gh pr create \
176+
--repo ${{ github.repository }} \
177+
--base "$TARGET_BRANCH" \
178+
--head "$CHERRY_PICK_BRANCH" \
179+
--title "Cherry-pick #$PR_NUMBER to $TARGET_BRANCH" \
180+
--body "Automatic cherry-pick of #$PR_NUMBER to \`$TARGET_BRANCH\`"
181+
182+
echo "✅ Cherry-pick completed successfully!"
183+
} 2>&1 | tee "$OUTPUT_FILE"
184+
185+
EXIT_CODE=${PIPESTATUS[0]}
186+
187+
# Save output for use in comments
188+
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
189+
echo "output<<$EOF" >> $GITHUB_OUTPUT
190+
cat "$OUTPUT_FILE" >> $GITHUB_OUTPUT
191+
echo "$EOF" >> $GITHUB_OUTPUT
192+
193+
rm -f "$OUTPUT_FILE"
194+
exit $EXIT_CODE
195+
196+
- name: Comment on existing PR
197+
if: steps.cherry-pick.outcome == 'success' && steps.cherry-pick.outputs.existing_pr_url != ''
198+
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
199+
with:
200+
token: ${{ secrets.CHATOPS_TOKEN }}
201+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
202+
issue-number: ${{ github.event.client_payload.github.payload.issue.number }}
203+
body: |
204+
ℹ️ **Cherry-pick to `${{ matrix.branch }}` already exists!**
205+
206+
A pull request for this cherry-pick already exists: #${{ steps.cherry-pick.outputs.existing_pr_number }}
207+
208+
**PR**: ${{ steps.cherry-pick.outputs.existing_pr_url }}
209+
210+
- name: Comment on success
211+
if: steps.cherry-pick.outcome == 'success' && steps.cherry-pick.outputs.existing_pr_url == ''
212+
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
213+
with:
214+
token: ${{ secrets.CHATOPS_TOKEN }}
215+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
216+
issue-number: ${{ github.event.client_payload.github.payload.issue.number }}
217+
body: |
218+
✅ **Cherry-pick to `${{ matrix.branch }}` successful!**
219+
220+
A new pull request has been created to cherry-pick this change to `${{ matrix.branch }}`.
221+
222+
Please review and merge the cherry-pick PR.
223+
224+
- name: Comment on failure
225+
if: steps.cherry-pick.outcome == 'failure'
226+
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
227+
with:
228+
token: ${{ secrets.CHATOPS_TOKEN }}
229+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
230+
issue-number: ${{ github.event.client_payload.github.payload.issue.number }}
231+
body: |
232+
❌ **Cherry-pick to `${{ matrix.branch }}` failed!**
233+
234+
The automatic cherry-pick to `${{ matrix.branch }}` failed.
235+
236+
**Output:**
237+
```
238+
${{ steps.cherry-pick.outputs.output }}
239+
```
240+
241+
**Next steps:**
242+
- Check the [action logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for complete details
243+
- If the PR is not merged, merge it first and try again
244+
- If there are conflicts, you'll need to manually cherry-pick this PR

.github/workflows/slash.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
# named <command>-command which must exist in the repository.
1313
#
1414
# Supported commands:
15-
# - /land: invokes the land-command workflow, to land (merge) PRs
16-
# stacked through ghstack
15+
# - /retest: re-trigger workflows that failed on a given PR
16+
# - /e2e-extras: run extra e2e tests on a given PR
17+
# - /cherry-pick <branch>: cherry-picks the merged PR to the specified branch
1718
#
1819
# When a command is recognised, the rocket and eyes emojis are added
1920

@@ -43,5 +44,11 @@ jobs:
4344
"permission": "write",
4445
"issue_type": "pull-request",
4546
"repository": "tektoncd/pipeline"
47+
},
48+
{
49+
"command": "cherry-pick",
50+
"permission": "write",
51+
"issue_type": "pull-request",
52+
"repository": "tektoncd/pipeline"
4653
}
4754
]

0 commit comments

Comments
 (0)