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