Skip to content

fix(ci): fetch author_association via REST API instead of webhook payload #39

fix(ci): fetch author_association via REST API instead of webhook payload

fix(ci): fetch author_association via REST API instead of webhook payload #39

Workflow file for this run

name: Vouch Check
on:
pull_request_target:
types: [opened, reopened]
permissions:
contents: read
pull-requests: write
jobs:
vouch-gate:
if: github.repository_owner == 'NVIDIA'
runs-on: ubuntu-latest
steps:
- name: Check if contributor is vouched
uses: actions/github-script@v7
with:
script: |
const author = context.payload.pull_request.user.login;
const authorType = context.payload.pull_request.user.type;
// Skip bots (dependabot, renovate, github-actions, etc.).
if (authorType === 'Bot') {
console.log(`${author} is a bot. Skipping vouch check.`);
return;
}
// Fetch author_association via the REST API. The webhook payload
// field (context.payload.pull_request.author_association) is
// unreliable under pull_request_target — it can be absent or stale.
// The pulls.get endpoint only needs pull-requests permission, which
// we already have, and reliably returns MEMBER for org members even
// when their membership is private.
const trustedAssociations = ['MEMBER', 'OWNER', 'COLLABORATOR'];
try {
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
const association = pr.author_association;
console.log(`${author}: author_association=${association}`);
if (trustedAssociations.includes(association)) {
console.log(`${author} has author_association=${association}. Skipping vouch check.`);
return;
}
} catch (e) {
console.log(`Failed to fetch PR author_association: ${e.message}`);
}
// Check the VOUCHED.td file on the dedicated "vouched" branch.
// NOT the PR branch — the PR author could add themselves in their fork.
let vouched = false;
try {
const { data } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/VOUCHED.td',
ref: 'vouched',
});
const content = Buffer.from(data.content, 'base64').toString('utf-8');
const usernames = content
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#') && !line.startsWith('-'));
vouched = usernames.some(
name => name.toLowerCase() === author.toLowerCase()
);
} catch (e) {
console.log(`Could not read VOUCHED.td: ${e.message}`);
}
if (vouched) {
console.log(`${author} is in VOUCHED.td. Approved.`);
return;
}
// Not vouched — close the PR with an explanation.
console.log(`${author} is not vouched. Closing PR.`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: [
`Thank you for your interest in contributing to OpenShell, @${author}.`,
'',
'This project uses a **vouch system** for first-time contributors. Before submitting a pull request, you need to be vouched by a maintainer.',
'',
'**To get vouched:**',
'1. Open a [Vouch Request](https://github.com/NVIDIA/OpenShell/discussions/new?category=vouch-request) discussion.',
'2. Describe what you want to change and why.',
'3. Write in your own words — do not have an AI generate the request.',
'4. A maintainer will comment `/vouch` if approved.',
'5. Once vouched, open a new PR (preferred) or reopen this one after a few minutes.',
'',
'See [CONTRIBUTING.md](https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md#first-time-contributors) for details.',
].join('\n'),
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
state: 'closed',
});