Versioning: 1.3.0 is the first stable release¶
Status: accepted, 2026-09-12. Resolves #61.
Context¶
Nine packages went to nuget.org as 1.0.0, 1.1.0, 1.2.0, 1.2.1, 1.2.2 and 1.2.3 on 9 and 10
September 2026. Three of them — CanKit.Pro.CANopen, CanKit.Pro.IsoTp and CanKit.Pro.Uds —
described themselves as Experimental in the <Description> that nuget.org renders, while
carrying major version 1.
Both statements cannot hold. Major 1 is a promise that the public API will not break without a major bump. "Experimental" asks nobody to rely on it yet. The two audiences — a package manager resolving a version range, and a human reading the package page — were told opposite things.
The 1.x line was not chosen. semantic-release starts at 1.0.0 when it finds no tag, and the first
release run found none: the v0.1.0 seed tag that the release runbook describes was never pushed.
The first release therefore claimed a stable API on a code base that had never been reviewed
against its own specifications.
The review of 2026-09-10 then found roughly 45 defects. Four of them cannot be fixed properly without changing the public surface:
| Issue | The fix that is actually right |
|---|---|
| #23 | Subscription items carry IsEcho and the bus timestamp instead of a bare frame, and a subscription chooses whether host echoes reach it |
| #37 | SPN extraction returns a value that can say "not available" or "error" instead of reporting 0xFFFF as 8191.875 rpm |
| #44 | SdoTransferMode.Expedited and .Segmented are removed, because nothing enforces them |
| #82 | FindOverlappingFilterSubscriptions() is replaced by a named FilterOverlap type instead of a bare tuple pair |
Under a strict reading of SemVer, each of these either costs a major version or has to ship as a
new member with an [Obsolete] shadow on the old one. src/ today contains no [Obsolete]
member at all. The additive route would introduce four of them into a clean surface — and there
is no major version scheduled that would ever remove them again.
Decision¶
-
1.0.0 – 1.2.3 are withdrawn. All six versions of all nine packages are unlisted and deprecated on nuget.org (see the checklist below). They were published as stable by mistake.
-
1.3.0 is the first release for which the SemVer promise holds. Everything before it is treated as never having made that promise, because it was not in a position to.
-
Until the
v1.3.0tag, breaking API changes are allowed and expected. This is the one window in which the surface can be corrected. It closes with the tag. -
Replace, do not shadow. No
[Obsolete]member is introduced in this window in order to dodge a break. After 1.3.0,[Obsolete]is the normal deprecation tool again — then with a major version in prospect that eventually removes it. -
No "Experimental" in package metadata. The maturity statement that belongs in a README is what has been validated and what has not; it is not a stability claim, and it does not contradict the version.
This has to answer to CON-004 in the SRS, which
requires an L3/L4 package to be marked experimental or excluded from packing while it is
functionally incomplete. That constraint stands; what changes is how it is met. The word in
the <Description> was doing the job badly — it said "do not rely on this" beside a version
number that said the opposite, and only on three of the nine packages. In its place: every
package README carries the same status block naming the withdrawn releases and the pre-1.3.0
window, and the release itself is gated on the checklist below. A reader now learns the same
thing from a consistent source instead of from prose contradicted by the version.
The arc42 entries that call ISO-TP experimental (§2.2, and the risk register at the end)
record the state at the 2026-07-14 review, when IsoTp.Open still threw
NotImplementedException. They are history, not live constraints, and are left as written.
-
No intermediate releases during the review series. One release at the end. Cutting 1.3.0, 1.4.0 and 1.5.0 each with breaks in them would dissolve the very statement this document makes. Enforced rather than remembered:
eng/verify-release-config.mjsruns as the exec plugin'sverifyReleaseCmdand aborts the release if the version semantic-release computed is below 1.3.0 — a batch of plainfix:commits would otherwise publish 1.2.4 and quietly contradict everything above. -
The commit analyzer maps breaking changes to a minor bump for the duration.
.releaserc.jsoncarries{ "breaking": true, "release": "minor" }so that a commit with a!or aBREAKING CHANGE:footer cannot silently publish 2.0.0. This is a temporary override and is listed on the release checklist below. The same script enforces both ends of it: while the window is open the mapping must be exactlyminor(patchwould hide a break as effectively asmajorwould over-announce it), and once 1.3.0 appears in the changelog it must be back tomajor.
Alternatives considered¶
2.0.0. Honest about the breaks and conventional. Rejected: the version number would be spending a major on a mistake rather than on a product milestone, and the 1.x line would stand on nuget.org forever as the "real" first version. The withdrawal achieves the same protection without inflating the number.
A pre-release line (1.3.0-preview.N or 2.0.0-preview.N). Rejected: NuGet does not resolve
pre-release versions by default, so dotnet add package would have continued to install the
mistaken 1.2.3 until it was withdrawn — and once it is withdrawn, the pre-release suffix protects
nobody who is left. It would have added a semantic-release channel, a --prerelease flag in
nineteen install snippets and a second transition (preview → final) for no gain.
Going back below 1.2.3. Not possible. SemVer has no reverse gear, and NuGet would never serve a lower version as the latest.
What this costs¶
The deviation is real and worth naming: 1.3.0 will break code compiled against 1.2.x, and it will do so without a major bump.
What bounds it is the withdrawal. Unlisted packages disappear from search and stop resolving through version ranges; only an exact pin still restores. Across all six releases nuget.org counts 30 to 68 downloads per package — the traffic profile of mirrors and vulnerability scanners, with no evidence of a human consumer. The promise is being broken towards versions that can no longer be discovered or installed by anything that was not already pinned to them.
nuget.org has no hard delete. Withdrawal is as close to undoing a publication as the platform
allows, and the badges in docs/packages/index.md and docs/index.md will show no version until
1.3.0 ships.
Release checklist for 1.3.0¶
- All
type: bugissues from the review series closed. - #52 — the normative negative tests exist.
- #23, #37, #44 and #82 landed. After the tag, none of them is possible without a major version.
- The API approval baselines reviewed as a whole and frozen.
- Each package README states what is validated and what is not (software doubles vs. hardware).
Immediately after the 1.3.0 release, not before¶
.releaserc.json back to { "breaking": true, "release": "major" }. Without it, a real
breaking change later becomes a silent minor.
This cannot be done in advance, and the ordering is worth spelling out because it looks like an
oversight otherwise. eng/verify-release-config.mjs decides which rule is correct by reading
CHANGELOG.md: while no 1.3-or-later release is recorded there, it requires minor and fails
on major. The changelog only gains that entry during the release itself, written by
semantic-release — which changes the changelog and nothing else, so it cannot flip the rule on
its way past.
So the sequence is: release 1.3.0 with the override in place, then flip it. The guard makes the
gap self-closing rather than a thing to remember — from the moment 1.3.0 is in the changelog, the
validate release config job fails on every pull request until the rule is restored, and no
further release can be cut.
The first real test of the release pipeline is this release¶
Nothing before it can be. release.yml runs only on workflow_dispatch, so no pull-request check
touches it, and a dry run cannot stand in either: the version gate above refuses anything that is
not 1.3.0 while the window is open, and until a feat commit lands the commit analyser computes a
patch version. Verified on 2026-09-12 against main at bd729e3, where it computes 1.2.4.
What that leaves is a real ordering, not a gap to paper over:
Phase 1 lands #23, which is aDone. With #23 on the branch the commit analyser computes 1.3.0, andfeatand therefore moves the computed version to 1.3.0.eng/verify-release-config.mjs 1.3.0passes where the same script still refuses 1.2.4.- From then on
Release → Run workflowwith the dry-run input checked exercises the whole path — version resolution, build, test, pack, the artifact handover between jobs — without tagging, publishing, or minting a NuGet credential. Possible from the merge of #23 onward, since a dispatch frommainrunsmain's workflow againstmain's commits. - Cut 1.3.0 only after that dry run has been green at least once.
Until step 1, the pipeline was unproven by construction. That was a deliberate consequence of the gate, not a defect in it: the gate is doing exactly what it was built to do, which is refuse to release before this checklist is done.
Withdrawing 1.0.0 – 1.2.3¶
Done on 2026-09-12. All six versions of the nine CanKit.Pro.* packages are deprecated and
unlisted on nuget.org.
Both, not just one: unlisting is silent for someone who already depends on the package, while deprecation is what surfaces in their IDE and build log. Unlisting is what stops the versions appearing in search and resolving through version ranges.
nuget.org has no hard delete, so an exact pin still restores. That is as far as withdrawal goes.
The git tags v1.0.0 … v1.2.3 stay. They are history, and GitVersion reads them to derive CI
versions.
After 1.3.0¶
Normal SemVer. A breaking change costs a major version; a deprecation is an [Obsolete] member
that a later major removes. The API approval tests are what make an accidental break visible in
the pull request that causes it.