Skip to content

ci: apidiff API-compatibility gate with golden baselines#127

Merged
joaomdsg merged 1 commit into
mainfrom
feat/apidiff-gate
Jun 11, 2026
Merged

ci: apidiff API-compatibility gate with golden baselines#127
joaomdsg merged 1 commit into
mainfrom
feat/apidiff-gate

Conversation

@joaomdsg

Copy link
Copy Markdown
Member

Finding #15 — make the stability contract mechanical

docs/stability.md defines the pre-1.0 stable Core vs EXPERIMENTAL surface, but it was review-hope: nothing stopped an accidental incompatible change to the public API from landing. This adds a CI gate that enforces "no silent public break" mechanically.

The gate

ci-check.sh gains an API compatibility step that, for each committed golden baseline, runs golang.org/x/exp/cmd/apidiff against the current package and fails closed on any INCOMPATIBLE change. Additive (compatible) changes pass.

  • apidiff version pinned (v0.0.0-20260603202125-055de637280b), installed on demand — same pattern as the existing golangci-lint / govulncheck installs.
  • apidiff exits 0 even when it reports incompatible changes, so the gate greps its output for the Incompatible changes: header and fails the build if found.

Golden baselines

Committed under api/ for the public, importable packages: github.com/go-via/via and its /h, /on, /sess, /mw. internal/, viashowcase, plugin example dirs, and test-only packages are excluded — not part of the importable surface.

The baselines are gob-encoded binary (apidiff's on-disk format), marked binary in .gitattributes. They are not caught by the existing "no committed binaries" check (that matches executable mime types, not data).

Regen workflow

./api/regen.sh rewrites all baselines. An intentional public change is landed by regenerating + committing the baseline; the source diff is the readable review artifact, and the baseline changing signals the API moved.

Whole-package caveat

apidiff works at whole-package granularity — it cannot subset stable-Core from EXPERIMENTAL symbols. So the gate is a no-silent-public-break detector; the Core-vs-experimental tier call is made in review against docs/stability.md (documented in the new "Enforcing the contract" section).

Gate-behavior proofs

Incompatible → FAILS (scratch edit changing via.New signature, then reverted):

ERROR: INCOMPATIBLE API change in github.com/go-via/via vs api/via.api:
Incompatible changes:
- New: changed from func(...Option) *App to func(int, ...Option) *App
API step exit=1

Additive → PASSES (scratch new exported func ScratchAdded(), then reverted): API step exit=0.

Regen reproduces baselines byte-for-byte (no dirty diff after rerun) and full ./ci-check.sh passes green.

Make the pre-1.0 stability contract in docs/stability.md mechanical
(critic-fix #15). ci-check.sh now diffs each public importable package
against a committed golden baseline via golang.org/x/exp/cmd/apidiff and
fails closed on any INCOMPATIBLE change; additive changes pass.

- Pin apidiff version, install on demand (matching golangci/govulncheck).
- Commit golden baselines under api/ for via, h, on, sess, mw.
- api/regen.sh rewrites the baselines for intentional public changes.
- apidiff exits 0 even on breaks, so the gate greps the
  "Incompatible changes:" header and fails closed.
- Document the regen workflow and the whole-package caveat: the gate is a
  no-silent-public-break detector; the Core-vs-experimental tier call is
  made in review against docs/stability.md.
@joaomdsg
joaomdsg merged commit 0e52c8f into main Jun 11, 2026
6 checks passed
@joaomdsg
joaomdsg deleted the feat/apidiff-gate branch June 11, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant