ci: enforce PMC vote on format-spec changes via CI gate#7399
Conversation
Format-specification changes (the `.proto` definitions and `docs/src/format/**`) require 3 binding +1 votes from PMC members, but nothing structurally enforced it — the requirement was only a reminder comment, caught socially if at all. This adds a CI gate that blocks merging a format-spec change until it has the required votes. A touched PR is labelled `format-change` and the gate publishes a `format-spec-vote` commit status that stays red until: - 3 PMC members have approved the PR (excluding the author), counted only on the latest commit so new pushes invalidate stale approvals; - no PMC member has an outstanding "Request changes" review (a veto); and - the 1-week voting period (from when the label was first applied) has elapsed. The gate posts a live tally comment and runs on PR events, review events, and an 8-hourly cron (so the voting-period clock is re-checked even when no event fires). It uses pull_request_target to label/status fork PRs but never checks out or runs PR code. A PMC member can waive a trivial edit by removing the `format-change` label. The PMC roster moves to `docs/src/community/pmc.yaml` as the source of truth; `ci/sync_pmc_docs.py` regenerates the table in pmc.md (checked in docs-check), and the gate reads the roster from the base checkout so a PR cannot enlarge the electorate. The old reminder-only job is removed. To activate, add `format-spec-vote` as a required status check on protected branches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
question: does this script need to be run manually? Or can we integrate with the docs framework to create the table on-build?
There was a problem hiding this comment.
Good call — switched to generating the table at docs build time via an MkDocs hook (docs/hooks/pmc_roster.py), following the same build-time pattern as the existing mkdocs_protobuf plugin. pmc.yaml stays the source of truth, pmc.md just holds a <!-- PMC_ROSTER_TABLE --> placeholder, and ci/sync_pmc_docs.py (and its docs-check step) are gone. No manual run needed.
There was a problem hiding this comment.
issue(blocking): Could we rewrite this script in Python? More of the maintainers are familiar with Python so it would be easy to read. I believe GHA has the GH python package pre-installed.
There was a problem hiding this comment.
Done — rewritten in Python as ci/format_vote_gate.py using PyGithub (pip-installed in the workflow). Pure vote-counting logic (tally_reviews, decide_verdict) is unit-tested with pytest in ci/test_format_vote_gate.py.
There was a problem hiding this comment.
issue(blocking): do we not filter just for PRs that have no format-change label? I think we should leave those PRs alone.
There was a problem hiding this comment.
Agreed. The gate now keys off the format-change label only. Labeling moved to the existing path labeler (.github/labeler-area.yml), and the gate leaves unlabeled PRs alone — for non-format PRs it posts just a passing format-spec-vote status and nothing else (needed so the required check never blocks them). Trivial edits are now waived by a PMC applying a format-waived label, rather than removing format-change (the path labeler would otherwise re-add it on the next push).
| schedule: | ||
| # Re-evaluate open format-change PRs so the voting-period clock is re-checked | ||
| # even when no PR event fires (e.g. the 1-week period elapses days after the | ||
| # third approval lands). | ||
| - cron: "0 */8 * * *" |
There was a problem hiding this comment.
issue(blocking): how does this trigger find which PRs to look at? IIRC this trigger isn't PR specific, but runs on a schedule globally for the whole repo, am I wrong?
There was a problem hiding this comment.
You're right — the schedule trigger runs repo-wide, with no PR in context. The script handles that by listing all open PRs carrying the format-change label and re-evaluating each (so the voting-period clock advances even if no PR event fires). The PR/review event paths use the PR from the event payload. See the event_name == 'schedule' branch in main().
Responds to PR review feedback: - Rewrite the gate in Python (PyGithub) instead of JS, for readability — most maintainers are more familiar with Python. - Apply the `format-change` label via the existing path labeler (.github/labeler-area.yml) instead of the gate. The gate now reads the label and leaves PRs without it alone, posting only a passing status (so the required check never blocks non-format PRs). Trivial edits are waived with a `format-waived` label applied by a PMC member. - Render the PMC roster table at docs build time via an MkDocs hook (docs/hooks/pmc_roster.py) instead of a committed table kept in sync by a script; pmc.yaml stays the source of truth. Drops ci/sync_pmc_docs.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the accepted part of #7112: a CI gate that structurally blocks merging a format-specification change until it has the required PMC votes, replacing the previous reminder-only comment that was enforced socially, if at all.
How it works
The path labeler (
.github/labeler-area.yml) applies theformat-changelabel to PRs that touch the format spec (protos/**/*.protoordocs/src/format/**). The gate (ci/format_vote_gate.py, run byformat-vote-gate.yml) reads that label and publishes aformat-spec-votecommit status that stays red until all hold:-1veto cannot be overruled).format-changelabel was first applied.It posts/updates a single live-tally comment and runs on PR events, review events, and an 8-hourly
cron. The cron has no PR context, so it sweeps every openformat-changePR — this is what re-checks the voting-period clock when no PR event fires (e.g. the week elapses days after the third approval). It usespull_request_targetso it can status/comment fork PRs, but never checks out or runs PR code — it reads the trusted base checkout and the API only.PRs without the
format-changelabel are left alone: the gate posts only a passingformat-spec-votestatus (so the required check never blocks unrelated PRs) and does nothing else. A PMC member waives a trivial edit (typo, wording, formatting) by applying theformat-waivedlabel.PMC roster
docs/src/community/pmc.yamlis the source of truth. The roster table inpmc.mdis rendered at docs build time by an MkDocs hook (docs/hooks/pmc_roster.py), and the gate reads the roster from the base checkout so a PR can't enlarge the electorate by editing it.Tests
ci/test_format_vote_gate.py(pytest) covers the pure vote-counting logic — latest-review-wins, stale-approval filtering, author/non-PMC/dismissed exclusion, and verdict priority — run byci-scripts.yml.Activation (after merge)
Add
format-spec-voteas a required status check on protected branches (main+ release branches). It's posted on every PR — success immediately for non-format PRs — so it never leaves a required check pending.Note
On this PR itself the gate run errors red, because
pull_request_targetchecks out the base branch, which doesn't yet containci/format_vote_gate.py. That resolves once merged; don't make the check required until then.🤖 Generated with Claude Code