feat(ci): gate external pull requests behind a vouch list - #2947
feat(ci): gate external pull requests behind a vouch list#2947jordan-simonovski wants to merge 1 commit into
Conversation
Pull requests from anyone without write access are now closed automatically unless the author is listed in .github/VOUCHED.td. Issues stay open to everyone — opening one is how a contributor introduces themselves and gets vouched. Maintainers manage the list with /vouch, /unvouch and /denounce comments. The bot opens a PR against the list; that PR needs an approval rather than a green CI run, since GitHub does not run workflows on PRs it creates. Seeded with the hyperdxio org plus every external contributor with two or more merged pull requests.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
🔵 Tier 2 — Low RiskSmall, isolated change with no API route or data model modifications. Why this tier:
Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns. Stats
|
Greptile SummaryThe PR introduces a trust-list gate that closes pull requests from unvouched external authors and adds comment-driven list management, contributor documentation, and issue templates. The pull-request check is wired to the default-branch trust list, but the management action is missing a required input and cannot process maintainer commands.
Confidence Score: 4/5The pull request should not merge until the missing repository input is supplied, because all comment-based vouch management currently fails. The pull-request gate itself is coherently configured, but the only advertised mechanism for maintainers to update its trust list fails action input validation before making any change. Files Needing Attention: .github/workflows/vouch.yml
|
| Filename | Overview |
|---|---|
| .github/workflows/vouch.yml | Adds the pull-request gate and management jobs, but the management action omits its required repository input. |
| .github/VOUCHED.td | Adds the seeded, normalized trust list consumed from the action's default path. |
| .github/vouch-unvouched-pr.md | Adds a clear closure message and recovery path for unvouched authors. |
| CONTRIBUTING.md | Documents contributor onboarding and maintainer list-management commands. |
| .github/ISSUE_TEMPLATE/introduce-yourself.md | Adds the introduction issue used as the onboarding path. |
| .github/ISSUE_TEMPLATE/bug-report.md | Adds a structured public bug-report template. |
| README.md | Adds a concise explanation of the vouch requirement and links to contributor guidance. |
Sequence Diagram
sequenceDiagram
participant M as Maintainer
participant GH as GitHub Actions
participant V as manage-by-issue
participant L as VOUCHED.td
M->>GH: "Comment /vouch @user"
GH->>V: Invoke without required repo input
V--xGH: Input validation failure
GH-->>M: Post command-failed comment
Note over L: Trust list remains unchanged
Reviews (1): Last reviewed commit: "feat(ci): gate external pull requests be..." | Re-trigger Greptile
| - uses: mitchellh/vouch/action/manage-by-issue@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0 | ||
| with: | ||
| issue-id: ${{ github.event.issue.number }} | ||
| comment-id: ${{ github.event.comment.id }} | ||
| vouch-keyword: /vouch | ||
| denounce-keyword: /denounce | ||
| unvouch-keyword: /unvouch | ||
| pull-request: true |
There was a problem hiding this comment.
Required repository input is missing
When an authorized maintainer submits a vouch-management command, manage-by-issue receives no required repo input, causing action validation to fail before the trust list is updated.
| - uses: mitchellh/vouch/action/manage-by-issue@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0 | |
| with: | |
| issue-id: ${{ github.event.issue.number }} | |
| comment-id: ${{ github.event.comment.id }} | |
| vouch-keyword: /vouch | |
| denounce-keyword: /denounce | |
| unvouch-keyword: /unvouch | |
| pull-request: true | |
| - uses: mitchellh/vouch/action/manage-by-issue@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0 | |
| with: | |
| issue-id: ${{ github.event.issue.number }} | |
| comment-id: ${{ github.event.comment.id }} | |
| repo: ${{ github.repository }} | |
| vouch-keyword: /vouch | |
| denounce-keyword: /denounce | |
| unvouch-keyword: /unvouch | |
| pull-request: true |
There was a problem hiding this comment.
This is incorrect. Looking at the SHA for v1.5.0, the repo is not a requirement as part of the action.
Only comment-id and issue-id are required: true.
The action resolves the repo itself using ${{ inputs.repo || github.repository }}
There was a problem hiding this comment.
You're right, I apologize. I've verified against the action source at the pinned SHA — repo is required: false with an empty default, and the implementation falls back to ${{ inputs.repo || github.repository }} automatically. The original code is correct as-is and no change is needed.
Deep ReviewScope: 7 files vs base Intent: Gate external pull requests behind a ✅ No critical issues found. The workflow is defensively sound: 🟡 P2 -- recommended
🔵 P3 nitpicks (8)
Reviewers (9): correctness, security, adversarial, reliability, maintainability, project-standards, testing, agent-native, learnings-researcher. Testing gaps: No |
E2E Test Results✅ All tests passed • 305 passed • 1 skipped • 1137s
Tests ran across 4 shards in parallel. |
External pull requests are now closed automatically unless the author is on a trust list at
.github/VOUCHED.td. This is a response to the run of agent-generated PRs against core code we saw recently. The team is small and large PRs cost more to review than they might be worth. Issues stay open to everyone, and getting vouched is one introduction issue.Closes HDX-5122.
What changed
.github/VOUCHED.td. Unvouched authors get a comment pointing at the introduction issue template and the PR is closed; their branch is untouched and reopening after being vouched passes. Bots and anyone with write access are never affected, so nothing changes for the team./vouch @user,/unvouch @user,/denounce @useron any issue or PR. The bot opens a PR against the list; merging it applies the change.hyperdxioorg plus every external contributor with two or more merged PRs, so no established contributor hits the gate on their next PR.introduce-yourself(the on-ramp the close comment links to) andbug-report. The repo had none before, so/issues/newnow shows a chooser; blank issues remain enabled.Key decisions
GITHUB_TOKENwith PR mode, not a GitHub App. An App would let the bot commit straight to protectedmain, but needs org admin and two secrets. PR mode needs neither and gives a human a look at every list change. The tradeoff is that the bot's PR needs an approval instead of a green CI run — GitHub doesn't run workflows on PRs it creates. The changesets release PR already works this way.@v1, since the manage job hands the action acontents: writetoken on a trigger any commenter can reach.Background
Vouch is a small trust-list system by Mitchell Hashimoto, currently used by Ghostty. The list is a flat text file. one GH handle per line,
-handleto block someone, and a set of GitHub Actions that check authors against it. It carries no policy of its own; who gets vouched and what a vouch permits are entirely ours. It's worth being clear that this is not a security control: a vouched person gains the ability to open a PR at all, and review and merge permissions are unchanged.Impact
introductionlabel doesn't exist yet. GitHub silently drops unknown labels from issue templates, so introduction issues arrive unlabelled until someone creates it.claude-code-review.ymlfires on the samepull_request_targetevent withallowed_non_write_users: '*', so an unvouched fork PR still gets a full Claude review before vouch closes it. That's pre-existing and the flag looks deliberate, so it's left alone here — worth a follow-up ticket to gate it on vouch status.external-contributor-alerts.ymlstill posts to Slack for external PRs regardless of vouch status, so expect alerts for PRs that get closed moments later.Implementation detail
Both jobs live in one workflow with per-job permissions, since
check-prneeds onlycontents: read+pull-requests: writewhilemanageneeds write on three scopes.Points worth a look:
check-prchecks out the default branch explicitly. Onpull_request_targetthe default checkout is the base branch, which is safe, but a PR targetingv1would check out a branch with no template file and error, leaving the gate open.persist-credentials: falsesince the action takes its token from the environment.ready_for_review, matchingexternal-contributor-alerts.yml. Without this a draft external PR is closed while the Slack alert — which skips drafts and waits forready_for_review— never fires at all.managejob is role-gated in theif:before the runner starts. The action re-checks the commenter's permission authoritatively; the workflow-level check just stops anyone on the internet from starting a write-token runner.contains()rather thanstartsWith()so a leading space doesn't silently drop a real command.failure()step comments back on the issue. The action is silent on every failure path andissue_commentruns aren't surfaced on the issue timeline, so a maintainer would otherwise assume a failed/vouchhad worked.copilotis on the list. Vouch only auto-exempts logins ending in[bot]; GitHub's Copilot coding agent has the loginCopilot(confirmed on Make @hyperdx/common-utils forward-compatible with @clickhouse/client 1.23 #2500), so without an entry its PRs would be closed.Reviewed with four parallel reviewers covering Actions security, action wiring, adversarial abuse paths and repo standards. All 47 seeded handles were checked against the GitHub API. No changeset —
AGENTS.mdexempts CI and docs changes.