1- name : Compute Linked Issue Labels
1+ name : Sync Linked Issue Labels
22
33on :
44 pull_request :
5- types : [opened, edited, reopened, synchronize, ready_for_review]
6- workflow_dispatch :
7- inputs :
8- pr_number :
9- description : " PR number to sync labels for"
10- required : true
11- type : number
12- dry-run-enabled :
13- description : " Dry run (log only, do not apply labels)"
14- required : false
15- type : boolean
16- default : true
5+ types : [opened, edited, reopened, synchronize]
176
187permissions :
19- actions : write
20- pull-requests : read
8+ pull-requests : write
219 issues : read
2210 contents : read
2311
2412jobs :
25- compute-labels :
13+ sync :
2614 runs-on : ubuntu-latest
27- outputs :
28- pr_number : ${{ steps.compute.outputs.pr_number }}
29- dry_run : ${{ steps.compute.outputs.dry_run }}
30- is_fork_pr : ${{ steps.compute.outputs.is_fork_pr }}
31- has_labels : ${{ steps.compute.outputs.has_labels }}
32- is_bot : ${{ steps.compute.outputs.is_bot }}
3315 steps :
34- - name : Harden the runner
16+ - name : Harden Runner
3517 uses : step-security/harden-runner@v2
3618 with :
3719 egress-policy : audit
3820
39- - name : Compute linked issue labels
40- id : compute
41- env :
42- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
43- PR_NUMBER : ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
44- REQUESTED_DRY_RUN : ${{ github.event_name == 'workflow_dispatch' && format('{0}', github.event.inputs['dry-run-enabled']) || 'false' }}
45- IS_FORK_PR : ' false'
21+ - name : Compute and Apply Labels
4622 uses : actions/github-script@v7
4723 with :
48- result-encoding : json
24+ github-token : ${{ secrets.GITHUB_TOKEN }}
4925 script : |
50- const prNumber = Number(process.env.PR_NUMBER);
26+ const prNumber = context.payload.pull_request.number;
27+
5128 const { data: prData } = await github.rest.pulls.get({
52- owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber
29+ owner: context.repo.owner,
30+ repo: context.repo.repo,
31+ pull_number: prNumber
5332 });
5433
55- const prAuthor = (prData.user && prData.user.login) || "";
56- const isBot = /\[bot\]$/i.test(prAuthor) || /dependabot/i.test(prAuthor);
57- core.setOutput('is_bot', String(isBot));
58-
59- if (isBot) {
60- core.setOutput('has_labels', 'false');
34+ const prAuthor = prData.user.login;
35+ if (/\[bot\]$/i.test(prAuthor) || /dependabot/i.test(prAuthor)) {
36+ console.log("Skipping bot PR.");
6137 return;
6238 }
6339
6440 const regex = /(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)[:\s]+\s*#(\d+)\b/gi;
65- const numbers = new Set();
66- let m;
67- while ((m = regex.exec(prData.body || "")) !== null) {
68- numbers.add(Number(m[1]));
41+ const issueNumbers = new Set();
42+ let match;
43+ while ((match = regex.exec(prData.body || "")) !== null) {
44+ issueNumbers.add(Number(match[1]));
45+ }
46+
47+ if (issueNumbers.size === 0) {
48+ console.log("No linked issues found.");
49+ return;
6950 }
7051
71- const linkedIssues = Array.from(numbers);
72- const allLabels = [];
73- for (const num of linkedIssues) {
52+ const labelsToAdd = new Set();
53+ for (const num of issueNumbers) {
7454 try {
75- const { data } = await github.rest.issues.get({
76- owner: context.repo.owner, repo: context.repo.repo, issue_number: num
55+ const { data: issue } = await github.rest.issues.get({
56+ owner: context.repo.owner,
57+ repo: context.repo.repo,
58+ issue_number: num
7759 });
78- if (!data.pull_request) {
79- const names = (data.labels || []).map(l => typeof l === 'string' ? l : l.name);
80- allLabels.push(...names );
60+
61+ if (!issue.pull_request) {
62+ issue.labels.forEach(l => labelsToAdd.add(typeof l === 'string' ? l : l.name) );
8163 }
82- } catch (e) { console.log(`Error fetching #${num}`); }
64+ } catch (e) {
65+ console.log(`Could not fetch labels for issue #${num}`);
66+ }
8367 }
8468
85- const deduped = Array.from(new Set(allLabels));
86- core.setOutput('has_labels', deduped.length > 0 ? 'true' : 'false');
87- core.setOutput('labels', JSON.stringify(deduped));
88- core.setOutput('pr_number', String(prNumber));
89- core.setOutput('dry_run', process.env.REQUESTED_DRY_RUN);
90- core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR));
91- core.setOutput('source_event', context.eventName);
92-
93- - name : Write labels artifact
94- if : steps.compute.outputs.has_labels == 'true'
95- run : |
96- # The Add workflow specifically looks for "artifacts/labels.json"
97- # We construct exactly what its 'Read labels payload' step expects
98- echo '${{ steps.compute.outputs.labels }}' > labels_only.json
99- echo "{" > labels.json
100- echo " \"pr_number\": \"${{ steps.compute.outputs.pr_number }}\"," >> labels.json
101- echo " \"labels\": $(cat labels_only.json)," >> labels.json
102- echo " \"is_fork_pr\": ${{ steps.compute.outputs.is_fork_pr }}," >> labels.json
103- echo " \"dry_run\": \"${{ steps.compute.outputs.dry_run }}\"," >> labels.json
104- echo " \"source_event\": \"workflow_dispatch\"" >> labels.json
105- echo "}" >> labels.json
106-
107- - name : Upload
108- if : steps.compute.outputs.has_labels == 'true'
109- uses : actions/upload-artifact@v4
110- with :
111- name : pr-labels-${{ steps.compute.outputs.pr_number }}
112- path : labels.json
113- retention-days : 1
114-
115- dispatch-add :
116- needs : compute-labels
117- if : |
118- needs.compute-labels.outputs.is_bot != 'true' &&
119- needs.compute-labels.outputs.has_labels == 'true'
120- runs-on : ubuntu-latest
121- steps :
122- - name : Trigger add workflow
123- uses : actions/github-script@v7
124- with :
125- github-token : ${{ secrets.GH_ACCESS_TOKEN }}
126- script : |
127- const targetRef = context.payload.pull_request
128- ? context.payload.pull_request.base.ref
129- : 'main';
130-
131- console.log(`Dispatching to branch: ${targetRef}`);
132-
133- await github.rest.actions.createWorkflowDispatch({
134- owner: context.repo.owner,
135- repo: context.repo.repo,
136- workflow_id: 'sync-issue-labels-add.yml',
137- ref: targetRef,
138- inputs: {
139- upstream_run_id: "${{ github.run_id }}",
140- pr_number: "${{ needs.compute-labels.outputs.pr_number }}",
141- dry_run: "${{ needs.compute-labels.outputs.dry_run }}",
142- is_fork_pr: "${{ needs.compute-labels.outputs.is_fork_pr }}"
143- }
144- });
69+ if (labelsToAdd.size > 0) {
70+ const labelList = Array.from(labelsToAdd);
71+ console.log(`Adding labels: ${labelList.join(', ')}`);
72+
73+ await github.rest.issues.addLabels({
74+ owner: context.repo.owner,
75+ repo: context.repo.repo,
76+ issue_number: prNumber,
77+ labels: labelList
78+ });
79+ } else {
80+ console.log("No labels discovered from linked issues.");
81+ }
0 commit comments