Versioning and release policy¶
Decided 2026-10-15. This binds releases, the images, the fix looper and every consumer of tacplus.
Versions¶
tacplus releases are MAJOR.MINOR.PATCH git tags vMAJOR.MINOR.PATCH
(.github/workflows/release.yml builds the binaries from the tag).
- A patch is fixes only. A new feature, a new annotation, a new generated surface or a changed generated surface never goes into a patch. A patch changes behaviour only where the previous behaviour was a defect.
- Features come in on a minor. Anything a consumer would have to learn
about lands in
MAJOR.MINOR+1.0. - A major is a breaking change to what consumers already generate against. Generated code is regenerated by its consumers; there is no deprecation window (a window is hand-maintained compatibility). The major says so.
What consumers pin¶
A consumer pins major and minor, never patch. 2.1 means "the newest
2.1.x", and every 2.1.x must be safe to take without reading the notes,
because a patch is fixes only. Pinning a patch is allowed for reproducing a
problem, never for staying on a known defect.
Backports¶
Every fix is made on main first. It is then backported to every minor
released within the last twelve months, as its own patch release of that
minor. A minor older than a year is out of support: it gets no patches, and
a consumer still on it upgrades.
Each supported minor has a branch release/MAJOR.MINOR, cut at its .0 tag
(or at its first good patch). A patch release is a tag on that branch. The
branch is protected: changes land only by pull request.
Supported today: release/2.0 (cut at v2.0.1; 2.0.0 published images but
its release build failed, so 2.0.1 is the first usable 2.0 release). Consumers
pin 2.0: the tacplus:2.0 image and tacplus upgrade both follow its
newest patch. The fix looper opens one pull
request against main and one against each supported release branch; the
release-branch pull request carries only the fix and its test.
Images¶
Three images. build is FROM base; tacplus copies its tools out of
build onto a slim OS, so a consumer pulls a few hundred megabytes, not the
SDKs:
| image | contents | rebuilt when |
|---|---|---|
ghcr.io/tacplus-dev/tacplus/base |
OS and every SDK tacplus is built and tested with: Go, Java (JDK, Maven), Rust, Python, Node (TypeScript), Swift, .NET | on a schedule, or when an SDK pin changes |
ghcr.io/tacplus-dev/tacplus/build |
base plus every tool: protoc and every plugin, grpcio-tools, JBMC and OpenJML, just, the docker CLI, gcloud. The one image the gates run in: CI, the nightly gates, just gates-container. Not for consumers. |
when a tool pin changes |
ghcr.io/tacplus-dev/tacplus/tacplus |
the tacplus binary and the protoc plugins for every language that needs its own (Go, Java, TypeScript, Rust, Swift, Python); C# needs none, its generated project runs Grpc.Tools from NuGet | every push to main and every release |
A consumer's build does not start FROM the tacplus image. It runs it as its
own step (tacplus build, writing the generated model) and compiles the
result in its own builder image, so a new tacplus patch is picked up by the
generate step pulling tacplus:X.Y again, without rebuilding the consumer's
image.
Tags on the tacplus image follow the version policy:
| tag | meaning |
|---|---|
2.1.3 |
that release, immutable |
2.1 |
the newest 2.1.x patch (what a consumer pins) |
2 |
the newest 2.x |
latest |
the newest release |
main-<sha> |
an unreleased build of main |
base and build are tagged <yyyymmdd>-<sha> and latest.
The definitions are images/base.Dockerfile, images/build.Dockerfile and
images/tacplus.Dockerfile; images/smoke.sh ships in the build and
tacplus images as tacplus-smoke and runs inside every pushed image. .github/workflows/images.yml builds and pushes them for
linux/amd64 and linux/arm64 on every push to main and every release tag,
rebuilding base and build only when their file changed (or weekly for
base, or on demand). just images builds the set natively for one machine.
The first release tag is v2.0.0: the public tacplus-dev/tacplus line is a
new major over the private v1.2.x line it was forked from (new module path,
regenerated consumers).