Skip to content

feat(ci): gate external pull requests behind a vouch list - #2947

Open
jordan-simonovski wants to merge 1 commit into
mainfrom
jordansimonovski/hdx-5122-vouch
Open

feat(ci): gate external pull requests behind a vouch list#2947
jordan-simonovski wants to merge 1 commit into
mainfrom
jordansimonovski/hdx-5122-vouch

Conversation

@jordan-simonovski

@jordan-simonovski jordan-simonovski commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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

  • PRs are gated. On open, reopen and ready-for-review, the author is checked against .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.
  • Issues are not gated, on purpose. Bug reports from strangers are worth having, and an issue is how someone introduces themselves to get vouched in the first place.
  • Maintainers manage the list from comments/vouch @user, /unvouch @user, /denounce @user on any issue or PR. The bot opens a PR against the list; merging it applies the change.
  • The list is seeded with 47 handles: the hyperdxio org plus every external contributor with two or more merged PRs, so no established contributor hits the gate on their next PR.
  • Two issue templates, introduce-yourself (the on-ramp the close comment links to) and bug-report. The repo had none before, so /issues/new now shows a chooser; blank issues remain enabled.

Key decisions

  • Auto-close rather than label-and-warn. Labelling leaves the PRs in the queue, which is the thing we're trying to fix. The close comment is written to make the recovery path obvious and the tone is deliberately apologetic.
  • Two merged PRs as the seed threshold, not one. One merged PR includes the drive-by contributions this is meant to filter, including the author who prompted the ticket. The 41 single-PR authors can each be vouched with one comment if they return.
  • GITHUB_TOKEN with PR mode, not a GitHub App. An App would let the bot commit straight to protected main, 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.
  • Actions pinned to a commit SHA rather than the floating @v1, since the manage job hands the action a contents: write token 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, -handle to 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

  • Outside contributors need one introduction issue before their first PR. Everyone already contributing regularly is on the seed list.
  • The introduction label doesn't exist yet. GitHub silently drops unknown labels from issue templates, so introduction issues arrive unlabelled until someone creates it.
  • Known gap: claude-code-review.yml fires on the same pull_request_target event with allowed_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.
  • Currently-open external PRs are untouched; the check only fires on open, reopen and ready-for-review. Note that closing and reopening an old external PR to re-trigger CI will now auto-close it.
  • external-contributor-alerts.yml still 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-pr needs only contents: read + pull-requests: write while manage needs write on three scopes.

Points worth a look:

  • check-pr checks out the default branch explicitly. On pull_request_target the default checkout is the base branch, which is safe, but a PR targeting v1 would check out a branch with no template file and error, leaving the gate open. persist-credentials: false since the action takes its token from the environment.
  • Drafts are skipped and re-checked on ready_for_review, matching external-contributor-alerts.yml. Without this a draft external PR is closed while the Slack alert — which skips drafts and waits for ready_for_review — never fires at all.
  • The manage job is role-gated in the if: 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 than startsWith() so a leading space doesn't silently drop a real command.
  • Concurrency is keyed per comment. A shared group lets a newly queued run cancel a pending one, which would drop a vouch with no feedback anywhere. Each run cuts its own branch, so there's nothing to serialise.
  • A failure() step comments back on the issue. The action is silent on every failure path and issue_comment runs aren't surfaced on the issue timeline, so a maintainer would otherwise assume a failed /vouch had worked.
  • The list is seeded lower-case. Vouch lower-cases handles when it rewrites the file, so seeding mixed case would turn the first bot edit into a diff that looks like it renamed six people. Matching is case-insensitive either way.
  • copilot is on the list. Vouch only auto-exempts logins ending in [bot]; GitHub's Copilot coding agent has the login Copilot (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.md exempts CI and docs changes.

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.
@changeset-bot

changeset-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 22c7f6c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hyperdx-oss Ignored Ignored Aug 20, 2026 2:21am
hyperdx-storybook Ignored Ignored Aug 20, 2026 2:21am

Request Review

@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label Aug 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔵 Tier 2 — Low Risk

Small, isolated change with no API route or data model modifications.

Why this tier:

  • Standard feature/fix — introduces new logic or modifies core functionality

Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.
SLA: Resolve within 4 business hours.

Stats
  • Production files changed: 1
  • Production lines changed: 74
  • Branch: jordansimonovski/hdx-5122-vouch
  • Author: jordan-simonovski

To override this classification, remove the review/tier-2 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The 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.

  • Adds a pull-request-target check for external pull-request authors.
  • Adds /vouch, /unvouch, and /denounce management through issue comments.
  • Seeds the contributor trust list and documents the onboarding process.
  • Adds introduction and bug-report issue templates.

Confidence Score: 4/5

The 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

Important Files Changed

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
Loading

Fix all with Greploop Fix All in Claude Code Fix All in Conductor Fix All in Cursor Fix All in Codex

Reviews (1): Last reviewed commit: "feat(ci): gate external pull requests be..." | Re-trigger Greptile

Comment on lines +84 to +91
- 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
- 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

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

Copy link
Copy Markdown
Contributor

Deep Review

Scope: 7 files vs base 68d2ed2 — a new pull_request_target + issue_comment workflow (vouch.yml), two issue templates, a 47-handle seed list, a close-comment template, and CONTRIBUTING/README docs. Mode: report-only.

Intent: Gate external pull requests behind a mitchellh/vouch trust list, auto-closing PRs from unvouched non-write-access authors and letting maintainers manage the list via /vouch /unvouch /denounce comments.

