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; 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:
./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:
- builds and verifies the artifact set;
- builds the exact versioned container candidate;
- starts that image with production hardening and a temporary staging key;
- completes HTTP health, registration, browse, authenticated UDP mediation, and direct traffic;
- rejects all high or critical container findings and emits a container SPDX inventory;
- finalizes and verifies checksums over the publish-ready artifact set; and
- 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; andCOSIGN_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.