Skip to content

Commit 8bbf0ed

Browse files
vdemeesterclaude
authored 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 <vdemeest@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent b1f8f62 commit 8bbf0ed

File tree

3 files changed

+272
-3
lines changed

3 files changed

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

CONTRIBUTING.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,28 @@ Additionally, please read the following resources specific to Tekton Pipelines:
3131
- [Tekton Pipelines roadmap](roadmap.md)
3232
- [Tekton Pipelines API compatibility policy](api_compatibility_policy.md)
3333

34-
For support in contributing to specific areas, contact the relevant [Tekton Pipelines Topical Owner(s)](topical-ownership.md).
34+
For support in contributing to specific areas, contact the relevant [Tekton Pipelines Topical Owner(s)](topical-ownership.md).
35+
36+
## Slash Commands
37+
38+
The project includes GitHub slash commands to automate common workflows:
39+
40+
### `/cherry-pick`
41+
42+
Automatically cherry-picks a merged PR to one or more target branches.
43+
44+
**Usage**: `/cherry-pick <target-branch> [<target-branch2> ...]`
45+
46+
**Examples**:
47+
- `/cherry-pick release-v0.47.x`
48+
- `/cherry-pick release-v0.47.x release-v1.3.x`
49+
50+
**Requirements**:
51+
- PR must be merged
52+
- User must have write permissions
53+
- Target branch(es) must exist
54+
55+
The command creates a new PR with the cherry-picked changes for each target branch.
3556

3657
## Contributing to Tekton documentation
3758

0 commit comments

Comments
 (0)