fix(ci): fetch author_association via REST API instead of webhook payload #39
Workflow file for this run
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 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', | |
| }); |