feat(release): add reproducible signed artifacts (#19)
quality-gate / quality (push) Failing after 1m50s
quality-gate / container (push) Has been skipped

This commit is contained in:
KyuubiYoru
2026-07-16 17:48:21 +02:00
parent 07004cd75f
commit cc5793f935
52 changed files with 2568 additions and 73 deletions
@@ -1,30 +1,69 @@
using System.Xml.Linq;
namespace FinalFactory.Rendezvous.Tests.Contracts;
internal static class ContractTestFiles
{
public static string Read(string fileName) => File
.ReadAllText(Path.Combine(Directory, fileName))
.ReadAllText(Path.Combine(DirectoryFor(fileName), fileName))
.TrimEnd('\r', '\n');
public static string Directory
{
get
{
return VersionedDirectory(Property("RendezvousMajorVersion"));
}
}
public static string OpenApiDocument
{
get
{
string httpVersion = Property("HttpContractVersion");
return Path.Combine(RepositoryRoot, $"docs/api/rendezvous-v{httpVersion}.json");
}
}
private static string DirectoryFor(string fileName)
{
string property = fileName switch
{
"client-public-api.txt" or "contracts-public-api.txt" => "RendezvousMajorVersion",
"connection-ticket.json" => "ConnectionTicketFormatVersion",
_ when fileName.EndsWith(".hex", StringComparison.Ordinal) => "UdpContractVersion",
_ when fileName.EndsWith(".json", StringComparison.Ordinal) => "HttpContractVersion",
_ => throw new InvalidOperationException($"No contract version dimension maps {fileName}."),
};
return VersionedDirectory(Property(property));
}
private static string VersionedDirectory(string version) => Path.Combine(
RepositoryRoot,
$"tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v{version}");
private static string Property(string name)
{
XDocument versions = XDocument.Load(Path.Combine(RepositoryRoot, "eng/Versions.props"));
return versions.Descendants(name).Single().Value;
}
private static string RepositoryRoot
{
get
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
string solution = Path.Combine(directory.FullName, "Rendezvous.slnx");
if (File.Exists(solution))
if (File.Exists(Path.Combine(directory.FullName, "Rendezvous.slnx")))
{
return Path.Combine(
directory.FullName,
"tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v1");
return directory.FullName;
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not locate contract test data.");
throw new DirectoryNotFoundException("Could not locate repository root.");
}
}
}
@@ -40,12 +40,10 @@ public sealed class OpenApiCompatibilityTests
];
[Fact]
public void GeneratedOpenApiContainsTheFrozenV1Surface()
public void GeneratedOpenApiContainsTheFrozenVersionedSurface()
{
string path = Path.Combine(
ContractTestFiles.Directory,
"../../../../../docs/api/rendezvous-v1.json");
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(Path.GetFullPath(path)));
using JsonDocument document = JsonDocument.Parse(
File.ReadAllText(ContractTestFiles.OpenApiDocument));
JsonElement root = document.RootElement;
Assert.Equal("3.1.1", root.GetProperty("openapi").GetString());
@@ -1,3 +1,4 @@
using System.Text.Json;
using FinalFactory.Rendezvous.Client;
using FinalFactory.Rendezvous.Contracts;
@@ -22,6 +23,20 @@ public sealed class TraversalTokenCodecTests
Assert.DoesNotContain(encoded, decoded.ToString(), StringComparison.Ordinal);
}
[Fact]
public void ConnectionTicketMatchesTheVersionedV1Vector()
{
using JsonDocument vector = JsonDocument.Parse(ContractTestFiles.Read("connection-ticket.json"));
JsonElement root = vector.RootElement;
JoinAttemptId attemptId = new(Guid.Parse(root.GetProperty("attemptId").GetString()!));
string authenticator = root.GetProperty("derivedAuthenticator").GetString()!;
string ticket = NatIntroductionTokenCodec.Encode(attemptId, authenticator);
Assert.Equal(root.GetProperty("connectionTicket").GetString(), ticket);
Assert.Equal(root.GetProperty("digest").GetString(), NatIntroductionTokenCodec.ComputeDigest(ticket));
}
[Fact]
public void IntroductionTokenRejectsNonCanonicalOrAlteredFields()
{
@@ -101,6 +101,16 @@ public sealed class OperatorEndpointTests
tenant.GameId == "space-game"
&& tenant.EnvironmentId == "production"
&& tenant.Status == "enabled");
Assert.Equal("1.0.0", operatorStatus.Compatibility.ServerVersion);
Assert.Equal("1.0.0", operatorStatus.Compatibility.MinimumClientVersion);
Assert.Equal(1, operatorStatus.Compatibility.MaximumClientMajorVersion);
Assert.Equal([1], operatorStatus.Compatibility.HttpContractVersions);
Assert.Equal([1], operatorStatus.Compatibility.UdpContractVersions);
Assert.Equal([1], operatorStatus.Compatibility.ConnectionTicketFormatVersions);
Assert.Equal(2, operatorStatus.Compatibility.LiteNetLibMajorVersion);
Assert.Equal(
"exact-per-tenant",
operatorStatus.Compatibility.GameplayProtocolCompatibility);
Assert.Contains(operatorStatus.SigningKeys, static key =>
key.KeyId == OperatorTestHost.OperatorKeyId
&& key.Status == "signing"
@@ -0,0 +1,185 @@
using System.Reflection;
using System.Text.Json;
using System.Xml.Linq;
using FinalFactory.Rendezvous.Client;
using FinalFactory.Rendezvous.Contracts;
using FinalFactory.Rendezvous.Server.Operations;
namespace FinalFactory.Rendezvous.Tests.Release;
public sealed class ReleaseCompatibilityTests
{
[Fact]
public void CentralVersionsRuntimeWindowAndPublishedMatrixStayAligned()
{
string root = FindRepositoryRoot();
XDocument versions = XDocument.Load(Path.Combine(root, "eng/Versions.props"));
string releaseVersion = Property(versions, "RendezvousVersion");
int releaseMajor = int.Parse(Property(versions, "RendezvousMajorVersion"), System.Globalization.CultureInfo.InvariantCulture);
string[] numericVersion = releaseVersion.Split(['-', '+'], 2)[0].Split('.');
Assert.Equal(releaseMajor, int.Parse(numericVersion[0], System.Globalization.CultureInfo.InvariantCulture));
Assert.Equal(Property(versions, "RendezvousMinorVersion"), numericVersion[1]);
Assert.Equal(Property(versions, "RendezvousPatchVersion"), numericVersion[2]);
int httpVersion = int.Parse(Property(versions, "HttpContractVersion"), System.Globalization.CultureInfo.InvariantCulture);
int udpVersion = int.Parse(Property(versions, "UdpContractVersion"), System.Globalization.CultureInfo.InvariantCulture);
int ticketVersion = int.Parse(Property(versions, "ConnectionTicketFormatVersion"), System.Globalization.CultureInfo.InvariantCulture);
Assert.Equal(releaseVersion, typeof(RendezvousPublisherClient).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.Split('+')[0]);
Assert.Equal(releaseVersion, typeof(ContractLimits).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.Split('+')[0]);
Assert.Equal(ContractLimits.ContractVersion, httpVersion);
OperatorCompatibilityResponse runtime = ReleaseCompatibility.CreateResponse();
Assert.Equal(releaseVersion, runtime.ServerVersion);
Assert.Equal(Property(versions, "MinimumClientVersion"), runtime.MinimumClientVersion);
Assert.Equal(int.Parse(Property(versions, "MaximumClientMajorVersion"), System.Globalization.CultureInfo.InvariantCulture), runtime.MaximumClientMajorVersion);
Assert.Equal([httpVersion], runtime.HttpContractVersions);
Assert.Equal([udpVersion], runtime.UdpContractVersions);
Assert.Equal([ticketVersion], runtime.ConnectionTicketFormatVersions);
Assert.Equal(int.Parse(Property(versions, "LiteNetLibMajorVersion"), System.Globalization.CultureInfo.InvariantCulture), runtime.LiteNetLibMajorVersion);
Assert.Equal("exact-per-tenant", runtime.GameplayProtocolCompatibility);
using JsonDocument matrix = JsonDocument.Parse(File.ReadAllText(Path.Combine(root, "docs/releases/compatibility.json")));
JsonElement document = matrix.RootElement;
Assert.Equal(releaseVersion, document.GetProperty("release").GetString());
Assert.Equal(releaseVersion, document.GetProperty("packages").GetProperty("FinalFactory.Rendezvous.Client").GetString());
Assert.Equal(releaseVersion, document.GetProperty("packages").GetProperty("FinalFactory.Rendezvous.Contracts").GetString());
Assert.Equal(runtime.MinimumClientVersion, document.GetProperty("server").GetProperty("minimumClientVersion").GetString());
Assert.Equal(runtime.MaximumClientMajorVersion, document.GetProperty("server").GetProperty("maximumClientMajorVersion").GetInt32());
Assert.Equal(httpVersion, Assert.Single(document.GetProperty("contracts").GetProperty("http").EnumerateArray()).GetInt32());
Assert.Equal(udpVersion, Assert.Single(document.GetProperty("contracts").GetProperty("udp").EnumerateArray()).GetInt32());
Assert.Equal(ticketVersion, Assert.Single(document.GetProperty("contracts").GetProperty("connectionTicket").EnumerateArray()).GetInt32());
Assert.Equal(runtime.GameplayProtocolCompatibility, document.GetProperty("contracts").GetProperty("gameplay").GetString());
Assert.Equal("LiteNetLib", document.GetProperty("transport").GetProperty("package").GetString());
Assert.Equal(Property(versions, "LiteNetLibVersion"), document.GetProperty("transport").GetProperty("version").GetString());
Assert.Equal(runtime.LiteNetLibMajorVersion, document.GetProperty("transport").GetProperty("major").GetInt32());
foreach ((string consumer, string fixture) in new[]
{
("SpaceGame", "spacegame/SpaceGame.Rendezvous.Consumer.csproj"),
("Unscouted", "unscouted/Unscouted.Rendezvous.Consumer.csproj"),
})
{
XDocument fixtureProject = XDocument.Load(Path.Combine(root, "tests/consumers", fixture));
Assert.Equal(
fixtureProject.Descendants("TargetFramework").Single().Value,
document.GetProperty("consumers").GetProperty(consumer).GetString());
}
string snapshots = Path.Combine(root, $"tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v{releaseMajor}");
Assert.True(File.Exists(Path.Combine(snapshots, "client-public-api.txt")));
Assert.True(File.Exists(Path.Combine(snapshots, "contracts-public-api.txt")));
Assert.True(File.Exists(Path.Combine(root, $"docs/api/rendezvous-v{httpVersion}.json")));
}
[Fact]
public void ReleaseDefinitionIsImmutableTagOnlyAndDocumentsRequiredNotes()
{
string root = FindRepositoryRoot();
XDocument versions = XDocument.Load(Path.Combine(root, "eng/Versions.props"));
string releaseVersion = Property(versions, "RendezvousVersion");
string workflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows/release.yml"));
Assert.Contains("tags:", workflow, StringComparison.Ordinal);
Assert.Contains("RELEASE_TOKEN", workflow, StringComparison.Ordinal);
Assert.Contains("RELEASE_USERNAME", workflow, StringComparison.Ordinal);
Assert.Contains("COSIGN_PRIVATE_KEY", workflow, StringComparison.Ordinal);
Assert.DoesNotContain(":latest", workflow, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("skip-duplicate", workflow, StringComparison.OrdinalIgnoreCase);
Assert.Contains("check-compatibility.sh", workflow, StringComparison.Ordinal);
Assert.Contains("--platform linux/amd64", workflow, StringComparison.Ordinal);
Assert.Contains("ignore-unfixed: false", workflow, StringComparison.Ordinal);
Assert.Contains("format: spdx-json", workflow, StringComparison.Ordinal);
Assert.Contains("normalize-container-sbom", workflow, StringComparison.Ordinal);
Assert.Contains("finalize-release-candidate.sh", workflow, StringComparison.Ordinal);
Assert.Contains("verify-real-consumers.sh", workflow, StringComparison.Ordinal);
Assert.Contains("RENDEZVOUS_RELEASE_BUILDER", workflow, StringComparison.Ordinal);
Assert.Contains("--image-id", workflow, StringComparison.Ordinal);
string publisher = File.ReadAllText(Path.Combine(root, "scripts/publish-release.sh"));
Assert.Contains("expected_image_id", publisher, StringComparison.Ordinal);
Assert.Contains("finalize-signing-ready-release.sh", publisher, StringComparison.Ordinal);
XDocument packages = XDocument.Load(Path.Combine(root, "Directory.Packages.props"));
XElement liteNetLib = Assert.Single(packages.Descendants("PackageVersion"), static item => (string?)item.Attribute("Include") == "LiteNetLib");
Assert.Equal("[$(LiteNetLibVersion)]", (string?)liteNetLib.Attribute("Version"));
string changelog = File.ReadAllText(Path.Combine(root, "CHANGELOG.md"));
Assert.Contains("### Compatibility", changelog, StringComparison.Ordinal);
Assert.Contains("### Security and configuration", changelog, StringComparison.Ordinal);
Assert.Contains("### Migration", changelog, StringComparison.Ordinal);
Assert.DoesNotContain($"{releaseVersion} - Unreleased", changelog, StringComparison.Ordinal);
foreach (string consumer in new[] { "spacegame", "unscouted" })
{
string consumerDirectory = Path.Combine(root, "tests/consumers", consumer);
string projectPath = Assert.Single(Directory.GetFiles(consumerDirectory, "*.csproj"));
XDocument consumerProject = XDocument.Load(projectPath);
XElement[] references = consumerProject.Descendants("PackageReference")
.Where(static item => ((string?)item.Attribute("Include"))?.StartsWith("FinalFactory.Rendezvous.", StringComparison.Ordinal) == true)
.ToArray();
Assert.Equal(2, references.Length);
Assert.All(references, static reference =>
Assert.Equal("[$(RendezvousPackageVersion)]", (string?)reference.Attribute("Version")));
Assert.Equal(
"false",
consumerProject.Descendants("RestorePackagesWithLockFile").Single().Value);
}
XDocument unscouted = XDocument.Load(Path.Combine(
root,
"tests/consumers/unscouted/Unscouted.Rendezvous.Consumer.csproj"));
XElement directTransport = Assert.Single(
unscouted.Descendants("PackageReference"),
static item => (string?)item.Attribute("Include") == "LiteNetLib");
Assert.Equal("[2.1.4]", (string?)directTransport.Attribute("Version"));
using JsonDocument consumers = JsonDocument.Parse(
File.ReadAllText(Path.Combine(root, "eng/consumer-revisions.json")));
JsonElement[] pinnedConsumers = consumers.RootElement.GetProperty("consumers")
.EnumerateArray()
.ToArray();
Assert.Equal(
["SpaceGame", "Unscouted"],
pinnedConsumers.Select(static item => item.GetProperty("name").GetString()!).ToArray());
Assert.All(pinnedConsumers, static item =>
Assert.Matches("^[0-9a-f]{40}$", item.GetProperty("revision").GetString()));
}
[Fact]
public void ServerSourceManifestIsCompleteSortedAndDeterministic()
{
string root = FindRepositoryRoot();
string projectDirectory = Path.Combine(root, "src/FinalFactory.Rendezvous.Server");
XDocument project = XDocument.Load(Path.Combine(projectDirectory, "FinalFactory.Rendezvous.Server.csproj"));
string[] declared = project.Descendants("Compile")
.Select(static item => (string)item.Attribute("Include")!)
.ToArray();
string[] actual = Directory.GetFiles(projectDirectory, "*.cs", SearchOption.AllDirectories)
.Where(static path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
&& !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
.Select(path => Path.GetRelativePath(projectDirectory, path).Replace(Path.DirectorySeparatorChar, '/'))
.Order(StringComparer.Ordinal)
.ToArray();
Assert.Equal(actual, declared);
}
private static string Property(XDocument document, string name) =>
document.Descendants(name).Single().Value;
private static string FindRepositoryRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "Rendezvous.slnx")))
{
return directory.FullName;
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not locate repository root.");
}
}
@@ -0,0 +1,6 @@
{
"attemptId": "00000000-0000-0000-0000-000000000301",
"derivedAuthenticator": "TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT",
"connectionTicket": "AAAAAAAAAAAAAAAAAAADAU000000000000000000000",
"digest": "d6yCrbIOzwKoaOZQ8A9eggclDY0yzmhJH36FDz4L7wE"
}
@@ -97,7 +97,7 @@
"type": "Project",
"dependencies": {
"FinalFactory.Rendezvous.Contracts": "[1.0.0, )",
"LiteNetLib": "[2.1.4, )"
"LiteNetLib": "[2.1.4, 2.1.4]"
}
},
"finalfactory.rendezvous.contracts": {
@@ -107,7 +107,7 @@
"type": "Project",
"dependencies": {
"FinalFactory.Rendezvous.Contracts": "[1.0.0, )",
"LiteNetLib": "[2.1.4, )",
"LiteNetLib": "[2.1.4, 2.1.4]",
"Microsoft.AspNetCore.OpenApi": "[10.0.9, )"
}
},
@@ -116,12 +116,12 @@
"dependencies": {
"FinalFactory.Rendezvous.Client": "[1.0.0, )",
"FinalFactory.Rendezvous.Contracts": "[1.0.0, )",
"LiteNetLib": "[2.1.4, )"
"LiteNetLib": "[2.1.4, 2.1.4]"
}
},
"LiteNetLib": {
"type": "CentralTransitive",
"requested": "[2.1.4, )",
"requested": "[2.1.4, 2.1.4]",
"resolved": "2.1.4",
"contentHash": "KWlxvMw3Urpqj9joD96LRiK+LC62pQNs/zkXRJc+rHnxgkGp+vV703xzDrxRmv+V1YhCFfIGzs5nrVWtREIlyA=="
},