✅ No critical issues found. The workflow is defensively sound: check-pr never checks out fork code (ref: default_branch, persist-credentials: false), manage interpolates only trusted context.* values (no script/shell injection surface), the author_association gate is server-set and re-checked authoritatively by the pinned action, the third-party action is SHA-pinned, and the seed list is verified sorted, lowercase, and duplicate-free (47 handles). The /vouch-vs-/unvouch substring concern is a non-issue — the leading slash means /unvouch does not contain /vouch. The no-changeset decision is correct (AGENTS.md line 218 exempts CI and docs). No P0/P1 findings.

🟡 P2 -- recommended

  • .github/workflows/vouch.yml:22 -- Closing an unvouched PR does not cancel the other workflows the same webhook already dispatched, so claude-code-review.yml (fork HEAD checked out with contents: write + ANTHROPIC_API_KEY) and main.yml CI still run on untrusted fork code before closure; drafts are skipped entirely and never closed while CI keeps running on each push. This is pre-existing sibling-workflow behavior surfaced by the new gate, not a regression, but it means the gate does not achieve "vouched before we review their code."
    • Fix: File a follow-up to gate claude-code-review.yml and secret-bearing/CI workflows on the same vouch/author_association check so untrusted fork code and API spend are withheld until the trust decision resolves.
    • security, adversarial
  • .github/workflows/vouch.yml:49 -- The security-critical check-pr job has no failure() notifier and fails open silently: if the vouch action errors (checkout failure, API rate limit, template missing), the unvouched PR stays open with no maintainer alert, whereas the manage job does notify on failure.
    • Fix: Add an if: failure() annotation/comment step to check-pr mirroring the manage job so a gate that did not evaluate is visible rather than silently permissive.
    • reliability, testing
🔵 P3 nitpicks (8)
  • .github/vouch-unvouched-pr.md:1 -- The placeholders {author}, {owner}, {repo}, {default_branch} are unverified against the substitution names mitchellh/vouch@v1.5.0 actually uses; a wrong token renders literal braces or a broken URL in the close comment shown to every rejected contributor.
    • Fix: Confirm the exact placeholder names against the pinned action's template contract, or trigger one real close on a scratch fork to verify rendering.
  • .github/ISSUE_TEMPLATE/introduce-yourself.md:5 -- labels: introduction references a label that does not exist yet; GitHub silently drops it, so introduction issues open unlabeled.
    • Fix: Create the introduction label in the repo or remove the labels: line until it exists.
  • .github/workflows/vouch.yml:49 -- The pinned vouch SHA d66fa29… is duplicated at lines 49 and 84; a manual bump that edits one occurrence leaves the two steps on mismatched action versions with no error.
    • Fix: Keep the two references in sync on every bump, or note the coupling so a hand-edit updates both.
  • .github/workflows/vouch.yml:82 -- The write-token manage job pins first-party actions to floating major tags (actions/checkout@v6, actions/github-script@v9) while SHA-pinning the third-party action, inconsistent within a contents: write/issues: write/pull-requests: write job.
    • Fix: SHA-pin actions/checkout and actions/github-script to match the pinning discipline applied to the vouch action.
  • .github/workflows/vouch.yml:29 -- check-pr has no concurrency group, so rapid reopened/ready_for_review events or reopen-spam of an auto-closed PR can run auto-close concurrently and post duplicate close comments plus repeated CI.
    • Fix: Add a per-PR concurrency group (as external-contributor-alerts.yml does) and confirm whether the vouch action already dedupes its close comment.
  • .github/workflows/vouch.yml:66 -- The manage if: matches /vouch anywhere in the comment body, but the action requires the keyword first on the first line; a conversational mention (e.g. "I'll /vouch them later") spawns the job, no-ops, and — if the action exits zero — never fires the failure() notifier, so a maintainer may believe a vouch succeeded.
    • Fix: Verify the action exits non-zero on a non-command match, or add a success reaction/ack so absence of acknowledgement signals a no-op.
  • .github/workflows/vouch.yml:62 -- The role gate accepts COLLABORATOR/MEMBER, which includes read/triage-only collaborators and public org members without push access, letting them spawn the contents: write manage runner via a keyword comment; impact is bounded because the action re-checks permission authoritatively before editing the list.
    • Fix: Optionally gate on an authoritative permission check before the job starts rather than on author_association.
  • .github/workflows/vouch.yml:30 -- The draft-skip and trigger-type logic is duplicated with external-contributor-alerts.yml and coupled only by a comment; the two must agree on "what counts as an external PR" but nothing enforces it, so a future edit to one can silently diverge the gate from the Slack alert.
    • Fix: Cross-reference each workflow's header from the other so a future edit updates both.

Reviewers (9): correctness, security, adversarial, reliability, maintainability, project-standards, testing, agent-native, learnings-researcher.

Testing gaps: No actionlint or dry-run validates the if: gating and contains(fromJSON(...)) expressions before they run in production; a malformed gate expression degrades open (permissive). The 47 seeded handles are not validated against real GitHub logins by any checked-in artifact; a typo silently auto-closes a legitimate contributor. Core gate behavior (auto-close on unvouched, re-check on reopen, dependence on the action's default .github/VOUCHED.td path) is delegated entirely to the third-party action and is not exercised end-to-end.

@github-actions

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 305 passed • 1 skipped • 1137s

Status Count
✅ Passed 305
❌ Failed 0
⚠️ Flaky 1
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant