From 9299d7b541d09359b5c43065afbfb75b0d130905 Mon Sep 17 00:00:00 2001 From: JNHFlow21 Date: Tue, 11 Aug 2026 22:32:42 -0400 Subject: [PATCH] fix: require clean release manifests --- .github/workflows/release.yml | 2 ++ scripts/check.py | 8 +++++++- scripts/verify_distribution.py | 2 ++ tests/e2e/test_provider_package_install.py | 11 +++++++++++ tests/test_agent_runtime_benchmark_contract.py | 6 ++++++ 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index efbb1e0..e75f973 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,6 +20,8 @@ jobs: python-version: '3.11' - run: bash scripts/bootstrap_runtime.sh - run: ./scripts/trove-python scripts/check.py release + - name: Require a clean source checkout after release gates + run: git diff --exit-code - name: Build and verify distribution shell: bash run: | diff --git a/scripts/check.py b/scripts/check.py index 5124525..8f885ca 100755 --- a/scripts/check.py +++ b/scripts/check.py @@ -3,14 +3,17 @@ import argparse from dataclasses import dataclass +import os from pathlib import Path import subprocess import sys +import tempfile from typing import Sequence ROOT = Path(__file__).resolve().parents[1] PYTHON = ROOT / 'scripts' / 'trove-python' +AGENT_RUNTIME_OUTPUT = Path(tempfile.gettempdir()) / f'trove-agent-runtime-budgets-{os.getpid()}.json' @dataclass(frozen=True) @@ -56,7 +59,10 @@ class Check: Check('agent-product-acceptance', (str(PYTHON), 'scripts/run_agent_product_acceptance.py')), ), 'perf': ( - Check('agent-runtime', (str(PYTHON), 'scripts/benchmark_agent_runtime.py', '--rounds', '3', '--warmups', '1')), + Check('agent-runtime', ( + str(PYTHON), 'scripts/benchmark_agent_runtime.py', + '--rounds', '3', '--warmups', '1', '--out', str(AGENT_RUNTIME_OUTPUT), + )), ), } RELEASE_ORDER = ('unit', 'contract', 'package', 'e2e', 'perf') diff --git a/scripts/verify_distribution.py b/scripts/verify_distribution.py index 87f17e6..ff5eaac 100755 --- a/scripts/verify_distribution.py +++ b/scripts/verify_distribution.py @@ -136,6 +136,8 @@ def verify_distribution(manifest_path: Path) -> dict[str, Any]: or type(manifest.get('source_dirty')) is not bool ): raise DistributionVerificationError('distribution manifest identity is invalid') + if manifest.get('source_dirty') is not False: + raise DistributionVerificationError('distribution source checkout is dirty') for field in ('runtime_build_hash', 'catalog_hash', 'provider_package_hash', 'distribution_set_sha256'): if not re.fullmatch(r'[0-9a-f]{64}', str(manifest.get(field) or '')): raise DistributionVerificationError('distribution hash is invalid') diff --git a/tests/e2e/test_provider_package_install.py b/tests/e2e/test_provider_package_install.py index 89e48a7..dfe332d 100644 --- a/tests/e2e/test_provider_package_install.py +++ b/tests/e2e/test_provider_package_install.py @@ -73,6 +73,17 @@ def test_tampered_provider_artifact_is_rejected_before_install(self): with self.assertRaisesRegex(DistributionVerificationError, 'artifact hash mismatch'): verify_distribution(copied / 'distribution-manifest.json') + def test_dirty_source_manifest_is_rejected_before_install(self): + with tempfile.TemporaryDirectory() as directory: + copied = Path(directory) / 'distribution' + shutil.copytree(distribution_dir(), copied) + manifest_path = copied / 'distribution-manifest.json' + manifest = json.loads(manifest_path.read_text()) + manifest['source_dirty'] = True + manifest_path.write_text(json.dumps(manifest, sort_keys=True, separators=(',', ':')) + '\n') + with self.assertRaisesRegex(DistributionVerificationError, 'source checkout is dirty'): + verify_distribution(manifest_path) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_agent_runtime_benchmark_contract.py b/tests/test_agent_runtime_benchmark_contract.py index a9ed374..60eff4b 100644 --- a/tests/test_agent_runtime_benchmark_contract.py +++ b/tests/test_agent_runtime_benchmark_contract.py @@ -15,6 +15,7 @@ ) from scripts.measure_agent_surface import estimate_json_tokens from scripts.generate_fixture_vault import redacted_fixture_metadata +from scripts.check import ROOT as CHECK_ROOT, selected_checks from scripts.release_gate_contracts import agent_runtime_budget_contract_valid @@ -27,6 +28,11 @@ def test_cli_help_renders_on_supported_python_versions(self): self.assertIn('10% p95', output.getvalue()) self.assertIn('regression gate', output.getvalue()) + def test_perf_gate_writes_its_receipt_outside_the_source_checkout(self): + command = selected_checks('perf')[0].command + output = Path(command[command.index('--out') + 1]).resolve() + self.assertFalse(output.is_relative_to(CHECK_ROOT.resolve())) + def _artifact(self) -> dict: measurements = { name: {