name: PR Gate on: pull_request_target: types: [opened] 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()); let weekendState = null; try { weekendState = JSON.parse(await getTextFile('.github/oss-weekend.json')); } catch (error) { if (!(error && typeof error === 'object' && 'status' in error && error.status === 404)) { throw error; } } if (weekendState?.active && isApprovedContributor) { console.log(`${prAuthor} is approved, but OSS weekend is active`); const reopenDate = weekendState.reopensOnText || weekendState.reopensOn || 'after the weekend'; const discordUrl = weekendState.discordUrl || 'https://discord.com/invite/3cU7Bz4UPx'; const message = [ `Hi @${prAuthor}, thanks for the PR.`, '', `OSS weekend is active until ${reopenDate}, so external PRs are being paused for now.`, '', 'You are already on the approved contributors list, so you can resubmit this PR after the weekend without reapproval.', '', `This PR will be closed automatically. For support, join [Discord](${discordUrl}).`, ].join('\n'); await closePullRequest(message); return; } 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.', '', '**Next steps:**', '1. Open an issue describing what you want to change and why (keep it concise, write in your human voice, AI slop will be closed)', '2. Once a maintainer approves with `lgtm`, you\'ll be added to the approved contributors list', '3. 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);