Skip to content

ci(protocol): enforce the frozen service protocol revision with buf breaking - #2299

Draft
flemzord wants to merge 3 commits into
fix/grpcprotocol-back-to-40from
poc/protocol-version-buf-check
Draft

flemzord wants to merge 3 commits into
fix/grpcprotocol-back-to-40from
poc/protocol-version-buf-check

Conversation

@flemzord

@flemzord flemzord commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Stacked on #2298 (freeze the revision at 40). Merge #2298 first: this check compares with the target tip, so against main (revision 41) it correctly reports the 41 → 40 change.

Summary

#2298 freezes pkg/grpcprotocol.Version during the v3 release-candidate phase: service gRPC contract changes must stay compatible instead of bumping the revision. Until now this rule relied only on review. This PR enforces its wire part deterministically.

go run ./scripts/checkprotocolversion --base <sha> fails when:

  1. Version differs from the base, in either direction.
  2. buf breaking (WIRE, SERVICE_NO_DELETE, RPC_NO_DELETE, see misc/proto/buf.yaml) reports a finding on a type reachable from BucketService, ClusterService or RestoreService. A revision bump does not excuse it.
Change on a type reachable from a gated service Result
Add a field PASS
int32 → int64 (same varint wire type) PASS
Delete, renumber, or incompatibly retype a field FAIL
Remove an RPC or a service FAIL
Change Version FAIL
Any change to Raft-only or persisted-only messages PASS (reported as internal-only)

Scope follows #2298: persisted and Raft-only messages keep the unreleased-v3 "remove and realign" rule. A finding is attributed to its innermost enclosing declaration; one that cannot be attributed counts when its file declares a gated type (conservative).

Semantic changes that keep the .proto text unchanged are not detected and remain a review classification.

