fix(CLI-39): bump Contracts nupkg to 0.2.0; scope pack-clients.ps1 regexes

Code review of the CLI-39 branch caught an Important gap: Contracts.csproj
was left at the already-published 0.1.2 while the .NET Client moved to
0.2.0. Invoke-PackDotnet in scripts/pack-clients.ps1 packs and publishes
both ZB.MOM.WW.MxGateway.Contracts and .Client through the same -Publish
loop, and the new collision guard runs every nupkg it finds through
Assert-GiteaPackageNotPublished. Left as-is, the next real .NET publish
would pack Contracts at 0.1.2, the guard would correctly refuse to
republish it, and the loop would abort mid-way with Client (alphabetically
first) possibly already pushed -- the two packages permanently out of
lockstep.

- src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj:
  <Version> 0.1.2 -> 0.2.0, matching the .NET Client (they have always
  released together).
- src/Directory.Build.props: corrected a comment that was now stale --
  it claimed the repo-wide 0.1.2 default was kept to match the Contracts
  package, which is no longer true now that Contracts.csproj overrides it.
  The <Version> value itself is unchanged; Server/Worker/Tests staying at
  0.1.2 is a separate, not-yet-made decision, out of scope for CLI-39.
- docs/ClientPackaging.md: Contracts.csproj added as a fifth manifest in
  the Versioning section, with the near-miss recorded.

Also hardened scripts/pack-clients.ps1 per the same review: the Python
(pyproject.toml) and Rust (Cargo.toml) version-extraction regexes now
scope to the [project]/[package] section header instead of matching the
first "version = ..." line anywhere in the file (Cargo.toml has an
identical second one under [workspace.package] -- matching whichever came
first was luck of ordering, not correctness). One-line comment added on
the nuget filename-parse regex.

