Skip to content

ci: enforce PMC vote on format-spec changes via CI gate#7399

Draft
wjones127 wants to merge 2 commits into
lance-format:mainfrom
wjones127:format-spec-vote-gate
Draft

ci: enforce PMC vote on format-spec changes via CI gate#7399
wjones127 wants to merge 2 commits into
lance-format:mainfrom
wjones127:format-spec-vote-gate

Conversation

@wjones127

@wjones127 wjones127 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

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 the format-change label to PRs that touch the format spec (protos/**/*.proto or docs/src/format/**). The gate (ci/format_vote_gate.py, run by format-vote-gate.yml) reads that label and publishes a format-spec-vote commit status that stays red until all hold:

  • 3 binding +1 votes — three PMC members have approved the PR, excluding the author, counted only on the latest commit (new pushes invalidate stale approvals).
  • No veto — no PMC member has an outstanding "Request changes" review (a -1 veto cannot be overruled).
  • 1-week voting period — measured from when the format-change label 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 open format-change PR — 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 uses pull_request_target so 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-change label are left alone: the gate posts only a passing format-spec-vote status (so the required check never blocks unrelated PRs) and does nothing else. A PMC member waives a trivial edit (typo, wording, formatting) by applying the format-waived label.

PMC roster

docs/src/community/pmc.yaml is the source of truth. The roster table in pmc.md is 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 by ci-scripts.yml.

Activation (after merge)

Add format-spec-vote as 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_target checks out the base branch, which doesn't yet contain ci/format_vote_gate.py. That resolves once merged; don't make the check required until then.

🤖 Generated with Claude Code

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>
@github-actions github-actions Bot added A-docs Documentation A-ci CI / build workflows ci Github Action or Test issues labels Jun 22, 2026
Comment thread ci/sync_pmc_docs.py Outdated

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.

question: does this script need to be run manually? Or can we integrate with the docs framework to create the table on-build?

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.

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.

Comment thread ci/format_vote_gate.js Outdated

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.

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.

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.

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.

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.

issue(blocking): do we not filter just for PRs that have no format-change label? I think we should leave those PRs alone.

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.

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

Comment on lines +28 to +32
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 * * *"

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.

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?

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.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ci CI / build workflows A-docs Documentation ci Github Action or Test issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant