diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8f6905426..73735594f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,10 @@ +## Linked Issue + +> **Required.** PRs without a linked issue are closed without review. +> Open an issue first if one doesn't exist: https://github.com/gsd-build/get-shit-done/issues/new/choose + +Closes # + ## What @@ -6,8 +13,6 @@ -Closes # - ## How @@ -35,6 +40,7 @@ Closes # ## Checklist +- [ ] Issue linked above (`Closes #NNN`) — **PR will be auto-closed if missing** - [ ] Follows GSD style (no enterprise patterns, no filler) - [ ] Updates CHANGELOG.md for user-facing changes - [ ] No unnecessary dependencies added diff --git a/.github/workflows/require-issue-link.yml b/.github/workflows/require-issue-link.yml new file mode 100644 index 000000000..38e36b407 --- /dev/null +++ b/.github/workflows/require-issue-link.yml @@ -0,0 +1,52 @@ +name: Require Issue Link + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +permissions: + pull-requests: write + +jobs: + check-issue-link: + name: Issue link required + runs-on: ubuntu-latest + steps: + - name: Check PR body for issue reference + id: check + env: + # Bound to env var — never interpolated into shell directly + PR_BODY: ${{ github.event.pull_request.body }} + run: | + if echo "$PR_BODY" | grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'; then + echo "found=true" >> "$GITHUB_OUTPUT" + else + echo "found=false" >> "$GITHUB_OUTPUT" + fi + + - name: Comment and fail if no issue link + if: steps.check.outputs.found == 'false' + uses: actions/github-script@v7 + with: + # Uses GitHub API SDK — no shell string interpolation of untrusted input + script: | + const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: [ + '## Missing issue link', + '', + 'This PR does not reference an issue. **All PRs must link to an open issue** using a closing keyword in the PR body:', + '', + '```', + 'Closes #123', + '```', + '', + `If no issue exists for this change, [open one first](${repoUrl}/issues/new/choose), then update this PR body with the reference.`, + '', + 'This PR will remain blocked until a valid `Closes #NNN`, `Fixes #NNN`, or `Resolves #NNN` line is present in the description.', + ].join('\n') + }); + core.setFailed('PR body must contain a closing issue reference (e.g. "Closes #123")'); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ffb4ace9..c1a8a6328 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,9 +16,12 @@ npm test ## Pull Request Guidelines +**Every PR must link to an open issue.** PRs without a linked issue are closed without review, no exceptions. If no issue exists for your change, open one first. + +- **Open an issue first** — describe the bug or feature before writing code. This lets maintainers confirm the direction before you invest time. +- **Link with a closing keyword** — use `Closes #123`, `Fixes #123`, or `Resolves #123` in the PR body. The CI check will fail and the PR will be auto-closed if no valid issue reference is found. - **One concern per PR** — bug fixes, features, and refactors should be separate PRs - **No drive-by formatting** — don't reformat code unrelated to your change -- **Link issues** — use `Fixes #123` or `Closes #123` in PR body for auto-close - **CI must pass** — all matrix jobs (Ubuntu, macOS, Windows × Node 22, 24) must be green ## Testing Standards