Verified live against the real Gitea registry: Contracts and Client both
still refuse at 0.1.2 and both now pass at 0.2.0, including running the
actual Invoke-PackDotnet filename-parse-then-guard logic against two
freshly packed real .nupkg files. dotnet build of Contracts.csproj and the
client slnx both clean. No publish performed.
This commit is contained in:
Joseph Doherty
2026-08-07 08:07:40 -04:00
parent 9b2abef4e1
commit 440e7cf03d
5 changed files with 57 additions and 13 deletions
File diff suppressed because one or more lines are too long
+28 -6
View File
@@ -45,6 +45,21 @@ mirrored by the hand-maintained `MxGatewayClientVersion.CLIENT_VERSION`
constant — the two have drifted before and there is no build-time link constant — the two have drifted before and there is no build-time link
between them, so bump both together). between them, so bump both together).
`src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj`
(`<Version>`) is a fifth, easy-to-miss manifest: it is not itself a
language client, but `Invoke-PackDotnet` in `scripts/pack-clients.ps1`
packs and publishes it in lockstep with the .NET Client (both
`ZB.MOM.WW.MxGateway.*` nupkgs go through the same `-Publish` loop), and
Contracts and the .NET Client have always released at the same version.
Bump Contracts' `<Version>` alongside the .NET Client's — leaving it behind
means the next `-Publish` packs a stale Contracts version, the collision
guard below correctly refuses to re-publish it, and the loop aborts
mid-way with the Client possibly already pushed (nupkgs are enumerated
alphabetically, and `Client` sorts before `Contracts`). This is distinct
from `src/Directory.Build.props`'s repo-wide `<Version>` default, which
stamps the Server/Worker/test assemblies and is not part of the published
client package set — see the comment there.
**Bump the version before every publish, never after.** A Gitea package feed **Bump the version before every publish, never after.** A Gitea package feed
rejects re-uploading an existing name+version, and `scripts/pack-clients.ps1` rejects re-uploading an existing name+version, and `scripts/pack-clients.ps1`
enforces this before it ever attempts a push: each per-language `-Publish` enforces this before it ever attempts a push: each per-language `-Publish`
@@ -57,12 +72,19 @@ carries the equivalent guard for the Go module: it refuses to create a
`ClientVersion` already equals `X.Y.Z` (CLI-21/CLI-39), so a forgotten `ClientVersion` already equals `X.Y.Z` (CLI-21/CLI-39), so a forgotten
version bump fails the tag instead of shipping a mismatched module. version bump fails the tag instead of shipping a mismatched module.
As of 2026-08-07 (CLI-39) all five clients moved to **0.2.0**, converging on As of 2026-08-07 (CLI-39) all five clients — plus `ZB.MOM.WW.MxGateway.Contracts`,
one number after four of the five had drifted onto the *already-published* which releases in lockstep with the .NET Client — moved to **0.2.0**,
0.1.2/0.1.1 while their public APIs kept changing underneath it (see converging on one number after four of the five had drifted onto the
`archreview/2026-07-12/remediation/50-clients.md` CLI-39). Verified against *already-published* 0.1.2/0.1.1 while their public APIs kept changing
the live Gitea package API at that time: `nuget` had `ZB.MOM.WW.MxGateway.Client` underneath it (see `archreview/2026-07-12/remediation/50-clients.md` CLI-39).
and `.Contracts` published through 0.1.2; `pypi` (`zb-mom-ww-mxaccess-gateway-client`) A code-review follow-up on the same branch caught that the initial CLI-39
pass bumped the .NET Client but left `Contracts.csproj` at 0.1.2 — since
both publish through the same `Invoke-PackDotnet` `-Publish` loop, that
would have made the very next `.NET` publish abort on the new collision
guard partway through (Client already pushed, Contracts refused as a
re-publish of the already-published 0.1.2). Fixed in the same branch.
Verified against the live Gitea package API at that time: `nuget` had
`ZB.MOM.WW.MxGateway.Client` and `.Contracts` published through 0.1.2; `pypi` (`zb-mom-ww-mxaccess-gateway-client`)
and `cargo` (`zb-mom-ww-mxgateway-client`) had only reached 0.1.1 despite their and `cargo` (`zb-mom-ww-mxgateway-client`) had only reached 0.1.1 despite their
source pinning 0.1.2; **`maven` source pinning 0.1.2; **`maven`
(`com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client`) had already published (`com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client`) had already published
+19 -2
View File
@@ -185,6 +185,9 @@ function Invoke-PackDotnet {
if ($Publish) { if ($Publish) {
Write-Host 'Publishing .NET packages to Gitea...' -ForegroundColor Yellow Write-Host 'Publishing .NET packages to Gitea...' -ForegroundColor Yellow
Get-ChildItem $OutputDir -Filter 'ZB.MOM.WW.MxGateway.*.nupkg' | ForEach-Object { Get-ChildItem $OutputDir -Filter 'ZB.MOM.WW.MxGateway.*.nupkg' | ForEach-Object {
# nupkg filenames are '<PackageId>.<Version>.nupkg'; the id itself contains
# dots (e.g. 'ZB.MOM.WW.MxGateway.Client'), so the id capture is lazy and the
# version capture anchors on the leading digit to split at the right dot.
$fileBaseName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) $fileBaseName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name)
if ($fileBaseName -notmatch '^(?<id>.+?)\.(?<version>\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$') { if ($fileBaseName -notmatch '^(?<id>.+?)\.(?<version>\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$') {
throw "Could not parse a NuGet package id/version out of '$($_.Name)'." throw "Could not parse a NuGet package id/version out of '$($_.Name)'."
@@ -231,7 +234,14 @@ function Invoke-PackPython {
if ($Publish) { if ($Publish) {
$pyprojectPath = Join-Path $RepoRoot 'clients/python/pyproject.toml' $pyprojectPath = Join-Path $RepoRoot 'clients/python/pyproject.toml'
$pyprojectContent = Get-Content $pyprojectPath -Raw $pyprojectContent = Get-Content $pyprojectPath -Raw
if ($pyprojectContent -notmatch '(?m)^\s*version\s*=\s*"([^"]+)"') { # Scope to the [project] section (not just the first "version = ..." line
# in the file) — [build-system]/[tool.*] sections can carry their own
# version-shaped keys, and matching the file's first hit would be luck
# of ordering, not correctness.
if ($pyprojectContent -notmatch '(?ms)^\[project\](?<section>.*?)(?=^\[|\z)') {
throw "Could not find a [project] section in '$pyprojectPath'."
}
if ($Matches.section -notmatch '(?m)^\s*version\s*=\s*"([^"]+)"') {
throw "Could not find [project].version in '$pyprojectPath'." throw "Could not find [project].version in '$pyprojectPath'."
} }
Assert-GiteaPackageNotPublished -Type 'pypi' -Name 'zb-mom-ww-mxaccess-gateway-client' -Version $Matches[1] Assert-GiteaPackageNotPublished -Type 'pypi' -Name 'zb-mom-ww-mxaccess-gateway-client' -Version $Matches[1]
@@ -285,7 +295,14 @@ function Invoke-PackRust {
if ($Publish) { if ($Publish) {
$cargoTomlPath = Join-Path $rustDir 'Cargo.toml' $cargoTomlPath = Join-Path $rustDir 'Cargo.toml'
$cargoTomlContent = Get-Content $cargoTomlPath -Raw $cargoTomlContent = Get-Content $cargoTomlPath -Raw
if ($cargoTomlContent -notmatch '(?m)^\s*version\s*=\s*"([^"]+)"') { # Scope to the [package] section specifically — Cargo.toml also carries a
# [workspace.package] section with its own "version = ..." line (today
# identical, by convention, not by anything this regex can rely on), and
# matching whichever comes first in the file is luck of ordering.
if ($cargoTomlContent -notmatch '(?ms)^\[package\](?<section>.*?)(?=^\[|\z)') {
throw "Could not find a [package] section in '$cargoTomlPath'."
}
if ($Matches.section -notmatch '(?m)^\s*version\s*=\s*"([^"]+)"') {
throw "Could not find [package] version in '$cargoTomlPath'." throw "Could not find [package] version in '$cargoTomlPath'."
} }
Assert-GiteaPackageNotPublished -Type 'cargo' -Name 'zb-mom-ww-mxgateway-client' -Version $Matches[1] Assert-GiteaPackageNotPublished -Type 'cargo' -Name 'zb-mom-ww-mxgateway-client' -Version $Matches[1]
+8 -4
View File
@@ -11,10 +11,14 @@
<!-- TST-11: single-source the .NET-side version for Server, Worker, Contracts, and tests <!-- TST-11: single-source the .NET-side version for Server, Worker, Contracts, and tests
(they otherwise stamp the SDK default 1.0.0, so a deployed gateway cannot be correlated (they otherwise stamp the SDK default 1.0.0, so a deployed gateway cannot be correlated
to a release). Kept at 0.1.2 to match the Contracts package and the aligned Python/Rust/ to a release). Server/Worker/Tests stay at this default. CLI-39 (2026-08-07) moved the
Go clients; the Java client leads at 0.2.0 after its JDK-17 retarget. The git short SHA is published `ZB.MOM.WW.MxGateway.Contracts` and `.Client` nuget packages to 0.2.0 via an
appended to InformationalVersion (0.1.2+<sha>) so support can map a running binary to a explicit <Version> override in Contracts.csproj (MSBuild property-last-write-wins over
commit; the query is guarded so a build outside a git checkout still succeeds. --> this Directory.Build.props default) — Server/Worker assembly stamping and the published
client packages are deliberately decoupled; a broader 0.2.0 alignment for Server/Worker
is a separate, not-yet-made decision. The git short SHA is appended to
InformationalVersion (0.1.2+<sha>) so support can map a running binary to a commit; the
query is guarded so a build outside a git checkout still succeeds. -->
<PropertyGroup> <PropertyGroup>
<Version>0.1.2</Version> <Version>0.1.2</Version>
</PropertyGroup> </PropertyGroup>
@@ -7,7 +7,7 @@
<PropertyGroup> <PropertyGroup>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<PackageId>ZB.MOM.WW.MxGateway.Contracts</PackageId> <PackageId>ZB.MOM.WW.MxGateway.Contracts</PackageId>
<Version>0.1.2</Version> <Version>0.2.0</Version>
<Authors>Joseph Doherty</Authors> <Authors>Joseph Doherty</Authors>
<Company>ZB MOM WW</Company> <Company>ZB MOM WW</Company>
<Copyright>Copyright (c) ZB MOM WW. All rights reserved.</Copyright> <Copyright>Copyright (c) ZB MOM WW. All rights reserved.</Copyright>