1- name : Link Issue to PR
1+ name : Link issue to PR
22
33on :
44 workflow_call :
55 inputs :
66 pr_branch :
7- description : ' Pull Request branch'
8- type : string
7+ description : ' PR branch name'
98 required : true
9+ type : string
1010 pr_url :
11- description : ' Pull Request URL'
11+ description : ' PR URL'
12+ required : true
1213 type : string
14+ pr_title :
15+ description : ' PR title'
1316 required : true
14- secrets :
15- CI_MACHINE_TOKEN :
16- description : ' CI machine token '
17+ type : string
18+ pr_body :
19+ description : ' PR body '
1720 required : true
18-
19- workflow_dispatch :
20- inputs :
21- pr_branch :
22- description : ' Pull Request branch'
2321 type : string
22+ pr_author :
23+ description : ' PR author'
2424 required : true
25- pr_url :
26- description : ' Pull Request URL'
2725 type : string
26+ pr_merged :
27+ description : ' PR merged status'
28+ required : true
29+ type : string
30+ secrets :
31+ GITHUB_TOKEN :
32+ description : ' GitHub token'
2833 required : true
29-
30- env :
31- GH_TOKEN : ${{ secrets.CI_MACHINE_TOKEN }}
3234
3335jobs :
34- add-issue-to-pr :
35- runs-on : ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }}
36-
36+ link-issue :
37+ runs-on : ubuntu-latest
38+ env :
39+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
40+
3741 steps :
38- - run : echo 'GitHub context'
42+ - name : Checkout code
43+ uses : actions/checkout@v4
44+ with :
45+ fetch-depth : 0
46+
47+ - name : Debug workflow inputs
48+ run : |
49+ echo "PR Branch: ${{ inputs.pr_branch }}"
50+ echo "PR URL: ${{ inputs.pr_url }}"
51+ echo "PR Title: ${{ inputs.pr_title }}"
52+ echo "PR Body: ${{ inputs.pr_body }}"
53+ echo "PR Author: ${{ inputs.pr_author }}"
54+ echo "PR Merged: ${{ inputs.pr_merged }}"
3955 env :
4056 GITHUB_CONTEXT : ${{ toJson(github) }}
4157
58+ - name : Check if PR already has linked issues
59+ id : check_existing_issues
60+ run : |
61+ pr_url="${{ inputs.pr_url }}"
62+ pr_number=$(echo "$pr_url" | grep -o '[0-9]*$')
63+
64+ # Get PR details
65+ pr_details=$(curl -s \
66+ -H "Accept: application/vnd.github+json" \
67+ -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \
68+ -H "X-GitHub-Api-Version: 2022-11-28" \
69+ "https://api.github.com/repos/${{ github.repository }}/pulls/$pr_number")
70+
71+ # Check for issues linked via GitHub's Development section (timeline events)
72+ timeline_events=$(curl -s \
73+ -H "Accept: application/vnd.github+json" \
74+ -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \
75+ -H "X-GitHub-Api-Version: 2022-11-28" \
76+ "https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/timeline")
77+
78+ # Look for connected/disconnected events that indicate manual linking
79+ connected_issue=$(echo "$timeline_events" | jq -r '.[] | select(.event == "connected") | .source.issue.number' | head -1)
80+
81+ if [[ -n "$connected_issue" && "$connected_issue" != "null" ]]; then
82+ echo "Found manually linked issue via Development section: $connected_issue"
83+ echo "has_linked_issues=true" >> "$GITHUB_OUTPUT"
84+ echo "linked_issue_number=$connected_issue" >> "$GITHUB_OUTPUT"
85+ echo "link_method=development_section" >> "$GITHUB_OUTPUT"
86+ else
87+ # Check if PR body contains issue references (fixes #123, closes #456, etc.)
88+ pr_body=$(echo "$pr_details" | jq -r '.body // ""')
89+
90+ # Extract issue numbers from PR body using various keywords
91+ linked_issues=$(echo "$pr_body" | grep -oiE '(fixes?|closes?|resolves?)\s+#([0-9]+)' | grep -oE '[0-9]+' | head -1)
92+
93+ if [[ -n "$linked_issues" ]]; then
94+ echo "Found linked issue in PR body: $linked_issues"
95+ echo "has_linked_issues=true" >> "$GITHUB_OUTPUT"
96+ echo "linked_issue_number=$linked_issues" >> "$GITHUB_OUTPUT"
97+ echo "link_method=pr_body" >> "$GITHUB_OUTPUT"
98+ else
99+ echo "No linked issues found in Development section or PR body"
100+ echo "has_linked_issues=false" >> "$GITHUB_OUTPUT"
101+ fi
102+ fi
103+
42104 - name : Extract issue number from branch name
43105 id : extract_issue_number
44106 run : |
45107 branch_name="${{ inputs.pr_branch }}"
108+ issue_number=""
109+
110+ # Try multiple patterns to extract issue number (more flexible but specific)
46111 if [[ "$branch_name" =~ ^([0-9]+)- ]]; then
47112 issue_number="${BASH_REMATCH[1]}"
113+ echo "Found issue number at start of branch: $issue_number"
48114 elif [[ "$branch_name" =~ ^issue-([0-9]+)- ]]; then
49115 issue_number="${BASH_REMATCH[1]}"
116+ echo "Found issue number with 'issue-' prefix: $issue_number"
117+ elif [[ "$branch_name" =~ issue-([0-9]+) ]]; then
118+ issue_number="${BASH_REMATCH[1]}"
119+ echo "Found issue number with 'issue-' anywhere in branch: $issue_number"
50120 else
51- echo "Branch name doesn't match the expected pattern"
52- exit 1
121+ echo "No issue number found in branch name: $branch_name"
53122 fi
54123
55124 echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT"
56125
126+ - name : Determine final issue number
127+ id : determine_issue
128+ run : |
129+ # Priority: 1) Manually linked issues (Development section or PR body), 2) Branch name extraction
130+ if [[ "${{ steps.check_existing_issues.outputs.has_linked_issues }}" == "true" ]]; then
131+ final_issue_number="${{ steps.check_existing_issues.outputs.linked_issue_number }}"
132+ link_method="${{ steps.check_existing_issues.outputs.link_method }}"
133+ echo "Using manually linked issue: $final_issue_number (via $link_method)"
134+ elif [[ -n "${{ steps.extract_issue_number.outputs.issue_number }}" ]]; then
135+ final_issue_number="${{ steps.extract_issue_number.outputs.issue_number }}"
136+ echo "Using issue from branch name: $final_issue_number"
137+ else
138+ echo "::error::No issue number found in Development section, PR body, or branch name"
139+ echo "::error::Please link an issue using one of these methods:"
140+ echo "::error::1. Link via GitHub UI: Go to PR → Development section → Link issue"
141+ echo "::error::2. Add 'fixes #123' (or closes/resolves) to PR body, or"
142+ echo "::error::3. Use branch naming like 'issue-123-feature' or '123-feature'"
143+ exit 1
144+ fi
145+
146+ echo "final_issue_number=$final_issue_number" >> "$GITHUB_OUTPUT"
147+
57148 - name : Get existing issue comments
58149 id : get_comments
59150 run : |
60- comments="$(\
61- curl -s \
151+ comments=$(curl -s \
62152 -H "Accept: application/vnd.github+json" \
63153 -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \
64154 -H "X-GitHub-Api-Version: 2022-11-28" \
65- https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract_issue_number .outputs.issue_number }}/comments \
155+ https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.determine_issue .outputs.final_issue_number }}/comments \
66156 | jq -c .
67157 )"
68158
69- echo "comments=${ comments} " >> "$GITHUB_OUTPUT"
159+ echo "comments=$comments" >> "$GITHUB_OUTPUT"
70160
71- - name : Check if PR comment exists
161+ - name : Check if comment already exists
72162 id : check_comment
73- uses : actions/github-script@v7
74- with :
75- script : |
76- const prUrl = '${{ inputs.pr_url }}';
77- let prList = `PRs:\n- ${prUrl}`;
78- let existingCommentId = '';
79- const comments = JSON.parse(${{ toJSON(steps.get_comments.outputs.comments) }});
163+ run : |
164+ comments='${{ steps.get_comments.outputs.comments }}'
165+ pr_url="${{ inputs.pr_url }}"
166+
167+ # Check if our bot comment already exists
168+ existing_comment=$(echo "$comments" | jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("PRs linked to this issue"))) | .id' | head -1)
169+
170+ if [[ -n "$existing_comment" && "$existing_comment" != "null" ]]; then
171+ echo "Found existing comment: $existing_comment"
172+ echo "existing_comment_id=$existing_comment" >> "$GITHUB_OUTPUT"
173+ else
174+ echo "No existing comment found"
175+ echo "existing_comment_id=" >> "$GITHUB_OUTPUT"
176+ fi
177+
178+ # Get existing PR list from the comment if it exists
179+ if [[ -n "$existing_comment" && "$existing_comment" != "null" ]]; then
180+ existing_body=$(echo "$comments" | jq -r --arg id "$existing_comment" '.[] | select(.id == ($id | tonumber)) | .body')
181+ pr_list=$(echo "$existing_body" | grep -o "- \[.*\](.*).*" | sort -u)
80182
81- for(comment of comments) {
82- const commentBody = comment.body;
83- if (commentBody.startsWith('PRs:')) {
84- existingCommentId = comment.id;
85- prList = `${commentBody}\n- ${prUrl}`;
86- break;
87- }
88- }
183+ # Check if current PR is already in the list
184+ if echo "$pr_list" | grep -q "$pr_url"; then
185+ echo "PR already exists in comment, using existing list"
186+ echo "pr_list=$existing_body" >> "$GITHUB_OUTPUT"
187+ else
188+ # Add new PR to the list
189+ new_pr_line="- [${{ inputs.pr_title }}](${{ inputs.pr_url }}) by @${{ inputs.pr_author }}"
190+ if [[ "${{ inputs.pr_merged }}" == "true" ]]; then
191+ new_pr_line="$new_pr_line ✅"
192+ fi
193+
194+ updated_list=$(echo -e "$pr_list\n$new_pr_line" | sort -u)
195+ new_body="## PRs linked to this issue\n\n$updated_list"
196+ echo "pr_list=$new_body" >> "$GITHUB_OUTPUT"
197+ fi
198+ else
199+ # Create new PR list
200+ new_pr_line="- [${{ inputs.pr_title }}](${{ inputs.pr_url }}) by @${{ inputs.pr_author }}"
201+ if [[ "${{ inputs.pr_merged }}" == "true" ]]; then
202+ new_pr_line="$new_pr_line ✅"
203+ fi
89204
90- core.setOutput('pr_list', prList);
91- core.setOutput('existing_comment_id', existingCommentId);
92- console.log(`pr_list: [${prList}]`);
93- console.log(`existing_comment_id: [${existingCommentId}]`);
205+ new_body="## PRs linked to this issue\n\n$new_pr_line"
206+ echo "pr_list=$new_body" >> "$GITHUB_OUTPUT"
207+ fi
94208
95- - name : Update or create comment
209+ - name : Create new comment
96210 if : steps.check_comment.outputs.existing_comment_id == ''
97211 uses : peter-evans/create-or-update-comment@v4
98212 with :
99- issue-number : ${{ steps.extract_issue_number .outputs.issue_number }}
213+ issue-number : ${{ steps.determine_issue .outputs.final_issue_number }}
100214 body : ${{ steps.check_comment.outputs.pr_list }}
101215
102216 - name : Update existing comment
103217 if : steps.check_comment.outputs.existing_comment_id != ''
104- uses : actions/github-script@v7
218+ uses : peter-evans/create-or-update-comment@v4
105219 with :
106- script : |
107- const prs = ${{ toJSON(steps.check_comment.outputs.pr_list) }}.split('\n');
108- await github.rest.issues.updateComment({
109- owner: '${{ github.repository_owner }}',
110- repo: '${{ github.event.repository.name }}',
111- comment_id: ${{ steps.check_comment.outputs.existing_comment_id }},
112- body: prs.join('\n')
113- });
220+ comment-id : ${{ steps.check_comment.outputs.existing_comment_id }}
221+ body : ${{ steps.check_comment.outputs.pr_list }}
0 commit comments