feat: make Pod the public roster surface #1785
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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@v9 | |
| 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 = '<!-- codewhale-pr-gate -->'; | |
| 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', | |
| }); |