|
|
|
@@ -0,0 +1,150 @@
|
|
|
|
|
# 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.
|