feat(release): add reproducible signed artifacts (#19)
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user