fix(CLI-39): bump client versions off published 0.1.2; guard the publish pipeline

Converges all five clients on one version after four had drifted onto the
already-published 0.1.2/0.1.1 while their APIs kept changing underneath it:

- Rust Cargo.toml [package] + [workspace.package] -> 0.2.0 (CLIENT_VERSION
  already derives from CARGO_PKG_VERSION, no separate edit).
- Python pyproject.toml + version.py -> 0.2.0; new test asserts __version__
  matches pyproject.toml (closes the CLI-26 residual drift mode).
- Go mxgateway/version.go ClientVersion -> 0.2.0.
- .NET ZB.MOM.WW.MxGateway.Client.csproj <Version> -> 0.2.0.
- Java -> 0.2.1, not 0.2.0: the live Gitea Maven feed already had 0.2.0
  published (2026-06-26), before the CLI-37/38/40/41 conformance fixes
  changed the client's observable behavior, so reusing 0.2.0 would label
  two different APIs identically. Recorded as an exception in
  docs/ClientPackaging.md's new Versioning section.

Publish-pipeline guards:

- scripts/tag-go-module.ps1 implements the CLI-21 guard: after semver
  validation it refuses to tag unless clients/go/mxgateway/version.go's
  ClientVersion already matches the requested tag version.
- scripts/pack-clients.ps1 gains a Gitea package-registry collision guard
  wired into every per-language -Publish step; it aborts if the target
  name+version already exists rather than force-overwriting. Verified live
  against the real Gitea registry (credentials already present in this
  environment) — correctly refuses on every known-published artifact and
  passes on every unpublished target.

Docs updated in the same commit: docs/ClientPackaging.md (new Versioning
section), and the five client READMEs' stale 0.1.1/0.1.2 example versions.

