From 83e87e3ec069bf42997a60f3059c8e202b216878 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 13 Jun 2026 16:13:00 -0400 Subject: [PATCH] feat(#1180): hard-gate the GSD Version requirement on bug reports (#1181) Auto-close bug reports opened without a valid GSD Version (Issue Forms enforce required only in the web UI). Bug reports only; version-shaped validation; version-exempt opt-out. Closes #1180 --- .changeset/issue-version-gate.md | 5 + .github/workflows/version-gate.yml | 47 +++ docs/agents/triage-labels.md | 13 + scripts/issue-version-gate.cjs | 140 ++++++++ tests/issue-version-gate.test.cjs | 546 +++++++++++++++++++++++++++++ 5 files changed, 751 insertions(+) create mode 100644 .changeset/issue-version-gate.md create mode 100644 .github/workflows/version-gate.yml create mode 100644 scripts/issue-version-gate.cjs create mode 100644 tests/issue-version-gate.test.cjs diff --git a/.changeset/issue-version-gate.md b/.changeset/issue-version-gate.md new file mode 100644 index 000000000..506d68708 --- /dev/null +++ b/.changeset/issue-version-gate.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1181 +--- +Bug-report issues that lack a valid GSD Version are now auto-closed on open by a new `version-gate.yml` GitHub Actions workflow. GitHub Issue Forms only enforce `required: true` in the web UI, so issues filed via the REST API, `gh issue create`, or AI reporters can arrive without a version; values like `idk`, `_No response_`, or an empty field are treated as missing. Affected issues receive a closing comment with instructions to add the version (e.g. `1.18.0`) and reopen; maintainers can add the `version-exempt` label to opt an issue out. diff --git a/.github/workflows/version-gate.yml b/.github/workflows/version-gate.yml new file mode 100644 index 000000000..44cf4ebf4 --- /dev/null +++ b/.github/workflows/version-gate.yml @@ -0,0 +1,47 @@ +name: Issue Version Gate + +on: + issues: + types: [opened] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: false + +permissions: + issues: write + contents: read + +jobs: + gate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const gate = require(`${process.env.GITHUB_WORKSPACE}/scripts/issue-version-gate.cjs`); + const issue = context.payload.issue; + if (!issue) return; + if (issue.pull_request) return; + const labels = (issue.labels || []).map((l) => (typeof l === 'string' ? l : l.name)); + const result = gate.evaluateVersionGate({ labels, body: issue.body || '' }); + core.info(`version-gate: #${issue.number} → ${result.action} (${result.reason})`); + if (result.action !== 'close') return; + const { owner, repo } = context.repo; + try { + await github.rest.issues.addLabels({ + owner, repo, issue_number: issue.number, + labels: [gate.NEEDS_VERSION_LABEL], + }); + } catch (err) { + core.warning(`could not add ${gate.NEEDS_VERSION_LABEL} label: ${err.message}`); + } + await github.rest.issues.createComment({ + owner, repo, issue_number: issue.number, + body: gate.renderCloseComment(), + }); + await github.rest.issues.update({ + owner, repo, issue_number: issue.number, + state: 'closed', state_reason: 'not_planned', + }); diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md index a78db6082..2772301e9 100644 --- a/docs/agents/triage-labels.md +++ b/docs/agents/triage-labels.md @@ -10,6 +10,8 @@ Maps the five canonical triage roles to the actual label strings in `open-gsd/gs | `ready-for-human` | `approved-enhancement` / `approved-feature` | Enhancement/feature approved by maintainer — human codes it | | `wontfix` | `wontfix` | Will not be actioned | | `possible-duplicate` | `possible-duplicate` | Applied by the Duplicate check workflow when a new issue's title closely matches existing open issues. The reporter (or a maintainer) replies justifying why it is not a duplicate within 24h, or the Duplicate auto-close sweep closes it. A reply clears this label and applies needs-maintainer-review for human adjudication. React 👎 to the bot comment to veto auto-close. | +| `needs-version` | `needs-version` | Applied by the Version gate workflow when a bug report is auto-closed for missing a valid GSD Version. Edit the issue to add the version and reopen it. | +| `version-exempt` | `version-exempt` | Maintainer-only opt-out label. Prevents the Version gate from closing an issue where a version genuinely does not apply. | ## Notes on this repo's label model @@ -27,3 +29,14 @@ The `possible-duplicate` label is managed by three GitHub Actions workflows that 2. **Challenge comment + reporter window** — The reporter (or a maintainer) has `DEFAULT_WINDOW_HOURS` (24h) to reply explaining why the issue is not a duplicate. Reacting 👎 to the bot comment also signals the reporter objects to auto-close. 3. **Daily sweep auto-close** — `duplicate-sweep.yml` runs at 07:00 UTC daily. For each open issue with `possible-duplicate`, it checks whether the window has elapsed, whether the reporter replied, and whether a 👎 reaction exists. Issues with exempt labels (`priority: critical`, `pinned`, `confirmed-bug`, `confirmed`, `fix-pending`) are never auto-closed. Issues that pass the close check receive a closing comment and are closed with `state_reason: duplicate`. 4. **Reporter reply clears label** — `remove-duplicate-label.yml` fires on every new non-bot comment. If the issue still carries `possible-duplicate`, it removes that label and applies `needs-maintainer-review` (the value of `HUMAN_REVIEW_LABEL` in `scripts/issue-dedupe.cjs`), routing the issue to a maintainer for manual adjudication. + +## Version gate lifecycle + +The `needs-version` label is managed by a single GitHub Actions workflow that runs immediately on issue open: + +1. **Gate on open** — When an issue is opened, `version-gate.yml` checks whether it is a bug report (has the `bug` label, or its body contains a `### GSD Version` heading from the bug template). Non-bug issues are skipped. +2. **Version check** — The gate extracts the value under the `### GSD Version` heading. A value is considered valid if it contains a semver-ish token (e.g. `1.18.0`, `v1.4.1`, `1.18.0-dev`) or a git commit SHA (7-40 hex chars). Missing, blank, `_No response_`, or junk values like `idk` are treated as absent. +3. **Auto-close** — If the version is absent or invalid, the workflow posts a comment with instructions and closes the issue as `not_planned`, then applies `needs-version`. +4. **Reopen path** — The reporter edits the issue to add a valid version and reopens it. Alternatively, a maintainer can add the `version-exempt` label to any bug where a version does not apply (e.g. docs bugs, spec questions), which prevents the gate from closing it on future edits. + +The gate logic lives in `scripts/issue-version-gate.cjs` (pure exports, no GitHub API dependency) and is covered by `tests/issue-version-gate.test.cjs`. diff --git a/scripts/issue-version-gate.cjs b/scripts/issue-version-gate.cjs new file mode 100644 index 000000000..8217091af --- /dev/null +++ b/scripts/issue-version-gate.cjs @@ -0,0 +1,140 @@ +#!/usr/bin/env node +'use strict'; + +const { PACKAGE_NAME } = require('../gsd-core/bin/lib/package-identity.cjs'); + +/** + * Version gate for bug-report issues. + * + * GitHub Issue Forms enforce `required: true` only in the web form; issues + * filed via the REST API, `gh issue create`, or automated/AI reporters can + * omit the GSD Version field entirely. This module provides the pure logic + * for detecting bug reports missing a usable version so a workflow can + * auto-close them. See .github/workflows/version-gate.yml. + */ + +const BUG_LABEL = 'bug'; +const NEEDS_VERSION_LABEL = 'needs-version'; +const VERSION_GATE_MARKER = ''; + +// Labels that opt an issue out of the version gate. +const EXEMPT_LABELS = ['version-exempt']; + +// Heading GitHub renders for the bug template's `label: GSD Version` field. +// Issue Forms render input labels as `###