Skip to content

feat(release): API compatibility gate in go-package-release.yml (ADR-0017) - #22

Merged
boeboe merged 1 commit into
mainfrom
feat/release-api-gate
Nov 14, 2026
Merged

boeboe merged 1 commit into
mainfrom
feat/release-api-gate

Conversation

@boeboe

@boeboe boeboe commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

go-package-release.yml now checks the exported API against the previous published version with a pinned gorelease, and stops before Create and push annotated tag when the version is rejected or the tool fails. This implements ADR-0017 (accepted 2026-11-14) and REL-003 of otfabric/engineering policy 1.6 (companion PR: https://github.com/otfabric/engineering/pull/125).

Today the workflow runs tidy, vet and tests and then tags. It picks the previous version with git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -1, which also matches prerelease tags, and on bump: major it tags v2.0.0 for a module path without /v2.

Behaviour

  • Previous version: the highest exact release tag of this module (vX.Y.Z, or <module-dir>/vX.Y.Z), in the major of its module path, compared by semver. Prereleases, moving tags and other majors' tags never count. A base that is not an ancestor of the release commit, or whose go.mod names another module, fails; no other base is picked.
  • Majors: v2+ needs the /vN module path. A first /vN release is vN.0.0, and first releases use -base=none.
  • Results: pass, incompatible, or error (tool missing, base not downloadable, dirty tree). An error is never read as a pass. With api-compatibility: enforce (the default) the job stops; report warns and releases, and exists for staged adoption only.
  • v0: a patch changes no exported API. A breaking minor or major needs ### Breaking in its RELEASE.md section.
  • Nested modules: module-dir, with tags <dir>/vX.Y.Z.
  • Evidence: release-evidence.json is uploaded as an artifact and attached as a release asset. It records the commit, module, previous and proposed versions, the gorelease result and its changes, Go version, vet/test/race, consumers, exceptions, baseline, release-notes source, and the checks this job does not run (coverage, govulncheck, lint, fuzz, interop).
  • Consumers (optional): a separate read-only job builds, vets and tests direct dependants against the candidate, using a replace in a temporary clone. The read token exists only in the fetch step, and consumer code builds offline. A failure names the consumer, its revision, the version it requires and the candidate commit, and the release job does not run.
  • Permissions: contents: read at workflow level; contents: write only on the release job.
  • New inputs: module-dir, api-compatibility, gorelease-version (exact pseudo-version, never latest), gorelease-go-version, consumers. New output: api-result. Existing callers keep working: the new inputs have defaults and the root-module behaviour is unchanged except for the gate.

Selftest

  • scripts/selftest/release-gate.sh runs 48 checks with the exact step scripts on fixture repositories, with "published" versions in a file:// module proxy; no tag is pushed. It covers:
    • identical patch passes; additive patch, incompatible patch and incompatible minor stop at the gate; additive minor passes;
    • a v1 major without /v2 fails;
    • the v0 rules; first release and first /v2 release;
    • prerelease, moving and other-major tags are ignored; v1.10.0 ranks above v1.9.0;
    • a non-ancestor base and a wrong module path fail;
    • an unpublished base, a missing tool and GOPROXY=off give error, not incompatible;
    • nested modules; report mode; the evidence fields;
    • the gate sits before the tag step, and the tag steps never run after a failure.
  • scripts/selftest/consumers.sh runs 14 checks: a consumer that no longer compiles is reported with its revision; clone failures and path escapes are reported, not skipped; the token is limited to the fetch step.
  • New Selftest jobs: release-gate (Go 1.26, installs the pinned gorelease read from the workflow default) and release-consumers.
  • 18 mutations of the workflow, such as treating a rejection as a pass, ignoring consumers, installing @latest or running the tag step after a failure, each make the selftest fail.
  • Run locally: all eight scripts in scripts/selftest/ pass; the YAML parses; every step passes bash -n; otfabric-policy check reports compliant.
  • Not run: actionlint and shellcheck (not installed), and the new jobs under GitHub Actions. The if: expressions, artifact upload and download, and the release asset are first exercised by this PR's Selftest run.

Release

The maintainer should tag v3.8.0 after merge. v3.7.0 (6334e54) has no gate, and REL-003 requires v3.8.0 or later.

🤖 Generated with Claude Code

…0017)

go-package-release.yml checks the exported API against the previous
published version with a pinned gorelease and stops before the tag step
when the version is rejected or the tool fails (otfabric/engineering
ADR-0017, REL-003).

- Previous version: the highest exact release tag of this module in the
  major of its module path, by semver; prerelease, moving and other-major
  tags never count. A base that is not an ancestor of the release commit,
  or whose go.mod names another module, fails; no other base is chosen.
- Majors: v2+ needs the /vN module path; a first /vN release is vN.0.0.
  First releases use -base=none.
- Results: pass, incompatible, or error (tool not installed, base not
  downloadable, dirty tree), never read as a pass. api-compatibility:
  enforce (default) stops; report warns, for staged adoption.
- v0: a patch changes no exported API; a breaking minor or major needs a
  "### Breaking" heading in its RELEASE.md section.
- Nested modules: module-dir, tags <dir>/vX.Y.Z.
- release-evidence.json (artifact and release asset): commit, module,
  versions, gorelease result and changes, Go, vet/test/race, consumers,
  exceptions, baseline, and the checks this job does not run.
- consumers: optional read-only job that builds, vets and tests direct
  dependants against the candidate; the token exists only in the fetch
  step, and consumer code builds offline. A failure names the consumer,
  its revision, the required version and the candidate commit.
- Workflow permissions drop to contents: read; the release job keeps write.

Selftest: scripts/selftest/release-gate.sh (48 checks over the D7 cases,
file:// module proxy, no tag pushed) and consumers.sh (14 checks); new
jobs release-gate and release-consumers. README documents the inputs, the
gate and the api-result output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@boeboe
boeboe merged commit b6030b4 into main Nov 14, 2026
45 checks passed
@boeboe
boeboe deleted the feat/release-api-gate branch November 14, 2026 02:00
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