返回 CodeWhale
pr-issue-link.yml
根目录 / .github / workflows / pr-issue-link.yml
1 name: PR closes an issue
2
3 # 342 open issues, 329 of them touched within the month: nothing here is rotting,
4 # the drain is just clogged. Only 8 of 35 open PRs carried a closing keyword, so
5 # work ships and its issue stays open, and nobody can tell which of the 342 are
6 # already done. That is how 121 issues end up on one milestone.
7 #
8 # This check asks every PR to either close an issue or say why it doesn't. The
9 # opt-out is one line, so this is a prompt, not a wall.
10
11 on:
12 pull_request:
13 types: [opened, edited, reopened, synchronize]
14
15 permissions:
16 contents: read
17 pull-requests: read
18
19 jobs:
20 link:
21 runs-on: ubuntu-latest
22 steps:
23 # Automated dependency bumps (dependabot and any other GitHub-verified
24 # bot account) are machine-generated and can never carry a closing
25 # keyword; failing them here would require hand-editing every bot body,
26 # which defeats the automation. The gate stays strict for every human
27 # PR. `user.type` is set by GitHub for verified bot accounts, so a PR
28 # author cannot spoof it to dodge the check.
29 - name: Require a closing keyword or an explicit opt-out
30 if: github.event.pull_request.user.type != 'Bot'
31 env:
32 # Fetched live rather than read from the event payload. A rerun
33 # replays the payload the run started with, so a body-only fix could
34 # never turn this check green: the obvious operator move — add the
35 # missing line, rerun the failed check — re-read the old body and
36 # failed again with no hint why. Reading the current body makes a
37 # rerun mean what everyone already assumes it means.
38 GH_TOKEN: ${{ github.token }}
39 PR_NUMBER: ${{ github.event.pull_request.number }}
40 REPO: ${{ github.repository }}
41 run: |
42 set -euo pipefail
43 # Through a variable, never interpolated into the script body:
44 # a PR body is attacker-controlled text.
45 PR_BODY=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json body --jq '.body // ""')
46 # Body only, deliberately. GitHub resolves closing keywords from the
47 # PR description; a "Closes #123" in the title auto-closes nothing.
48 # Accepting the title here would pass PRs that never close an issue,
49 # which is the exact false-assurance this check exists to prevent.
50 text="${PR_BODY:-}"
51
52 # GitHub's own closing-keyword set, plus the #N it must attach to.
53 if grep -qiE '\b(close[sd]?|fix(e[sd])?|resolve[sd]?)\b[[:space:]]*:?[[:space:]]*#[0-9]+' <<<"$text"; then
54 echo "Closing keyword found — this PR will close its issue on merge."
55 exit 0
56 fi
57
58 # One-line escape hatch. Anything after the marker is the reason.
59 if grep -qiE '^[[:space:]]*No-Issue:[[:space:]]*\S' <<<"$text"; then
60 reason=$(grep -iE '^[[:space:]]*No-Issue:' <<<"$text" | head -1)
61 echo "Opted out — ${reason}"
62 exit 0
63 fi
64
65 cat >&2 <<'MSG'
66 This PR neither closes an issue nor says why it doesn't.
67
68 Add one of these to the PR body:
69
70 Closes #1234 (or Fixes / Resolves — any of GitHub's keywords)
71 No-Issue: <one-line why> (chores, docs typos, revert, dependency bump)
72
73 Why this is a required check: work here ships faster than issues close,
74 so an unlinked PR leaves its issue open forever and the backlog stops
75 reflecting reality. Either line takes five seconds and keeps the
76 milestone honest.
77 MSG
78 exit 1
79
79 lines YAML