From fa3c7ab1823305dd72b688737c6fae55a679d3c9 Mon Sep 17 00:00:00 2001 From: ugi-dev Date: Sun, 25 Oct 2026 22:49:06 +1100 Subject: [PATCH 1/4] chore: require pull requests to close an issue Add a Linked issue workflow that fails pull requests without a closing issue reference, and document the issue-first rule in CONTRIBUTING.md. --- .github/workflows/linked-issue.yml | 26 ++++++++++++++++++++++++++ CONTRIBUTING.md | 6 +++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/linked-issue.yml diff --git a/.github/workflows/linked-issue.yml b/.github/workflows/linked-issue.yml new file mode 100644 index 0000000..c363bea --- /dev/null +++ b/.github/workflows/linked-issue.yml @@ -0,0 +1,26 @@ +name: Linked issue + +on: + pull_request_target: + types: [opened, edited, reopened, synchronize] + +permissions: + issues: read + pull-requests: read + +jobs: + linked-issue: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login != 'dependabot[bot]' + steps: + - env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + count=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json closingIssuesReferences --jq '.closingIssuesReferences | length') + if [ "$count" -eq 0 ]; then + echo "::error::This PR doesn't close an issue. Open an issue first, then add 'Closes #' to the PR description (CONTRIBUTING.md, Pull requests)." + exit 1 + fi + echo "Closes $count issue(s)." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a3775c..728d66f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,7 +76,11 @@ types, tests, package and docs builds, and installing packed packages outside th workspace. The separate browser harness still needs to be opened in a browser; CI does not execute it. -- Describe what changed and why, and link the issue it addresses. +- **Issue first.** Every pull request closes an issue. Open one (or find the existing one) + before you open the pull request, then put `Closes #` in its description. A pull + request with no linked issue fails the [Linked issue](.github/workflows/linked-issue.yml) + check and won't be reviewed. +- Describe what changed and why. - Keep pull requests small. Split unrelated changes. - Make sure `bun run check` passes and a changeset is included when needed. From 9d2c5c45247793cdcd8ff6421678afee28e2875c Mon Sep 17 00:00:00 2001 From: ugi-dev Date: Sun, 25 Oct 2026 22:53:21 +1100 Subject: [PATCH 2/4] fix: check closing keyword in pull request description closingIssuesReferences also counts issues linked from the sidebar, so a pull request without 'Closes #N' could pass. Parse the description for a closing keyword and confirm the number is an issue. Drop the Dependabot skip, since the repo has no Dependabot. --- .github/workflows/linked-issue.yml | 19 ++++++++++--------- CONTRIBUTING.md | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/linked-issue.yml b/.github/workflows/linked-issue.yml index c363bea..2da7b31 100644 --- a/.github/workflows/linked-issue.yml +++ b/.github/workflows/linked-issue.yml @@ -6,21 +6,22 @@ on: permissions: issues: read - pull-requests: read jobs: linked-issue: runs-on: ubuntu-latest - if: github.event.pull_request.user.login != 'dependabot[bot]' steps: - env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} + PR_BODY: ${{ github.event.pull_request.body }} REPO: ${{ github.repository }} run: | - count=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json closingIssuesReferences --jq '.closingIssuesReferences | length') - if [ "$count" -eq 0 ]; then - echo "::error::This PR doesn't close an issue. Open an issue first, then add 'Closes #' to the PR description (CONTRIBUTING.md, Pull requests)." - exit 1 - fi - echo "Closes $count issue(s)." + numbers=$(printf '%s\n' "$PR_BODY" | grep -oiE '\b(close[sd]?|fix(e[sd])?|resolve[sd]?):?[[:space:]]+#[0-9]+' | grep -oE '[0-9]+$' | sort -u || true) + for number in $numbers; do + if [ "$(gh api "repos/$REPO/issues/$number" --jq '.pull_request == null' 2>/dev/null)" = true ]; then + echo "Closes #$number." + exit 0 + fi + done + echo "::error::This PR doesn't close an issue. Open an issue first, then add 'Closes #' to the PR description (CONTRIBUTING.md, Pull requests)." + exit 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 728d66f..8c9d0b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ CI does not execute it. - **Issue first.** Every pull request closes an issue. Open one (or find the existing one) before you open the pull request, then put `Closes #` in its description. A pull - request with no linked issue fails the [Linked issue](.github/workflows/linked-issue.yml) + request without one fails the [Linked issue](.github/workflows/linked-issue.yml) check and won't be reviewed. - Describe what changed and why. - Keep pull requests small. Split unrelated changes. From 917c7f361a6a2ee8de47cbcd948521b529153219 Mon Sep 17 00:00:00 2001 From: ugi-dev Date: Sun, 25 Oct 2026 22:58:17 +1100 Subject: [PATCH 3/4] fix: require whole issue numbers and run only on main 'Closes #12abc' matched issue 12. Require a word boundary after the number. GitHub ignores closing keywords on pull requests into other branches, so check only pull requests into main. --- .github/workflows/linked-issue.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linked-issue.yml b/.github/workflows/linked-issue.yml index 2da7b31..52a3a2c 100644 --- a/.github/workflows/linked-issue.yml +++ b/.github/workflows/linked-issue.yml @@ -2,6 +2,7 @@ name: Linked issue on: pull_request_target: + branches: [main] types: [opened, edited, reopened, synchronize] permissions: @@ -16,7 +17,7 @@ jobs: PR_BODY: ${{ github.event.pull_request.body }} REPO: ${{ github.repository }} run: | - numbers=$(printf '%s\n' "$PR_BODY" | grep -oiE '\b(close[sd]?|fix(e[sd])?|resolve[sd]?):?[[:space:]]+#[0-9]+' | grep -oE '[0-9]+$' | sort -u || true) + numbers=$(printf '%s\n' "$PR_BODY" | grep -oiE '\b(close[sd]?|fix(e[sd])?|resolve[sd]?):?[[:space:]]+#[0-9]+\b' | grep -oE '[0-9]+$' | sort -u || true) for number in $numbers; do if [ "$(gh api "repos/$REPO/issues/$number" --jq '.pull_request == null' 2>/dev/null)" = true ]; then echo "Closes #$number." From cca2a55531f45ff312ff0a4a9ce082bb22979d23 Mon Sep 17 00:00:00 2001 From: ugi-dev Date: Sun, 25 Oct 2026 23:04:57 +1100 Subject: [PATCH 4/4] docs: scope issue-first rule to pull requests into main --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c9d0b5..ac369e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,7 +76,7 @@ types, tests, package and docs builds, and installing packed packages outside th workspace. The separate browser harness still needs to be opened in a browser; CI does not execute it. -- **Issue first.** Every pull request closes an issue. Open one (or find the existing one) +- **Issue first.** Every pull request into `main` closes an issue. Open one (or find the existing one) before you open the pull request, then put `Closes #` in its description. A pull request without one fails the [Linked issue](.github/workflows/linked-issue.yml) check and won't be reviewed.