diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-08 01:11:14 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-08 01:11:14 +0300 |
| commit | a1b803ae64a8ef341356d7552e809c7c89f47b32 (patch) | |
| tree | d89dc4813af0ddece130d855e60ba0924b7bb1c4 /.github/workflows/pr-gate.yml | |
| parent | 25974892c779e994f9bc40ac357f40dc0b5e0347 (diff) | |
chore: add issue-first contributor gate
Diffstat (limited to '.github/workflows/pr-gate.yml')
| -rw-r--r-- | .github/workflows/pr-gate.yml | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml new file mode 100644 index 00000000..2083569f --- /dev/null +++ b/.github/workflows/pr-gate.yml @@ -0,0 +1,107 @@ +name: PR Gate + +on: + pull_request_target: + types: [opened, reopened] + +jobs: + check-contributor: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + steps: + - name: Check if contributor is approved + uses: actions/github-script@v7 + with: + script: | + const prAuthor = context.payload.pull_request.user.login; + const defaultBranch = context.payload.repository.default_branch; + + if (prAuthor.endsWith('[bot]') || prAuthor === 'dependabot[bot]') { + console.log(`Skipping bot: ${prAuthor}`); + return; + } + + 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; + } + } + + async function getTextFile(path) { + const { data: fileContent } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path, + ref: defaultBranch, + }); + + if (!('content' in fileContent) || typeof fileContent.content !== 'string') { + throw new Error(`Expected file content for ${path}`); + } + + return Buffer.from(fileContent.content, 'base64').toString('utf8'); + } + + async function closePullRequest(message) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: message, + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + state: 'closed', + }); + } + + const permission = await getPermission(prAuthor); + if (['admin', 'maintain', 'write'].includes(permission)) { + console.log(`${prAuthor} is a collaborator with ${permission} access`); + return; + } + + const approvedContent = await getTextFile('.github/APPROVED_CONTRIBUTORS'); + const approvedList = approvedContent + .split('\n') + .map(line => line.trim().toLowerCase()) + .filter(line => line && !line.startsWith('#')); + const isApprovedContributor = approvedList.includes(prAuthor.toLowerCase()); + + if (isApprovedContributor) { + console.log(`${prAuthor} is in the approved contributors list`); + return; + } + + console.log(`${prAuthor} is not approved, closing PR`); + + 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.', + '', + '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.', + '', + '**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 `lgtm`, you will be added to the approved contributors list', + '4. Then you can submit your PR', + '', + `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'); + + await closePullRequest(message); |
