11name : Auto Label PR from Linked Issue
22
33on :
4- pull_request_target :
4+ pull_request_target :
55 types : [opened, edited, synchronize, reopened]
66
77permissions :
@@ -12,30 +12,81 @@ permissions:
1212jobs :
1313 label-pr :
1414 runs-on : ubuntu-latest
15-
15+
1616 steps :
17+ - name : Checkout code
18+ uses : actions/checkout@v4
19+
1720 - name : Extract Issue Numbers from PR Body
1821 id : extract-issues
1922 uses : actions/github-script@v7
2023 with :
2124 github-token : ${{ secrets.GITHUB_TOKEN }}
2225 result-encoding : string
2326 script : |
24- const prBody = context.payload.pull_request.body || '';
25- const prTitle = context.payload.pull_request.title || '';
26- const patterns = [
27- /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi,
28- /#(\d+)/g
29- ];
30- const issueNumbers = new Set();
31- const textToSearch = prBody + ' ' + prTitle;
32- patterns.forEach(pattern => {
33- const matches = [...textToSearch.matchAll(pattern)];
34- matches.forEach(match => {
35- if (/^\d+$/.test(match[1])) issueNumbers.add(match[1]);
27+ let prNumber, prBody, prTitle;
28+
29+ // Check if triggered by issue event
30+ if (context.eventName === 'issues') {
31+ // Find all open PRs that link to this issue
32+ const issueNumber = context.payload.issue.number;
33+ console.log(`Issue #${issueNumber} labels were updated`);
34+
35+ // Search for PRs that mention this issue
36+ const { data: pullRequests } = await github.rest.pulls.list({
37+ owner: context.repo.owner,
38+ repo: context.repo.repo,
39+ state: 'open'
40+ });
41+
42+ const linkedPRs = [];
43+ for (const pr of pullRequests) {
44+ const prText = `${pr.title} ${pr.body || ''}`;
45+ const patterns = [
46+ new RegExp(`(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#${issueNumber}\\b`, 'i'),
47+ new RegExp(`#${issueNumber}\\b`)
48+ ];
49+
50+ if (patterns.some(p => p.test(prText))) {
51+ linkedPRs.push(pr.number);
52+ }
53+ }
54+
55+ if (linkedPRs.length === 0) {
56+ console.log('No linked PRs found for this issue');
57+ return JSON.stringify({ prs: [], issue: issueNumber });
58+ }
59+
60+ console.log(`Found linked PRs: ${linkedPRs.join(', ')}`);
61+ return JSON.stringify({ prs: linkedPRs, issue: issueNumber });
62+ } else {
63+ // Triggered by PR event - original logic
64+ prBody = context.payload.pull_request.body || '';
65+ prTitle = context.payload.pull_request.title || '';
66+
67+ const patterns = [
68+ /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi,
69+ /#(\d+)/g
70+ ];
71+
72+ const issueNumbers = new Set();
73+ const textToSearch = prBody + ' ' + prTitle;
74+
75+ patterns.forEach(pattern => {
76+ const matches = [...textToSearch.matchAll(pattern)];
77+ matches.forEach(match => {
78+ issueNumbers.add(match[1]);
79+ });
80+ });
81+
82+ const issues = Array.from(issueNumbers);
83+ console.log('Found linked issues:', issues);
84+
85+ return JSON.stringify({
86+ prs: [context.payload.pull_request.number],
87+ issues: issues
3688 });
37- });
38- return JSON.stringify([...issueNumbers]);
89+ }
3990
4091 - name : Get Labels from Linked Issues
4192 id : get-labels
@@ -44,43 +95,124 @@ jobs:
4495 github-token : ${{ secrets.GITHUB_TOKEN }}
4596 result-encoding : string
4697 script : |
47- const issueNumbers = JSON.parse('${{ steps.extract-issues.outputs.result }}');
98+ const extractData = JSON.parse('${{ steps.extract-issues.outputs.result }}');
99+
100+ // Labels to exclude from being applied to PRs
48101 const excludedLabels = ['recode', 'hacktoberfest-accepted'];
102+
103+ let issueNumbers = [];
104+ let prsToUpdate = [];
105+
106+ // Handle both PR and issue events
107+ if (extractData.issue) {
108+ // Issue event - update all linked PRs
109+ issueNumbers = [extractData.issue];
110+ prsToUpdate = extractData.prs || [];
111+ } else {
112+ // PR event - update the current PR
113+ issueNumbers = extractData.issues || [];
114+ prsToUpdate = extractData.prs || [];
115+ }
116+
117+ if (!issueNumbers || issueNumbers.length === 0) {
118+ console.log('No linked issues found');
119+ return JSON.stringify({ labels: [], prs: prsToUpdate });
120+ }
121+
49122 const allLabels = new Set();
50-
123+
51124 for (const issueNumber of issueNumbers) {
52125 try {
53126 const issue = await github.rest.issues.get({
54127 owner: context.repo.owner,
55128 repo: context.repo.repo,
56129 issue_number: parseInt(issueNumber)
57130 });
58- for (const label of issue.data.labels) {
59- if (!excludedLabels.includes(label.name.toLowerCase()))
131+
132+ console.log(`Issue #${issueNumber} labels:`, issue.data.labels.map(l => l.name));
133+
134+ issue.data.labels.forEach(label => {
135+ // Only add label if it's not in the excluded list
136+ if (!excludedLabels.includes(label.name.toLowerCase())) {
60137 allLabels.add(label.name);
61- }
138+ } else {
139+ console.log(`Excluding label: ${label.name}`);
140+ }
141+ });
62142 } catch (error) {
63143 console.log(`Could not fetch issue #${issueNumber}:`, error.message);
64144 }
65145 }
66-
67- return JSON.stringify([...allLabels]);
146+
147+ const labels = Array.from(allLabels);
148+ console.log('All labels to apply:', labels);
149+
150+ return JSON.stringify({ labels: labels, prs: prsToUpdate });
68151
69152 - name : Apply Labels to PR
70153 uses : actions/github-script@v7
71154 with :
72155 github-token : ${{ secrets.GITHUB_TOKEN }}
73156 script : |
74- const labels = JSON.parse('${{ steps.get-labels.outputs.result }}')
75- .filter(l => typeof l === 'string' && l.trim() !== '');
76- if (labels.length === 0) {
157+ const data = JSON.parse('${{ steps.get-labels.outputs.result }}');
158+ const labels = data.labels || [];
159+ const prsToUpdate = data.prs || [];
160+
161+ if (!labels || labels.length === 0) {
77162 console.log('No labels to apply');
78163 return;
79164 }
80- await github.rest.issues.addLabels({
81- owner: context.repo.owner,
82- repo: context.repo.repo,
83- issue_number: context.payload.pull_request.number,
84- labels
85- });
86- console.log(`Applied labels: ${labels.join(', ')}`);
165+
166+ if (!prsToUpdate || prsToUpdate.length === 0) {
167+ console.log('No PRs to update');
168+ return;
169+ }
170+
171+ // Update each PR
172+ for (const prNumber of prsToUpdate) {
173+ try {
174+ // First, get current PR labels
175+ const { data: pr } = await github.rest.pulls.get({
176+ owner: context.repo.owner,
177+ repo: context.repo.repo,
178+ pull_number: prNumber
179+ });
180+
181+ // Remove all existing labels first (to handle removed issue labels)
182+ const currentLabels = pr.labels.map(l => l.name);
183+ if (currentLabels.length > 0) {
184+ for (const label of currentLabels) {
185+ try {
186+ await github.rest.issues.removeLabel({
187+ owner: context.repo.owner,
188+ repo: context.repo.repo,
189+ issue_number: prNumber,
190+ name: label
191+ });
192+ } catch (e) {
193+ console.log(`Could not remove label ${label}: ${e.message}`);
194+ }
195+ }
196+ }
197+
198+ // Apply new labels
199+ await github.rest.issues.addLabels({
200+ owner: context.repo.owner,
201+ repo: context.repo.repo,
202+ issue_number: prNumber,
203+ labels: labels
204+ });
205+
206+ console.log(`Successfully applied ${labels.length} labels to PR #${prNumber}`);
207+
208+ // Add a comment to the PR
209+ await github.rest.issues.createComment({
210+ owner: context.repo.owner,
211+ repo: context.repo.repo,
212+ issue_number: prNumber,
213+ body: `🏷️ Labels automatically synced from linked issue(s): ${labels.map(l => `\`${l}\``).join(', ')}`
214+ });
215+ } catch (error) {
216+ console.error(`Error updating PR #${prNumber}:`, error.message);
217+ }
218+ }
0 commit comments