From 95c3a4aed620e8b4409240533885f697ffbfafb0 Mon Sep 17 00:00:00 2001 From: KyuubiYoru Date: Thu, 16 Jul 2026 22:39:50 +0200 Subject: [PATCH] docs(operations): record v1 readiness evidence (#23) --- docs/evidence/capacity/v2/candidate-2cpu.json | 70 +++---- docs/evidence/production-readiness-v1.json | 36 ++-- .../releases/v1.0.0-local-candidate.json | 58 ++++++ docs/operations/capacity-and-resilience.md | 2 +- docs/operations/production-readiness.md | 26 +++ eng/check_production_readiness.py | 180 +++++++++++++++++- scripts/run-real-network-canary.sh | 21 +- .../Release/ReleaseCompatibilityTests.cs | 3 + 8 files changed, 327 insertions(+), 69 deletions(-) create mode 100644 docs/evidence/releases/v1.0.0-local-candidate.json diff --git a/docs/evidence/capacity/v2/candidate-2cpu.json b/docs/evidence/capacity/v2/candidate-2cpu.json index 8479612..67522c0 100644 --- a/docs/evidence/capacity/v2/candidate-2cpu.json +++ b/docs/evidence/capacity/v2/candidate-2cpu.json @@ -1,7 +1,7 @@ { "schemaVersion": 2, "evidenceVersion": "v2", - "generatedAt": "2026-07-16T14:10:53.6981858+00:00", + "generatedAt": "2026-07-16T20:28:43.2873744+00:00", "profile": "candidate", "runtime": { "framework": ".NET 10.0.9", @@ -14,14 +14,14 @@ "cpuQuota": "not-enforced", "memoryLimit": "not-enforced", "garbageCollector": "workstation", - "commitSha": "cf14836d48b0b4aaa67f99433f4fba3585bcd2bb", + "commitSha": "00d5ff776408e7d80ce6648953e62a7233aca35c", "treeState": "clean", "command": "RENDEZVOUS_CAPACITY_PROFILE=candidate RENDEZVOUS_CAPACITY_CPUSET=0,1 ./scripts/run-capacity-gate.sh", "imageDigest": "not-containerized", "workloadSeed": "fixed-sequences-random-identifiers", - "capacityPhaseAverageCpuPercent": 56.37724115383554, - "peakWorkingSetBytes": 169705472, - "managedBytesAfterCleanup": 35615200 + "capacityPhaseAverageCpuPercent": 55.52666859166872, + "peakWorkingSetBytes": 176758784, + "managedBytesAfterCleanup": 35608984 }, "targets": { "visibleListings": 25000, @@ -39,10 +39,10 @@ { "operation": "registration-and-presence", "samples": 1000, - "p50Milliseconds": 0.003, + "p50Milliseconds": 0.0029, "p95Milliseconds": 0.0046, - "p99Milliseconds": 0.0054, - "operationsPerSecond": 282453.96000451926, + "p99Milliseconds": 0.0055, + "operationsPerSecond": 287918.9220315559, "minimumOperationsPerSecond": 200, "budgetMilliseconds": 200, "passed": true @@ -51,9 +51,9 @@ "operation": "lease-renewal", "samples": 1000, "p50Milliseconds": 0.0004, - "p95Milliseconds": 0.0009, - "p99Milliseconds": 0.0021, - "operationsPerSecond": 968992.2480620155, + "p95Milliseconds": 0.0007, + "p99Milliseconds": 0.0019, + "operationsPerSecond": 1076426.264800861, "minimumOperationsPerSecond": 200, "budgetMilliseconds": 200, "passed": true @@ -61,10 +61,10 @@ { "operation": "visible-session-browse", "samples": 250, - "p50Milliseconds": 0.9046, - "p95Milliseconds": 3.3704, - "p99Milliseconds": 3.9471, - "operationsPerSecond": 695.5799787597697, + "p50Milliseconds": 1.0232, + "p95Milliseconds": 3.6083, + "p99Milliseconds": 4.2925, + "operationsPerSecond": 650.0325926341947, "minimumOperationsPerSecond": 200, "budgetMilliseconds": 200, "passed": true @@ -72,10 +72,10 @@ { "operation": "join-attempt-issuance", "samples": 1000, - "p50Milliseconds": 0.0029, - "p95Milliseconds": 0.0045, - "p99Milliseconds": 0.0055, - "operationsPerSecond": 296428.042092782, + "p50Milliseconds": 0.0028, + "p95Milliseconds": 0.0042, + "p99Milliseconds": 0.0052, + "operationsPerSecond": 135253.93927098127, "minimumOperationsPerSecond": 200, "budgetMilliseconds": 200, "passed": true @@ -83,10 +83,10 @@ { "operation": "simultaneous-punch-pairing", "samples": 1000, - "p50Milliseconds": 0.0043, - "p95Milliseconds": 0.0073, - "p99Milliseconds": 0.0115, - "operationsPerSecond": 109212.03516627532, + "p50Milliseconds": 0.0039, + "p95Milliseconds": 0.0069, + "p99Milliseconds": 0.0087, + "operationsPerSecond": 110619.46902654869, "minimumOperationsPerSecond": 2000, "budgetMilliseconds": 100, "passed": true @@ -94,10 +94,10 @@ { "operation": "principal-revocation", "samples": 50, - "p50Milliseconds": 0.518, - "p95Milliseconds": 0.7049, - "p99Milliseconds": 11.8557, - "operationsPerSecond": 1320.1773262184577, + "p50Milliseconds": 0.495, + "p95Milliseconds": 0.6508, + "p99Milliseconds": 11.011, + "operationsPerSecond": 1393.258301729591, "minimumOperationsPerSecond": 50, "budgetMilliseconds": 200, "passed": true @@ -108,7 +108,7 @@ "p50Milliseconds": 0.0001, "p95Milliseconds": 0.0001, "p99Milliseconds": 0.0001, - "operationsPerSecond": 1438641.9220256077, + "operationsPerSecond": 1479289.9408284025, "minimumOperationsPerSecond": 10000, "budgetMilliseconds": 1, "passed": true @@ -116,10 +116,10 @@ { "operation": "coincident-listing-attempt-expiry", "samples": 1, - "p50Milliseconds": 29.6882, - "p95Milliseconds": 29.6882, - "p99Milliseconds": 29.6882, - "operationsPerSecond": 33.682962483916384, + "p50Milliseconds": 27.7056, + "p95Milliseconds": 27.7056, + "p99Milliseconds": 27.7056, + "operationsPerSecond": 36.093525543388026, "minimumOperationsPerSecond": 0, "budgetMilliseconds": 200, "passed": true @@ -134,10 +134,10 @@ "finalReplayMarkers": 0, "expiryChurn": 94906, "maintenanceSweeps": 36307, - "soakCyclesCompleted": 75126848, - "soakDurationSeconds": 300.0000015, + "soakCyclesCompleted": 77547145, + "soakDurationSeconds": 300.0000041, "soakPeakScheduledExpiryEntries": 7, - "soakManagedGrowthBytes": -257288, + "soakManagedGrowthBytes": -263432, "soakHandleGrowth": 2, "restartStartedEmpty": true, "overloadWasTyped": true, diff --git a/docs/evidence/production-readiness-v1.json b/docs/evidence/production-readiness-v1.json index 772c12d..55f22be 100644 --- a/docs/evidence/production-readiness-v1.json +++ b/docs/evidence/production-readiness-v1.json @@ -1,44 +1,44 @@ { "schemaVersion": 1, "kind": "rendezvous-production-readiness", - "evaluatedCommit": "6bad659c123ad45ad0d9d07f93217c2e8c42d459", + "evaluatedCommit": "00d5ff776408e7d80ce6648953e62a7233aca35c", "decision": "not-ready", "localGates": [ { "id": "immutable-release-artifacts", - "status": "pending", - "evidenceRef": "docs/operations/production-readiness.md", - "note": "Rebuild after the readiness tooling checkpoint." + "status": "pass", + "evidenceRef": "docs/evidence/releases/v1.0.0-local-candidate.json", + "note": "Clean candidate packages and server archive are byte reproducible and fully verified." }, { "id": "debug-and-release-verification", - "status": "pending", - "evidenceRef": "docs/operations/production-readiness.md", - "note": "Re-run after the readiness tooling checkpoint." + "status": "pass", + "evidenceRef": "docs/evidence/releases/v1.0.0-local-candidate.json", + "note": "All 300 tests pass in Debug and Release; the Release build has zero warnings and errors." }, { "id": "real-consumer-pilots", - "status": "pending", - "evidenceRef": "docs/integration/spacegame-pilot.md", - "note": "Pin and re-run both real consumer revisions." + "status": "pass", + "evidenceRef": "docs/evidence/releases/v1.0.0-local-candidate.json", + "note": "Pinned real projects restore the candidate and both game launch pilots pass direct traffic." }, { "id": "candidate-capacity-resilience", - "status": "pending", + "status": "pass", "evidenceRef": "docs/evidence/capacity/v2/candidate-2cpu.json", - "note": "Re-run the five-minute candidate on the tooling checkpoint." + "note": "The clean two-CPU five-minute candidate passes all budgets with zero retained state." }, { "id": "production-process-recovery", - "status": "pending", - "evidenceRef": "docs/operations/capacity-and-resilience.md", - "note": "Re-run process restart, drain, and rollback gates." + "status": "pass", + "evidenceRef": "docs/evidence/releases/v1.0.0-local-candidate.json", + "note": "All selected restart, drain, socket release, overload, and recovery tests pass." }, { "id": "security-privacy-observability", - "status": "pending", - "evidenceRef": "docs/operations/production-readiness.md", - "note": "Re-run the combined release verification matrix." + "status": "pass", + "evidenceRef": "docs/evidence/releases/v1.0.0-local-candidate.json", + "note": "The complete security, privacy, health, audit, telemetry, and release suite passes." } ], "externalGates": [ diff --git a/docs/evidence/releases/v1.0.0-local-candidate.json b/docs/evidence/releases/v1.0.0-local-candidate.json new file mode 100644 index 0000000..ceea5aa --- /dev/null +++ b/docs/evidence/releases/v1.0.0-local-candidate.json @@ -0,0 +1,58 @@ +{ + "schemaVersion": 1, + "kind": "rendezvous-local-release-candidate", + "version": "1.0.0", + "sourceCommit": "00d5ff776408e7d80ce6648953e62a7233aca35c", + "treeState": "clean", + "result": "pass", + "artifacts": [ + { + "name": "FinalFactory.Rendezvous.Client.1.0.0.nupkg", + "sha256": "f2a4b9727b5faeba284ddcb7fc575c7495f1e29b763faababa7cd71444dc2950" + }, + { + "name": "FinalFactory.Rendezvous.Contracts.1.0.0.nupkg", + "sha256": "92317f153911ebf7b8ea04cd3206ec2a17f882cb4eddb26aa8ab094ffdb06627" + }, + { + "name": "FinalFactory.Rendezvous.Server.1.0.0.linux-x64.tar.gz", + "sha256": "0dab8cfc696b4a55d6ffba46286c9e532df2c943ed8fd6347fb528516156b3ba" + } + ], + "verification": { + "lockedRestore": "pass", + "reportedVulnerabilities": 0, + "format": "pass", + "releaseBuildWarnings": 0, + "releaseBuildErrors": 0, + "debugTestsPassed": 300, + "debugTestsFailed": 0, + "releaseTestsPassed": 300, + "releaseTestsFailed": 0, + "selectedProductionFaultTestsPassed": 17, + "byteReproduciblePackages": "pass", + "byteReproducibleServerArchive": "pass", + "sbomChecksumsAndProvenance": "pass", + "candidateConsumerFixtures": "pass", + "realConsumerRestores": "pass" + }, + "consumers": [ + { + "name": "SpaceGame", + "revision": "f3f5bc29810c362656cd7143bec1ddc2cfaf9f22", + "candidateRestore": "pass", + "directTrafficPilot": "pass" + }, + { + "name": "Unscouted", + "revision": "f0574a7de82aadff6495ca5657dfc19cf7c2f67c", + "candidateRestore": "pass", + "directTrafficPilot": "pass" + } + ], + "limitations": { + "publicRegistryRestore": "pending", + "signedPublication": "pending", + "externalNetworkCanaries": "pending" + } +} diff --git a/docs/operations/capacity-and-resilience.md b/docs/operations/capacity-and-resilience.md index e19eb0c..c336e85 100644 --- a/docs/operations/capacity-and-resilience.md +++ b/docs/operations/capacity-and-resilience.md @@ -81,7 +81,7 @@ concurrent build, thermal throttling, or oversubscribed CI host. The checked-in baseline is [`candidate-2cpu.json`](../evidence/capacity/v2/candidate-2cpu.json). It was produced on .NET 10.0.9/Linux x64 with CPU affinity restricted to two logical -CPUs. It filled 25,000 listings and 10,000 attempts, peaked at about 162 MiB, +CPUs. It filled 25,000 listings and 10,000 attempts, peaked at about 169 MiB, and cleared all active/retained state. The five-minute baseline supersedes any earlier local probe when its timestamp and target duration differ. diff --git a/docs/operations/production-readiness.md b/docs/operations/production-readiness.md index c306c60..6d4d376 100644 --- a/docs/operations/production-readiness.md +++ b/docs/operations/production-readiness.md @@ -50,6 +50,32 @@ container bridge, or second process on one machine cannot prove it: Failure or missing evidence is blocking. It is never converted into an accepted risk by changing the wording of the readiness note. +When an external gate passes, add a redacted repository JSON attestation and +point that gate's `evidenceRef` to it. The checker requires this exact shape and +binds the gate to the evaluated candidate commit. `artifactDigest` is the SHA-256 +of the protected evidence bundle or public release record, not a peer endpoint, +listing identifier, account identifier, or credential: + +```json +{ + "schemaVersion": 1, + "kind": "rendezvous-external-gate-attestation", + "gateId": "replace-with-the-exact-gate-id", + "candidateCommit": "replace-with-the-40-character-candidate-commit", + "result": "pass", + "performedAtUtc": "2026-01-01T00:00:00Z", + "artifactDigest": "replace-with-the-64-character-sha256", + "evidenceLocation": "protected-operations-record", + "reviewerRole": "independent-operator" +} +``` + +Allowed evidence locations are `protected-operations-record` and +`public-release-record`. Allowed reviewer roles are `release-operator`, +`network-operator`, `security-operator`, and `independent-operator`. The checker +rejects a missing file, wrong gate, wrong candidate, malformed digest, naive +timestamp, extra fields, or sensitive-data-shaped contents. + ## Prepare one immutable canary build Use the exact release candidate on every canary machine. Verify a clean checkout, diff --git a/eng/check_production_readiness.py b/eng/check_production_readiness.py index 41d71cb..18468fe 100755 --- a/eng/check_production_readiness.py +++ b/eng/check_production_readiness.py @@ -7,6 +7,7 @@ import json import pathlib import re import sys +from datetime import datetime, timedelta from typing import Any @@ -47,7 +48,11 @@ FORBIDDEN_KEY_PARTS = { } UUID = re.compile(r"\b[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}\b") IPV4 = re.compile(r"(? None: for index, child in enumerate(value): reject_sensitive(child, f"{path}[{index}]") elif isinstance(value, str): - if UUID.search(value) or IPV4.search(value) or "://" in value or "@" in value: + if UUID.search(value) or IPV4.search(value) or IPV6.search(value) \ + or "://" in value or "@" in value: raise InvalidRecord(f"{path} contains endpoint, identifier, or account-shaped data") -def validate_gate_set(items: Any, expected: set[str], path: str) -> list[dict[str, str]]: +def evidence_path(repository_root: pathlib.Path, value: str, path: str) -> pathlib.Path: + relative = pathlib.PurePosixPath(value) + if relative.is_absolute() or ".." in relative.parts or not value: + raise InvalidRecord(f"{path} must be a repository-relative reference") + candidate = (repository_root / pathlib.Path(*relative.parts)).resolve() + if not candidate.is_relative_to(repository_root.resolve()) or not candidate.is_file(): + raise InvalidRecord(f"{path} does not resolve to a repository evidence file") + return candidate + + +def load_json(path: pathlib.Path, label: str) -> Any: + try: + with path.open("r", encoding="utf-8") as source: + return json.load(source) + except (OSError, json.JSONDecodeError) as error: + raise InvalidRecord(f"{label} is not readable JSON: {error}") from error + + +def validate_gate_set( + items: Any, + expected: set[str], + path: str, + repository_root: pathlib.Path, +) -> list[dict[str, str]]: if not isinstance(items, list): raise InvalidRecord(f"{path} must be an array") gates: list[dict[str, str]] = [] @@ -80,9 +109,7 @@ def validate_gate_set(items: Any, expected: set[str], path: str) -> list[dict[st raise InvalidRecord(f"{path}[{index}] fields must be strings") if item["status"] not in STATUSES: raise InvalidRecord(f"{path}[{index}] has an invalid status") - evidence = pathlib.PurePosixPath(item["evidenceRef"]) - if evidence.is_absolute() or ".." in evidence.parts or not item["evidenceRef"]: - raise InvalidRecord(f"{path}[{index}].evidenceRef must be a repository-relative reference") + evidence_path(repository_root, item["evidenceRef"], f"{path}[{index}].evidenceRef") if len(item["note"]) > 240: raise InvalidRecord(f"{path}[{index}].note is too long") gates.append(item) @@ -96,7 +123,135 @@ def validate_gate_set(items: Any, expected: set[str], path: str) -> list[dict[st return gates -def validate(record: Any) -> tuple[bool, list[str]]: +def validate_local_evidence( + record: dict[str, Any], + gates: list[dict[str, str]], + repository_root: pathlib.Path, +) -> None: + if any(gate["status"] != "pass" for gate in gates): + return + commit = record["evaluatedCommit"] + release_path = evidence_path( + repository_root, + "docs/evidence/releases/v1.0.0-local-candidate.json", + "local release evidence", + ) + release = load_json(release_path, "local release evidence") + if not isinstance(release, dict) or release.get("schemaVersion") != 1 \ + or release.get("kind") != "rendezvous-local-release-candidate" \ + or release.get("sourceCommit") != commit \ + or release.get("treeState") != "clean" \ + or release.get("result") != "pass": + raise InvalidRecord("local release evidence is not a passing clean build of evaluatedCommit") + verification = release.get("verification") + if not isinstance(verification, dict): + raise InvalidRecord("local release evidence has no verification object") + exact_passes = { + "lockedRestore": "pass", + "format": "pass", + "byteReproduciblePackages": "pass", + "byteReproducibleServerArchive": "pass", + "sbomChecksumsAndProvenance": "pass", + "candidateConsumerFixtures": "pass", + "realConsumerRestores": "pass", + } + if any(verification.get(key) != value for key, value in exact_passes.items()) \ + or verification.get("reportedVulnerabilities") != 0 \ + or verification.get("releaseBuildWarnings") != 0 \ + or verification.get("releaseBuildErrors") != 0 \ + or verification.get("debugTestsPassed", 0) < 300 \ + or verification.get("debugTestsFailed") != 0 \ + or verification.get("releaseTestsPassed", 0) < 300 \ + or verification.get("releaseTestsFailed") != 0 \ + or verification.get("selectedProductionFaultTestsPassed", 0) < 17: + raise InvalidRecord("local release evidence does not satisfy every required verification") + consumers = release.get("consumers") + if not isinstance(consumers, list) or { + item.get("name") for item in consumers if isinstance(item, dict) + } != {"SpaceGame", "Unscouted"} or any( + not isinstance(item, dict) + or item.get("candidateRestore") != "pass" + or item.get("directTrafficPilot") != "pass" + for item in consumers + ): + raise InvalidRecord("local release evidence does not prove both required consumers") + + capacity_path = evidence_path( + repository_root, + "docs/evidence/capacity/v2/candidate-2cpu.json", + "candidate capacity evidence", + ) + capacity = load_json(capacity_path, "candidate capacity evidence") + runtime = capacity.get("runtime") if isinstance(capacity, dict) else None + state = capacity.get("state") if isinstance(capacity, dict) else None + if not isinstance(runtime, dict) or not isinstance(state, dict) \ + or capacity.get("schemaVersion") != 2 \ + or capacity.get("profile") != "candidate" \ + or capacity.get("passed") is not True \ + or capacity.get("failures") != [] \ + or runtime.get("commitSha") != commit \ + or runtime.get("treeState") != "clean" \ + or runtime.get("processorCount") != 2 \ + or state.get("soakDurationSeconds", 0) < 300 \ + or state.get("finalListings") != 0 \ + or state.get("finalAttempts") != 0 \ + or state.get("finalReplayMarkers") != 0 \ + or state.get("restartStartedEmpty") is not True \ + or state.get("overloadWasTyped") is not True \ + or state.get("recoverySucceeded") is not True: + raise InvalidRecord("candidate capacity evidence does not satisfy the clean evaluated commit") + + +def validate_external_attestations( + record: dict[str, Any], + gates: list[dict[str, str]], + repository_root: pathlib.Path, +) -> None: + for gate in gates: + if gate["status"] != "pass": + continue + path = evidence_path(repository_root, gate["evidenceRef"], f"{gate['id']} evidence") + attestation = load_json(path, f"{gate['id']} evidence") + if not isinstance(attestation, dict) or set(attestation) != { + "schemaVersion", + "kind", + "gateId", + "candidateCommit", + "result", + "performedAtUtc", + "artifactDigest", + "evidenceLocation", + "reviewerRole", + }: + raise InvalidRecord(f"{gate['id']} requires a complete external-gate attestation") + reject_sensitive(attestation, f"external evidence {gate['id']}") + if attestation["schemaVersion"] != 1 \ + or attestation["kind"] != "rendezvous-external-gate-attestation" \ + or attestation["gateId"] != gate["id"] \ + or attestation["candidateCommit"] != record["evaluatedCommit"] \ + or attestation["result"] != "pass" \ + or not isinstance(attestation["artifactDigest"], str) \ + or not DIGEST.fullmatch(attestation["artifactDigest"]) \ + or attestation["evidenceLocation"] not in { + "protected-operations-record", + "public-release-record", + } \ + or attestation["reviewerRole"] not in { + "release-operator", + "network-operator", + "security-operator", + "independent-operator", + }: + raise InvalidRecord(f"{gate['id']} external attestation does not match the candidate gate") + try: + performed = datetime.fromisoformat(attestation["performedAtUtc"].replace("Z", "+00:00")) + except (AttributeError, ValueError) as error: + raise InvalidRecord(f"{gate['id']} has an invalid performedAtUtc") from error + if performed.tzinfo is None or performed.utcoffset() != timedelta(0): + raise InvalidRecord(f"{gate['id']} performedAtUtc must be UTC") + + +def validate(record: Any, repository_root: pathlib.Path) -> tuple[bool, list[str]]: if not isinstance(record, dict) or set(record) != { "schemaVersion", "kind", @@ -111,8 +266,15 @@ def validate(record: Any) -> tuple[bool, list[str]]: if not isinstance(record["evaluatedCommit"], str) or not COMMIT.fullmatch(record["evaluatedCommit"]): raise InvalidRecord("evaluatedCommit must be a full lowercase Git commit") reject_sensitive(record) - gates = validate_gate_set(record["localGates"], LOCAL_GATES, "$.localGates") - gates += validate_gate_set(record["externalGates"], EXTERNAL_GATES, "$.externalGates") + local_gates = validate_gate_set( + record["localGates"], LOCAL_GATES, "$.localGates", repository_root + ) + external_gates = validate_gate_set( + record["externalGates"], EXTERNAL_GATES, "$.externalGates", repository_root + ) + validate_local_evidence(record, local_gates, repository_root) + validate_external_attestations(record, external_gates, repository_root) + gates = local_gates + external_gates blockers = sorted(gate["id"] for gate in gates if gate["status"] != "pass") ready = not blockers expected_decision = "ready" if ready else "not-ready" @@ -130,7 +292,7 @@ def main() -> int: try: with open(sys.argv[1], "r", encoding="utf-8") as source: record = json.load(source) - ready, blockers = validate(record) + ready, blockers = validate(record, pathlib.Path(__file__).resolve().parent.parent) except (OSError, json.JSONDecodeError, InvalidRecord) as error: print(f"INVALID: {error}", file=sys.stderr) return 2 diff --git a/scripts/run-real-network-canary.sh b/scripts/run-real-network-canary.sh index c72f37d..2f760a7 100755 --- a/scripts/run-real-network-canary.sh +++ b/scripts/run-real-network-canary.sh @@ -19,6 +19,7 @@ COORDINATION_FILE="${RENDEZVOUS_CANARY_COORDINATION_FILE:-}" LISTING_ID="${RENDEZVOUS_CANARY_LISTING_ID:-}" REQUIRE_CLEAN="${RENDEZVOUS_CANARY_REQUIRE_CLEAN:-true}" KEEP_RAW="${RENDEZVOUS_CANARY_KEEP_RAW:-false}" +UUID_PATTERN='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' usage() { printf '%s\n' \ @@ -28,7 +29,7 @@ usage() { exit 2 } -for command in date dotnet git jq mktemp; do +for command in date dotnet git jq mktemp tail; do command -v "$command" >/dev/null || { printf 'Missing required command: %s\n' "$command" >&2 exit 2 @@ -98,12 +99,16 @@ if [[ "$ROLE" == host ]]; then printf 'The host role requires RENDEZVOUS_CANARY_COORDINATION_FILE.\n' >&2 exit 2 fi + if [[ -e "$COORDINATION_FILE" ]]; then + printf 'The host coordination file already exists; remove it explicitly before a new canary.\n' >&2 + exit 2 + fi if [[ -z "${RENDEZVOUS_PUBLISHER_CREDENTIAL:-}" ]]; then printf 'The host role requires RENDEZVOUS_PUBLISHER_CREDENTIAL.\n' >&2 exit 2 fi else - if [[ ! "$LISTING_ID" =~ ^[0-9a-fA-F-]{36}$ ]]; then + if [[ ! "$LISTING_ID" =~ $UUID_PATTERN ]]; then printf 'A client role requires a UUID in RENDEZVOUS_CANARY_LISTING_ID.\n' >&2 exit 2 fi @@ -168,7 +173,7 @@ if [[ "$ROLE" == host ]]; then exit 1 fi observed_listing="$(jq -r 'select(.event == "host.registered") | .listingId' "$raw_log" | tail -n 1)" - if [[ ! "$observed_listing" =~ ^[0-9a-f-]{36}$ ]]; then + if [[ ! "$observed_listing" =~ $UUID_PATTERN ]]; then printf 'The canary host did not produce a valid coordination identifier.\n' >&2 kill -TERM "$host_pid" 2>/dev/null || true wait "$host_pid" 2>/dev/null || true @@ -176,9 +181,8 @@ if [[ "$ROLE" == host ]]; then fi coordination_parent="$(dirname "$COORDINATION_FILE")" mkdir -p "$coordination_parent" - coordination_temp="$COORDINATION_FILE.tmp.$$" + coordination_temp="$(mktemp "$COORDINATION_FILE.tmp.XXXXXXXX")" printf '%s\n' "$observed_listing" >"$coordination_temp" - chmod 600 "$coordination_temp" mv "$coordination_temp" "$COORDINATION_FILE" printf 'Host ready; securely transfer the private coordination file to the client operator.\n' set +e @@ -219,6 +223,10 @@ else fi mkdir -p "$(dirname "$OUTPUT")" +raw_retention=deleted-after-success +if [[ "$KEEP_RAW" == true ]]; then + raw_retention=retained-private-on-request +fi jq -n \ --arg commit "$commit" \ --arg treeState "$tree_state" \ @@ -226,8 +234,9 @@ jq -n \ --arg role "$ROLE" \ --arg topology "$TOPOLOGY" \ --arg addressFamily "$ADDRESS_FAMILY" \ + --arg rawEvents "$raw_retention" \ --argjson checks "$checks" \ - '{schemaVersion:1,kind:"rendezvous-real-network-canary",commit:$commit,treeState:$treeState,timestampUtc:$timestampUtc,role:$role,topology:$topology,addressFamily:$addressFamily,result:"pass",checks:$checks,dataRetention:{rawEvents:"deleted-after-success",identifiers:"not-in-summary",networkEndpoints:"not-in-summary"}}' \ + '{schemaVersion:1,kind:"rendezvous-real-network-canary",commit:$commit,treeState:$treeState,timestampUtc:$timestampUtc,role:$role,topology:$topology,addressFamily:$addressFamily,result:"pass",checks:$checks,dataRetention:{rawEvents:$rawEvents,identifiers:"not-in-summary",networkEndpoints:"not-in-summary"}}' \ >"$OUTPUT" run_succeeded=true diff --git a/tests/FinalFactory.Rendezvous.Tests/Release/ReleaseCompatibilityTests.cs b/tests/FinalFactory.Rendezvous.Tests/Release/ReleaseCompatibilityTests.cs index cac0e31..e565cda 100644 --- a/tests/FinalFactory.Rendezvous.Tests/Release/ReleaseCompatibilityTests.cs +++ b/tests/FinalFactory.Rendezvous.Tests/Release/ReleaseCompatibilityTests.cs @@ -209,6 +209,9 @@ public sealed class ReleaseCompatibilityTests 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) =>