Skip to content

Commit 9be20a4

Browse files
committed
Add enhanced issue linking workflow from PR dotCMS#32704
- Improved branch name pattern detection - Added manual issue linking recovery options - Better error messages with actionable guidance - Priority order: Development section > PR body > branch name
1 parent 5024b14 commit 9be20a4

File tree

2 files changed

+171
-59
lines changed

2 files changed

+171
-59
lines changed
Lines changed: 166 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,221 @@
1-
name: Link Issue to PR
1+
name: Link issue to PR
22

33
on:
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

3335
jobs:
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 }}

.github/workflows/issue_open-pr.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ jobs:
1111
with:
1212
pr_branch: ${{ github.head_ref }}
1313
pr_url: ${{ github.event.pull_request.html_url }}
14+
pr_title: ${{ github.event.pull_request.title }}
15+
pr_body: ${{ github.event.pull_request.body }}
16+
pr_author: ${{ github.event.pull_request.user.login }}
17+
pr_merged: ${{ github.event.pull_request.merged }}
1418
secrets:
15-
CI_MACHINE_TOKEN: ${{ secrets.CI_MACHINE_TOKEN }}
19+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)