# Releases and compatibility Tracking: #19 Rendezvous releases are immutable, reproducible, and promoted only after the same candidate has passed package, consumer, server, container, and staging checks. A release consists of matching Client and Contracts NuGet packages, a framework-dependent Linux server archive, a versioned linux/amd64 OCI image, package/runtime and container SPDX inventories, checksums, provenance, release notes, and signatures. No workflow publishes a `latest` tag. ## Version dimensions The central values in `eng/Versions.props` are the authority. Client, Contracts, and Server use SemVer. HTTP, UDP mediation, and connection-ticket formats advance independently so a wire change cannot hide inside a package patch release. The current machine-readable matrix is [`compatibility.json`](compatibility.json); the authenticated operator status endpoint exposes the server's supported window at runtime. | Surface | Current | Compatibility rule | | --- | ---: | --- | | Client and Contracts | 1.0.0 | Matching exact versions; source/API breaks require a package major bump. | | Server | 1.0.0 | Accepts Client 1.0.0 through compatible 1.x releases. | | HTTP contract | 1 | Frozen OpenAPI, JSON vectors, and public API snapshot. | | UDP mediation | 1 | Frozen codec vectors; incompatible bytes require UDP v2. | | Connection ticket | 1 | A format change requires a new accepted ticket version and migration window. | | LiteNetLib | 2.1.4 | Exact dependency; LiteNetLib 1.x is rejected by package and consumer gates. | | Gameplay protocol | Per tenant | Exact match; Rendezvous does not translate gameplay protocols. | `scripts/check-compatibility.sh` compares protected snapshots against the base revision. A changed public API snapshot requires a package major increase; an HTTP or UDP golden surface requires the corresponding contract increase. The normal tests also compare implementation output with the current versioned snapshots. For a deliberate break, add a new versioned contract directory and documentation instead of replacing the prior version's evidence. ## Candidate build From a clean tagged checkout: ```bash ./scripts/check-release-tag.sh v1.0.0 ./scripts/build-release.sh 1.0.0 ./scripts/verify-release.sh 1.0.0 ``` The tag build runs inside the digest-pinned `release-builder` Docker stage, which combines the pinned SDK with a pinned Python runtime. It uses the locked dependency graph, enforces NuGet advisories and approved licenses, runs formatting/build/tests, regenerates the OpenAPI drift check, and packs twice after a clean rebuild. NuGet's random OPC relationship identifiers are canonicalized before comparison; both `.nupkg` and `.snupkg` outputs must then be byte-identical. Package metadata identifies the exact repository commit, portable PDBs carry SourceLink data, and the Linux archive, runtime SBOM timestamp, and checksum ordering are deterministic. Buildx and BuildKit are also pinned for the linux/amd64 OCI build. Provenance records each artifact-producing tool version; an out-of-band rebuild must use the pinned builder and recorded versions rather than treating the runner label as a reproducibility guarantee. All project-authored artifact normalization and checksum updates run inside the same pinned builder; host tools only orchestrate or verify. The separately pinned Trivy and Cosign tools produce the container inventory and signatures. The tag workflow also performs two no-cache image builds with the commit time and revision fixed, disables unsigned builder-generated attestations, and requires identical OCI image IDs before signing the project provenance. The local gate builds net8.0 SpaceGame- and Unscouted-shaped API fixtures using only the candidate feed plus NuGet.org; the Unscouted fixture also carries its real direct LiteNetLib 2.1.4 pin. The tag gate separately checks out the exact SpaceGame and Unscouted revisions in `eng/consumer-revisions.json`, injects exact candidate references without modifying those repositories, and restores their real game/network projects. Both paths must resolve the matching Client and Contracts version and LiteNetLib 2.1.4. Updating a consumer revision is a reviewed compatibility change, not a floating-main check. ## Promotion and publication Pushing the matching `vMAJOR.MINOR.PATCH` tag starts the tag-only release workflow. Before any external write it: 1. builds and verifies the artifact set; 2. builds the exact versioned container candidate; 3. starts that image with production hardening and a temporary staging key; 4. completes HTTP health, registration, browse, authenticated UDP mediation, and direct traffic; 5. rejects all high or critical container findings and emits a container SPDX inventory; 6. finalizes and verifies checksums over the publish-ready artifact set; and 7. confirms the two NuGet versions, container version, and Gitea release do not already exist. Publication has no skip-duplicate behavior. Gitea's immutable package versions, the workflow concurrency lock, and the preflight make a successful tag a single publication event. The workflow pushes symbols, publishes only the versioned container tag, records its `sha256` digest in both a digest file and provenance, regenerates the checksum manifest so that digest and public key are covered, signs the checksum manifest and image, attaches signed provenance, verifies the complete published-set schema and all signatures, and creates the Gitea release with exactly those artifacts. The detached checksum signature bundle is the sole envelope excluded from its own signed manifest. The loaded image ID is captured immediately after the byte-reproducible build; publication refuses to push if staging or another process retagged that local name to different bytes. The protected `production` environment requires these secrets: - `RELEASE_TOKEN`: a dedicated Gitea token limited to this repository and the HeiKyu package registry, with repository and package write access; - `RELEASE_USERNAME`: the dedicated Gitea service-account name that owns the release token; - `COSIGN_PRIVATE_KEY`: the encrypted Cosign release private key; and - `COSIGN_PASSWORD`: its password, stored separately. No development signing key, registry credential, or deployable configuration is stored in source or packages. Keep the Cosign public key with operational records. Rotate the release key between releases: retain the old public key for historical verification, install the new encrypted private key and password as one reviewed change, verify a signed non-release blob, and only then retire the old secret. Suspected compromise requires token/key revocation and a new version; never overwrite or delete evidence to reuse a released version. ## Release notes and migration Every dated `CHANGELOG.md` entry must contain Compatibility, Security and configuration, and Migration sections. Before tagging, state the supported Client/server window, all HTTP/UDP/ticket changes, security fixes, required configuration, and operator/consumer migration steps. For a protocol migration, first make the server read both old and new versions within an explicit bounded window, publish a Client that writes the new version, verify adoption through bounded telemetry, then remove the old reader only in a subsequent breaking release. Never silently reinterpret old bytes. Consumers pin both Rendezvous packages to one exact version and choose gameplay protocol compatibility per tenant. ## Rollback and interrupted publication Runtime rollback means redeploying the previous known-good image by digest and its matching configuration; it does not move a tag. Packages and release records remain available so already restored clients stay reproducible. If a new release is faulty, revoke affected publisher or signing keys when relevant, mark the release notes as withdrawn, and publish the fix under a new SemVer. The registries cannot provide a transaction spanning NuGet, OCI, signatures, and release attachments. If publication stops after its first external write, the preflight intentionally prevents an automatic rerun. An operator must inventory every destination, preserve logs and hashes, complete or withdraw the partial version under change control, and then issue a new version. This avoids turning a partial failure into an untraceable overwrite.