chore: require PRs to link an issue or discussion

- Add PR template with required linked issue/discussion section
- Add check-linked-issue CI job that validates PR body contains a
  reference (#123, Closes/Fixes/Relates, GitHub URL, or CF-# ticket)
- Wire into required-checks-passed gate so it blocks merge
- Update CONTRIBUTING.md with the policy and motivation
This commit is contained in:
Kevin Turcios 2026-04-23 02:27:49 -05:00
parent 67cf123929
commit 972d88c108
3 changed files with 72 additions and 1 deletions

18
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,18 @@
## Linked issue or discussion
<!-- Every PR must link to an issue or discussion — this ensures the approach has been discussed with maintainers before implementation begins, so your work fits the project's direction and doesn't need to be reworked. -->
<!-- Replace the line below with one of: -->
<!-- Closes #<number> -->
<!-- Fixes #<number> -->
<!-- Relates to #<number> -->
<!-- Discussion: <url> -->
**Required:** <!-- CI will fail if no linked issue or discussion is found. -->
## What changed
<!-- Brief description of the changes. -->
## Test plan
<!-- How was this tested? Link to passing CI, new tests, or manual verification steps. -->

View file

@ -22,6 +22,57 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
# ---------------------------------------------------------------------------
# Linked issue check — every PR must reference an issue or discussion.
# Skipped on push to main and workflow_dispatch.
# ---------------------------------------------------------------------------
check-linked-issue:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- name: Check PR body for linked issue or discussion
env:
PR_BODY: ${{ github.event.pull_request.body }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
# Skip for bots (dependabot, renovate, github-actions)
if [[ "$PR_AUTHOR" == *"[bot]"* || "$PR_AUTHOR" == "dependabot" ]]; then
echo "Bot PR — skipping linked issue check."
exit 0
fi
if [ -z "$PR_BODY" ]; then
echo "::error::PR body is empty. Every PR must link an issue or discussion."
echo "Use 'Closes #<number>', 'Fixes #<number>', 'Relates to #<number>', or include a discussion URL."
exit 1
fi
# Match: #123, GH-123, org/repo#123, Closes/Fixes/Relates/Resolves #123,
# or a github.com URL to an issue or discussion
if echo "$PR_BODY" | grep -qiP '(close[sd]?|fix(e[sd])?|relate[sd]?\s+to|resolve[sd]?)\s+#\d+'; then
echo "Found linked issue keyword."
exit 0
fi
if echo "$PR_BODY" | grep -qP '#\d+'; then
echo "Found issue reference."
exit 0
fi
if echo "$PR_BODY" | grep -qiP 'github\.com/[^\s]+/(issues|discussions)/\d+'; then
echo "Found GitHub issue/discussion URL."
exit 0
fi
if echo "$PR_BODY" | grep -qiP 'CF-#?\d+'; then
echo "Found Linear ticket reference."
exit 0
fi
echo "::error::No linked issue or discussion found in PR body."
echo "Every PR must reference an issue or discussion. See CONTRIBUTING.md for details."
echo "Use 'Closes #<number>', 'Fixes #<number>', 'Relates to #<number>', or include a discussion URL."
exit 1
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Change detection — decides which downstream jobs actually run. # Change detection — decides which downstream jobs actually run.
# On push/workflow_dispatch every flag is true so all jobs execute. # On push/workflow_dispatch every flag is true so all jobs execute.
@ -506,6 +557,7 @@ jobs:
name: required checks passed name: required checks passed
if: always() if: always()
needs: needs:
- check-linked-issue
- unit-tests - unit-tests
- type-check - type-check
- prek - prek

View file

@ -101,10 +101,11 @@ The full ruleset is in [`.claude/rules/code-style.md`](.claude/rules/code-style.
## Branches, commits, and pull requests ## Branches, commits, and pull requests
- **Every PR must link an issue or discussion.** Use `Closes #<number>`, `Fixes #<number>`, or `Relates to #<number>` in the PR body. CI will fail if no linked issue or discussion is found. For trivial fixes (typos, formatting), open a lightweight issue first — it only takes a moment and keeps the history traceable. The goal is to have a conversation before the code — discussing the approach on an issue or discussion helps maintainers point you in the right direction early, so your implementation fits the project's needs and you don't spend time on work that gets reworked.
- Create a feature branch off an up-to-date `main`. Never commit directly to `main`. - Create a feature branch off an up-to-date `main`. Never commit directly to `main`.
- Use conventional-commit prefixes: `fix:`, `feat:`, `refactor:`, `docs:`, `test:`, `chore:`. Keep commit messages concise (1-2 sentence body max). - Use conventional-commit prefixes: `fix:`, `feat:`, `refactor:`, `docs:`, `test:`, `chore:`. Keep commit messages concise (1-2 sentence body max).
- Keep commits atomic - one logical change per commit. - Keep commits atomic - one logical change per commit.
- PR titles also use the conventional format. The PR body should be short and link any related issues. - PR titles also use the conventional format. The PR body should be short and link the related issue.
- If the change corresponds to a Linear ticket, include `CF-#<number>` in the PR body. - If the change corresponds to a Linear ticket, include `CF-#<number>` in the PR body.
- Run `uv run prek` (or `uv run prek run --from-ref origin/main`) before pushing. CI will block merge if hooks fail. - Run `uv run prek` (or `uv run prek run --from-ref origin/main`) before pushing. CI will block merge if hooks fail.