[v0.1/P0] Make protobuf compatibility checks fail against the CI base #19
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis#19
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent: #12 (P0.4). Supersedes the CI-gate part of #5 and #9.
Why
buf breaking --against '.git#ref=HEAD'compares the schema with itself, while the Nix store path skips the check when.gitis absent. A breaking wire edit can therefore pass CI.Scope
mainpushes against the prior commit or latest release tag.Acceptance criteria
Triage plan (2026-09-03)
Source-grounded triage against
mainat69076d42, reviewed through seven rounds of an independent reviewer (pisol/luna) until it passed with no findings. The dependency order that supersedes the tracker's is posted on #12. Where a "Review amendments" section below conflicts with an earlier section, the amendment is the decision.Claim check
Accurate, and slightly worse than stated.
buf breaking --against '.git#ref=HEAD':flake.nix:384-396defines thebuf-breakingpre-commit hook with exactly that--against. As a pre-commit hook on a developer's machine it compares the staged tree with HEAD, which is meaningful. As a CI check it is not:pr.yml:22-29runsnix flake check, whose pre-commit check evaluates a store copy of the tree with no.git, so the guard onflake.nix:390(git rev-parse --is-inside-work-tree || exit 0) exits 0 — the check is skipped, silently, in the only CI job that runs it. The comment atflake.nix:379-383acknowledges this. Even if.gitwere present, the checked-out tree is HEAD in CI, so it would compare the schema with itself. Either way a breaking edit passes.pr.yml:45-52(protojob) says wire-compat "run[s] as pre-commit hooks, so the flake-check job is their CI gate", anddocs/reference/testing.md:1075-1078repeats thatnix flake checkcoversbuf lint / buf breaking. Theprotojob itself only regenerates and diffs (pr.yml:58-63);just proto(justfile:157-160) runsbuf lint+buf generate, neverbuf breaking. So the documented gate does not exist.fetch-depthinpr.yml(onlybench.yml:68setsfetch-depth: 0), so even a corrected--against '.git#ref=<base>'would fail to resolve the base until the workflow fetches it — this is the "missing git history cannot silently turn into success" criterion, and today the failure mode would be a hard error from buf (good) if the guard did not skip first (bad).buf.yaml:12-14usesWIRE_JSON, the right category for a protobuf-binary wire plus the JSON bridge (docs/reference/ipc.md:1624-1630).docs/reference/ipc.md:1738-1752) is prose with no code check thatPROTOCOL_MINOR(crates/felis-protocol/src/preface.rs:57) matches the last row; the row-codec golden vectors are unit tests incrates/felis-grid/src/wire.rs:959-985and the carrier block vector inpreface.rs:614, which run undercargo nextestin thebuildjob — these are already gates but nothing names them as the compatibility gate. The issue's "keep them as separate gates" is accurate.SKIP=buf-breakingat commit time (flake.nix:378,387), invisible to CI.docs/reference/ipc.md:1685-1687states pre-freeze breaks ride major 1 unbumped, which is the policy the acknowledgement must encode.Verdict
accept, with one addition and one clarification.
Addition: the hook's
git rev-parseguard must not be the CI mechanism at all. Move the CI-facing check into a dedicatedpr.ymljob that runsbuf breakinginsidenix developwith an explicit base, and leave the pre-commit hook as the developer-side early warning (itsHEADcomparison is correct there). Do not try to makenix flake checkdo this: the sandbox has no history by design.Clarification on "explicit pre-release acknowledgement": use a file, not a commit-message trailer or a
SKIPenv, because a file is visible to the PR diff, togit log -p, and to the release gate (#18). Recommended:crates/felis-protocol/proto/BREAKING.md(or abreaking:field in a smallproto/compat.toml) that names the base revision the break is acknowledged against; the CI job passes when either buf is clean or the file namesgithub.event.pull_request.base.sha/github.event.beforeexactly. On tag (#18) the file must be absent or reference a revision before the previous tag, and post-tag a break additionally requiresPROTOCOL_MAJORto have moved — the #30 rule.Principles: pure process/CI, no product surface. Pass.
Approach
pr.yml— newproto-compatjob (runs onnix)scripts/proto/compat.sh(new; thescripts/tree already holdsbench/andmake-macos-app.sh):BASE:pull_request→github.event.pull_request.base.sha;pushtomain→github.event.before, falling back to the latestv*tag whenbeforeis the null sha (first push / force) — and once a tag exists, prefergit describe --tags --abbrev=0 HEAD~1so the comparison is against the released baseline, which is what the issue's "prior commit or latest release tag" asks for. Fail with a named error ifBASEcannot be resolved orgit cat-file -e "$BASE^{commit}"fails (this is the "missing history cannot become success" criterion:set -eu, no|| exit 0).buf breaking --against ".git#ref=$BASE"; on failure, accept only if the acknowledgement file names$BASE(exact 40-hex match) and the workflow is not a tag build.Also amend the
protojob comment atpr.yml:45-52and the hook comment atflake.nix:376-383so neither claimsnix flake checkis the CI gate.justfilejust proto-compat [base]: same script, defaulting tomain's merge-base locally (git merge-base HEAD origin/main), so the local runner mirrors CI 1:1 as the header atjustfile:1-10promises.Fixtures / tests
felis.protoand record the red run URL in the issue; a permanent negative fixture would need a second buf module and anexpect-failharness that Forgejo Actions cannot express cleanly. Alternatively,scripts/proto/compat_test.shcan build a temp git repo with two commits (additive vs breaking) and assert the script's exit codes — cheap, hermetic, and runs injust check; recommend this.protojob'sgit diff --cached --exit-code(pr.yml:63); no new work, just cite it.felis-protocolthat parsesdocs/reference/ipc.md's ledger table (or a mirrored const list inpreface.rs) and asserts the highest row equalsPROTOCOL_MINOR. Keeping the ledger in prose only means a minor bump with no row cannot be caught; aconst MINOR_LEDGER: &[(u16, &str)]inpreface.rswith a doc-sync test (same pattern as the config schema sync test atcrates/felis-client-core/src/config.rs:2476) is the smallest form. If #30 drops the ledger for the 2.0 baseline, this becomes a one-row table; keep the test.wire.rs:959-985; name them in testing.md as the codec half of the compatibility gate.Docs cascade
docs/reference/testing.md:1073-1078and the pipeline list: describeproto-compat(base selection, acknowledgement file, tag behavior) and list the three compatibility gates (buf WIRE_JSON, ledger sync test, row-codec/carrier golden vectors) with what each covers.docs/reference/ipc.md:1674-1687"Versioning": one paragraph on how a pre-freeze break is acknowledged (the file) and what changes after the first tag (major bump mandatory).docs/explanation/architecture/ipc.md(schema-evolution section around:896-920): the why — Buf checks field-level wire compatibility, not semantics (renamed meaning, kinds/variants fatal rule atipc.md:1699-1706), which is why the ledger and vectors stay separate; rejected alternatives (SKIP=env: invisible to CI; commit trailer: not diffable;nix flake check: no history in sandbox); "Revisit if" Buf gains a Rust-aware semantic plugin or the wire moves to a registry..claude/skills/extend-ipc/SKILL.mdstep for proto edits: add "runjust proto-compat; for an intended break write the acknowledgement file".CHANGELOG.md: not user-affecting; no entry.skills/felis: no change.Dependencies
felis.protoand should be the first changes the new gate sees.Risk/effort
S–M. One workflow job, one script with a hermetic test, one const + sync test, docs. Main risk: base resolution edge cases on Forgejo (
github.event.beforeon the first push after a force, or a PR from a fork wherebase.shais stale after the base branch moves — use the merge-base ofbase.shaandHEADto be safe). Secondary:fetch-depth: 0cost on a runner with a warm checkout is negligible for this repo's size.Labels
Keep
priority/P0,release/v0.1.0. Suggest adding an acceptance line: "the pre-commit hook's.gitguard is no longer the only CI path;nix flake checkis not claimed as the compatibility gate anywhere indocs/or workflow comments".Review amendments (round 4)
v*tag". A tag exists even when its release run failed, and rehearsal tags are pushed deliberately. The base forproto-compaton a tag build is the schema recorded in a committed baseline filecrates/felis-protocol/proto/baseline/felis.protoplusbaseline/RELEASE(the final release version and commit it was published from), which #18'sreleasejob updates (commits tomain) only after a final (non-prerelease) release publishes successfully. PR/push builds keep the merge-base /event.beforebase. When the baseline file is absent (before the first final release) the workflow requires the committedfirst-releaseacknowledgment instead; that acknowledgment is tied to the absence of the baseline, not to tag ordinality, so rc tags before v0.1.0 do not consume it. Prerelease tags never update the baseline.Review amendments (round 5)
buf.yamlroots the module atcrates/felis-protocol/proto, so a copy inside it would be a duplicate declaration. Store the baseline at repo rootproto-baseline/felis.proto+proto-baseline/RELEASE(outside every Buf module;buf breaking --againsttakes it as a separate input directory). Add a test toscripts/proto/compat.sh's self-test thatbuf linton the module and the baseline comparison both pass with the committed layout.verify-tagqueries the Forgejo release API: (a) if any final release exists,proto-baseline/RELEASEmust name the latest one and its schema must equal that release's attachedfelis.proto(byte compare of the release asset), otherwise fail with "baseline behind the published release; reconcile first"; (b)first-releaseis accepted only when the API reports no final release. The baseline update is therefore reconciled before publication, and a failed post-publish baseline commit blocks the next tag rather than silently narrowing the check.