name: Contribution gate - issues on: issues: types: [opened, reopened] permissions: contents: read issues: 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 issues uses: actions/github-script@v7 with: script: | const issue = context.payload.issue; 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(issue.author_association)) return; if (issue.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 = issue.user.login.toLowerCase(); if ( allowlist.has(`all:${login}`) || allowlist.has(`issue:${login}`) ) { return; } const gateMessage = enforceGate ? 'This repository currently uses a maintainer-managed contribution gate, so issues from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS` are closed automatically.' : 'This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this issue is staying open. When enforcement is enabled, issues from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS` will be closed automatically.'; await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: [ `Thanks @${issue.user.login} for the report.`, '', gateMessage, '', 'Please read `CONTRIBUTING.md` for the expected issue shape. A maintainer can grant issue access by commenting `/lgtmi` on an issue.', ].join('\n'), }); if (!enforceGate) return; await github.rest.issues.update({ owner, repo, issue_number: issue.number, state: 'closed', state_reason: 'not_planned', });