Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 101 additions & 5 deletions .github/workflows/vouch-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,105 @@ jobs:
if: github.repository_owner == 'NVIDIA'
runs-on: ubuntu-latest
steps:
- uses: mitchellh/vouch/action/check-pr@f44860978966ace98fb11aaaa20f2b27d7543e13 # v1
- name: Check if contributor is vouched
uses: actions/github-script@v7
with:
pr-number: ${{ github.event.pull_request.number }}
auto-close: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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;
}

// Check org membership — members bypass the vouch gate.
try {
const { status } = await github.rest.orgs.checkMembershipForUser({
org: context.repo.owner,
username: author,
});
if (status === 204 || status === 302) {
console.log(`${author} is an org member. Skipping vouch check.`);
return;
}
} catch (e) {
if (e.status !== 404) {
console.log(`Org membership check error: ${e.message}`);
}
}

// Check collaborator status — direct collaborators bypass.
try {
const { status } = await github.rest.repos.checkCollaborator({
owner: context.repo.owner,
repo: context.repo.repo,
username: author,
});
if (status === 204) {
console.log(`${author} is a collaborator. Skipping vouch check.`);
return;
}
} catch (e) {
if (e.status !== 404) {
console.log(`Collaborator check error: ${e.message}`);
}
}

// Check the VOUCHED.td file. Read from the default 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: context.payload.repository.default_branch,
});
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',
});
154 changes: 143 additions & 11 deletions .github/workflows/vouch-command.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,149 @@ permissions:

jobs:
process-vouch:
if: github.repository_owner == 'NVIDIA'
if: >-
github.repository_owner == 'NVIDIA' &&
github.event.comment.body == '/vouch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: mitchellh/vouch/action/manage-by-discussion@f44860978966ace98fb11aaaa20f2b27d7543e13 # v1
- name: Process /vouch command
uses: actions/github-script@v7
with:
discussion-number: ${{ github.event.discussion.number }}
comment-node-id: ${{ github.event.comment.node_id }}
vouch-keyword: "/vouch"
denounce-keyword: "/denounce"
unvouch-keyword: "/unvouch"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
script: |
const commenter = context.payload.comment.user.login;
const discussionAuthor = context.payload.discussion.user.login;
const discussionNumber = context.payload.discussion.number;

// --- Helpers ---

async function getDiscussionId() {
const query = `query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
discussion(number: $number) { id }
}
}`;
const { repository } = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
number: discussionNumber,
});
return repository.discussion.id;
}

async function postDiscussionComment(body) {
const discussionId = await getDiscussionId();
const mutation = `mutation($discussionId: ID!, $body: String!) {
addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
comment { id }
}
}`;
await github.graphql(mutation, { discussionId, body });
}

// --- Authorization ---

let isMaintainer = false;
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: commenter,
});
isMaintainer = ['admin', 'maintain', 'write'].includes(data.permission);
} catch (e) {
console.log(`Permission check failed: ${e.message}`);
}

if (!isMaintainer) {
console.log(`${commenter} does not have maintainer permissions. Ignoring.`);
return;
}

// --- Read VOUCHED.td ---

const filePath = '.github/VOUCHED.td';
const branch = context.payload.repository.default_branch;

let currentContent = '';
let sha = '';
try {
const { data } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: filePath,
ref: branch,
});
currentContent = Buffer.from(data.content, 'base64').toString('utf-8');
sha = data.sha;
} catch (e) {
console.log(`Could not read VOUCHED.td: ${e.message}`);
return;
}

// --- Parse .td format ---

function isVouched(content, username) {
return content
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#') && !line.startsWith('-'))
.some(name => name.toLowerCase() === username.toLowerCase());
}

if (isVouched(currentContent, discussionAuthor)) {
console.log(`${discussionAuthor} is already vouched.`);
await postDiscussionComment(
`@${discussionAuthor} is already vouched. They can submit pull requests.`
);
return;
}

// --- Append username and commit ---

async function commitVouch(content, fileSha) {
const updatedContent = content.trimEnd() + '\n' + discussionAuthor + '\n';
await github.rest.repos.createOrUpdateFileContents({
owner: context.repo.owner,
repo: context.repo.repo,
path: filePath,
message: `chore: vouch ${discussionAuthor}`,
content: Buffer.from(updatedContent).toString('base64'),
sha: fileSha,
branch,
});
}

try {
await commitVouch(currentContent, sha);
} catch (e) {
if (e.status === 409) {
// Concurrent write — re-read and retry once.
console.log('409 conflict. Re-reading VOUCHED.td and retrying.');
const { data: fresh } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: filePath,
ref: branch,
});
const freshContent = Buffer.from(fresh.content, 'base64').toString('utf-8');
if (isVouched(freshContent, discussionAuthor)) {
console.log(`${discussionAuthor} was vouched by a concurrent operation.`);
} else {
await commitVouch(freshContent, fresh.sha);
}
} else {
throw e;
}
}

// --- Confirm ---

await postDiscussionComment([
`@${discussionAuthor} has been vouched by @${commenter}.`,
'',
'You can now submit pull requests to OpenShell. Welcome aboard.',
'',
'Please read [CONTRIBUTING.md](https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md) before submitting.',
].join('\n'));

console.log(`Vouched ${discussionAuthor} (approved by ${commenter}).`);
Loading