.NET/Go/Python/Rust clients (plus Contracts, which releases in lockstep
with the .NET Client) move 0.2.0 -> 0.2.1; Java moves 0.2.1 -> 0.2.2. This
starts the next release train the finding recommended, since post-publish
source changes (including the Python connect() event-loop fix, a8f86b5)
had been sitting at already-published version numbers for nine days with
nothing recording the divergence.
- Note the pypi 0.2.0 connect() defect and its supersession by 0.2.1 in
clients/python/README.md and docs/ClientPackaging.md.
- Add scripts/check-client-versions.ps1: a PR-time guard that finds each
client's latest publish tag (clients/go/vX.Y.Z for Go, the only client
with a git tag; the other four publish straight to a registry with no
tag, so the check reports "no tags found" and passes for them) and fails
if clients/<lang> changed since that tag while the version constant
still equals the tag's version. Wired into .gitea/workflows/ci.yml's
portable job next to the codegen freshness checks (needs fetch-depth: 0
for tag history).
- Mark remediation batch A Done in
archreview/2026-08-19/remediation/00-tracking.md.
Verified on macOS: dotnet build clients/dotnet/...slnx (pass), cargo check
--workspace (pass), python -m pytest (168 passed, 1 skipped), gradle test
with JDK 17 (pass, no generated-tree churn to revert this time), gofmt -l
+ go build ./... + go test ./... (clean), and
scripts/check-client-versions.ps1 via pwsh (pass; also verified it fails
correctly when a version constant is rolled back).
17 KiB
Client Packaging
This document defines the clean-checkout commands for building, packaging, and
running the official MXAccess Gateway clients. Use the tool paths and versions
in Toolchain Links when a command is missing from
PATH.
Shared Inputs
All clients generate bindings from the shared protobuf files under
src/ZB.MOM.WW.MxGateway.Contracts/Protos. Regenerate the published client descriptor
after changing either .proto file or clients/proto/proto-inputs.json:
scripts/publish-client-proto-inputs.ps1
scripts/publish-client-proto-inputs.ps1 -Check
Generated protobuf and gRPC files are generator output. Do not edit them by hand.
Environment
The examples use these common variables:
$env:MXGATEWAY_ENDPOINT = 'localhost:5000'
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
$env:MXGATEWAY_TEST_ITEM = 'TestObject.TestInt'
Use plaintext only for a local gateway. Use TLS when the gateway crosses a machine boundary or uses a production certificate.
Versioning
Every client's version lives in its own manifest: clients/rust/Cargo.toml
([package] and [workspace.package], both must match — crates/mxgw-cli
inherits via version.workspace = true), clients/python/pyproject.toml
([project].version) and clients/python/src/zb_mom_ww_mxgateway/version.py
(__version__, must match pyproject.toml), clients/go/mxgateway/version.go
(ClientVersion), clients/dotnet/ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj
(<Version>), and clients/java/build.gradle (subprojects { version = ... },
mirrored by the hand-maintained MxGatewayClientVersion.CLIENT_VERSION
constant — the two have drifted before and there is no build-time link
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
rejects re-uploading an existing name+version, and scripts/pack-clients.ps1
enforces this before it ever attempts a push: each per-language -Publish
step queries the Gitea package API
(GET /api/v1/packages/dohertj2/{type}/{name}/{version}) for the version
about to be published and aborts with a clear error if it already exists —
the script never force-overwrites a published artifact. scripts/tag-go-module.ps1
carries the equivalent guard for the Go module: it refuses to create a
clients/go/vX.Y.Z tag unless clients/go/mxgateway/version.go's
ClientVersion already equals X.Y.Z (CLI-21/CLI-39), so a forgotten
version bump fails the tag instead of shipping a mismatched module.
Both of those guards only fire when someone actually runs a publish.
CLI-46 found the same hazard recurring silently between publishes: a
real bug fix (the pypi 0.2.0 connect() fix above, commit a8f86b5) plus
later wire-surface changes shipped into the tree while every version
constant still read the already-published number, and nothing recorded
that the registry artifact and the in-repo source had diverged.
scripts/check-client-versions.ps1 is the PR-time half of that guard —
wired into CI (.gitea/workflows/ci.yml, the portable job, next to the
codegen freshness checks): for each client it finds the latest publish tag
(clients/go/vX.Y.Z for Go — the only client with a git tag at all; .NET,
Python, Rust, and Java publish straight to a package registry via
pack-clients.ps1 -Publish with no corresponding git tag, so the check
reports "no tags found" and passes for those four today) and fails if
clients/<lang> differs from that tag while the version constant still
equals the tag's version. A client with no publish tag is not a
violation — there is no "since the last publish" window to check yet.
As of 2026-08-07 (CLI-39) all five clients — plus ZB.MOM.WW.MxGateway.Contracts,
which releases in lockstep with the .NET Client — moved to 0.2.0,
converging on one number after four of the five had drifted onto the
already-published 0.1.2/0.1.1 while their public APIs kept changing
underneath it (see archreview/2026-07-12/remediation/50-clients.md CLI-39).
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
source pinning 0.1.2; maven
(com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client) had already published
0.2.0 on 2026-06-26 — before the CLI-37/38/40/41 conformance fixes changed
the client's observable behavior (category-based status validation,
hresult < 0, exact-secret redaction, typed malformed-reply errors). Reusing
0.2.0 for the conformant Java build would have labeled two different APIs
with the same coordinate, so Java is the one exception: it shipped as
0.2.1, not 0.2.0. Operators publishing a future release must re-check the
target version against the live registry before assuming any of these
numbers are still unclaimed — the guards above do this automatically at
publish time, but a version bump in the source is still a manual step per
client.
On 2026-08-07 that release shipped. Published coordinates on
gitea.dohertylan.com: nuget ZB.MOM.WW.MxGateway.Client 0.2.0 and
ZB.MOM.WW.MxGateway.Contracts 0.2.0, pypi
zb-mom-ww-mxaccess-gateway-client 0.2.0, cargo
zb-mom-ww-mxgateway-client 0.2.0, maven
com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client 0.2.1 (the Java
exception described above). Go publishes no artifact — it ships as the module
tag clients/go/v0.2.0, created at commit a346d51. Each coordinate was
confirmed present through the Gitea package API after the push, and
go list -m resolves the Go tag. These are the numbers a future release
bumps off.
The published pypi 0.2.0 artifact has a broken connect() (CLI-46).
Its connect() built the grpc.aio channel inside asyncio.to_thread, and
a grpc.aio channel binds to the event loop current on the constructing
thread, so every non-stub connection raised RuntimeError: There is no current event loop in thread — TST-24's first real-wire run caught it.
Fixed in-tree by commit a8f86b5 (2026-08-10): resolve_channel_security
now runs off-loop, create_channel runs on-loop. That fix, plus later
wire-surface additions (d9ea8a8, c748361), shipped after the 2026-08-07
publish while the version constants still read the already-published
0.2.0/0.2.1 numbers above — see clients/python/README.md for the
user-facing warning. 0.2.0 is superseded by 0.2.1; do not install it.
As of this remediation (CLI-46), the in-source version constants for the
.NET, Go, Python, and Rust clients (plus Contracts) read 0.2.1, and the
Java client reads 0.2.2. These are not yet published — the numbers
above (0.2.0 four clients + Contracts, 0.2.1 Java) remain the live registry
state until the next pack-clients.ps1 / tag-go-module.ps1 run. Once that
publish lands, update this section with the new coordinates and commit(s),
the same way the 2026-08-07 record above was written after the fact.
.NET
The .NET client uses .NET 10 and references
src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj for generated C# contract
types. clients/dotnet/generated remains reserved for client-local generator
output if the client later decouples from the contracts project.
Regenerate the generated C# contract types:
dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj
Build and test from the repository root:
dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx
dotnet test clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx --no-build
Create local package artifacts:
$dotnetPackageOutput = Join-Path (Get-Location) 'artifacts/clients/dotnet'
dotnet pack clients/dotnet/ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj -c Release -p:PackageOutputPath="$dotnetPackageOutput"
dotnet publish clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/ZB.MOM.WW.MxGateway.Client.Cli.csproj -c Release -o artifacts/clients/dotnet/mxgw-dotnet
Run the CLI from source:
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- version --json
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint "http://$env:MXGATEWAY_ENDPOINT" --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint "https://mxgateway.example.local:5001" --tls --ca-file C:\certs\mxgateway-ca.pem --server-name mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
Go
The Go client is the module
gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go.
Generated Go files live under clients/go/internal/generated.
Regenerate the Go bindings:
Push-Location clients/go
./generate-proto.ps1
Pop-Location
Build and test from clients/go:
Push-Location clients/go
go test ./...
go build ./...
go vet ./...
Pop-Location
Create a local CLI executable:
Push-Location clients/go
New-Item -ItemType Directory -Force ../../artifacts/clients/go | Out-Null
go build -o ../../artifacts/clients/go/mxgw-go.exe ./cmd/mxgw-go
Pop-Location
Run the CLI from source:
Push-Location clients/go
go run ./cmd/mxgw-go version -json
go run ./cmd/mxgw-go smoke -endpoint $env:MXGATEWAY_ENDPOINT -plaintext -api-key-env MXGATEWAY_API_KEY -item $env:MXGATEWAY_TEST_ITEM -json
go run ./cmd/mxgw-go smoke -endpoint mxgateway.example.local:5001 -ca-cert C:\certs\mxgateway-ca.pem -server-name-override mxgateway.example.local -api-key-env MXGATEWAY_API_KEY -item $env:MXGATEWAY_TEST_ITEM -json
Pop-Location
Rust
The Rust workspace builds the mxgateway-client library crate and the mxgw
CLI crate. build.rs generates tonic and prost modules into Cargo build
output on each build that needs updated protobuf output.
build.rs resolves its .proto inputs repo-path-first, then vendored: it
prefers the canonical protos under src/ZB.MOM.WW.MxGateway.Contracts/Protos
so an in-repo edit is live immediately, and falls back to the copies vendored
into clients/rust/protos/ only when the canonical directory is absent — the
case for a published crate unpacked outside this repo. The vendored copies
are declared in Cargo.toml's include list, so cargo package/cargo publish ship them inside the .crate, making the crate buildable standalone
with no access to the rest of the mxaccessgw repo. Any Contracts proto change
must refresh clients/rust/protos/ in the same commit; scripts/check-codegen.ps1
Check 3 byte-compares the vendored copies against the canonical protos and
fails on drift — in both directions, so a newly added canonical proto that
was never vendored fails there rather than at a consumer's standalone build. Because the vendored protos make a standalone build possible,
cargo package/cargo publish run with verification (no --no-verify) —
a cargo package that cannot build from the vendored tree alone would mean
the vendored copies are stale, and verification is what catches that before
publish.
Publishing to the dohertj2-gitea alternative registry reads the token from
CARGO_REGISTRIES_DOHERTJ2_GITEA_TOKEN, and that variable must hold
Bearer <token> — cargo sends the value as the Authorization header
verbatim and Gitea's cargo registry rejects a bare token with 401, unlike
the other feeds, which authenticate with a username/token basic-auth pair.
Regenerate and compile Rust bindings:
Push-Location clients/rust
cargo check --workspace
Pop-Location
Build and test from clients/rust:
Push-Location clients/rust
cargo fmt --all --check
cargo test --workspace
cargo check --workspace
Pop-Location
Create local release artifacts:
Push-Location clients/rust
cargo build --workspace --release
cargo install --path crates/mxgw-cli --locked --force
Pop-Location
Run the CLI from source:
Push-Location clients/rust
cargo run -p mxgw-cli -- version --json
cargo run -p mxgw-cli -- smoke --endpoint "http://127.0.0.1:5000" --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
cargo run -p mxgw-cli -- smoke --endpoint "https://mxgateway.example.local:5001" --tls --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
Pop-Location
Python
The Python package is zb-mom-ww-mxaccess-gateway-client. Generated modules
live under clients/python/src/zb_mom_ww_mxgateway/generated.
Regenerate the Python bindings:
Push-Location clients/python
./generate-proto.ps1
Pop-Location
Install, test, and build a wheel from clients/python:
Push-Location clients/python
python -m pip install -e ".[dev]"
python -m pytest
python -m pip wheel . --no-deps --wheel-dir "$env:TEMP\mxgateway-python-wheel"
Pop-Location
Run the CLI from the editable install or with python -m:
Push-Location clients/python
mxgw-py version --json
mxgw-py smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
mxgw-py smoke --endpoint mxgateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
python -m zb_mom_ww_mxgateway_cli version --json
Pop-Location
Java
The Java workspace uses Gradle, Java 17, zb-mom-ww-mxgateway-client, and
zb-mom-ww-mxgateway-cli. The Gradle protobuf plugin writes generated Java protobuf and
gRPC sources under clients/java/src/main/generated.
Regenerate Java bindings:
Push-Location clients/java
gradle :zb-mom-ww-mxgateway-client:generateProto
Pop-Location
Build and test from clients/java:
Push-Location clients/java
gradle test
Pop-Location
Create local library and CLI artifacts:
Push-Location clients/java
gradle :zb-mom-ww-mxgateway-client:jar :zb-mom-ww-mxgateway-cli:installDist
Pop-Location
Run the CLI through Gradle:
Push-Location clients/java
gradle :zb-mom-ww-mxgateway-cli:run --args="version --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="smoke --endpoint mxgateway.example.local:5001 --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json"
Pop-Location
Integration Tests
Client integration checks are opt-in because they need a live gateway and a gateway host that can create MXAccess worker sessions. Set the common environment before running a client smoke:
$env:MXGATEWAY_INTEGRATION = '1'
$env:MXGATEWAY_ENDPOINT = 'localhost:5000'
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
$env:MXGATEWAY_TEST_ITEM = 'TestObject.TestInt'
$env:MXGATEWAY_TEST_CONTEXT = ''
$env:MXGATEWAY_TEST_WRITE_VALUE = '123'
Run the bounded smoke command for each client against the same item. The
smoke commands open a session, register a client name, add one item, advise it,
and close the session. The .NET and Python smoke commands also read a bounded
event stream; the Go, Rust, and Java smoke commands exercise the command path
and can be paired with their stream-events commands after a session is open.
Client-side cancellation or timeout stops waiting for the gateway response. It does not abort an MXAccess COM call that is already executing on the worker STA.