44  pull_request_target :
55    types : [closed, labeled] 
66    branches : [main] 
7+   workflow_dispatch :
8+     inputs :
9+       pr_number :
10+         description : ' PR number to backport' 
11+         required : true 
12+         type : string 
13+       force_rerun :
14+         description : ' Force rerun even if backports exist' 
15+         required : false 
16+         type : boolean 
17+         default : false 
718
819jobs :
920  backport :
10-     if : github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport') 
21+     if : > 
22+       (github.event_name == 'pull_request_target' &&  
23+        github.event.pull_request.merged == true &&  
24+        contains(github.event.pull_request.labels.*.name, 'needs-backport')) || 
25+       github.event_name == 'workflow_dispatch' 
1126runs-on : ubuntu-latest 
1227    permissions :
1328      contents : write 
1429      pull-requests : write 
1530      issues : write 
1631
1732    steps :
33+       - name : Validate inputs for manual triggers 
34+         if : github.event_name == 'workflow_dispatch' 
35+         run : | 
36+           # Validate PR number format 
37+           if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then 
38+             echo "::error::Invalid PR number format. Must be a positive integer." 
39+             exit 1 
40+           fi 
41+            
42+           # Validate PR exists and is merged 
43+           if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then 
44+             echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible." 
45+             exit 1 
46+           fi 
47+            
48+           MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged') 
49+           if [ "$MERGED" != "true" ]; then 
50+             echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported." 
51+             exit 1 
52+           fi 
53+            
54+           # Validate PR has needs-backport label 
55+           if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then 
56+             echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label." 
57+             exit 1 
58+           fi 
59+ env :
60+           GH_TOKEN : ${{ secrets.GITHUB_TOKEN }} 
61+ 
1862      - name : Checkout repository 
1963        uses : actions/checkout@v4 
2064        with :
2973        id : check-existing 
3074        env :
3175          GH_TOKEN : ${{ secrets.GITHUB_TOKEN }} 
32-           PR_NUMBER : ${{ github.event.pull_request.number }} 
76+           PR_NUMBER : ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github. event.pull_request.number }} 
3377        run : | 
3478          # Check for existing backport PRs for this PR number 
3579          EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName') 
3983            exit 0 
4084          fi 
4185           
86+           # For manual triggers with force_rerun, proceed anyway 
87+           if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then 
88+             echo "skip=false" >> $GITHUB_OUTPUT 
89+             echo "::warning::Force rerun requested - existing backports will be updated" 
90+             exit 0 
91+           fi 
92+            
4293          echo "Found existing backport PRs:" 
4394          echo "$EXISTING_BACKPORTS" 
4495          echo "skip=true" >> $GITHUB_OUTPUT 
@@ -50,8 +101,17 @@ jobs:
50101        run : | 
51102          # Extract version labels (e.g., "1.24", "1.22") 
52103          VERSIONS="" 
53-           LABELS='${{ toJSON(github.event.pull_request.labels) }}' 
54-           for label in $(echo "$LABELS" | jq -r '.[].name'); do 
104+            
105+           if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 
106+             # For manual triggers, get labels from the PR 
107+             LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name') 
108+           else 
109+             # For automatic triggers, extract from PR event 
110+             LABELS='${{ toJSON(github.event.pull_request.labels) }}' 
111+             LABELS=$(echo "$LABELS" | jq -r '.[].name') 
112+           fi 
113+            
114+           for label in $LABELS; do 
55115            # Match version labels like "1.24" (major.minor only) 
56116            if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then 
57117              # Validate the branch exists before adding to list 
@@ -75,12 +135,20 @@ jobs:
75135        if : steps.check-existing.outputs.skip != 'true' 
76136        id : backport 
77137        env :
78-           PR_NUMBER : ${{ github.event.pull_request.number }} 
79-           PR_TITLE : ${{ github.event.pull_request.title }} 
80-           MERGE_COMMIT : ${{ github.event.pull_request.merge_commit_sha }} 
138+           PR_NUMBER : ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} 
81139        run : | 
82140          FAILED="" 
83141          SUCCESS="" 
142+            
143+           # Get PR data for manual triggers 
144+           if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 
145+             PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit) 
146+             PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') 
147+             MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') 
148+           else 
149+             PR_TITLE="${{ github.event.pull_request.title }}" 
150+             MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" 
151+           fi 
84152
85153          for version in ${{ steps.versions.outputs.versions }}; do 
86154            echo "::group::Backporting to core/${version}" 
@@ -133,10 +201,18 @@ jobs:
133201        if : steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success 
134202        env :
135203          GH_TOKEN : ${{ secrets.PR_GH_TOKEN }} 
136-           PR_TITLE : ${{ github.event.pull_request.title }} 
137-           PR_NUMBER : ${{ github.event.pull_request.number }} 
138-           PR_AUTHOR : ${{ github.event.pull_request.user.login }} 
204+           PR_NUMBER : ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} 
139205        run : | 
206+           # Get PR data for manual triggers 
207+           if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 
208+             PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author) 
209+             PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') 
210+             PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') 
211+           else 
212+             PR_TITLE="${{ github.event.pull_request.title }}" 
213+             PR_AUTHOR="${{ github.event.pull_request.user.login }}" 
214+           fi 
215+            
140216          for backport in ${{ steps.backport.outputs.success }}; do 
141217            IFS=':' read -r version branch <<< "${backport}" 
142218
@@ -165,9 +241,16 @@ jobs:
165241        env :
166242          GH_TOKEN : ${{ github.token }} 
167243        run : | 
168-           PR_NUMBER="${{ github.event.pull_request.number }}" 
169-           PR_AUTHOR="${{ github.event.pull_request.user.login }}" 
170-           MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" 
244+           if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 
245+             PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit) 
246+             PR_NUMBER="${{ inputs.pr_number }}" 
247+             PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') 
248+             MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') 
249+           else 
250+             PR_NUMBER="${{ github.event.pull_request.number }}" 
251+             PR_AUTHOR="${{ github.event.pull_request.user.login }}" 
252+             MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" 
253+           fi 
171254
172255          for failure in ${{ steps.backport.outputs.failed }}; do 
173256            IFS=':' read -r version reason conflicts <<< "${failure}" 
0 commit comments