1+ name : Auto Label PR from Linked Issue
2+
3+ on :
4+ pull_request_target :
5+ types : [opened, edited, synchronize, reopened]
6+
7+ permissions :
8+ pull-requests : write
9+ issues : write
10+ contents : read
11+
12+ jobs :
13+ label-pr :
14+ runs-on : ubuntu-latest
15+
16+ steps :
17+ - name : Checkout code
18+ uses : actions/checkout@v4
19+
20+ - name : Extract Issue Numbers from PR Body
21+ id : extract-issues
22+ uses : actions/github-script@v7
23+ with :
24+ github-token : ${{ secrets.GITHUB_TOKEN }}
25+ result-encoding : string
26+ script : |
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
88+ });
89+ }
90+
91+ - name : Get Labels from Linked Issues
92+ id : get-labels
93+ uses : actions/github-script@v7
94+ with :
95+ github-token : ${{ secrets.GITHUB_TOKEN }}
96+ result-encoding : string
97+ script : |
98+ const extractData = JSON.parse('${{ steps.extract-issues.outputs.result }}');
99+
100+ // Labels to exclude from being applied to PRs
101+ 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+
122+ const allLabels = new Set();
123+
124+ for (const issueNumber of issueNumbers) {
125+ try {
126+ const issue = await github.rest.issues.get({
127+ owner: context.repo.owner,
128+ repo: context.repo.repo,
129+ issue_number: parseInt(issueNumber)
130+ });
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())) {
137+ allLabels.add(label.name);
138+ } else {
139+ console.log(`Excluding label: ${label.name}`);
140+ }
141+ });
142+ } catch (error) {
143+ console.log(`Could not fetch issue #${issueNumber}:`, error.message);
144+ }
145+ }
146+
147+ const labels = Array.from(allLabels);
148+ console.log('All labels to apply:', labels);
149+
150+ return JSON.stringify({ labels: labels, prs: prsToUpdate });
151+
152+ - name : Apply Labels to PR
153+ uses : actions/github-script@v7
154+ with :
155+ github-token : ${{ secrets.GITHUB_TOKEN }}
156+ script : |
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) {
162+ console.log('No labels to apply');
163+ return;
164+ }
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