9b2abef4e1
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.
414 lines
16 KiB
PowerShell
414 lines
16 KiB
PowerShell
#Requires -Version 7
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Packs all MxAccessGateway clients into a single dist/ directory.
|
|
|
|
.DESCRIPTION
|
|
Runs each language client's native packaging command:
|
|
.NET -> dotnet pack (NuGet)
|
|
Python -> python -m build (sdist + wheel)
|
|
Rust -> cargo package (.crate)
|
|
Java -> gradle assemble + jars (jar + sources + javadoc + pom)
|
|
Go -> skipped; use scripts/tag-go-module.ps1
|
|
|
|
All artifacts land in -OutputDir (default: dist/).
|
|
|
|
With -Publish, each language pushes its package to the internal Gitea
|
|
feed. Requires GITEA_USERNAME and GITEA_TOKEN env vars.
|
|
|
|
.PARAMETER OutputDir
|
|
Where to drop the packed artifacts. Default: ./dist
|
|
|
|
.PARAMETER Languages
|
|
Subset of languages to pack. Default: all five.
|
|
Values: dotnet, python, rust, java, go
|
|
|
|
.PARAMETER Publish
|
|
After packing, upload to Gitea feeds. Requires:
|
|
GITEA_USERNAME
|
|
GITEA_TOKEN
|
|
Will refuse to publish if either is missing.
|
|
|
|
.PARAMETER SkipTests
|
|
Skip per-language regression tests before packing. Default: false.
|
|
|
|
.EXAMPLE
|
|
pwsh scripts/pack-clients.ps1
|
|
pwsh scripts/pack-clients.ps1 -Languages dotnet,python
|
|
pwsh scripts/pack-clients.ps1 -Publish
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$OutputDir = (Join-Path $PSScriptRoot '..' 'dist'),
|
|
[string[]]$Languages = @('dotnet', 'python', 'rust', 'java', 'go'),
|
|
[switch]$Publish,
|
|
[switch]$SkipTests
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# Normalize comma-separated strings that shells may pass as a single element.
|
|
$validLanguages = @('dotnet', 'python', 'rust', 'java', 'go')
|
|
$Languages = @($Languages | ForEach-Object { $_ -split ',' } | ForEach-Object {
|
|
$_.Trim().ToLowerInvariant()
|
|
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
|
|
foreach ($lang in $Languages) {
|
|
if ($validLanguages -notcontains $lang) {
|
|
throw "Unsupported language '$lang'. Supported values: $($validLanguages -join ', ')."
|
|
}
|
|
}
|
|
|
|
if ($Languages.Count -eq 0) {
|
|
throw "At least one language is required. Supported values: $($validLanguages -join ', ')."
|
|
}
|
|
|
|
# Resolve absolute output dir
|
|
$OutputDir = [System.IO.Path]::GetFullPath($OutputDir)
|
|
$RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
|
|
|
if (-not (Test-Path $OutputDir)) {
|
|
New-Item -ItemType Directory -Path $OutputDir | Out-Null
|
|
}
|
|
|
|
if ($Publish) {
|
|
if ([string]::IsNullOrEmpty($env:GITEA_USERNAME)) {
|
|
throw 'Publish requires GITEA_USERNAME env var.'
|
|
}
|
|
if ([string]::IsNullOrEmpty($env:GITEA_TOKEN)) {
|
|
throw 'Publish requires GITEA_TOKEN env var.'
|
|
}
|
|
}
|
|
|
|
$GiteaNugetFeed = 'https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json'
|
|
$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 ''
|
|
Write-Host '=== ' -NoNewline -ForegroundColor Cyan
|
|
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 {
|
|
Write-Header '.NET'
|
|
|
|
if (-not $SkipTests) {
|
|
Write-Host 'Running .NET client tests...'
|
|
$testProject = Join-Path $RepoRoot 'clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/ZB.MOM.WW.MxGateway.Client.Tests.csproj'
|
|
& dotnet test $testProject --no-restore
|
|
if ($LASTEXITCODE -ne 0) { throw '.NET tests failed.' }
|
|
}
|
|
|
|
Write-Host 'Packing ZB.MOM.WW.MxGateway.Contracts...'
|
|
& dotnet pack (Join-Path $RepoRoot 'src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj') `
|
|
-c Release -o $OutputDir
|
|
if ($LASTEXITCODE -ne 0) { throw '.NET Contracts pack failed.' }
|
|
|
|
Write-Host 'Packing ZB.MOM.WW.MxGateway.Client...'
|
|
& dotnet pack (Join-Path $RepoRoot 'clients/dotnet/ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj') `
|
|
-c Release -o $OutputDir
|
|
if ($LASTEXITCODE -ne 0) { throw '.NET Client pack failed.' }
|
|
|
|
Write-Host "Packed .NET artifacts -> $OutputDir" -ForegroundColor Green
|
|
|
|
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)'." }
|
|
}
|
|
}
|
|
}
|
|
|
|
# -------- Python --------
|
|
|
|
function Invoke-PackPython {
|
|
Write-Header 'Python'
|
|
|
|
# Use a persistent venv in /tmp so repeated runs skip reinstall.
|
|
$Venv = '/tmp/mxgw-py'
|
|
if (-not (Test-Path "$Venv/bin/python")) {
|
|
Write-Host "Creating Python venv at $Venv..."
|
|
& python3 -m venv $Venv
|
|
if ($LASTEXITCODE -ne 0) { throw 'python3 -m venv failed.' }
|
|
& "$Venv/bin/pip" install --quiet --upgrade pip
|
|
& "$Venv/bin/pip" install --quiet build twine
|
|
& "$Venv/bin/pip" install --quiet -e (Join-Path $RepoRoot 'clients/python[dev]')
|
|
}
|
|
|
|
if (-not $SkipTests) {
|
|
Write-Host 'Running Python tests...'
|
|
Push-Location (Join-Path $RepoRoot 'clients/python')
|
|
try {
|
|
& "$Venv/bin/python" -m pytest -q
|
|
if ($LASTEXITCODE -ne 0) { throw 'Python tests failed.' }
|
|
} finally { Pop-Location }
|
|
}
|
|
|
|
Write-Host 'Building Python sdist + wheel...'
|
|
& "$Venv/bin/python" -m build (Join-Path $RepoRoot 'clients/python') --outdir $OutputDir
|
|
if ($LASTEXITCODE -ne 0) { throw 'Python build failed.' }
|
|
|
|
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')
|
|
$files = ($wheels + $sdists) | ForEach-Object { $_.FullName }
|
|
& "$Venv/bin/python" -m twine upload `
|
|
--repository-url $GiteaPypiFeed `
|
|
-u $env:GITEA_USERNAME `
|
|
-p $env:GITEA_TOKEN `
|
|
@files
|
|
if ($LASTEXITCODE -ne 0) { throw 'twine upload failed.' }
|
|
}
|
|
}
|
|
|
|
# -------- Rust --------
|
|
|
|
function Invoke-PackRust {
|
|
Write-Header 'Rust'
|
|
|
|
$rustDir = Join-Path $RepoRoot 'clients/rust'
|
|
Push-Location $rustDir
|
|
try {
|
|
if (-not $SkipTests) {
|
|
Write-Host 'Running Rust tests...'
|
|
& cargo test --workspace
|
|
if ($LASTEXITCODE -ne 0) { throw 'Rust tests failed.' }
|
|
}
|
|
|
|
Write-Host 'Running cargo package...'
|
|
# No --no-verify: the crate vendors its protos (clients/rust/protos) so
|
|
# the verify build proves it compiles standalone from the tarball (CLI-02).
|
|
& cargo package
|
|
if ($LASTEXITCODE -ne 0) { throw 'cargo package failed.' }
|
|
|
|
$packageDir = Join-Path $rustDir 'target/package'
|
|
$crates = @(Get-ChildItem $packageDir -Filter '*.crate')
|
|
if ($crates.Count -eq 0) {
|
|
throw 'cargo package produced no .crate files.'
|
|
}
|
|
foreach ($crate in $crates) {
|
|
Copy-Item $crate.FullName -Destination $OutputDir -Force
|
|
Write-Host " Copied $($crate.Name)"
|
|
}
|
|
} finally { Pop-Location }
|
|
|
|
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 {
|
|
& cargo publish --registry dohertj2-gitea
|
|
if ($LASTEXITCODE -ne 0) { throw 'cargo publish failed.' }
|
|
} finally { Pop-Location }
|
|
}
|
|
}
|
|
|
|
# -------- Java --------
|
|
|
|
function Invoke-PackJava {
|
|
Write-Header 'Java'
|
|
|
|
$env:JAVA_HOME = $JavaHome
|
|
$javaDir = Join-Path $RepoRoot 'clients/java'
|
|
Push-Location $javaDir
|
|
try {
|
|
if (-not $SkipTests) {
|
|
Write-Host 'Running Java tests...'
|
|
& gradle ':zb-mom-ww-mxgateway-client:test' --no-daemon
|
|
if ($LASTEXITCODE -ne 0) { throw 'Java tests failed.' }
|
|
}
|
|
|
|
Write-Host 'Assembling Java jars + pom...'
|
|
& gradle `
|
|
':zb-mom-ww-mxgateway-client:assemble' `
|
|
':zb-mom-ww-mxgateway-client:sourcesJar' `
|
|
':zb-mom-ww-mxgateway-client:javadocJar' `
|
|
':zb-mom-ww-mxgateway-client:generatePomFileForMavenPublication' `
|
|
--no-daemon
|
|
if ($LASTEXITCODE -ne 0) { throw 'Java assemble failed.' }
|
|
|
|
$libsDir = Join-Path $javaDir 'zb-mom-ww-mxgateway-client/build/libs'
|
|
$jars = @(Get-ChildItem $libsDir -Filter 'zb-mom-ww-mxgateway-client-*.jar')
|
|
if ($jars.Count -eq 0) {
|
|
throw "No jars found under '$libsDir'."
|
|
}
|
|
foreach ($jar in $jars) {
|
|
Copy-Item $jar.FullName -Destination $OutputDir -Force
|
|
Write-Host " Copied $($jar.Name)"
|
|
}
|
|
|
|
$pomSrc = Join-Path $javaDir 'zb-mom-ww-mxgateway-client/build/publications/maven/pom-default.xml'
|
|
if (Test-Path $pomSrc) {
|
|
# Derive the version from the jar filename (e.g. zb-mom-ww-mxgateway-client-0.1.0.jar).
|
|
$versionJar = $jars | Where-Object { $_.Name -notmatch '-(sources|javadoc)\.jar$' } | Select-Object -First 1
|
|
$version = if ($versionJar) {
|
|
[System.IO.Path]::GetFileNameWithoutExtension($versionJar.Name) -replace '^zb-mom-ww-mxgateway-client-', ''
|
|
} else {
|
|
'0.1.0'
|
|
}
|
|
$pomDest = Join-Path $OutputDir "zb-mom-ww-mxgateway-client-$version.pom"
|
|
Copy-Item $pomSrc -Destination $pomDest -Force
|
|
Write-Host " Copied pom -> $([System.IO.Path]::GetFileName($pomDest))"
|
|
} else {
|
|
Write-Warning "POM not found at '$pomSrc'; skipping."
|
|
}
|
|
} finally { Pop-Location }
|
|
|
|
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 {
|
|
& gradle ':zb-mom-ww-mxgateway-client:publish' --no-daemon
|
|
if ($LASTEXITCODE -ne 0) { throw 'gradle publish failed.' }
|
|
} finally { Pop-Location }
|
|
}
|
|
}
|
|
|
|
# -------- Go --------
|
|
|
|
function Invoke-PackGo {
|
|
Write-Header 'Go'
|
|
Write-Host 'Go modules are released by git-tagging — no artifact to pack.' -ForegroundColor Yellow
|
|
Write-Host 'To publish a Go release, run:' -ForegroundColor Yellow
|
|
Write-Host ' pwsh scripts/tag-go-module.ps1 -Version v0.1.0 -Push' -ForegroundColor Yellow
|
|
Write-Host '(skipping)' -ForegroundColor DarkGray
|
|
}
|
|
|
|
# -------- Dispatch --------
|
|
|
|
$wanted = @{}
|
|
foreach ($lang in $Languages) { $wanted[$lang.ToLower()] = $true }
|
|
|
|
if ($wanted.ContainsKey('dotnet')) { Invoke-PackDotnet }
|
|
if ($wanted.ContainsKey('python')) { Invoke-PackPython }
|
|
if ($wanted.ContainsKey('rust')) { Invoke-PackRust }
|
|
if ($wanted.ContainsKey('java')) { Invoke-PackJava }
|
|
if ($wanted.ContainsKey('go')) { Invoke-PackGo }
|
|
|
|
# -------- Summary --------
|
|
|
|
Write-Header 'Summary'
|
|
$artifacts = @(Get-ChildItem $OutputDir)
|
|
if ($artifacts.Count -eq 0) {
|
|
Write-Host ' (no artifacts)' -ForegroundColor DarkGray
|
|
} else {
|
|
foreach ($a in $artifacts) {
|
|
Write-Host (' {0,10} {1}' -f $a.Length, $a.Name)
|
|
}
|
|
}
|
|
Write-Host ''
|
|
Write-Host "All artifacts in: $OutputDir" -ForegroundColor Green
|