# 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](./ToolchainLinks.md) 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`:

```powershell
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:

```powershell
$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.

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.

## .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:

```powershell
dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj
```

Build and test from the repository root:

```powershell
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:

```powershell
$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:

```powershell
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:

```powershell
Push-Location clients/go
./generate-proto.ps1
Pop-Location
```

Build and test from `clients/go`:

```powershell
Push-Location clients/go
go test ./...
go build ./...
go vet ./...
Pop-Location
```

Create a local CLI executable:

```powershell
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:

```powershell
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:

```powershell
Push-Location clients/rust
cargo check --workspace
Pop-Location
```

Build and test from `clients/rust`:

```powershell
Push-Location clients/rust
cargo fmt --all --check
cargo test --workspace
cargo check --workspace
Pop-Location
```

Create local release artifacts:

```powershell
Push-Location clients/rust
cargo build --workspace --release
cargo install --path crates/mxgw-cli --locked --force
Pop-Location
```

Run the CLI from source:

```powershell
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:

```powershell
Push-Location clients/python
./generate-proto.ps1
Pop-Location
```

Install, test, and build a wheel from `clients/python`:

```powershell
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`:

```powershell
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:

```powershell
Push-Location clients/java
gradle :zb-mom-ww-mxgateway-client:generateProto
Pop-Location
```

Build and test from `clients/java`:

```powershell
Push-Location clients/java
gradle test
Pop-Location
```

Create local library and CLI artifacts:

```powershell
Push-Location clients/java
gradle :zb-mom-ww-mxgateway-client:jar :zb-mom-ww-mxgateway-cli:installDist
Pop-Location
```

Run the CLI through Gradle:

```powershell
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:

```powershell
$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.

## Related Documentation

- [Client Proto Generation](./ClientProtoGeneration.md)
- [Client Libraries Detailed Design](./ClientLibrariesDesign.md)
- [Client Behavior Fixtures](./ClientBehaviorFixtures.md)
- [Toolchain Links](./ToolchainLinks.md)
