name: Contribution gate - pull requests on: pull_request_target: types: [opened, reopened] permissions: contents: read issues: write pull-requests: write env: # Keep new gates observable first. Switch to "enforce" only after maintainers # have seeded active contributors and reviewed the dry-run signal. CONTRIBUTION_GATE_MODE: dry-run jobs: gate: runs-on: ubuntu-latest steps: - name: Gate unapproved external pull requests uses: actions/github-script@v7 with: script: | const pr = context.payload.pull_request; const owner = context.repo.owner; const repo = context.repo.repo; const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); const gateMode = (process.env.CONTRIBUTION_GATE_MODE || 'dry-run').trim().toLowerCase(); const enforceGate = gateMode === 'enforce'; if (!['dry-run', 'enforce'].includes(gateMode)) { core.warning(`Unknown CONTRIBUTION_GATE_MODE "${gateMode}"; defaulting to dry-run.`); } if (privileged.has(pr.author_association)) return; if (pr.user.login === 'github-actions[bot]') return; function parseAllowlist(content) { return new Set( content .split(/\r?\n/) .map(line => line.replace(/#.*/, '').trim().toLowerCase()) .filter(Boolean) ); } async function readAllowlist() { try { const { data } = await github.rest.repos.getContent({ owner, repo, path: '.github/APPROVED_CONTRIBUTORS', ref: context.payload.repository.default_branch, }); if (Array.isArray(data) || data.type !== 'file') return new Set(); return parseAllowlist( Buffer.from(data.content, data.encoding || 'base64').toString('utf8') ); } catch (error) { if (error.status === 404) return new Set(); throw error; } } const allowlist = await readAllowlist(); const login = pr.user.login.toLowerCase(); if ( allowlist.has(`all:${login}`) || allowlist.has(`pr:${login}`) ) { return; } const gateMessage = enforceGate ? 'This repository currently limits automated PR intake to contributors listed in `.github/APPROVED_CONTRIBUTORS`. This is a maintainer-safety control for code review and CI load, not a judgment on the contribution. A maintainer can grant recurring PR access with `/lgtm` after review; once the generated allowlist PR is merged, this pull request can be reopened or resubmitted.' : 'This repository is observing a maintainer-managed PR intake gate in dry-run mode, so this pull request is staying open. This note helps maintainers prepare the allowlist before any enforcement is considered.'; const marker = ''; const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: pr.number, per_page: 100, }); const alreadyNoted = comments.some(comment => (comment.body || '').includes(marker)); if (!alreadyNoted) { await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: [ marker, `Thanks @${pr.user.login} for taking the time to contribute.`, '', gateMessage, '', 'Please read `CONTRIBUTING.md` for the expected contribution shape. A maintainer can grant recurring PR access by commenting `/lgtm` on a pull request.', ].join('\n'), }); } if (!enforceGate) return; await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: 'closed', });