upload-artifact@v4 speaks the v2 artifacts API, which this Gitea answers only at its v3-era shape, so the sixth release attempt cleared every gate and then aborted at "Preserve verified candidate artifacts" with the GHES compatibility error. Move the step to v3.2.1, the commit the upstream v3 tag resolves to, which keeps name, path, if-no-files-found and retention-days unchanged. The step also becomes continue-on-error: it is a pre-publish debugging backstop, and publish-release.sh attaches the same directory as Gitea release assets, so losing it must never cost a release. Replace cosign-installer with the direct fetch the scanner already uses. The action issues no API call on this path, but it is a composite action resting on envsubst and a resolved runner.arch, neither of which this runner has exercised. Asked for the version it bootstraps, it downloads exactly cosign-linux-amd64 from the v3.0.6 release, compares it against c956e5df..., and exits; that digest matches the release checksums file, so fetching the asset directly verifies identically with nothing unproven left before the one-way publication gate. The binary joins the PATH that publish-release.sh already resolves dotnet through.
340 lines
21 KiB
C#
340 lines
21 KiB
C#
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.DoesNotContain("aquasecurity/trivy-action", workflow, StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-64bit.tar.gz",
|
|
workflow,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"1816b632dfe529869c740c0913e36bd1629cb7688bd5634f4a858c1d57c88b75",
|
|
workflow,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("sha256sum --check --strict", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("TRIVY_CACHE_DIR=$RENDEZVOUS_WORK_DIR/trivy-cache", workflow, StringComparison.Ordinal);
|
|
|
|
// Gitea answers the v3-era artifacts API only, so upload-artifact stays on
|
|
// v3 and never gates the release it is only meant to help debug.
|
|
Assert.DoesNotContain(
|
|
"actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02",
|
|
workflow,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1",
|
|
workflow,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("continue-on-error: true", workflow, StringComparison.Ordinal);
|
|
|
|
// The signing client is fetched the way the scanner is: a pinned asset
|
|
// checked against a pinned digest, with no action resolved off github.com.
|
|
Assert.DoesNotContain("sigstore/cosign-installer", workflow, StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"https://github.com/sigstore/cosign/releases/download/v3.0.6/cosign-linux-amd64",
|
|
workflow,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"c956e5dfcac53d52bcf058360d579472f0c1d2d9b69f55209e256fe7783f4c74",
|
|
workflow,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("echo \"$cosign_bin_dir\" >>\"$GITHUB_PATH\"", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("--exit-code 1", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("--severity HIGH,CRITICAL", workflow, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("--ignore-unfixed", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("--format spdx-json", workflow, StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"--output \"$RENDEZVOUS_RELEASE_DIR/FinalFactory.Rendezvous.Container.$RENDEZVOUS_VERSION.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()));
|
|
|
|
string realConsumerGate = File.ReadAllText(Path.Combine(root, "scripts", "verify-real-consumers.sh"));
|
|
Assert.Contains("<PackageReference Remove=\"FinalFactory.Rendezvous.Client\" />", realConsumerGate, StringComparison.Ordinal);
|
|
Assert.Contains("<PackageReference Remove=\"FinalFactory.Rendezvous.Contracts\" />", realConsumerGate, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void AutomationProvidesNet8RuntimeForProcessVerification()
|
|
{
|
|
string root = FindRepositoryRoot();
|
|
string qualityWorkflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows/ci.yml"));
|
|
Assert.Equal(2, qualityWorkflow.Split("8.0.128", StringSplitOptions.None).Length - 1);
|
|
|
|
string releaseWorkflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows/release.yml"));
|
|
Assert.Contains("8.0.128", releaseWorkflow, StringComparison.Ordinal);
|
|
|
|
string releaseBuilder = File.ReadAllText(Path.Combine(root, "eng/release-builder.Dockerfile"));
|
|
Assert.Contains("runtime:8.0.28-noble@sha256:", releaseBuilder, StringComparison.Ordinal);
|
|
Assert.Contains("Microsoft.NETCore.App/8.0.28", releaseBuilder, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void AutomationUsesPerRunContainerPorts()
|
|
{
|
|
string root = FindRepositoryRoot();
|
|
string compose = File.ReadAllText(Path.Combine(root, "deploy/compose/compose.yaml"));
|
|
Assert.Contains("${RENDEZVOUS_HTTP_HOST_PORT:-8080}:8080/tcp", compose, StringComparison.Ordinal);
|
|
Assert.Contains("${RENDEZVOUS_UDP_HOST_PORT:-9050}:9050/udp", compose, StringComparison.Ordinal);
|
|
Assert.Contains("${RENDEZVOUS_CONFIG_SOURCE:-./appsettings.Production.json}", compose, StringComparison.Ordinal);
|
|
Assert.Contains("${RENDEZVOUS_SECRET_SOURCE:-./secrets/signing-key}", compose, StringComparison.Ordinal);
|
|
|
|
foreach (string workflowName in new[] { "ci.yml", "release.yml" })
|
|
{
|
|
string workflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows", workflowName));
|
|
Assert.Contains("GITHUB_RUN_ID", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("--network \"container:${container_id}\"", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("publisher_credential", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("RENDEZVOUS_PUBLISHER_CREDENTIAL", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("for attempt in {1..180}", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("sleep 1", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("http://127.0.0.1:8080/", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("127.0.0.1:9050", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("RENDEZVOUS_HTTP_HOST_PORT", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("RENDEZVOUS_UDP_HOST_PORT", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("RENDEZVOUS_SMOKE_HTTP_URL", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("RENDEZVOUS_SMOKE_UDP_ENDPOINT", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("runner_workspace_source", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("RENDEZVOUS_CONFIG_SOURCE", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("RENDEZVOUS_SECRET_SOURCE", workflow, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ContainerGateUsesValidInspectTemplates()
|
|
{
|
|
string root = FindRepositoryRoot();
|
|
string workflow = File.ReadAllText(Path.Combine(root, ".gitea/workflows/ci.yml"));
|
|
Assert.Contains(
|
|
"eq .Destination \"/app/appsettings.Production.json\"",
|
|
workflow,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"eq .Destination \"/run/secrets/rendezvous-signing-key\"",
|
|
workflow,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("eq .Destination \\\"", workflow, StringComparison.Ordinal);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionReadinessRecordIsFailClosedAndCanaryEvidenceIsRedacted()
|
|
{
|
|
string root = FindRepositoryRoot();
|
|
using JsonDocument readiness = JsonDocument.Parse(File.ReadAllText(Path.Combine(
|
|
root,
|
|
"docs/evidence/production-readiness-v1.json")));
|
|
JsonElement document = readiness.RootElement;
|
|
Assert.Equal(1, document.GetProperty("schemaVersion").GetInt32());
|
|
Assert.Equal("rendezvous-production-readiness", document.GetProperty("kind").GetString());
|
|
Assert.Matches("^[0-9a-f]{40}$", document.GetProperty("evaluatedCommit").GetString());
|
|
|
|
JsonElement[] local = document.GetProperty("localGates").EnumerateArray().ToArray();
|
|
JsonElement[] external = document.GetProperty("externalGates").EnumerateArray().ToArray();
|
|
Assert.Equal(6, local.Length);
|
|
Assert.Equal(13, external.Length);
|
|
JsonElement[] gates = local.Concat(external).ToArray();
|
|
Assert.Equal(gates.Length, gates.Select(static gate => gate.GetProperty("id").GetString()).Distinct().Count());
|
|
Assert.All(gates, static gate =>
|
|
{
|
|
Assert.True(gate.GetProperty("status").GetString() is "pass" or "pending" or "fail");
|
|
string evidence = Assert.IsType<string>(gate.GetProperty("evidenceRef").GetString());
|
|
Assert.False(Path.IsPathRooted(evidence));
|
|
Assert.DoesNotContain("..", evidence, StringComparison.Ordinal);
|
|
Assert.True(gate.GetProperty("note").GetString()!.Length <= 240);
|
|
});
|
|
bool allPass = gates.All(static gate => gate.GetProperty("status").GetString() == "pass");
|
|
Assert.Equal(allPass ? "ready" : "not-ready", document.GetProperty("decision").GetString());
|
|
|
|
string canary = File.ReadAllText(Path.Combine(root, "scripts/run-real-network-canary.sh"));
|
|
Assert.Contains("umask 077", canary, StringComparison.Ordinal);
|
|
Assert.Contains("client-expected-failure", canary, StringComparison.Ordinal);
|
|
Assert.Contains("exit_code\" -eq 12", canary, StringComparison.Ordinal);
|
|
Assert.Contains(".addressFamily == $family", canary, StringComparison.Ordinal);
|
|
Assert.Contains("identifiers:\"not-in-summary\"", canary, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("jq -c . \"$raw_log\"", canary, StringComparison.Ordinal);
|
|
|
|
string checker = File.ReadAllText(Path.Combine(root, "eng/check_production_readiness.py"));
|
|
Assert.Contains("return 3", checker, StringComparison.Ordinal);
|
|
Assert.Contains("FORBIDDEN_KEY_PARTS", checker, StringComparison.Ordinal);
|
|
Assert.Contains("decision must be", checker, StringComparison.Ordinal);
|
|
Assert.Contains("candidate capacity evidence", checker, StringComparison.Ordinal);
|
|
Assert.Contains("rendezvous-external-gate-attestation", checker, StringComparison.Ordinal);
|
|
Assert.Contains("does not resolve to a repository evidence file", checker, StringComparison.Ordinal);
|
|
}
|
|
|
|
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.");
|
|
}
|
|
}
|