No .proto changes. No publish performed.
This commit is contained in:
Joseph Doherty
2026-08-07 07:58:49 -04:00
parent 0646c73e48
commit 9b2abef4e1
19 changed files with 209 additions and 21 deletions
+99
View File
@@ -87,6 +87,12 @@ $GiteaNugetFeed = 'https://gitea.dohertylan.com/api/packages/dohertj2/nuget/inde
$GiteaPypiFeed = 'https://gitea.dohertylan.com/api/packages/dohertj2/pypi'
$JavaHome = '/Users/dohertj2/.local/jdks/jdk-21.0.11+10/Contents/Home'
# Generic Gitea package registry API (https://gitea.dohertylan.com/api/v1/packages/{owner}/{type}/{name}/{version}):
# returns 200 when that exact name+version already exists in the given feed
# type, 404 when it does not. Used as a pre-publish collision guard (CLI-39)
# so a re-run of this script can never silently overwrite a published artifact.
$GiteaPackageApiBase = 'https://gitea.dohertylan.com/api/v1/packages/dohertj2'
function Write-Header {
param([string]$Text)
Write-Host ''
@@ -94,6 +100,64 @@ function Write-Header {
Write-Host $Text -ForegroundColor Cyan
}
function Test-GiteaPackageExists {
<#
.SYNOPSIS
Queries the Gitea package API for an existing name+version in a feed.
.OUTPUTS
$true if the package/version already exists, $false if it does not.
Throws if the registry cannot be reached or returns anything other
than 200/404 — callers must treat "cannot verify" as "do not publish".
#>
param(
[Parameter(Mandatory)][string]$Type,
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$Version
)
$uri = "$GiteaPackageApiBase/$Type/$Name/$Version"
$headers = @{}
if (-not [string]::IsNullOrEmpty($env:GITEA_TOKEN)) {
$user = if ([string]::IsNullOrEmpty($env:GITEA_USERNAME)) { 'dohertj2' } else { $env:GITEA_USERNAME }
$pair = "$($user):$($env:GITEA_TOKEN)"
$basic = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$headers['Authorization'] = "Basic $basic"
}
try {
$response = Invoke-WebRequest -Uri $uri -Headers $headers -Method Get -UseBasicParsing -ErrorAction Stop
return ($response.StatusCode -eq 200)
} catch {
$statusCode = $null
if ($_.Exception.PSObject.Properties['Response'] -and $_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
}
if ($statusCode -eq 404) {
return $false
}
throw "Unable to query the Gitea package API for '$Type/$Name/$Version' ($uri): $($_.Exception.Message). Refusing to publish without a collision check — verify manually (or check GITEA_USERNAME/GITEA_TOKEN/network) and retry."
}
}
function Assert-GiteaPackageNotPublished {
<#
.SYNOPSIS
Aborts the script if $Name/$Version already exists in the $Type feed.
Never force-overwrites a published artifact (CLI-39).
#>
param(
[Parameter(Mandatory)][string]$Type,
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$Version
)
Write-Host "Checking Gitea '$Type' feed for existing '$Name' $Version..."
if (Test-GiteaPackageExists -Type $Type -Name $Name -Version $Version) {
throw "Gitea package '$Name' version '$Version' already exists in the '$Type' feed. Bump the client version before publishing — this script never force-overwrites a published artifact."
}
Write-Host " Not found in the '$Type' feed — safe to publish '$Name' $Version." -ForegroundColor Green
}
# -------- .NET --------
function Invoke-PackDotnet {
@@ -121,6 +185,12 @@ function Invoke-PackDotnet {
if ($Publish) {
Write-Host 'Publishing .NET packages to Gitea...' -ForegroundColor Yellow
Get-ChildItem $OutputDir -Filter 'ZB.MOM.WW.MxGateway.*.nupkg' | ForEach-Object {
$fileBaseName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name)
if ($fileBaseName -notmatch '^(?<id>.+?)\.(?<version>\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$') {
throw "Could not parse a NuGet package id/version out of '$($_.Name)'."
}
Assert-GiteaPackageNotPublished -Type 'nuget' -Name $Matches.id -Version $Matches.version
& dotnet nuget push $_.FullName --source $GiteaNugetFeed --api-key $env:GITEA_TOKEN
if ($LASTEXITCODE -ne 0) { throw "dotnet nuget push failed for '$($_.Name)'." }
}
@@ -159,6 +229,13 @@ function Invoke-PackPython {
Write-Host "Packed Python artifacts -> $OutputDir" -ForegroundColor Green
if ($Publish) {
$pyprojectPath = Join-Path $RepoRoot 'clients/python/pyproject.toml'
$pyprojectContent = Get-Content $pyprojectPath -Raw
if ($pyprojectContent -notmatch '(?m)^\s*version\s*=\s*"([^"]+)"') {
throw "Could not find [project].version in '$pyprojectPath'."
}
Assert-GiteaPackageNotPublished -Type 'pypi' -Name 'zb-mom-ww-mxaccess-gateway-client' -Version $Matches[1]
Write-Host 'Publishing Python distribution to Gitea...' -ForegroundColor Yellow
$wheels = @(Get-ChildItem $OutputDir -Filter 'zb_mom_ww_mxaccess_gateway_client-*.whl')
$sdists = @(Get-ChildItem $OutputDir -Filter 'zb_mom_ww_mxaccess_gateway_client-*.tar.gz')
@@ -206,6 +283,13 @@ function Invoke-PackRust {
Write-Host "Packed Rust artifacts -> $OutputDir" -ForegroundColor Green
if ($Publish) {
$cargoTomlPath = Join-Path $rustDir 'Cargo.toml'
$cargoTomlContent = Get-Content $cargoTomlPath -Raw
if ($cargoTomlContent -notmatch '(?m)^\s*version\s*=\s*"([^"]+)"') {
throw "Could not find [package] version in '$cargoTomlPath'."
}
Assert-GiteaPackageNotPublished -Type 'cargo' -Name 'zb-mom-ww-mxgateway-client' -Version $Matches[1]
Write-Host 'Publishing Rust crate to Gitea...' -ForegroundColor Yellow
Push-Location (Join-Path $RepoRoot 'clients/rust')
try {
@@ -269,6 +353,21 @@ function Invoke-PackJava {
Write-Host "Packed Java artifacts -> $OutputDir" -ForegroundColor Green
if ($Publish) {
$buildGradlePath = Join-Path $javaDir 'build.gradle'
$buildGradleContent = Get-Content $buildGradlePath -Raw
if ($buildGradleContent -notmatch "(?m)^\s*group\s*=\s*'([^']+)'") {
throw "Could not find subprojects { group = '...' } in '$buildGradlePath'."
}
$javaGroup = $Matches[1]
if ($buildGradleContent -notmatch "(?m)^\s*version\s*=\s*'([^']+)'") {
throw "Could not find subprojects { version = '...' } in '$buildGradlePath'."
}
$javaVersion = $Matches[1]
# Gitea's Maven package API identifies the package as "groupId:artifactId",
# not the bare artifact id — passing just the artifact id here would query
# a name that never exists and silently defeat the guard.
Assert-GiteaPackageNotPublished -Type 'maven' -Name "$javaGroup`:zb-mom-ww-mxgateway-client" -Version $javaVersion
Write-Host 'Publishing Java artifacts to Gitea Maven feed...' -ForegroundColor Yellow
Push-Location $javaDir
try {
+17
View File
@@ -36,6 +36,23 @@ if ($Version -notmatch '^v\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$') {
throw "Version '$Version' must match semver vX.Y.Z (optionally with -prerelease suffix)."
}
# CLI-21 guard: the tag must match what the module itself reports via
# ClientVersion, or `go get <module>@vX.Y.Z` resolves a tag whose module
# code disagrees with its own version constant.
$versionGoPath = Join-Path $PSScriptRoot '..' 'clients/go/mxgateway/version.go'
if (-not (Test-Path $versionGoPath)) {
throw "Could not find '$versionGoPath' to verify ClientVersion before tagging."
}
$versionGoContent = Get-Content $versionGoPath -Raw
if ($versionGoContent -notmatch 'ClientVersion\s*=\s*"([^"]+)"') {
throw "Could not find a ClientVersion = `"...`" constant in '$versionGoPath'."
}
$clientVersion = $Matches[1]
$tagVersion = $Version.TrimStart('v')
if ($clientVersion -ne $tagVersion) {
throw "clients/go/mxgateway/version.go ClientVersion is '$clientVersion' but the requested tag is '$tagVersion'. Update ClientVersion to match before tagging."
}
$tag = "clients/go/$Version"
Write-Host "Creating Go-module tag: $tag" -ForegroundColor Cyan