Wiring

  • .github/workflows/invariants.yml: new step for pull_request and merge_group, compared with the target tip (the PR checkout is the merge commit).
  • scripts/agent-check-pr: protocol-revision gate when misc/proto/** or pkg/grpcprotocol/** changes.
  • flake.nix: buf added to the ci shell (1.72, pinned by the existing flake.lock). Code generation stays on protoc.
  • Docs: "Automated enforcement" section in protocol-compatibility.md; protobuf.md scopes "remove and realign" to persisted and Raft-only messages, which answers the point left open in fix(grpcprotocol): freeze service protocol revision at 40 during RC #2298.

Historical replay

Replayed on the 51 commits that touched misc/proto or pkg/grpcprotocol since the revision was introduced. Under the previous "bump on break" rule, buf found gated wire breaks without a bump in 70eb1d0c0 (#1933, CreatedIndexLog bool ↔ enum swap) and 23ea97c7f (#2169, LedgerLog field deletion), and correctly ignored internal-only breaks in 0dfa940de and a98bed9da. Under the freeze, every historical bump would also fail, as intended.

Open decision

Repository Invariants is not a required status check today (only Tests and Dirty are). The check blocks merges only after a ruleset change, or if it moves into the Dirty job.

Validation

  • bash scripts/agent-check: PASS
  • go test -race ./scripts/checkprotocolversion ./scripts/agentcheckpr: PASS. End-to-end cases drive real git and buf on a throwaway repository: addition, int32 → int64, retype, deletion, RPC removal, bump, internal-only break.
  • golangci-lint run ./scripts/checkprotocolversion/...: 0 issues
  • checkprotocolversion --base origin/fix/grpcprotocol-back-to-40: PASS; --base origin/main: FAIL on 41 → 40, as expected

https://claude.ai/code/session_01HxBUjBS5Loz1nkQZVxW1Mf

Add scripts/checkprotocolversion, which compares the worktree with a base
commit and fails when:

- pkg/grpcprotocol.Version decreases or moves by more than one;
- buf breaking (WIRE, SERVICE_NO_DELETE, RPC_NO_DELETE) reports a change
  on a type reachable from BucketService, ClusterService or RestoreService
  without a +1 revision.

Findings on Raft-only or persisted-only messages are ignored by walking the
gated services' type graph. The check runs in the invariants workflow for
pull requests and merge-queue entries, and in agent-check-pr when
misc/proto or pkg/grpcprotocol changes. buf joins the ci shell and is used
only for classification; code generation stays on protoc.

Claude-Session: https://claude.ai/code/session_01HxBUjBS5Loz1nkQZVxW1Mf
# pull_request checks out the merge commit, so comparing with the
# current target tip also catches a revision another PR consumed.
PROTOCOL_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
run: nix develop .#ci --command go run ./scripts/checkprotocolversion --base "$PROTOCOL_BASE_SHA"

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.

🟡 [minor] Expose the protocol check through a shared Just recipe

The new CI step invokes go run directly rather than a Just task. RFC-0015 requires CI checks to use the shared Nix → Just task interface; the local gate also duplicates this invocation, leaving two entry points to maintain.

Suggestion: Add a parameterized protocol-revision recipe and invoke it from CI with nix develop --impure .#ci --command just <recipe> "$PROTOCOL_BASE_SHA"; use the same recipe from the local gate.

@NumaryBot NumaryBot added risk: high NumaryBot classified this pull request as high risk. bot-reviewed NumaryBot completed its review workflow for the current head. draft-reviewed NumaryBot reviewed this draft, but published no readiness signal. labels Oct 15, 2026
The service protocol revision is frozen during the v3 release-candidate
phase (#2298). Switch checkprotocolversion from "bump on break" to the
freeze:

- any change to pkg/grpcprotocol.Version fails;
- any buf WIRE/SERVICE_NO_DELETE/RPC_NO_DELETE finding on a type
  reachable from a gated service fails, and a bump no longer excuses it.

Added fields and wire-compatible widenings such as int32 to int64 pass;
Raft-only and persisted-only messages stay out of scope. The docs scope
the "remove and realign" rule to those internal messages.

Claude-Session: https://claude.ai/code/session_01HxBUjBS5Loz1nkQZVxW1Mf
@flemzord
flemzord changed the base branch from main to fix/grpcprotocol-back-to-40 October 15, 2026 12:53
@flemzord flemzord changed the title ci(protocol): enforce the service protocol revision with buf breaking ci(protocol): enforce the frozen service protocol revision with buf breaking Oct 15, 2026
# pull_request checks out the merge commit, so comparing with the
# current target tip also catches a revision another PR consumed.
PROTOCOL_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
run: nix develop .#ci --command go run ./scripts/checkprotocolversion --base "$PROTOCOL_BASE_SHA"

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][blocking] Make the protocol gate a required merge check

I changed GetLedgerRequest.ledger from string to int64 in an isolated checkout: checkprotocolversion correctly failed with FIELD_WIRE_COMPATIBLE_TYPE. But the effective GitHub rules for main require only Tests and Dirty, not invariants; the current target branch fix/grpcprotocol-back-to-40 has no effective rules. Thus a PR with a detected breaking change can still become mergeable once it leaves draft. Require invariants on every protected merge target, or run this gate inside an already required check, and verify that a failing protocol check blocks merge.

@gfyrag

gfyrag commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

[P2][blocking] Re-run the protocol check when the PR base changes

invariants.yml subscribes to pull_request only for opened, synchronize, and reopened. Changing the base branch emits edited, so the protocol check is not re-evaluated against the new target. This PR demonstrates the stale result: its current base is 01104d2 (revision 40), but the completed invariants run used PROTOCOL_BASE_SHA=424c586 (revision 41) and failed; running the same checker against 01104d2 passes. The reverse transition can leave a green check from an old base even when the new base introduces a breaking delta. Add the base-change event to the workflow and verify the check is evaluated against the current base before merge.

This branch has not been deployed

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

Labels

bot-reviewed NumaryBot completed its review workflow for the current head. draft-reviewed NumaryBot reviewed this draft, but published no readiness signal. risk: high NumaryBot classified this pull request as high risk.

Development

Successfully merging this pull request may close these issues.

3 participants