Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions .github/workflows/issue-label-fixed-on-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: Label Fixed Issues on Merge

# Trigger: a pull request is closed.
# When the PR was actually MERGED, every issue it closed via a linking keyword
# (Closes #N / Fixes #N / Resolves #N) is labeled `close:fixed` so the close
# reason is visible at a glance and consistent with the manual triage taxonomy.
#
# ─── WHY pull_request_target ────────────────────────────────────────────────
# `pull_request` grants a read-only GITHUB_TOKEN to PRs opened from forks, which
# would make `issues: write` unavailable and silently skip fork contributions.
# `pull_request_target` runs with the base repository's trusted workflow file and
# a read/write token. This workflow NEVER checks out or executes PR head code —
# it only calls the GitHub API via github-script — so there is no untrusted-code
# execution risk from using pull_request_target.
# ────────────────────────────────────────────────────────────────────────────

on:
pull_request_target:
types: [closed]

permissions:
issues: write
contents: read

# The close:fixed label is defined and created by issue-labels-sync.yml
# (CLOSE_LABELS) — that workflow is the single source of truth for its
# color/description. This workflow only applies the existing label.
env:
FIXED_LABEL: 'close:fixed'

jobs:
label-fixed:
name: Label issues closed by merge
runs-on: ubuntu-latest
# Only act on merged PRs — a closed-but-unmerged PR fixes nothing.
if: github.event.pull_request.merged == true

steps:
- name: Verify close:fixed label exists
uses: actions/github-script@v8
with:
script: |
const FIXED_LABEL = process.env.FIXED_LABEL;
const { owner, repo } = context.repo;

// The close:fixed label is owned by issue-labels-sync.yml. If it's
// missing, fail the run rather than letting a later addLabels call
// silently create a default-colored variant — that signals the
// label taxonomy is out of sync and needs the sync workflow to run.
try {
await github.rest.issues.getLabel({ owner, repo, name: FIXED_LABEL });
core.info(`Label "${FIXED_LABEL}" exists`);
} catch (err) {
if (err.status === 404) {
core.setFailed(
`Label "${FIXED_LABEL}" does not exist. Run the "Sync Labels" ` +
`workflow (issue-labels-sync.yml) to create it, then re-run.`
);
return;
}
throw err;
}

- name: Apply close:fixed to auto-closed issues
uses: actions/github-script@v8
with:
script: |
const FIXED_LABEL = process.env.FIXED_LABEL;
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;

// Resolve the issues this PR closes via linking keywords. GraphQL's
// closingIssuesReferences is the authoritative source — it mirrors
// exactly what GitHub itself closed on merge, so it stays correct
// even if the PR body wording is unusual.
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
closingIssuesReferences(first: 50) {
nodes { number }
}
}
}
}`;

const result = await github.graphql(query, {
owner,
repo,
number: prNumber
});

const issues = result.repository.pullRequest.closingIssuesReferences.nodes || [];
if (issues.length === 0) {
core.info(`PR #${prNumber} closed no linked issues — nothing to label`);
return;
}

core.info(`PR #${prNumber} closed issue(s): ${issues.map(i => '#' + i.number).join(', ')}`);

let labeled = 0;
for (const { number } of issues) {
// Read live labels so we don't post a redundant label event
// (which would needlessly re-trigger the enforce-unique workflow).
const fresh = await github.rest.issues.get({
owner,
repo,
issue_number: number
});

const labels = fresh.data.labels.map(l => (typeof l === 'string' ? l : l.name));
if (labels.includes(FIXED_LABEL)) {
core.info(`#${number} already has ${FIXED_LABEL} — skipping`);
continue;
}

await github.rest.issues.addLabels({
owner,
repo,
issue_number: number,
labels: [FIXED_LABEL]
});

core.info(`Labeled #${number} with ${FIXED_LABEL}`);
labeled++;
}

core.info(`Done — applied ${FIXED_LABEL} to ${labeled} issue(s)`);