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