Skip to content

Commit b84d2dc

Browse files
EMaherCopilot
andcommitted
feat: label issues close:fixed when a merged PR closes them
Add the "Label Fixed Issues on Merge" workflow. On pull_request_target closed, when the PR was merged, it resolves the issues closed via linking keywords (Closes/Fixes/Resolves) using GraphQL closingIssuesReferences and applies the existing close:fixed label to each, keeping the close reason visible in issue triage. Uses pull_request_target so fork PRs still get issues:write; no PR head code is checked out or executed. Skips issues that already carry the label and ensures the label exists with its canonical color/description. Also documents the auto-labeling in CONTRIBUTING.md. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent e9d79f1 commit b84d2dc

2 files changed

Lines changed: 125 additions & 0 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
name: Label Fixed Issues on Merge
2+
3+
# Trigger: a pull request is closed.
4+
# When the PR was actually MERGED, every issue it closed via a linking keyword
5+
# (Closes #N / Fixes #N / Resolves #N) is labeled `close:fixed` so the close
6+
# reason is visible at a glance and consistent with the manual triage taxonomy.
7+
#
8+
# ─── WHY pull_request_target ────────────────────────────────────────────────
9+
# `pull_request` grants a read-only GITHUB_TOKEN to PRs opened from forks, which
10+
# would make `issues: write` unavailable and silently skip fork contributions.
11+
# `pull_request_target` runs with the base repository's trusted workflow file and
12+
# a read/write token. This workflow NEVER checks out or executes PR head code —
13+
# it only calls the GitHub API via github-script — so there is no untrusted-code
14+
# execution risk from using pull_request_target.
15+
# ────────────────────────────────────────────────────────────────────────────
16+
17+
on:
18+
pull_request_target:
19+
types: [closed]
20+
21+
permissions:
22+
issues: write
23+
contents: read
24+
25+
# Canonical definition of the target label, kept in sync with
26+
# .github/workflows/issue-labels-sync.yml (CLOSE_LABELS → close:fixed).
27+
env:
28+
FIXED_LABEL: 'close:fixed'
29+
FIXED_LABEL_COLOR: '0E8A16'
30+
FIXED_LABEL_DESCRIPTION: 'Fixed by a previous PR or release'
31+
32+
jobs:
33+
label-fixed:
34+
name: Label issues closed by merge
35+
runs-on: ubuntu-latest
36+
# Only act on merged PRs — a closed-but-unmerged PR fixes nothing.
37+
if: github.event.pull_request.merged == true
38+
39+
steps:
40+
- name: Apply close:fixed to auto-closed issues
41+
uses: actions/github-script@v8
42+
with:
43+
script: |
44+
const FIXED_LABEL = process.env.FIXED_LABEL;
45+
const { owner, repo } = context.repo;
46+
const prNumber = context.payload.pull_request.number;
47+
48+
// Resolve the issues this PR closes via linking keywords. GraphQL's
49+
// closingIssuesReferences is the authoritative source — it mirrors
50+
// exactly what GitHub itself closed on merge, so it stays correct
51+
// even if the PR body wording is unusual.
52+
const query = `
53+
query($owner: String!, $repo: String!, $number: Int!) {
54+
repository(owner: $owner, name: $repo) {
55+
pullRequest(number: $number) {
56+
closingIssuesReferences(first: 50) {
57+
nodes { number }
58+
}
59+
}
60+
}
61+
}`;
62+
63+
const result = await github.graphql(query, {
64+
owner,
65+
repo,
66+
number: prNumber
67+
});
68+
69+
const issues = result.repository.pullRequest.closingIssuesReferences.nodes || [];
70+
if (issues.length === 0) {
71+
core.info(`PR #${prNumber} closed no linked issues — nothing to label`);
72+
return;
73+
}
74+
75+
core.info(`PR #${prNumber} closed issue(s): ${issues.map(i => '#' + i.number).join(', ')}`);
76+
77+
// Ensure the label exists with its canonical color/description so a
78+
// fresh repo doesn't get an auto-created default-colored variant.
79+
try {
80+
await github.rest.issues.getLabel({ owner, repo, name: FIXED_LABEL });
81+
} catch (err) {
82+
if (err.status === 404) {
83+
await github.rest.issues.createLabel({
84+
owner,
85+
repo,
86+
name: FIXED_LABEL,
87+
color: process.env.FIXED_LABEL_COLOR,
88+
description: process.env.FIXED_LABEL_DESCRIPTION
89+
});
90+
core.info(`Created missing label: ${FIXED_LABEL}`);
91+
} else {
92+
throw err;
93+
}
94+
}
95+
96+
let labeled = 0;
97+
for (const { number } of issues) {
98+
// Read live labels so we don't post a redundant label event
99+
// (which would needlessly re-trigger the enforce-unique workflow).
100+
const fresh = await github.rest.issues.get({
101+
owner,
102+
repo,
103+
issue_number: number
104+
});
105+
106+
const labels = fresh.data.labels.map(l => (typeof l === 'string' ? l : l.name));
107+
if (labels.includes(FIXED_LABEL)) {
108+
core.info(`#${number} already has ${FIXED_LABEL} — skipping`);
109+
continue;
110+
}
111+
112+
await github.rest.issues.addLabels({
113+
owner,
114+
repo,
115+
issue_number: number,
116+
labels: [FIXED_LABEL]
117+
});
118+
119+
core.info(`Labeled #${number} with ${FIXED_LABEL}`);
120+
labeled++;
121+
}
122+
123+
core.info(`Done — applied ${FIXED_LABEL} to ${labeled} issue(s)`);

‎CONTRIBUTING.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ export class ExtractCommand {
155155
Closes #42
156156
```
157157

158+
When the PR is merged, any issue it auto-closes this way is automatically labeled `close:fixed` by the [Label Fixed Issues on Merge](.github/workflows/issue-label-fixed-on-merge.yml) workflow, so the close reason stays visible in issue triage.
159+
158160
4. **Open a pull request** against `main`. CI automatically runs lint, build, and the full test suite. All checks must pass before merge.
159161

160162
5. **Address review feedback** promptly.

0 commit comments

Comments
 (0)