diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-01 02:13:30 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-01 02:14:52 +0300 |
| commit | 472e2b8453160affd05760b22a483b3c9ee100be (patch) | |
| tree | 3d72f0778c3a3e9f2bc0091d9f07fa4e526c0858 /.github/workflows/issue-gate.yml | |
| parent | 8307f2f712293970d80f624594d51e693d711cfb (diff) | |
chore: route feature intake to discussions
Diffstat (limited to '.github/workflows/issue-gate.yml')
| -rw-r--r-- | .github/workflows/issue-gate.yml | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml new file mode 100644 index 00000000..52f6a150 --- /dev/null +++ b/.github/workflows/issue-gate.yml @@ -0,0 +1,144 @@ +name: Issue Gate + +on: + issues: + types: [opened] + +permissions: + issues: write + +jobs: + check-template: + if: ${{ !github.event.issue.pull_request }} + runs-on: ubuntu-latest + + steps: + - name: Close non-template issues + uses: actions/github-script@v9 + with: + github-token: ${{ secrets.KANGAL_GITHUB_TOKEN }} + script: | + const issue = context.payload.issue; + const author = issue.user.login; + const sender = context.payload.sender?.login ?? author; + const bugConfirmationPattern = /^\s*-\s*\[[xX]\]\s*I confirm this is a reproducible bug, not a feature request, idea, question, contribution proposal, or direction check\.\s*$/m; + const requiredSections = [ + '### Is this a reproducible bug?', + '### Current behavior', + '### Expected behavior', + '### Reproduction', + '### Impact', + '### Environment', + ]; + const requiredEnvironmentFields = [ + /^\s*-\s*Herdr version:\s*\S.*$/m, + /^\s*-\s*Operating system:\s*\S.*$/m, + /^\s*-\s*Terminal:\s*\S.*$/m, + ]; + + function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + function extractSection(body, heading) { + const pattern = new RegExp(`^${escapeRegExp(heading)}\\s*$`, 'm'); + const match = pattern.exec(body); + if (!match) { + return null; + } + + const sectionStart = match.index + match[0].length; + const rest = body.slice(sectionStart); + const headingIndex = requiredSections.indexOf(heading); + const nextTemplateHeading = requiredSections + .slice(headingIndex + 1) + .map((section) => { + const nextPattern = new RegExp(`^${escapeRegExp(section)}\\s*$`, 'm'); + const nextMatch = nextPattern.exec(rest); + return nextMatch ? nextMatch.index : -1; + }) + .filter((index) => index !== -1) + .sort((left, right) => left - right)[0]; + return (nextTemplateHeading === undefined ? rest : rest.slice(0, nextTemplateHeading)).trim(); + } + + function hasContent(section) { + return typeof section === 'string' && section.trim().length > 0; + } + + async function getPermission(username) { + try { + const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username, + }); + return permissionLevel.permission; + } catch { + return null; + } + } + + const maintainerPermissions = ['admin', 'maintain', 'write']; + const authorPermission = await getPermission(author); + if (maintainerPermissions.includes(authorPermission)) { + console.log(`${author} is a collaborator with ${authorPermission} access; leaving issue open`); + return; + } + + const senderPermission = sender === author ? authorPermission : await getPermission(sender); + if (maintainerPermissions.includes(senderPermission)) { + console.log(`${sender} opened this issue with ${senderPermission} access; leaving issue open`); + return; + } + + const body = issue.body || ''; + const hasBugConfirmation = bugConfirmationPattern.test(body); + const hasBugTemplate = requiredSections.every((section) => body.includes(section)); + const currentBehavior = extractSection(body, '### Current behavior'); + const expectedBehavior = extractSection(body, '### Expected behavior'); + const reproduction = extractSection(body, '### Reproduction'); + const impact = extractSection(body, '### Impact'); + const environment = extractSection(body, '### Environment'); + const hasRequiredContent = [ + currentBehavior, + expectedBehavior, + reproduction, + impact, + ].every(hasContent); + const hasEnvironmentFields = hasContent(environment) && requiredEnvironmentFields.every((pattern) => pattern.test(environment)); + if (hasBugConfirmation && hasBugTemplate && hasRequiredContent && hasEnvironmentFields) { + console.log(`#${issue.number} matches the bug report template`); + return; + } + + const message = [ + `Hi @${author}, thanks for opening this.`, + '', + 'This issue does not appear to use the full bug report template. Herdr issues are only for reproducible bugs and maintainer-created or maintainer-converted work items.', + '', + 'Bug reports must include the reproducible-bug confirmation, current behavior, expected behavior, reproduction, impact, and environment fields with Herdr version, operating system, and terminal.', + '', + 'Feature requests, ideas, questions, contribution proposals, and direction checks belong in Discussions:', + `https://github.com/${context.repo.owner}/${context.repo.repo}/discussions`, + '', + 'Please read the contributing guidelines before opening issues or PRs:', + `https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${context.payload.repository.default_branch}/CONTRIBUTING.md`, + '', + 'Closing this so the issue tracker stays limited to actionable bug reports.', + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: message, + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed', + state_reason: 'not_planned', + }); |
