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 | |
| parent | 8307f2f712293970d80f624594d51e693d711cfb (diff) | |
chore: route feature intake to discussions
Diffstat (limited to '.github')
| -rw-r--r-- | .github/APPROVED_CONTRIBUTORS | 1 | ||||
| -rw-r--r-- | .github/ISSUE_TEMPLATE/bug.yml | 35 | ||||
| -rw-r--r-- | .github/ISSUE_TEMPLATE/change.yml | 62 | ||||
| -rw-r--r-- | .github/ISSUE_TEMPLATE/config.yml | 8 | ||||
| -rw-r--r-- | .github/workflows/approve-contributor.yml | 27 | ||||
| -rw-r--r-- | .github/workflows/issue-gate.yml | 144 | ||||
| -rw-r--r-- | .github/workflows/label-intends-to-pr.yml | 92 | ||||
| -rw-r--r-- | .github/workflows/pr-gate.yml | 17 |
8 files changed, 199 insertions, 187 deletions
diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 6434b802..be8c0c7e 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -20,6 +20,5 @@ icedac LaneBirmingham chenrui333 wayneleelwc -ogulcancelik reobin wardpeet diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 408707b1..318843cc 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,15 +1,26 @@ name: Bug report -description: Report something broken in herdr. +description: Report a reproducible bug in herdr. body: - type: markdown attributes: value: | **Before you start:** Read [CONTRIBUTING.md](https://github.com/ogulcancelik/herdr/blob/master/CONTRIBUTING.md). - New PRs from unapproved contributors are auto-closed by default. Open an issue first and wait for maintainer approval. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/ogulcancelik/herdr/blob/master/CONTRIBUTING.md) may be closed or left without a reply. + Issues are only for reproducible bugs and maintainer-created or maintainer-converted work items. Feature requests, ideas, questions, contribution proposals, and direction checks belong in [Discussions](https://github.com/ogulcancelik/herdr/discussions). + + New PRs from unapproved contributors are auto-closed by default. Do not open a PR unless a maintainer has approved the work and approved you to contribute. Keep this short. If it does not fit on one screen, it is too long. Write in your own voice. + - type: checkboxes + id: bug-confirmation + attributes: + label: Is this a reproducible bug? + description: Feature requests, ideas, questions, contribution proposals, and direction checks belong in Discussions and may be closed automatically if opened as issues. + options: + - label: I confirm this is a reproducible bug, not a feature request, idea, question, contribution proposal, or direction check. + required: true + - type: textarea id: current-behavior attributes: @@ -46,16 +57,12 @@ body: id: environment attributes: label: Environment - description: Herdr version, OS, terminal, shell, and any relevant config. + description: Herdr version, operating system, and terminal are required. Shell and relevant config are optional. + value: | + - Herdr version: + - Operating system: + - Terminal: + - Shell, if relevant: + - Relevant config, if any: validations: - required: false - - - type: checkboxes - id: contribution-intent - attributes: - label: Contribution intent - options: - - label: I intend to open a PR for this issue. - required: false - - label: I understand this does not mean the issue is approved. - required: false + required: true diff --git a/.github/ISSUE_TEMPLATE/change.yml b/.github/ISSUE_TEMPLATE/change.yml deleted file mode 100644 index a6cfbc48..00000000 --- a/.github/ISSUE_TEMPLATE/change.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Feature or behavior change -description: Propose a feature, behavior change, documentation change, or direction check. -body: - - type: markdown - attributes: - value: | - **Before you start:** Read [CONTRIBUTING.md](https://github.com/ogulcancelik/herdr/blob/master/CONTRIBUTING.md). - - New PRs from unapproved contributors are auto-closed by default. Open an issue first and wait for maintainer approval. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/ogulcancelik/herdr/blob/master/CONTRIBUTING.md) may be closed or left without a reply. - - Keep this short. If it does not fit on one screen, it is too long. Write in your own voice. - - - type: textarea - id: current-behavior - attributes: - label: Current behavior - description: What does herdr do today? - validations: - required: true - - - type: textarea - id: requested-change - attributes: - label: Requested change - description: What do you want to change? - validations: - required: true - - - type: textarea - id: reason - attributes: - label: Reason - description: Why does this belong in herdr? - validations: - required: true - - - type: dropdown - id: product-impact - attributes: - label: Does this change UI, interaction, workflow, or product direction? - options: - - "No" - - "Yes" - - "Not sure" - validations: - required: true - - - type: textarea - id: product-impact-details - attributes: - label: Impact details - description: If yes or not sure, explain what changes for users. - - - type: checkboxes - id: contribution-intent - attributes: - label: Contribution intent - options: - - label: I intend to open a PR for this issue. - required: false - - label: I understand this does not mean the issue is approved. - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8005e322..27316205 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,2 +1,8 @@ blank_issues_enabled: false -contact_links: [] +contact_links: + - name: Feature requests and ideas + url: https://github.com/ogulcancelik/herdr/discussions + about: Start a discussion for feature requests, ideas, behavior changes, and product direction. + - name: Questions and contribution proposals + url: https://github.com/ogulcancelik/herdr/discussions + about: Use discussions for questions, contribution proposals, and anything that is not a reproducible bug. diff --git a/.github/workflows/approve-contributor.yml b/.github/workflows/approve-contributor.yml index 0f66017d..069ef898 100644 --- a/.github/workflows/approve-contributor.yml +++ b/.github/workflows/approve-contributor.yml @@ -29,13 +29,18 @@ jobs: const commenter = context.payload.comment.user.login; const commentBody = context.payload.comment.body || ''; const approvedFile = '.github/APPROVED_CONTRIBUTORS'; + const approveMatch = commentBody.match(/(^|\s)\/approve(?:\s+@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?))?(?=$|\s)/i); + const maintainerPermissions = ['admin', 'maintain', 'write']; - if (!/(^|\s)\/approve(?=$|\s)/i.test(commentBody)) { + if (!approveMatch) { console.log('Comment does not contain /approve'); core.setOutput('status', 'skipped'); return; } + const approvedUser = approveMatch[2] || issueAuthor; + core.setOutput('approved_user', approvedUser); + try { const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({ owner: context.repo.owner, @@ -43,8 +48,8 @@ jobs: username: commenter, }); - if (permissionLevel.permission !== 'admin') { - console.log(`${commenter} does not have admin access`); + if (!maintainerPermissions.includes(permissionLevel.permission)) { + console.log(`${commenter} does not have maintainer access`); core.setOutput('status', 'skipped'); return; } @@ -60,22 +65,22 @@ jobs: .map(line => line.trim().toLowerCase()) .filter(line => line && !line.startsWith('#')); - if (approvedList.includes(issueAuthor.toLowerCase())) { - console.log(`${issueAuthor} is already approved`); + if (approvedList.includes(approvedUser.toLowerCase())) { + console.log(`${approvedUser} is already approved`); core.setOutput('status', 'already'); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - body: `@${issueAuthor} is already in the approved contributors list.`, + body: `@${approvedUser} is already in the approved contributors list.`, }); return; } - content = content.trimEnd() + '\n' + issueAuthor + '\n'; + content = content.trimEnd() + '\n' + approvedUser + '\n'; fs.writeFileSync(approvedFile, content); - console.log(`Added ${issueAuthor} to approved contributors`); + console.log(`Added ${approvedUser} to approved contributors`); core.setOutput('status', 'added'); - name: Commit and push @@ -86,7 +91,7 @@ jobs: git config user.name "kangal-bot" git config user.email "285672167+kangal-bot@users.noreply.github.com" git add .github/APPROVED_CONTRIBUTORS - git diff --staged --quiet || git commit -m "chore: approve contributor ${{ github.event.issue.user.login }}" + git diff --staged --quiet || git commit -m "chore: approve contributor ${{ steps.update.outputs.approved_user }}" git push "https://x-access-token:${KANGAL_GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:${{ github.event.repository.default_branch }} - name: Comment on issue @@ -95,10 +100,10 @@ jobs: with: github-token: ${{ secrets.KANGAL_GITHUB_TOKEN }} script: | - const issueAuthor = context.payload.issue.user.login; + const approvedUser = '${{ steps.update.outputs.approved_user }}'; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - body: `@${issueAuthor} has been added to the approved contributors list. You can now submit PRs. Thanks for contributing!`, + body: `@${approvedUser} has been added to the approved contributors list. You can now submit PRs for maintainer-approved work. Thanks for contributing!`, }); 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', + }); diff --git a/.github/workflows/label-intends-to-pr.yml b/.github/workflows/label-intends-to-pr.yml deleted file mode 100644 index d25985b7..00000000 --- a/.github/workflows/label-intends-to-pr.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Label contributor intent - -on: - issues: - types: [opened, edited] - -permissions: - issues: write - -env: - INTENDS_TO_PR_LABEL: intends-to-pr - -jobs: - label: - if: ${{ !github.event.issue.pull_request }} - runs-on: ubuntu-latest - - steps: - - name: Sync intends-to-pr label - uses: actions/github-script@v9 - with: - script: | - const label = process.env.INTENDS_TO_PR_LABEL; - const body = context.payload.issue.body || ''; - const issueNumber = context.payload.issue.number; - const { owner, repo } = context.repo; - const checkedIntentPattern = /^\s*-\s*\[[xX]\]\s*I intend to open a PR for this (?:change|issue)\.\s*$/m; - const uncheckedIntentPattern = /^\s*-\s*\[ \]\s*I intend to open a PR for this (?:change|issue)\.\s*$/m; - const cliIntentPattern = /^\s*\/i-intend-to-pr\s*$/m; - const shouldAddLabel = checkedIntentPattern.test(body) || cliIntentPattern.test(body); - const shouldRemoveLabel = uncheckedIntentPattern.test(body); - const labels = context.payload.issue.labels.map((item) => - typeof item === 'string' ? item : item.name, - ); - const hasLabel = labels.includes(label); - - if (shouldAddLabel && hasLabel) { - console.log(`${label} is already present on #${issueNumber}.`); - return; - } - - if (shouldRemoveLabel && !hasLabel) { - console.log(`${label} is already absent from #${issueNumber}.`); - return; - } - - if (!shouldAddLabel && !shouldRemoveLabel) { - console.log(`No contribution intent signal found for #${issueNumber}.`); - return; - } - - if (shouldAddLabel) { - try { - await github.rest.issues.getLabel({ owner, repo, name: label }); - } catch (error) { - if (error.status !== 404) { - throw error; - } - - await github.rest.issues.createLabel({ - owner, - repo, - name: label, - color: '0E8A16', - description: 'Contributor intends to open a PR for this issue.', - }); - } - - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: issueNumber, - labels: [label], - }); - console.log(`Added ${label} to #${issueNumber}.`); - return; - } - - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: issueNumber, - name: label, - }); - console.log(`Removed ${label} from #${issueNumber}.`); - } catch (error) { - if (error.status !== 404) { - throw error; - } - console.log(`${label} was already absent from #${issueNumber}.`); - } diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index f59f49ad..fadc4d28 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -15,6 +15,7 @@ jobs: - name: Check if contributor is approved uses: actions/github-script@v9 with: + github-token: ${{ secrets.KANGAL_GITHUB_TOKEN }} script: | const prAuthor = context.payload.pull_request.user.login; const reopener = context.payload.sender?.login ?? null; @@ -102,15 +103,19 @@ jobs: const message = [ `Hi @${prAuthor}, thanks for your interest in contributing!`, '', - 'We ask new contributors to open an issue first before submitting a PR. This helps us discuss the approach and avoid wasted effort.', + 'New contributors need maintainer approval before opening PRs. This keeps review time focused on accepted work and avoids wasted effort.', '', - 'Herdr is opinionated about how it should look, feel, and work. If your change affects UI, behavior, interaction patterns, or product direction, discuss it in an issue first.', + 'Herdr is opinionated about how it should look, feel, and work. Feature requests, ideas, questions, contribution proposals, and product-direction checks belong in Discussions, not issues.', '', '**Next steps:**', - '1. Open an issue describing what you want to change and why', - '2. Keep it concise and write in your own voice', - '3. If a maintainer approves with `/approve`, you will be added to the approved contributors list', - '4. Then you can submit your PR', + '1. If this is a feature or behavior change, open a Discussion describing what you want to change and why', + '2. If the work is accepted, a maintainer may convert the discussion into an issue or create a new issue for it', + '3. If this is a reproducible bug, open a bug report using the issue template', + '4. Wait for maintainer approval on an accepted issue before opening a PR', + ' Maintainers approve first-time PR paths with `/approve @your-github-username` on the accepted issue.', + '5. Keep it concise and write in your own voice', + '', + 'A discussion, issue, branch, or proposed implementation does not reserve the work and does not mean the PR path is approved.', '', `This PR will be closed automatically. See https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md for more details.`, ].join('\n'); |
