From 428be871948616edeb5d1d27785fda7e7454b3cc Mon Sep 17 00:00:00 2001 From: Matt Lee Date: Fri, 21 Aug 2026 01:28:40 +0000 Subject: [PATCH] release: mirror Trajectory 0.5.37 safely --- .github/scripts/public_release_from_source.py | 180 ++++++++++++++++++ .../public-release-breakglass-mirror.yml | 49 +++++ .github/workflows/validate.yml | 2 + RELEASES.json | 12 +- tests/test_public_release_from_source.py | 116 +++++++++++ 5 files changed, 353 insertions(+), 6 deletions(-) create mode 100644 .github/scripts/public_release_from_source.py create mode 100644 .github/workflows/public-release-breakglass-mirror.yml create mode 100644 tests/test_public_release_from_source.py diff --git a/.github/scripts/public_release_from_source.py b/.github/scripts/public_release_from_source.py new file mode 100644 index 0000000..06f8b78 --- /dev/null +++ b/.github/scripts/public_release_from_source.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Mirror an already-published Trajectory release without exposing private evidence.""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path + +import public_release_mirror as mirror + + +SOURCE_REPOSITORY = "DataDog/trajectory" +SOURCE_READ_POLICY = "trajectory-labs.public-release-read" +TARGET_RECEIPT_KIND = "trajectory-public-source-mirror-receipt" +MAX_TOTAL_ASSET_BYTES = 1_100_000_000 +REQUIRED_ASSETS = ( + "trajectory-darwin-amd64", + "trajectory-darwin-arm64", + "trajectory-darwin-universal", + "trajectory-linux-amd64", + "trajectory-linux-arm64", + "trajectory-windows-amd64.exe", + "trajectory-windows-amd64", + "trajectory-mdm-darwin-amd64", + "trajectory-mdm-darwin-arm64", + "trajectory-mdm-darwin-universal", + "trajectory-mdm-linux-amd64", + "trajectory-mdm-linux-arm64", + "trajectory-mdm-windows-amd64.exe", + "checksums.sha256", +) + + +def source_contract() -> dict[str, object]: + return { + "source_identity": { + "repository": SOURCE_REPOSITORY, + "read_policy": SOURCE_READ_POLICY, + }, + "required_assets": list(REQUIRED_ASSETS), + "limits": { + "max_asset_bytes": 300_000_000, + "max_total_asset_bytes": MAX_TOTAL_ASSET_BYTES, + }, + } + + +def build_request( + source_client: mirror.GitHubClient, + *, + version: str, + target_sha: str, +) -> tuple[dict[str, object], dict[str, object]]: + if not mirror.VERSION_RE.fullmatch(version): + raise mirror.MirrorError("version must be canonical X.Y.Z") + tag = f"v{version}" + release = source_client.get_release_by_tag(tag) + if release is None: + raise mirror.MirrorError("source release does not exist") + if release.get("draft") is not False or release.get("prerelease") is not False: + raise mirror.MirrorError("source release must be published and stable") + latest = source_client.get_latest_release() + if latest.get("id") != release.get("id") or latest.get("tag_name") != tag: + raise mirror.MirrorError("source release must be GitHub latest") + + name = mirror.require_string(release.get("name"), "source release name") + body = mirror.require_string(release.get("body"), "source release body", nonempty=False) + published_at = mirror.require_string( + release.get("published_at"), "source release published_at" + ) + if not mirror.TIMESTAMP_RE.fullmatch(published_at): + raise mirror.MirrorError("source release published_at must be a UTC timestamp") + + by_name = mirror.release_assets_by_name(release) + if set(by_name) != set(REQUIRED_ASSETS): + raise mirror.MirrorError("source release does not contain the canonical asset set") + assets: list[dict[str, object]] = [] + total_size = 0 + for asset_name in REQUIRED_ASSETS: + asset = by_name[asset_name] + size = mirror.require_positive_int(asset.get("size"), f"source asset {asset_name} size") + digest = mirror.require_string( + asset.get("digest"), f"source asset {asset_name} digest" + ) + if not digest.startswith("sha256:") or not mirror.SHA256_RE.fullmatch(digest[7:]): + raise mirror.MirrorError(f"source asset digest is not canonical: {asset_name}") + total_size += size + assets.append({"name": asset_name, "size": size, "sha256": digest[7:]}) + if total_size > MAX_TOTAL_ASSET_BYTES: + raise mirror.MirrorError("source release exceeds the bounded mirror size") + + request: dict[str, object] = { + "version": version, + "tag": tag, + "release": { + "source_release_id": mirror.require_positive_int( + release.get("id"), "source release id" + ), + "name": name, + "body": body, + "target_commitish": target_sha, + }, + "asset_manifest": {"assets": assets}, + "published_at": published_at, + } + return request, release + + +def validate_metadata(metadata: dict[str, object], request: dict[str, object]) -> None: + expected = { + "version": request["version"], + "tag": request["tag"], + "released_at": request["published_at"], + } + if metadata.get("stable") != expected or metadata.get("beta") != expected: + raise mirror.MirrorError("RELEASES.json must match the mirrored stable release") + + +def apply_release(args: argparse.Namespace) -> dict[str, object]: + target_token = os.environ.get("GITHUB_TOKEN") + if not target_token: + raise mirror.MirrorError("GITHUB_TOKEN is required") + contract = source_contract() + source_token = mirror.exchange_source_token(contract) + source_client = mirror.GitHubClient(SOURCE_REPOSITORY, source_token) + target_client = mirror.GitHubClient(args.expected_repository, target_token) + request, source_release = build_request( + source_client, + version=args.version, + target_sha=args.expected_target_sha, + ) + validate_metadata(mirror.load_json(args.metadata), request) + + with mirror.tempfile.TemporaryDirectory(prefix="trajectory-public-source-mirror-") as directory: + scratch = Path(directory) + source_paths = mirror.materialize_source_assets( + source_client, request, contract, scratch + ) + target_release, action = mirror.publish_target_release( + target_client, request, contract, source_paths, scratch + ) + + receipt = { + "schema_version": 1, + "kind": TARGET_RECEIPT_KIND, + "status": action, + "version": request["version"], + "tag": request["tag"], + "source_release_id": source_release["id"], + "target_release_id": target_release["id"], + "target_sha": args.expected_target_sha, + "workflow_run_id": args.workflow_run_id, + "assets": request["asset_manifest"]["assets"], + "latest": True, + } + args.receipt_out.parent.mkdir(parents=True, exist_ok=True) + args.receipt_out.write_bytes(mirror.canonical_json(receipt) + b"\n") + return receipt + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--version", required=True) + parser.add_argument("--metadata", required=True, type=Path) + parser.add_argument("--expected-repository", required=True) + parser.add_argument("--expected-target-sha", required=True) + parser.add_argument("--workflow-run-id", required=True, type=int) + parser.add_argument("--receipt-out", required=True, type=Path) + args = parser.parse_args() + try: + apply_release(args) + except mirror.MirrorError as error: + print(f"PUBLIC_SOURCE_MIRROR_ERROR {error}", file=mirror.sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/public-release-breakglass-mirror.yml b/.github/workflows/public-release-breakglass-mirror.yml new file mode 100644 index 0000000..5dd90cd --- /dev/null +++ b/.github/workflows/public-release-breakglass-mirror.yml @@ -0,0 +1,49 @@ +name: Attested Public Release Mirror +run-name: public-source-mirror-v${{ inputs.version }} + +on: + workflow_dispatch: + inputs: + version: + description: Canonical stable release version to mirror from DataDog/trajectory + required: true + type: string + +permissions: + contents: read + +concurrency: + group: public-source-mirror-v${{ inputs.version }} + cancel-in-progress: false + +jobs: + publish: + name: Mirror exact published release + runs-on: ubuntu-latest + environment: public-release-mirror + timeout-minutes: 30 + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Validate source and publish exact release + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + python3 .github/scripts/public_release_from_source.py \ + --version "${{ inputs.version }}" \ + --metadata RELEASES.json \ + --expected-repository "$GITHUB_REPOSITORY" \ + --expected-target-sha "$GITHUB_SHA" \ + --workflow-run-id "$GITHUB_RUN_ID" \ + --receipt-out "$RUNNER_TEMP/public-source-mirror-receipt.json" + + - name: Retain sanitized publication receipt + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: public-source-mirror-receipt-${{ github.run_id }} + path: ${{ runner.temp }}/public-source-mirror-receipt.json + if-no-files-found: error + retention-days: 90 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 31c4283..d7d8744 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -67,6 +67,8 @@ jobs: run: | python3 -m py_compile \ .github/scripts/public_release_mirror.py \ + .github/scripts/public_release_from_source.py \ + tests/test_public_release_from_source.py \ tests/test_public_release_mirror.py python3 .github/scripts/public_release_mirror.py validate-contract \ --contract contracts/public-release-mirror-v1.json diff --git a/RELEASES.json b/RELEASES.json index 5bf00b2..ecf30af 100644 --- a/RELEASES.json +++ b/RELEASES.json @@ -1,12 +1,12 @@ { "stable": { - "version": "0.5.36", - "tag": "v0.5.36", - "released_at": "2026-08-17T13:16:32Z" + "version": "0.5.37", + "tag": "v0.5.37", + "released_at": "2026-08-20T23:31:54Z" }, "beta": { - "version": "0.5.36", - "tag": "v0.5.36", - "released_at": "2026-08-17T13:16:32Z" + "version": "0.5.37", + "tag": "v0.5.37", + "released_at": "2026-08-20T23:31:54Z" } } diff --git a/tests/test_public_release_from_source.py b/tests/test_public_release_from_source.py new file mode 100644 index 0000000..f2d2787 --- /dev/null +++ b/tests/test_public_release_from_source.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import importlib.util +import sys +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / ".github" / "scripts" +sys.path.insert(0, str(SCRIPTS)) +SCRIPT = SCRIPTS / "public_release_from_source.py" +SPEC = importlib.util.spec_from_file_location("public_release_from_source", SCRIPT) +assert SPEC and SPEC.loader +SOURCE_MIRROR = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(SOURCE_MIRROR) + + +class FakeSourceClient: + def __init__(self, release: dict[str, object]) -> None: + self.release = release + + def get_release_by_tag(self, tag: str) -> dict[str, object] | None: + return self.release if self.release["tag_name"] == tag else None + + def get_latest_release(self) -> dict[str, object]: + return self.release + + +def release_fixture() -> dict[str, object]: + assets = [ + { + "id": index + 1, + "name": name, + "size": index + 10, + "digest": f"sha256:{index + 1:064x}", + "state": "uploaded", + } + for index, name in enumerate(SOURCE_MIRROR.REQUIRED_ASSETS) + ] + return { + "id": 1234, + "tag_name": "v0.5.37", + "name": "Trajectory 0.5.37", + "body": "Release notes\n", + "published_at": "2026-08-20T23:31:54Z", + "draft": False, + "prerelease": False, + "assets": assets, + } + + +class PublicReleaseFromSourceTests(unittest.TestCase): + def test_build_request_uses_only_public_release_facts(self) -> None: + release = release_fixture() + request, observed = SOURCE_MIRROR.build_request( + FakeSourceClient(release), + version="0.5.37", + target_sha="a" * 40, + ) + + self.assertIs(observed, release) + self.assertEqual(request["tag"], "v0.5.37") + self.assertEqual(request["published_at"], "2026-08-20T23:31:54Z") + self.assertEqual( + [asset["name"] for asset in request["asset_manifest"]["assets"]], + list(SOURCE_MIRROR.REQUIRED_ASSETS), + ) + self.assertEqual(request["release"]["target_commitish"], "a" * 40) + + def test_build_request_rejects_non_latest_release(self) -> None: + release = release_fixture() + client = FakeSourceClient(release) + client.get_latest_release = lambda: {"id": 9999, "tag_name": "v0.5.36"} + + with self.assertRaisesRegex(SOURCE_MIRROR.mirror.MirrorError, "GitHub latest"): + SOURCE_MIRROR.build_request( + client, + version="0.5.37", + target_sha="a" * 40, + ) + + def test_build_request_rejects_asset_set_drift(self) -> None: + release = release_fixture() + release["assets"] = release["assets"][:-1] + + with self.assertRaisesRegex(SOURCE_MIRROR.mirror.MirrorError, "canonical asset set"): + SOURCE_MIRROR.build_request( + FakeSourceClient(release), + version="0.5.37", + target_sha="a" * 40, + ) + + def test_metadata_must_advance_stable_and_beta_together(self) -> None: + request = { + "version": "0.5.37", + "tag": "v0.5.37", + "published_at": "2026-08-20T23:31:54Z", + } + expected = { + "version": "0.5.37", + "tag": "v0.5.37", + "released_at": "2026-08-20T23:31:54Z", + } + SOURCE_MIRROR.validate_metadata( + {"stable": expected, "beta": expected.copy()}, request + ) + with self.assertRaisesRegex(SOURCE_MIRROR.mirror.MirrorError, "RELEASES.json"): + SOURCE_MIRROR.validate_metadata( + {"stable": expected, "beta": {**expected, "version": "0.5.36"}}, + request, + ) + + +if __name__ == "__main__": + unittest.main()