#!/usr/bin/env pwsh <# .SYNOPSIS Fails a PR that changes a client's source without bumping its version past the last thing actually published (CLI-46). .DESCRIPTION CLI-39 (2026-07-12 cycle) added publish-time guards that refuse to re-publish an already-published name+version (scripts/pack-clients.ps1's Assert-GiteaPackageNotPublished, scripts/tag-go-module.ps1's ClientVersion-vs-tag check). Those guards only fire when someone actually runs a publish. CLI-46 (2026-08-19 cycle) found the same hazard recurring silently in between publishes: library source (including a real bug fix, commit a8f86b5) shipped while the version constant still equalled the already-published number, so nothing in the tree recorded that the published artifact and the in-repo source had diverged. This script is the PR-time half of that guard: catch the drift as soon as it happens, not at the next publish. For each client this repo ships, the script: 1. Finds the latest "publish tag" for that client, per the tag naming convention its own publish tooling actually uses. 2. If no such tag exists, says so and passes (nothing to compare against yet — this is expected for every client except Go; see below). 3. If a tag exists, reads the version recorded in-source at that tag and the version recorded in-source now. If `clients/` has changed since the tag AND the version constant is unchanged, that is exactly the CLI-46 pattern: source moved, the published coordinate did not, and nothing says so. Fail. Tag conventions, as implemented today (verified against scripts/pack-clients.ps1, scripts/tag-go-module.ps1, and `git tag -l`): - Go is the only client with a git tag at all: `tag-go-module.ps1` creates `clients/go/vX.Y.Z` (CLI-21/CLI-39 already keep this tag's version in lockstep with clients/go/mxgateway/version.go's ClientVersion at *tag time*; this script is what keeps it in lockstep *between* tags). - .NET, Python, Rust, and Java publish straight to a package registry via `pack-clients.ps1 -Publish` (NuGet/PyPI/cargo/Maven API pushes) with no corresponding git tag of any kind — there is nothing in this repo's history to diff against for those four. The script looks for a same-shaped tag anyway (`clients//v*`) so it stays correct if that ever changes, but today it will report "no tags found" and pass for all four. A client with no tag is not a violation of anything — it means no release train has ever tagged that client, so there is no "since the last publish" window to check. The check exists to catch drift once a baseline exists, not to demand one. .EXAMPLE pwsh scripts/check-client-versions.ps1 #> [CmdletBinding()] param() Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path Push-Location $repoRoot try { # One entry per official client. `TagGlob` is the tag naming pattern that # client's own publish tooling uses (see doc comment above); `VersionFile` # + `VersionPattern` locate the single version-of-record constant # docs/ClientPackaging.md's "Versioning" section names as authoritative # for that client; `SourceDir` is the diff scope (matches the literal # `clients/` scope described in the CLI-46 remediation task). $clients = @( [ordered]@{ Name = 'dotnet' SourceDir = 'clients/dotnet' TagGlob = 'clients/dotnet/v*' VersionFile = 'clients/dotnet/ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj' VersionPattern = '([^<]+)' }, [ordered]@{ Name = 'go' SourceDir = 'clients/go' TagGlob = 'clients/go/v*' VersionFile = 'clients/go/mxgateway/version.go' VersionPattern = 'ClientVersion\s*=\s*"([^"]+)"' }, [ordered]@{ Name = 'python' SourceDir = 'clients/python' TagGlob = 'clients/python/v*' VersionFile = 'clients/python/src/zb_mom_ww_mxgateway/version.py' VersionPattern = '__version__\s*=\s*"([^"]+)"' }, [ordered]@{ Name = 'rust' SourceDir = 'clients/rust' TagGlob = 'clients/rust/v*' VersionFile = 'clients/rust/Cargo.toml' VersionPattern = 'version\s*=\s*"([^"]+)"' }, [ordered]@{ Name = 'java' SourceDir = 'clients/java' TagGlob = 'clients/java/v*' VersionFile = 'clients/java/build.gradle' VersionPattern = "version\s*=\s*'([^']+)'" } ) function Get-LatestTag { param([string]$Glob) # --sort=-v:refname orders by version-aware descending semver, so the # first line is the newest tag matching the glob. Empty result means # no tag of this shape has ever been created for this client. $tags = git tag -l --sort='-v:refname' $Glob if (-not $tags) { return $null } return @($tags -split "`n")[0].Trim() } function Get-VersionFromFile { param([string]$Path, [string]$Pattern) if (-not (Test-Path $Path)) { throw "Version file not found: $Path" } $content = Get-Content -Path $Path -Raw $match = [regex]::Match($content, $Pattern) if (-not $match.Success) { throw "Could not find a version matching /$Pattern/ in $Path" } return $match.Groups[1].Value } function Get-VersionFromFileAtRevision { param([string]$Revision, [string]$Path, [string]$Pattern) $content = git show "${Revision}:${Path}" 2>$null if ($LASTEXITCODE -ne 0 -or -not $content) { # The version file did not exist (or was at a different path) at # that tag. Treat as "unknown" rather than failing the whole # check — the drift comparison below only needs "does the # current version still equal the tag's version", and an # unresolvable historical version can never equal the current # one, so this degrades to "assume it changed" (safe: it will # not mask a real drift). return $null } $match = [regex]::Match(($content -join "`n"), $Pattern) if (-not $match.Success) { return $null } return $match.Groups[1].Value } $failures = New-Object System.Collections.Generic.List[string] foreach ($client in $clients) { Write-Host '' Write-Host "== $($client.Name) ==" -ForegroundColor Cyan $tag = Get-LatestTag -Glob $client.TagGlob if (-not $tag) { Write-Host " No publish tags matching '$($client.TagGlob)' — nothing to compare against yet. Pass." -ForegroundColor Yellow continue } Write-Host " Latest publish tag: $tag" $currentVersion = Get-VersionFromFile -Path $client.VersionFile -Pattern $client.VersionPattern Write-Host " Current version constant ($($client.VersionFile)): $currentVersion" # Any diff at all in the client's source tree since its last publish # tag — matches the CLI-46 remediation task's literal check. git diff --quiet $tag -- $client.SourceDir $sourceChanged = ($LASTEXITCODE -ne 0) if (-not $sourceChanged) { Write-Host " No changes under $($client.SourceDir) since $tag. Pass." -ForegroundColor Green continue } $tagVersion = Get-VersionFromFileAtRevision -Revision $tag -Path $client.VersionFile -Pattern $client.VersionPattern Write-Host " Source under $($client.SourceDir) changed since $tag (tag's version constant: $(if ($tagVersion) { $tagVersion } else { '' }))." if ($currentVersion -eq $tagVersion) { $failures.Add("$($client.Name): source changed since publish tag '$tag' but $($client.VersionFile) still reads '$currentVersion' (the version at '$tag'). Bump the version before another publish (CLI-46).") Write-Host " FAIL: version constant unchanged since the publish tag." -ForegroundColor Red } else { Write-Host " Version constant already bumped ('$tagVersion' -> '$currentVersion') since $tag. Pass." -ForegroundColor Green } } Write-Host '' if ($failures.Count -gt 0) { Write-Host 'Client version freshness check FAILED:' -ForegroundColor Red foreach ($failure in $failures) { Write-Host " - $failure" -ForegroundColor Red } exit 1 } Write-Host 'Client version freshness check passed.' -ForegroundColor Green } finally { Pop-Location }