1- name : Auto Label PR from Linked Issue
1+ name : Sync Issue Metadata to PR
22
33on :
44 pull_request_target :
@@ -8,186 +8,123 @@ permissions:
88 pull-requests : write
99 issues : write
1010 contents : read
11+ repository-projects : write
1112
1213jobs :
13- label -pr :
14+ sync -pr-metadata :
1415 runs-on : ubuntu-latest
15-
16+
1617 steps :
17- - name : Checkout code
18+ - name : Checkout
1819 uses : actions/checkout@v4
1920
20- - name : Extract Issue Numbers from PR Body
21+ - name : Extract linked issue(s) from PR
2122 id : extract-issues
2223 uses : actions/github-script@v7
2324 with :
2425 github-token : ${{ secrets.GITHUB_TOKEN }}
25- result-encoding : string
2626 script : |
27- let prNumber, prBody, prTitle;
28-
29- // Check if triggered by issue event
30- if (context.eventName === 'issues') {
31- const issueNumber = context.payload.issue.number;
32- console.log(`Issue #${issueNumber} labels were updated`);
33-
34- const { data: pullRequests } = await github.rest.pulls.list({
35- owner: context.repo.owner,
36- repo: context.repo.repo,
37- state: 'open'
38- });
39-
40- const linkedPRs = [];
41- for (const pr of pullRequests) {
42- const prText = `${pr.title} ${pr.body || ''}`;
43- const patterns = [
44- new RegExp(`(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#${issueNumber}\\b`, 'i'),
45- new RegExp(`#${issueNumber}\\b`)
46- ];
47- if (patterns.some(p => p.test(prText))) {
48- linkedPRs.push(pr.number);
49- }
50- }
51-
52- if (linkedPRs.length === 0) {
53- console.log('No linked PRs found for this issue');
54- return JSON.stringify({ prs: [], issue: issueNumber });
27+ const prNumber = context.payload.pull_request.number;
28+ const prTitle = context.payload.pull_request.title || '';
29+ const prBody = context.payload.pull_request.body || '';
30+
31+ // Regex patterns for issue references
32+ const patterns = [
33+ /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi,
34+ /#(\d+)/g
35+ ];
36+
37+ const issueNumbers = new Set();
38+ const text = prTitle + ' ' + prBody;
39+
40+ for (const pattern of patterns) {
41+ for (const match of text.matchAll(pattern)) {
42+ issueNumbers.add(match[1]);
5543 }
56-
57- console.log(`Found linked PRs: ${linkedPRs.join(', ')}`);
58- return JSON.stringify({ prs: linkedPRs, issue: issueNumber });
59- } else {
60- prBody = context.payload.pull_request.body || '';
61- prTitle = context.payload.pull_request.title || '';
62-
63- const patterns = [
64- /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi,
65- /#(\d+)/g
66- ];
67-
68- const issueNumbers = new Set();
69- const textToSearch = prBody + ' ' + prTitle;
70-
71- patterns.forEach(pattern => {
72- const matches = [...textToSearch.matchAll(pattern)];
73- matches.forEach(match => issueNumbers.add(match[1]));
74- });
75-
76- const issues = Array.from(issueNumbers);
77- console.log('Found linked issues:', issues);
78-
79- return JSON.stringify({
80- prs: [context.payload.pull_request.number],
81- issues: issues
82- });
8344 }
8445
85- - name : Get Labels from Linked Issues
86- id : get-labels
46+ return JSON.stringify({ issues: Array.from(issueNumbers), pr: prNumber });
47+
48+ - name : Sync Issue Metadata to PR
8749 uses : actions/github-script@v7
8850 with :
8951 github-token : ${{ secrets.GITHUB_TOKEN }}
90- result-encoding : string
9152 script : |
92- const extractData = JSON.parse('${{ steps.extract-issues.outputs.result }}');
93-
94- let issueNumbers = [];
95- let prsToUpdate = [];
96-
97- if (extractData.issue) {
98- issueNumbers = [extractData.issue];
99- prsToUpdate = extractData.prs || [];
100- } else {
101- issueNumbers = extractData.issues || [];
102- prsToUpdate = extractData.prs || [];
103- }
104-
105- if (!issueNumbers || issueNumbers.length === 0) {
106- console.log('No linked issues found');
107- return JSON.stringify({ labels: [], prs: prsToUpdate });
53+ const data = JSON.parse('${{ steps.extract-issues.outputs.result }}');
54+ const prNumber = data.pr;
55+ const issueNumbers = data.issues || [];
56+
57+ if (issueNumbers.length === 0) {
58+ console.log("No linked issues found");
59+ return;
10860 }
109-
110- const allLabels = new Set();
111-
61+
11262 for (const issueNumber of issueNumbers) {
11363 try {
114- const issue = await github.rest.issues.get({
64+ // Fetch issue
65+ const { data: issue } = await github.rest.issues.get({
11566 owner: context.repo.owner,
11667 repo: context.repo.repo,
11768 issue_number: parseInt(issueNumber)
11869 });
119-
120- console.log(`Issue #${issueNumber} labels:`, issue.data.labels.map(l => l.name));
121- issue.data.labels.forEach(label => allLabels.add(label.name));
122- } catch (error) {
123- console.log(`Could not fetch issue #${issueNumber}:`, error.message);
124- }
125- }
126-
127- const labels = Array.from(allLabels);
128- console.log('All labels to apply:', labels);
129-
130- return JSON.stringify({ labels: labels, prs: prsToUpdate });
13170
132- - name : Apply Labels to PR
133- uses : actions/github-script@v7
134- with :
135- github-token : ${{ secrets.GITHUB_TOKEN }}
136- script : |
137- const data = JSON.parse('${{ steps.get-labels.outputs.result }}');
138- const labels = data.labels || [];
139- const prsToUpdate = data.prs || [];
140-
141- if (!labels || labels.length === 0) {
142- console.log('No labels to apply');
143- return;
144- }
145-
146- if (!prsToUpdate || prsToUpdate.length === 0) {
147- console.log('No PRs to update');
148- return;
149- }
150-
151- for (const prNumber of prsToUpdate) {
152- try {
71+ console.log(`Syncing metadata from Issue #${issueNumber} to PR #${prNumber}`);
72+
73+ // --- Sync Labels ---
74+ const issueLabels = issue.labels.map(l => l.name);
15375 const { data: pr } = await github.rest.pulls.get({
15476 owner: context.repo.owner,
15577 repo: context.repo.repo,
15678 pull_number: prNumber
15779 });
158-
159- const currentLabels = pr.labels.map(l => l.name);
160- if (currentLabels.length > 0) {
161- for (const label of currentLabels) {
162- try {
163- await github.rest.issues.removeLabel({
164- owner: context.repo.owner,
165- repo: context.repo.repo,
166- issue_number: prNumber,
167- name: label
168- });
169- } catch (e) {
170- console.log(`Could not remove label ${label}: ${e.message}`);
171- }
172- }
173- }
174-
80+ const currentPRLabels = pr.labels.map(l => l.name);
81+ const combinedLabels = Array.from(new Set([...currentPRLabels, ...issueLabels]));
82+
17583 await github.rest.issues.addLabels({
17684 owner: context.repo.owner,
17785 repo: context.repo.repo,
17886 issue_number: prNumber,
179- labels: labels
87+ labels: combinedLabels
18088 });
181-
182- console.log(`✅ Successfully applied ${labels.length} labels to PR #${prNumber}`);
183-
89+ console.log(`Labels applied: ${combinedLabels.join(', ')}`);
90+
91+ // --- Sync Milestone ---
92+ if (issue.milestone) {
93+ await github.rest.issues.update({
94+ owner: context.repo.owner,
95+ repo: context.repo.repo,
96+ issue_number: prNumber,
97+ milestone: issue.milestone.number
98+ });
99+ console.log(`Milestone synced: ${issue.milestone.title}`);
100+ }
101+
102+ // --- Sync Projects (GitHub Projects v2) ---
103+ if(issue.project_cards_url) {
104+ // Fetch project cards of issue
105+ const cardsResponse = await github.rest.projects.listCards({
106+ column_id: issue.project_cards_url.split('/').pop() // last part is column_id
107+ }).catch(()=>({data:[]}));
108+
109+ for(const card of cardsResponse.data || []) {
110+ await github.rest.projects.createCard({
111+ column_id: card.column_id,
112+ content_id: prNumber,
113+ content_type: 'PullRequest'
114+ });
115+ console.log(`Added PR #${prNumber} to project card in column ${card.column_id}`);
116+ }
117+ }
118+
119+ // --- Optionally: Add a comment on PR ---
184120 await github.rest.issues.createComment({
185121 owner: context.repo.owner,
186122 repo: context.repo.repo,
187123 issue_number: prNumber,
188- body: `🏷️ Labels automatically synced from linked issue(s): ${labels.map(l => `\`${l}\``) .join(', ')}`
124+ body: `✅ Synchronized metadata from Issue #${issueNumber}:\nLabels: ${issueLabels .join(', ')}\nMilestone: ${issue.milestone ? issue.milestone.title : 'None' }`
189125 });
126+
190127 } catch (error) {
191- console.error(`Error updating PR #${prNumber}:`, error.message);
128+ console.error(`Error syncing issue #${issueNumber} to PR #${prNumber}:`, error.message);
192129 }
193130 }
0 commit comments