Vouch Command #37
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: Vouch Command | |
| on: | |
| discussion_comment: | |
| types: [created] | |
| concurrency: | |
| group: vouch-manage | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| discussions: write | |
| jobs: | |
| process-vouch: | |
| if: >- | |
| github.repository_owner == 'NVIDIA' && | |
| github.event.comment.body == '/vouch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Process /vouch command | |
| uses: actions/github-script@v7 | |
| with: | |
| 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 = 'vouched'; | |
| // Ensure the "vouched" branch exists. If not, create it from main. | |
| try { | |
| await github.rest.repos.getBranch({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| branch, | |
| }); | |
| } catch (e) { | |
| if (e.status === 404) { | |
| console.log('Creating "vouched" branch from main.'); | |
| const { data: mainRef } = await github.rest.git.getRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `heads/${context.payload.repository.default_branch}`, | |
| }); | |
| await github.rest.git.createRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: 'refs/heads/vouched', | |
| sha: mainRef.object.sha, | |
| }); | |
| } else { | |
| throw e; | |
| } | |
| } | |
| 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 on "${branch}" branch: ${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}).`); |