1+ name : Mirror Issue Labels to PR
2+
3+ on :
4+ pull_request :
5+ types : [opened, reopened, ready_for_review]
6+
7+ permissions : {}
8+
9+ concurrency :
10+ group : ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11+ cancel-in-progress : true
12+
13+ jobs :
14+ mirror :
15+ name : Mirror linked issue labels to PR
16+ if : >-
17+ contains(github.event.pull_request.labels.*.name, 'coding-agent') ||
18+ github.event.pull_request.user.login == 'Copilot' ||
19+ github.actor == 'copilot-swe-agent[bot]'
20+ runs-on : ubuntu-latest
21+ permissions :
22+ pull-requests : write
23+ issues : read
24+ steps :
25+ - name : Copy labels from linked issue(s)
26+ uses : actions/github-script@v7
27+ with :
28+ script : |
29+ const body = context.payload.pull_request.body || '';
30+
31+ // Strip HTML comments and fenced code blocks before matching, so
32+ // example "Closes #N" lines in template guidance don't trigger.
33+ const stripped = body
34+ .replace(/<!--[\s\S]*?-->/g, '')
35+ .replace(/```[\s\S]*?```/g, '');
36+
37+ // Match "Closes #N", "Fixes #N", "Resolves #N" (case-insensitive,
38+ // with optional ing/d/s suffixes). Only same-repo references are mirrored.
39+ const pattern = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/gi;
40+ const issueNumbers = new Set();
41+ let m;
42+ while ((m = pattern.exec(stripped)) !== null) {
43+ issueNumbers.add(parseInt(m[1], 10));
44+ }
45+
46+ if (issueNumbers.size === 0) {
47+ core.info('No "Closes #N" references found in PR body; nothing to mirror.');
48+ return;
49+ }
50+
51+ const prNumber = context.payload.pull_request.number;
52+ const existingLabels = new Set(
53+ (context.payload.pull_request.labels || []).map(l => l.name)
54+ );
55+
56+ const toAdd = new Set();
57+ for (const num of issueNumbers) {
58+ try {
59+ const { data: issue } = await github.rest.issues.get({
60+ owner: context.repo.owner,
61+ repo: context.repo.repo,
62+ issue_number: num,
63+ });
64+ for (const label of issue.labels || []) {
65+ const name = typeof label === 'string' ? label : label.name;
66+ if (!name) continue;
67+ if (existingLabels.has(name)) continue;
68+ toAdd.add(name);
69+ }
70+ } catch (err) {
71+ core.warning(`Failed to fetch issue #${num}: ${err.message}`);
72+ }
73+ }
74+
75+ if (toAdd.size === 0) {
76+ core.info('Linked issue(s) had no new labels to mirror.');
77+ return;
78+ }
79+
80+ const labels = Array.from(toAdd);
81+ await github.rest.issues.addLabels({
82+ owner: context.repo.owner,
83+ repo: context.repo.repo,
84+ issue_number: prNumber,
85+ labels,
86+ });
87+ core.info(`Mirrored ${labels.length} label(s) to PR: ${labels.join(', ')}`);
0 commit comments