From 031f8ef00c5514da8744e7bc24f9793016bb9b3e Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 17:57:52 +0200 Subject: [PATCH 01/22] feat: add bounded version preparation delivery contract --- .../version_preparation_delivery.py | 109 ++++++++++++++++++ .../test_version_preparation_delivery.py | 43 +++++++ 2 files changed, 152 insertions(+) create mode 100644 src/engineering_platform/version_preparation_delivery.py create mode 100644 tests/engineering/test_version_preparation_delivery.py diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py new file mode 100644 index 00000000..682163dd --- /dev/null +++ b/src/engineering_platform/version_preparation_delivery.py @@ -0,0 +1,109 @@ +"""Bounded EP execution of a product-owned version preparation operation. + +The product helper owns version rules; this module owns only admission, +isolated candidate construction and allow-listed diff verification. It never +infers a release, runs arbitrary commands, or grants merge authority. +""" +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +import json +from pathlib import Path +import re +from typing import Mapping, Protocol + +from .execution_errors import RunnerError +from .providers import GitProvider, ProcessProvider + +_SHA = re.compile(r"^[0-9a-f]{40}$") +_OPERATION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$") +_PATH = re.compile(r"^(?:[A-Za-z0-9][A-Za-z0-9._-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*$") +_REQUEST_KEYS = frozenset({"contract_version", "operation_id", "product_id", "component_id", "repository_id", "policy_revision", "policy_digest", "source_event_set", "source_event_policy", "expected_source_revision", "expected_target_branch_revision", "expected_version", "requested_change", "determined_target_version", "allowed_projection_paths", "prepared_operation_digest", "authorization_reference", "delivery_mode"}) + + +class VersionPreparationError(RunnerError): + pass + + +@dataclass(frozen=True) +class VersionPreparationRequest: + contract_version: str + operation_id: str + product_id: str + component_id: str | None + repository_id: str + policy_revision: str + policy_digest: str + source_event_set: tuple[str, ...] + source_event_policy: str + expected_source_revision: str + expected_target_branch_revision: str | None + expected_version: str + requested_change: str + determined_target_version: str + allowed_projection_paths: tuple[str, ...] + prepared_operation_digest: str + authorization_reference: str + delivery_mode: str + + @classmethod + def parse(cls, value: object) -> "VersionPreparationRequest": + if not isinstance(value, dict) or set(value) != _REQUEST_KEYS: + raise VersionPreparationError("version preparation request has unknown or missing fields") + def text(key: str, optional: bool = False) -> str | None: + item = value[key] + if optional and item is None: return None + if not isinstance(item, str) or not item or len(item) > 512: raise VersionPreparationError(f"invalid {key}") + return item + events = value["source_event_set"] + paths = value["allowed_projection_paths"] + if (not isinstance(events, list) or not events or len(set(events)) != len(events) + or not all(isinstance(item, str) and item for item in events)): + raise VersionPreparationError("source event set is invalid") + if (not isinstance(paths, list) or not paths or len(set(paths)) != len(paths) + or not all(isinstance(item, str) and _PATH.fullmatch(item) for item in paths)): + raise VersionPreparationError("allowed projection paths are invalid") + request = cls(*(text(key, key in {"component_id", "expected_target_branch_revision"}) for key in ( + "contract_version", "operation_id", "product_id", "component_id", "repository_id", "policy_revision", "policy_digest")), + tuple(events), text("source_event_policy"), text("expected_source_revision"), + text("expected_target_branch_revision", True), text("expected_version"), text("requested_change"), + text("determined_target_version"), tuple(paths), text("prepared_operation_digest"), + text("authorization_reference"), text("delivery_mode")) + if request.contract_version != "1" or not _OPERATION.fullmatch(request.operation_id): + raise VersionPreparationError("unsupported contract version or operation ID") + if not _SHA.fullmatch(request.expected_source_revision): + raise VersionPreparationError("expected source revision must be an exact SHA") + if request.delivery_mode not in {"EXISTING_FEATURE_CANDIDATE", "PROTECTED_VERSION_PREPARATION_CANDIDATE"}: + raise VersionPreparationError("unsupported delivery mode") + if request.requested_change not in {"patch", "minor", "exact-version"}: + raise VersionPreparationError("unsupported requested change") + return request + + +class ProductHelper(Protocol): + def apply(self, worktree: Path, request: VersionPreparationRequest) -> None: ... + + +class VersionPreparationDelivery: + """Deterministic candidate builder; publication/merge remain provider authority.""" + def __init__(self, git: GitProvider, helper: ProductHelper) -> None: + self.git, self.helper = git, helper + + def prepare(self, repository: Path, worktree: Path, request: VersionPreparationRequest) -> dict[str, object]: + head = self.git.command(repository, "git", "rev-parse", "HEAD") + if head != request.expected_source_revision: + raise VersionPreparationError("stale expected source revision") + status = self.git.command(repository, "git", "status", "--porcelain", "--untracked-files=all") + if status: + raise VersionPreparationError("repository checkout is not clean") + self.helper.apply(worktree, request) + changed = tuple(filter(None, self.git.command(worktree, "git", "diff", "--name-only").splitlines())) + receipt = tuple(path for path in changed if path.endswith(f"/{request.operation_id}.json")) + allowed = set(request.allowed_projection_paths) | set(receipt) + if not receipt or set(changed) - allowed: + raise VersionPreparationError("version preparation changed a path outside its declared operation") + digest = hashlib.sha256("\n".join(changed).encode()).hexdigest() + if digest != request.prepared_operation_digest: + raise VersionPreparationError("prepared operation digest does not bind the candidate diff") + return {"operation_id": request.operation_id, "changed_paths": changed, "prepared_operation_digest": digest} diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py new file mode 100644 index 00000000..8cc96a22 --- /dev/null +++ b/tests/engineering/test_version_preparation_delivery.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from pathlib import Path +import unittest + +from engineering_platform.version_preparation_delivery import VersionPreparationError, VersionPreparationRequest + + +def request(**overrides: object) -> dict[str, object]: + value: dict[str, object] = { + "contract_version": "1", "operation_id": "operation-0001", "product_id": "forge", + "component_id": "product", "repository_id": "pcvantol/forge", "policy_revision": "v1", + "policy_digest": "sha256:policy", "source_event_set": ["merge:1"], "source_event_policy": "main", + "expected_source_revision": "a" * 40, "expected_target_branch_revision": None, + "expected_version": "2.3.0", "requested_change": "minor", "determined_target_version": "2.4.0", + "allowed_projection_paths": ["product-version.json"], "prepared_operation_digest": "sha256:diff", + "authorization_reference": "grant:bounded", "delivery_mode": "PROTECTED_VERSION_PREPARATION_CANDIDATE", + } + value.update(overrides) + return value + + +class VersionPreparationRequestTest(unittest.TestCase): + def test_accepts_exact_bounded_contract(self) -> None: + parsed = VersionPreparationRequest.parse(request()) + self.assertEqual(parsed.operation_id, "operation-0001") + self.assertEqual(parsed.allowed_projection_paths, ("product-version.json",)) + + def test_rejects_unknown_and_untrusted_paths(self) -> None: + with self.assertRaisesRegex(VersionPreparationError, "unknown"): + VersionPreparationRequest.parse({**request(), "shell": "rm"}) + with self.assertRaisesRegex(VersionPreparationError, "paths"): + VersionPreparationRequest.parse(request(allowed_projection_paths=["../outside"])) + + def test_rejects_duplicate_events_and_wrong_source_sha(self) -> None: + with self.assertRaisesRegex(VersionPreparationError, "event"): + VersionPreparationRequest.parse(request(source_event_set=["merge:1", "merge:1"])) + with self.assertRaisesRegex(VersionPreparationError, "exact SHA"): + VersionPreparationRequest.parse(request(expected_source_revision="main")) + + +if __name__ == "__main__": + unittest.main() From 1fd1c3029d695dcffc138d503850d387f758add8 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:01:46 +0200 Subject: [PATCH 02/22] feat: publish bounded version preparation candidates --- .../execution_repository.py | 27 +++++++++++++++ .../version_preparation_delivery.py | 33 ++++++++++++++++++- .../test_version_preparation_delivery.py | 6 +++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/engineering_platform/execution_repository.py b/src/engineering_platform/execution_repository.py index 43435dc6..4bd9f0cd 100644 --- a/src/engineering_platform/execution_repository.py +++ b/src/engineering_platform/execution_repository.py @@ -44,6 +44,7 @@ def pull_request_for_head_branch(self, branch: str) -> PullRequestEvidence | Non def ready(self, number: int) -> None: ... def normalize_markdown_body(self, number: int) -> bool: ... def merge(self, number: int) -> None: ... + def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: ... class SubprocessRepositoryClient: @@ -226,6 +227,32 @@ def pull_request_for_head_branch(self, branch: str) -> PullRequestEvidence | Non if len(numbers) != 1: raise RunnerError("Finalization recovery found more than one pull request for its checkpointed branch.") return self.pull_request(numbers[0]) + + def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: + """Create one bounded PR or recover the sole existing branch identity.""" + existing = self.pull_request_for_head_branch(branch) + if existing is not None: + if existing.base_branch != base: + raise RunnerError("Version preparation branch already has a pull request for another base.") + return existing + try: + raw = self._github("pr", "create", "--head", branch, "--base", base, "--title", title, "--body", body) + except RuntimeError as error: + # A successful create may lose its acknowledgement. Only recover + # the deterministic branch identity; never create a second PR. + recovered = self.pull_request_for_head_branch(branch) + if recovered is None: + raise RunnerError(str(error)) from error + if recovered.base_branch != base: + raise RunnerError("Version preparation PR recovery found wrong base.") from error + return recovered + match = re.search(r"/pull/(\d+)(?:\s|$)", raw) + if match is None: + recovered = self.pull_request_for_head_branch(branch) + if recovered is None: + raise RunnerError("Version preparation PR create acknowledgement is ambiguous.") + return recovered + return self.pull_request(int(match.group(1))) def ready(self, number: int) -> None: try: self._github("pr", "ready", str(number)) except RuntimeError as error: diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 682163dd..4743bf15 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -14,7 +14,8 @@ from typing import Mapping, Protocol from .execution_errors import RunnerError -from .providers import GitProvider, ProcessProvider +from .providers import GitProvider +from .execution_repository import GitHubClient _SHA = re.compile(r"^[0-9a-f]{40}$") _OPERATION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$") @@ -107,3 +108,33 @@ def prepare(self, repository: Path, worktree: Path, request: VersionPreparationR if digest != request.prepared_operation_digest: raise VersionPreparationError("prepared operation digest does not bind the candidate diff") return {"operation_id": request.operation_id, "changed_paths": changed, "prepared_operation_digest": digest} + + @staticmethod + def branch_name(request: VersionPreparationRequest) -> str: + return f"ep/version-preparation/{request.operation_id}" + + def publish_candidate( + self, worktree: Path, request: VersionPreparationRequest, prepared: Mapping[str, object], github: GitHubClient, + *, base_branch: str, + ) -> dict[str, object]: + """Commit only the verified candidate diff and create/recover one PR.""" + branch = self.branch_name(request) + paths = prepared.get("changed_paths") + if not isinstance(paths, tuple) or not paths or not all(isinstance(path, str) for path in paths): + raise VersionPreparationError("prepared candidate has no bounded changed paths") + if self.git.command(worktree, "git", "branch", "--show-current") != branch: + raise VersionPreparationError("isolated worktree branch does not bind the operation ID") + self.git.command(worktree, "git", "add", "--", *paths) + self.git.command(worktree, "git", "commit", "-m", f"build: prepare version operation {request.operation_id}") + candidate_sha = self.git.command(worktree, "git", "rev-parse", "HEAD") + self.git.command(worktree, "git", "push", "origin", f"HEAD:{branch}") + body = "\n".join(( + "Bounded EP version-preparation candidate.", + f"operation_id: `{request.operation_id}`", + f"prepared_operation_digest: `{request.prepared_operation_digest}`", + f"expected_source_revision: `{request.expected_source_revision}`", + )) + pr = github.create_or_recover_pull_request(branch, base_branch, f"build: prepare version {request.determined_target_version}", body) + if pr.head_branch != branch: + raise VersionPreparationError("recovered pull request does not bind the candidate branch") + return {**prepared, "candidate_commit_sha": candidate_sha, "branch": branch, "pull_request_id": pr.number} diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 8cc96a22..167835fb 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -3,7 +3,7 @@ from pathlib import Path import unittest -from engineering_platform.version_preparation_delivery import VersionPreparationError, VersionPreparationRequest +from engineering_platform.version_preparation_delivery import VersionPreparationDelivery, VersionPreparationError, VersionPreparationRequest def request(**overrides: object) -> dict[str, object]: @@ -38,6 +38,10 @@ def test_rejects_duplicate_events_and_wrong_source_sha(self) -> None: with self.assertRaisesRegex(VersionPreparationError, "exact SHA"): VersionPreparationRequest.parse(request(expected_source_revision="main")) + def test_candidate_branch_is_deterministically_bound_to_operation(self) -> None: + parsed = VersionPreparationRequest.parse(request()) + self.assertEqual(VersionPreparationDelivery.branch_name(parsed), "ep/version-preparation/operation-0001") + if __name__ == "__main__": unittest.main() From 6cad11f5fab8e61cf98c2c257a86c15bfe23f8a6 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:03:08 +0200 Subject: [PATCH 03/22] feat: bind version candidates to exact qualification heads --- .../execution_repository.py | 19 +++++++++++++++++++ .../version_preparation_delivery.py | 11 +++++++++++ .../test_version_preparation_delivery.py | 7 +++++++ 3 files changed, 37 insertions(+) diff --git a/src/engineering_platform/execution_repository.py b/src/engineering_platform/execution_repository.py index 4bd9f0cd..8cab19c8 100644 --- a/src/engineering_platform/execution_repository.py +++ b/src/engineering_platform/execution_repository.py @@ -45,6 +45,7 @@ def ready(self, number: int) -> None: ... def normalize_markdown_body(self, number: int) -> bool: ... def merge(self, number: int) -> None: ... def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: ... + def qualification_for_exact_head(self, number: int, head_sha: str) -> dict[str, object]: ... class SubprocessRepositoryClient: @@ -253,6 +254,24 @@ def create_or_recover_pull_request(self, branch: str, base: str, title: str, bod raise RunnerError("Version preparation PR create acknowledgement is ambiguous.") return recovered return self.pull_request(int(match.group(1))) + + def qualification_for_exact_head(self, number: int, head_sha: str) -> dict[str, object]: + """Read real check evidence and reject merge-ref/old-head substitution.""" + if not re.fullmatch(r"[0-9a-f]{40}", head_sha): + raise RunnerError("Qualification requires an exact candidate SHA.") + try: + raw = json.loads(self._github("pr", "view", str(number), "--json", "headRefOid,baseRefOid,statusCheckRollup")) + except (RuntimeError, json.JSONDecodeError) as error: + raise RunnerError("Version preparation qualification could not be read.") from error + if raw.get("headRefOid") != head_sha: + raise RunnerError("Qualification evidence belongs to a different pull request head.") + checks = [item for item in (raw.get("statusCheckRollup") or []) if isinstance(item, dict) and isinstance(item.get("status"), str)] + if not checks or any(item.get("status") != "COMPLETED" for item in checks): + raise RunnerError("Version preparation qualification is incomplete.") + failed = [str(item.get("name") or "unnamed check") for item in checks if item.get("conclusion") not in {"SUCCESS", "NEUTRAL", "SKIPPED"}] + if failed: + raise RunnerError("Version preparation qualification failed: " + ", ".join(failed)) + return {"pull_request_id": number, "exact_qualified_sha": head_sha, "base_revision": raw.get("baseRefOid"), "checks": checks, "conclusion": "PASS"} def ready(self, number: int) -> None: try: self._github("pr", "ready", str(number)) except RuntimeError as error: diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 4743bf15..ba43a2b5 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -138,3 +138,14 @@ def publish_candidate( if pr.head_branch != branch: raise VersionPreparationError("recovered pull request does not bind the candidate branch") return {**prepared, "candidate_commit_sha": candidate_sha, "branch": branch, "pull_request_id": pr.number} + + @staticmethod + def qualify_candidate(candidate: Mapping[str, object], github: GitHubClient) -> dict[str, object]: + """Bind read-only repository qualification to the candidate's exact SHA.""" + sha, number = candidate.get("candidate_commit_sha"), candidate.get("pull_request_id") + if not isinstance(sha, str) or not _SHA.fullmatch(sha) or not isinstance(number, int): + raise VersionPreparationError("candidate lacks exact SHA/PR binding") + evidence = github.qualification_for_exact_head(number, sha) + if evidence.get("exact_qualified_sha") != sha or evidence.get("conclusion") != "PASS": + raise VersionPreparationError("candidate qualification is not bound to the exact candidate SHA") + return dict(evidence) diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 167835fb..5cbacbe3 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -42,6 +42,13 @@ def test_candidate_branch_is_deterministically_bound_to_operation(self) -> None: parsed = VersionPreparationRequest.parse(request()) self.assertEqual(VersionPreparationDelivery.branch_name(parsed), "ep/version-preparation/operation-0001") + def test_qualification_cannot_substitute_an_old_head(self) -> None: + class GitHub: + def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, object]: + return {"pull_request_id": number, "exact_qualified_sha": "b" * 40, "conclusion": "PASS"} + with self.assertRaisesRegex(VersionPreparationError, "exact candidate SHA"): + VersionPreparationDelivery.qualify_candidate({"candidate_commit_sha": "a" * 40, "pull_request_id": 9}, GitHub()) + if __name__ == "__main__": unittest.main() From abfdb4872faf99cc938c3aa72e8623daf2c0221f Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:03:52 +0200 Subject: [PATCH 04/22] feat: retain immutable version delivery evidence --- .../version_preparation_delivery.py | 28 +++++++++++++++++++ .../test_version_preparation_delivery.py | 10 +++++++ 2 files changed, 38 insertions(+) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index ba43a2b5..ef956cd3 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -9,6 +9,7 @@ from dataclasses import dataclass import hashlib import json +import os from pathlib import Path import re from typing import Mapping, Protocol @@ -149,3 +150,30 @@ def qualify_candidate(candidate: Mapping[str, object], github: GitHubClient) -> if evidence.get("exact_qualified_sha") != sha or evidence.get("conclusion") != "PASS": raise VersionPreparationError("candidate qualification is not bound to the exact candidate SHA") return dict(evidence) + + @staticmethod + def record_delivery_evidence(evidence_root: Path, candidate: Mapping[str, object], qualification: Mapping[str, object]) -> Path: + """Append immutable delivery evidence outside the tracked prepared receipt.""" + operation, sha, branch, pr = (candidate.get(key) for key in ("operation_id", "candidate_commit_sha", "branch", "pull_request_id")) + if not all(isinstance(value, str) and value for value in (operation, sha, branch)) or not isinstance(pr, int): + raise VersionPreparationError("candidate is incomplete for delivery evidence") + if qualification.get("exact_qualified_sha") != sha or qualification.get("conclusion") != "PASS": + raise VersionPreparationError("delivery evidence requires exact successful qualification") + payload = {"schema_version": 1, "operation_id": operation, "candidate_commit_sha": sha, "branch": branch, + "pull_request_id": pr, "qualification": dict(qualification)} + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + directory = evidence_root / "version-preparation-delivery" + directory.mkdir(parents=True, exist_ok=True) + target = directory / f"{operation}-{sha}.json" + if target.exists(): + if target.read_bytes() != encoded: + raise VersionPreparationError("delivery evidence identity conflicts with existing bytes") + return target + temporary = target.with_suffix(".tmp") + try: + with temporary.open("xb") as handle: + handle.write(encoded); handle.flush(); os.fsync(handle.fileno()) + os.replace(temporary, target) + except FileExistsError as error: + raise VersionPreparationError("delivery evidence write collided") from error + return target diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 5cbacbe3..cdc39a02 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -2,6 +2,7 @@ from pathlib import Path import unittest +import tempfile from engineering_platform.version_preparation_delivery import VersionPreparationDelivery, VersionPreparationError, VersionPreparationRequest @@ -49,6 +50,15 @@ def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, objec with self.assertRaisesRegex(VersionPreparationError, "exact candidate SHA"): VersionPreparationDelivery.qualify_candidate({"candidate_commit_sha": "a" * 40, "pull_request_id": 9}, GitHub()) + def test_delivery_evidence_is_idempotent_and_exact_head_bound(self) -> None: + candidate = {"operation_id": "operation-0001", "candidate_commit_sha": "a" * 40, "branch": "ep/version-preparation/operation-0001", "pull_request_id": 9} + qualification = {"exact_qualified_sha": "a" * 40, "conclusion": "PASS", "checks": []} + with tempfile.TemporaryDirectory() as directory: + first = VersionPreparationDelivery.record_delivery_evidence(Path(directory), candidate, qualification) + self.assertEqual(first, VersionPreparationDelivery.record_delivery_evidence(Path(directory), candidate, qualification)) + with self.assertRaisesRegex(VersionPreparationError, "exact successful"): + VersionPreparationDelivery.record_delivery_evidence(Path(directory), candidate, {**qualification, "exact_qualified_sha": "b" * 40}) + if __name__ == "__main__": unittest.main() From 63783f033e28c90ec42f22b4f0f745953a6efeab Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:04:37 +0200 Subject: [PATCH 05/22] feat: orchestrate bounded version preparation delivery --- .../version_preparation_delivery.py | 11 +++++++++ .../test_version_preparation_delivery.py | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index ef956cd3..16872a42 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -177,3 +177,14 @@ def record_delivery_evidence(evidence_root: Path, candidate: Mapping[str, object except FileExistsError as error: raise VersionPreparationError("delivery evidence write collided") from error return target + + def execute( + self, repository: Path, worktree: Path, request: VersionPreparationRequest, github: GitHubClient, + *, base_branch: str, evidence_root: Path, + ) -> dict[str, object]: + """Run one bounded preparation transaction; never merge or publish a release.""" + prepared = self.prepare(repository, worktree, request) + candidate = self.publish_candidate(worktree, request, prepared, github, base_branch=base_branch) + qualification = self.qualify_candidate(candidate, github) + evidence = self.record_delivery_evidence(evidence_root, candidate, qualification) + return {**candidate, "qualification": qualification, "delivery_evidence_path": str(evidence)} diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index cdc39a02..acdb7254 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -3,8 +3,10 @@ from pathlib import Path import unittest import tempfile +import hashlib from engineering_platform.version_preparation_delivery import VersionPreparationDelivery, VersionPreparationError, VersionPreparationRequest +from engineering_platform.execution_models import PullRequestEvidence def request(**overrides: object) -> dict[str, object]: @@ -59,6 +61,28 @@ def test_delivery_evidence_is_idempotent_and_exact_head_bound(self) -> None: with self.assertRaisesRegex(VersionPreparationError, "exact successful"): VersionPreparationDelivery.record_delivery_evidence(Path(directory), candidate, {**qualification, "exact_qualified_sha": "b" * 40}) + def test_execute_binds_prepare_candidate_qualification_and_evidence(self) -> None: + class Git: + def command(self, _root: Path, *args: str) -> str: + if args[-1] == "HEAD": return "a" * 40 + if args[-1] == "--untracked-files=all": return "" + if args[-2:] == ("diff", "--name-only"): return "product-version.json\n.version-operations/operation-0001.json" + if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" + return "" + class Helper: + def apply(self, _worktree: Path, _request: object) -> None: pass + class GitHub: + def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: + return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base) + def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, object]: + return {"pull_request_id": number, "exact_qualified_sha": sha, "conclusion": "PASS", "checks": []} + digest = hashlib.sha256(b"product-version.json\n.version-operations/operation-0001.json").hexdigest() + parsed = VersionPreparationRequest.parse(request(prepared_operation_digest=digest)) + with tempfile.TemporaryDirectory() as directory: + result = VersionPreparationDelivery(Git(), Helper()).execute(Path(directory), Path(directory), parsed, GitHub(), base_branch="main", evidence_root=Path(directory)) + self.assertEqual(result["pull_request_id"], 9) + self.assertTrue(Path(str(result["delivery_evidence_path"])).is_file()) + if __name__ == "__main__": unittest.main() From 0ac625612d9d08aa36894099ab7ef3fb42d2221d Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:05:18 +0200 Subject: [PATCH 06/22] feat: isolate version preparation worktrees --- .../version_preparation_delivery.py | 12 ++++++++++++ .../engineering/test_version_preparation_delivery.py | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 16872a42..16663956 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -114,6 +114,18 @@ def prepare(self, repository: Path, worktree: Path, request: VersionPreparationR def branch_name(request: VersionPreparationRequest) -> str: return f"ep/version-preparation/{request.operation_id}" + def create_isolated_worktree(self, repository: Path, worktree: Path, request: VersionPreparationRequest) -> None: + """Create the one deterministic candidate worktree from the pinned source.""" + if worktree.exists(): + raise VersionPreparationError("version preparation worktree path already exists") + if self.git.command(repository, "git", "rev-parse", request.expected_source_revision) != request.expected_source_revision: + raise VersionPreparationError("expected source revision is unavailable") + self.git.command(repository, "git", "worktree", "add", "-b", self.branch_name(request), str(worktree), request.expected_source_revision) + if self.git.command(worktree, "git", "rev-parse", "HEAD") != request.expected_source_revision: + raise VersionPreparationError("isolated worktree did not start at the expected source revision") + if self.git.command(worktree, "git", "status", "--porcelain", "--untracked-files=all"): + raise VersionPreparationError("isolated version preparation worktree is not clean") + def publish_candidate( self, worktree: Path, request: VersionPreparationRequest, prepared: Mapping[str, object], github: GitHubClient, *, base_branch: str, diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index acdb7254..613415e2 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -83,6 +83,14 @@ def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, objec self.assertEqual(result["pull_request_id"], 9) self.assertTrue(Path(str(result["delivery_evidence_path"])).is_file()) + def test_existing_worktree_path_is_rejected_before_git_write(self) -> None: + class Git: + def command(self, *_args: str) -> str: raise AssertionError("must not invoke Git") + with tempfile.TemporaryDirectory() as directory: + parsed = VersionPreparationRequest.parse(request()) + with self.assertRaisesRegex(VersionPreparationError, "already exists"): + VersionPreparationDelivery(Git(), object()).create_isolated_worktree(Path(directory), Path(directory), parsed) + if __name__ == "__main__": unittest.main() From f66f1e230e1f6c78f701250d0fc164ef5183306b Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:06:28 +0200 Subject: [PATCH 07/22] feat: validate product helper delivery declarations --- .../version_preparation_delivery.py | 32 +++++++++++++++++++ .../test_version_preparation_delivery.py | 1 + 2 files changed, 33 insertions(+) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 16663956..a4f1a50c 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -22,12 +22,39 @@ _OPERATION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$") _PATH = re.compile(r"^(?:[A-Za-z0-9][A-Za-z0-9._-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*$") _REQUEST_KEYS = frozenset({"contract_version", "operation_id", "product_id", "component_id", "repository_id", "policy_revision", "policy_digest", "source_event_set", "source_event_policy", "expected_source_revision", "expected_target_branch_revision", "expected_version", "requested_change", "determined_target_version", "allowed_projection_paths", "prepared_operation_digest", "authorization_reference", "delivery_mode"}) +_HELPER_KEYS = frozenset({"contract_version", "product_id", "repository_id", "helper_path", "receipt_directory", "allowed_projection_paths", "policy_revision"}) class VersionPreparationError(RunnerError): pass +@dataclass(frozen=True) +class ProductHelperDeclaration: + product_id: str + repository_id: str + helper_path: str + receipt_directory: str + allowed_projection_paths: tuple[str, ...] + policy_revision: str + + @classmethod + def load(cls, worktree: Path) -> "ProductHelperDeclaration": + try: value = json.loads((worktree / ".version-preparation.json").read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: raise VersionPreparationError("product version helper declaration is unreadable") from error + if not isinstance(value, dict) or set(value) != _HELPER_KEYS or value.get("contract_version") != "1": + raise VersionPreparationError("product version helper declaration has unknown fields or schema") + fields = ("product_id", "repository_id", "helper_path", "policy_revision") + receipt_directory = value.get("receipt_directory") + if (not all(isinstance(value.get(key), str) and _PATH.fullmatch(value[key]) for key in fields) + or not isinstance(receipt_directory, str) or "/.." in receipt_directory or receipt_directory.startswith("/")): + raise VersionPreparationError("product version helper declaration has invalid paths or identities") + paths = value.get("allowed_projection_paths") + if not isinstance(paths, list) or not paths or not all(isinstance(path, str) and _PATH.fullmatch(path) for path in paths): + raise VersionPreparationError("product version helper declaration has invalid projection paths") + return cls(value["product_id"], value["repository_id"], value["helper_path"], receipt_directory, tuple(paths), value["policy_revision"]) + + @dataclass(frozen=True) class VersionPreparationRequest: contract_version: str @@ -99,6 +126,11 @@ def prepare(self, repository: Path, worktree: Path, request: VersionPreparationR status = self.git.command(repository, "git", "status", "--porcelain", "--untracked-files=all") if status: raise VersionPreparationError("repository checkout is not clean") + declaration = ProductHelperDeclaration.load(worktree) + if (declaration.product_id != request.product_id or declaration.repository_id != request.repository_id + or declaration.policy_revision != request.policy_revision + or tuple(request.allowed_projection_paths) != declaration.allowed_projection_paths): + raise VersionPreparationError("product helper declaration does not bind the admitted operation") self.helper.apply(worktree, request) changed = tuple(filter(None, self.git.command(worktree, "git", "diff", "--name-only").splitlines())) receipt = tuple(path for path in changed if path.endswith(f"/{request.operation_id}.json")) diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 613415e2..57fbd1f9 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -79,6 +79,7 @@ def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, objec digest = hashlib.sha256(b"product-version.json\n.version-operations/operation-0001.json").hexdigest() parsed = VersionPreparationRequest.parse(request(prepared_operation_digest=digest)) with tempfile.TemporaryDirectory() as directory: + Path(directory, ".version-preparation.json").write_text(__import__("json").dumps({"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"}), encoding="utf-8") result = VersionPreparationDelivery(Git(), Helper()).execute(Path(directory), Path(directory), parsed, GitHub(), base_branch="main", evidence_root=Path(directory)) self.assertEqual(result["pull_request_id"], 9) self.assertTrue(Path(str(result["delivery_evidence_path"])).is_file()) From 238c5699ea409fa97acf90e373361c2c1c7e6315 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:16:14 +0200 Subject: [PATCH 08/22] fix: bind version receipts to candidate scope --- .../version_preparation_delivery.py | 31 ++++++++-- .../test_version_preparation_delivery.py | 62 ++++++++++++++++++- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index a4f1a50c..1df7b7d0 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -21,6 +21,7 @@ _SHA = re.compile(r"^[0-9a-f]{40}$") _OPERATION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$") _PATH = re.compile(r"^(?:[A-Za-z0-9][A-Za-z0-9._-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*$") +_REPOSITORY_PATH = re.compile(r"^(?:[A-Za-z0-9.][A-Za-z0-9._-]*/)*[A-Za-z0-9.][A-Za-z0-9._-]*$") _REQUEST_KEYS = frozenset({"contract_version", "operation_id", "product_id", "component_id", "repository_id", "policy_revision", "policy_digest", "source_event_set", "source_event_policy", "expected_source_revision", "expected_target_branch_revision", "expected_version", "requested_change", "determined_target_version", "allowed_projection_paths", "prepared_operation_digest", "authorization_reference", "delivery_mode"}) _HELPER_KEYS = frozenset({"contract_version", "product_id", "repository_id", "helper_path", "receipt_directory", "allowed_projection_paths", "policy_revision"}) @@ -47,7 +48,8 @@ def load(cls, worktree: Path) -> "ProductHelperDeclaration": fields = ("product_id", "repository_id", "helper_path", "policy_revision") receipt_directory = value.get("receipt_directory") if (not all(isinstance(value.get(key), str) and _PATH.fullmatch(value[key]) for key in fields) - or not isinstance(receipt_directory, str) or "/.." in receipt_directory or receipt_directory.startswith("/")): + or not isinstance(receipt_directory, str) or not _REPOSITORY_PATH.fullmatch(receipt_directory) + or any(part in {".", ".."} for part in receipt_directory.split("/"))): raise VersionPreparationError("product version helper declaration has invalid paths or identities") paths = value.get("allowed_projection_paths") if not isinstance(paths, list) or not paths or not all(isinstance(path, str) and _PATH.fullmatch(path) for path in paths): @@ -119,6 +121,23 @@ class VersionPreparationDelivery: def __init__(self, git: GitProvider, helper: ProductHelper) -> None: self.git, self.helper = git, helper + def _changed_paths(self, worktree: Path) -> tuple[str, ...]: + """Return every changed path, including the helper's new receipt. + + ``git diff --name-only`` alone omits untracked receipts, which would + let a candidate commit omit its operation evidence. NUL-delimited + Git path inventories avoid whitespace/quote interpretation entirely. + """ + tracked = self.git.command(worktree, "git", "diff", "--no-renames", "--name-only", "-z", "HEAD") + untracked = self.git.command(worktree, "git", "ls-files", "--others", "--exclude-standard", "-z") + paths = [path for path in (tracked + untracked).split("\0") if path] + for path in paths: + if not _REPOSITORY_PATH.fullmatch(path) or any(part in {".", ".."} for part in path.split("/")): + raise VersionPreparationError("version preparation has an invalid changed path") + if len(set(paths)) != len(paths): + raise VersionPreparationError("version preparation has duplicate changed paths") + return tuple(sorted(paths)) + def prepare(self, repository: Path, worktree: Path, request: VersionPreparationRequest) -> dict[str, object]: head = self.git.command(repository, "git", "rev-parse", "HEAD") if head != request.expected_source_revision: @@ -126,16 +145,18 @@ def prepare(self, repository: Path, worktree: Path, request: VersionPreparationR status = self.git.command(repository, "git", "status", "--porcelain", "--untracked-files=all") if status: raise VersionPreparationError("repository checkout is not clean") + if self._changed_paths(worktree): + raise VersionPreparationError("isolated version preparation worktree is not clean") declaration = ProductHelperDeclaration.load(worktree) if (declaration.product_id != request.product_id or declaration.repository_id != request.repository_id or declaration.policy_revision != request.policy_revision or tuple(request.allowed_projection_paths) != declaration.allowed_projection_paths): raise VersionPreparationError("product helper declaration does not bind the admitted operation") self.helper.apply(worktree, request) - changed = tuple(filter(None, self.git.command(worktree, "git", "diff", "--name-only").splitlines())) - receipt = tuple(path for path in changed if path.endswith(f"/{request.operation_id}.json")) - allowed = set(request.allowed_projection_paths) | set(receipt) - if not receipt or set(changed) - allowed: + changed = self._changed_paths(worktree) + receipt = f"{declaration.receipt_directory}/{request.operation_id}.json" + allowed = set(request.allowed_projection_paths) | {receipt} + if receipt not in changed or set(changed) - allowed: raise VersionPreparationError("version preparation changed a path outside its declared operation") digest = hashlib.sha256("\n".join(changed).encode()).hexdigest() if digest != request.prepared_operation_digest: diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 57fbd1f9..a55d464c 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -4,6 +4,7 @@ import unittest import tempfile import hashlib +import subprocess from engineering_platform.version_preparation_delivery import VersionPreparationDelivery, VersionPreparationError, VersionPreparationRequest from engineering_platform.execution_models import PullRequestEvidence @@ -63,10 +64,14 @@ def test_delivery_evidence_is_idempotent_and_exact_head_bound(self) -> None: def test_execute_binds_prepare_candidate_qualification_and_evidence(self) -> None: class Git: + def __init__(self) -> None: self.status_calls = 0 def command(self, _root: Path, *args: str) -> str: + if args[-2:] == ("-z", "HEAD"): + self.status_calls += 1 + return "" if self.status_calls == 1 else "product-version.json\0" + if args[-1] == "-z": return "" if self.status_calls == 1 else ".version-operations/operation-0001.json\0" if args[-1] == "HEAD": return "a" * 40 if args[-1] == "--untracked-files=all": return "" - if args[-2:] == ("diff", "--name-only"): return "product-version.json\n.version-operations/operation-0001.json" if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" return "" class Helper: @@ -76,7 +81,7 @@ def create_or_recover_pull_request(self, branch: str, base: str, title: str, bod return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base) def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, object]: return {"pull_request_id": number, "exact_qualified_sha": sha, "conclusion": "PASS", "checks": []} - digest = hashlib.sha256(b"product-version.json\n.version-operations/operation-0001.json").hexdigest() + digest = hashlib.sha256(b".version-operations/operation-0001.json\nproduct-version.json").hexdigest() parsed = VersionPreparationRequest.parse(request(prepared_operation_digest=digest)) with tempfile.TemporaryDirectory() as directory: Path(directory, ".version-preparation.json").write_text(__import__("json").dumps({"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"}), encoding="utf-8") @@ -92,6 +97,59 @@ def command(self, *_args: str) -> str: raise AssertionError("must not invoke Git with self.assertRaisesRegex(VersionPreparationError, "already exists"): VersionPreparationDelivery(Git(), object()).create_isolated_worktree(Path(directory), Path(directory), parsed) + def test_receipt_must_be_in_the_declared_directory(self) -> None: + class Git: + def __init__(self) -> None: self.status_calls = 0 + def command(self, _root: Path, *args: str) -> str: + if args[-2:] == ("-z", "HEAD"): + self.status_calls += 1 + return "" if self.status_calls == 1 else "product-version.json\0" + if args[-1] == "-z": return "" if self.status_calls == 1 else "other/operation-0001.json\0" + if args[-1] == "HEAD": return "a" * 40 + if args[-1] == "--untracked-files=all": return "" + return "" + class Helper: + def apply(self, _worktree: Path, _request: object) -> None: pass + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / ".version-preparation.json").write_text(__import__("json").dumps({"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"}), encoding="utf-8") + digest = hashlib.sha256(b"other/operation-0001.json\nproduct-version.json").hexdigest() + with self.assertRaisesRegex(VersionPreparationError, "declared operation"): + VersionPreparationDelivery(Git(), Helper()).prepare(root, root, VersionPreparationRequest.parse(request(prepared_operation_digest=digest))) + + def test_isolated_git_worktree_includes_an_untracked_receipt_in_candidate_scope(self) -> None: + """A real temporary Git checkout proves receipts cannot be omitted by diff.""" + from engineering_platform.providers import GitProvider + + class Helper: + def apply(self, worktree: Path, _request: object) -> None: + (worktree / "product-version.json").write_text('{"version":"2.4.0"}\n', encoding="utf-8") + receipt = worktree / ".version-operations" + receipt.mkdir() + (receipt / "operation-0001.json").write_text('{"operation_id":"operation-0001"}\n', encoding="utf-8") + + def git(root: Path, *args: str) -> None: + subprocess.run(("git", *args), cwd=root, check=True, text=True, capture_output=True) + + declaration = {"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"} + digest = hashlib.sha256(b".version-operations/operation-0001.json\nproduct-version.json").hexdigest() + with tempfile.TemporaryDirectory() as directory: + root, candidate = Path(directory, "source"), Path(directory, "candidate") + root.mkdir() + git(root, "init", "-q") + git(root, "config", "user.email", "test@example.invalid") + git(root, "config", "user.name", "Version Preparation Test") + (root / "product-version.json").write_text('{"version":"2.3.0"}\n', encoding="utf-8") + (root / ".version-preparation.json").write_text(__import__("json").dumps(declaration), encoding="utf-8") + git(root, "add", "product-version.json", ".version-preparation.json") + git(root, "commit", "-qm", "baseline") + sha = subprocess.run(("git", "rev-parse", "HEAD"), cwd=root, check=True, text=True, capture_output=True).stdout.strip() + parsed = VersionPreparationRequest.parse(request(expected_source_revision=sha, prepared_operation_digest=digest)) + delivery = VersionPreparationDelivery(GitProvider(), Helper()) + delivery.create_isolated_worktree(root, candidate, parsed) + prepared = delivery.prepare(root, candidate, parsed) + self.assertEqual(prepared["changed_paths"], (".version-operations/operation-0001.json", "product-version.json")) + if __name__ == "__main__": unittest.main() From a34a3b42f66c5eaa8e411376bad5acbd02a1434c Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:17:15 +0200 Subject: [PATCH 09/22] fix: bind version candidates to published PR heads --- src/engineering_platform/execution_models.py | 1 + src/engineering_platform/execution_repository.py | 4 ++-- .../version_preparation_delivery.py | 2 ++ .../test_version_preparation_delivery.py | 16 +++++++++++++++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/engineering_platform/execution_models.py b/src/engineering_platform/execution_models.py index e6faca9f..e394f03d 100644 --- a/src/engineering_platform/execution_models.py +++ b/src/engineering_platform/execution_models.py @@ -25,6 +25,7 @@ class PullRequestEvidence: head_branch: str | None = None base_branch: str | None = None merge_state_status: str | None = None + head_sha: str | None = None @dataclass(frozen=True) diff --git a/src/engineering_platform/execution_repository.py b/src/engineering_platform/execution_repository.py index 8cab19c8..b7c1d8c8 100644 --- a/src/engineering_platform/execution_repository.py +++ b/src/engineering_platform/execution_repository.py @@ -195,7 +195,7 @@ def _github(self, *args: str) -> str: return self.provider.github(*scoped) def pull_request(self, number: int) -> PullRequestEvidence: - try: raw = json.loads(self._github("pr", "view", str(number), "--json", "number,state,isDraft,mergeCommit,statusCheckRollup,headRefName,baseRefName,mergeStateStatus")) + try: raw = json.loads(self._github("pr", "view", str(number), "--json", "number,state,isDraft,mergeCommit,statusCheckRollup,headRefName,headRefOid,baseRefName,mergeStateStatus")) except RuntimeError as error: raise RunnerError(str(error)) from error # GitHub can append an empty rollup entry to an otherwise completed # merged PR. It is not a check and must not keep terminal evidence in @@ -211,7 +211,7 @@ def pull_request(self, number: int) -> PullRequestEvidence: merge = raw.get("mergeCommit") or {} return PullRequestEvidence( raw["number"], raw["state"], terminal, passed, merge.get("oid"), raw["isDraft"], failed, - raw.get("headRefName"), raw.get("baseRefName"), raw.get("mergeStateStatus"), + raw.get("headRefName"), raw.get("baseRefName"), raw.get("mergeStateStatus"), raw.get("headRefOid"), ) def pull_request_for_head_branch(self, branch: str) -> PullRequestEvidence | None: diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 1df7b7d0..3e988731 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -203,6 +203,8 @@ def publish_candidate( pr = github.create_or_recover_pull_request(branch, base_branch, f"build: prepare version {request.determined_target_version}", body) if pr.head_branch != branch: raise VersionPreparationError("recovered pull request does not bind the candidate branch") + if pr.head_sha != candidate_sha: + raise VersionPreparationError("pull request head changed after version candidate publication") return {**prepared, "candidate_commit_sha": candidate_sha, "branch": branch, "pull_request_id": pr.number} @staticmethod diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index a55d464c..51664f1c 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -78,7 +78,7 @@ class Helper: def apply(self, _worktree: Path, _request: object) -> None: pass class GitHub: def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: - return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base) + return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base, head_sha="a" * 40) def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, object]: return {"pull_request_id": number, "exact_qualified_sha": sha, "conclusion": "PASS", "checks": []} digest = hashlib.sha256(b".version-operations/operation-0001.json\nproduct-version.json").hexdigest() @@ -89,6 +89,20 @@ def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, objec self.assertEqual(result["pull_request_id"], 9) self.assertTrue(Path(str(result["delivery_evidence_path"])).is_file()) + def test_publish_rejects_a_pull_request_head_that_raced_the_candidate_push(self) -> None: + class Git: + def command(self, _root: Path, *args: str) -> str: + if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" + if args[-1] == "HEAD": return "a" * 40 + return "" + class GitHub: + def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: + return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base, head_sha="b" * 40) + parsed = VersionPreparationRequest.parse(request()) + prepared = {"operation_id": parsed.operation_id, "changed_paths": ("product-version.json",), "prepared_operation_digest": parsed.prepared_operation_digest} + with self.assertRaisesRegex(VersionPreparationError, "head changed"): + VersionPreparationDelivery(Git(), object()).publish_candidate(Path("."), parsed, prepared, GitHub(), base_branch="main") + def test_existing_worktree_path_is_rejected_before_git_write(self) -> None: class Git: def command(self, *_args: str) -> str: raise AssertionError("must not invoke Git") From 772d72cc09e6ab5b6acb23d0fb34f031586c1258 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:18:32 +0200 Subject: [PATCH 10/22] feat: retain candidate tree delivery evidence --- .../version_preparation_delivery.py | 42 ++++++++++++++++--- .../test_version_preparation_delivery.py | 7 +++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 3e988731..56289e00 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -193,6 +193,9 @@ def publish_candidate( self.git.command(worktree, "git", "add", "--", *paths) self.git.command(worktree, "git", "commit", "-m", f"build: prepare version operation {request.operation_id}") candidate_sha = self.git.command(worktree, "git", "rev-parse", "HEAD") + candidate_tree_sha = self.git.command(worktree, "git", "rev-parse", "HEAD^{tree}") + if not _SHA.fullmatch(candidate_tree_sha): + raise VersionPreparationError("candidate tree identity is unavailable") self.git.command(worktree, "git", "push", "origin", f"HEAD:{branch}") body = "\n".join(( "Bounded EP version-preparation candidate.", @@ -205,7 +208,16 @@ def publish_candidate( raise VersionPreparationError("recovered pull request does not bind the candidate branch") if pr.head_sha != candidate_sha: raise VersionPreparationError("pull request head changed after version candidate publication") - return {**prepared, "candidate_commit_sha": candidate_sha, "branch": branch, "pull_request_id": pr.number} + return { + **prepared, + "candidate_commit_sha": candidate_sha, + "candidate_tree_sha": candidate_tree_sha, + "branch": branch, + "pull_request_id": pr.number, + "pull_request_head_sha": pr.head_sha, + "authorization_reference": request.authorization_reference, + "delivery_mode": request.delivery_mode, + } @staticmethod def qualify_candidate(candidate: Mapping[str, object], github: GitHubClient) -> dict[str, object]: @@ -221,13 +233,33 @@ def qualify_candidate(candidate: Mapping[str, object], github: GitHubClient) -> @staticmethod def record_delivery_evidence(evidence_root: Path, candidate: Mapping[str, object], qualification: Mapping[str, object]) -> Path: """Append immutable delivery evidence outside the tracked prepared receipt.""" - operation, sha, branch, pr = (candidate.get(key) for key in ("operation_id", "candidate_commit_sha", "branch", "pull_request_id")) - if not all(isinstance(value, str) and value for value in (operation, sha, branch)) or not isinstance(pr, int): + operation, sha, tree, branch, pr, digest, authorization = ( + candidate.get(key) for key in ( + "operation_id", "candidate_commit_sha", "candidate_tree_sha", "branch", "pull_request_id", + "prepared_operation_digest", "authorization_reference", + ) + ) + if (not all(isinstance(value, str) and value for value in (operation, sha, tree, branch, digest, authorization)) + or not _SHA.fullmatch(sha) or not _SHA.fullmatch(tree) or not isinstance(pr, int)): raise VersionPreparationError("candidate is incomplete for delivery evidence") if qualification.get("exact_qualified_sha") != sha or qualification.get("conclusion") != "PASS": raise VersionPreparationError("delivery evidence requires exact successful qualification") - payload = {"schema_version": 1, "operation_id": operation, "candidate_commit_sha": sha, "branch": branch, - "pull_request_id": pr, "qualification": dict(qualification)} + payload = { + "schema_version": 1, + "operation_id": operation, + "prepared_operation_digest": digest, + "candidate_commit_sha": sha, + "candidate_tree_sha": tree, + "branch": branch, + "pull_request_id": pr, + "pull_request_head_sha": candidate.get("pull_request_head_sha"), + "qualification": dict(qualification), + "authorization_reference": authorization, + # This adapter never merges. A later authorized delivery route + # may append separate merge evidence; it cannot relabel this as a + # completed protected delivery. + "delivery": {"state": "PENDING_PROTECTED_MERGE"}, + } encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() directory = evidence_root / "version-preparation-delivery" directory.mkdir(parents=True, exist_ok=True) diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 51664f1c..a7b7a0a8 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -54,11 +54,14 @@ def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, objec VersionPreparationDelivery.qualify_candidate({"candidate_commit_sha": "a" * 40, "pull_request_id": 9}, GitHub()) def test_delivery_evidence_is_idempotent_and_exact_head_bound(self) -> None: - candidate = {"operation_id": "operation-0001", "candidate_commit_sha": "a" * 40, "branch": "ep/version-preparation/operation-0001", "pull_request_id": 9} + candidate = {"operation_id": "operation-0001", "prepared_operation_digest": "sha256:diff", "candidate_commit_sha": "a" * 40, "candidate_tree_sha": "c" * 40, "branch": "ep/version-preparation/operation-0001", "pull_request_id": 9, "pull_request_head_sha": "a" * 40, "authorization_reference": "grant:bounded"} qualification = {"exact_qualified_sha": "a" * 40, "conclusion": "PASS", "checks": []} with tempfile.TemporaryDirectory() as directory: first = VersionPreparationDelivery.record_delivery_evidence(Path(directory), candidate, qualification) self.assertEqual(first, VersionPreparationDelivery.record_delivery_evidence(Path(directory), candidate, qualification)) + recorded = __import__("json").loads(first.read_text(encoding="utf-8")) + self.assertEqual(recorded["candidate_tree_sha"], "c" * 40) + self.assertEqual(recorded["delivery"]["state"], "PENDING_PROTECTED_MERGE") with self.assertRaisesRegex(VersionPreparationError, "exact successful"): VersionPreparationDelivery.record_delivery_evidence(Path(directory), candidate, {**qualification, "exact_qualified_sha": "b" * 40}) @@ -70,6 +73,7 @@ def command(self, _root: Path, *args: str) -> str: self.status_calls += 1 return "" if self.status_calls == 1 else "product-version.json\0" if args[-1] == "-z": return "" if self.status_calls == 1 else ".version-operations/operation-0001.json\0" + if args[-1] == "HEAD^{tree}": return "c" * 40 if args[-1] == "HEAD": return "a" * 40 if args[-1] == "--untracked-files=all": return "" if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" @@ -93,6 +97,7 @@ def test_publish_rejects_a_pull_request_head_that_raced_the_candidate_push(self) class Git: def command(self, _root: Path, *args: str) -> str: if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" + if args[-1] == "HEAD^{tree}": return "c" * 40 if args[-1] == "HEAD": return "a" * 40 return "" class GitHub: From 54a1043ede6cbcf368b8eb4256b1f96ee90935d8 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:18:59 +0200 Subject: [PATCH 11/22] fix: reject stale version preparation target branches --- .../version_preparation_delivery.py | 6 ++++++ .../test_version_preparation_delivery.py | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 56289e00..35a30794 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -105,6 +105,8 @@ def text(key: str, optional: bool = False) -> str | None: raise VersionPreparationError("unsupported contract version or operation ID") if not _SHA.fullmatch(request.expected_source_revision): raise VersionPreparationError("expected source revision must be an exact SHA") + if request.expected_target_branch_revision is not None and not _SHA.fullmatch(request.expected_target_branch_revision): + raise VersionPreparationError("expected target branch revision must be an exact SHA") if request.delivery_mode not in {"EXISTING_FEATURE_CANDIDATE", "PROTECTED_VERSION_PREPARATION_CANDIDATE"}: raise VersionPreparationError("unsupported delivery mode") if request.requested_change not in {"patch", "minor", "exact-version"}: @@ -190,6 +192,10 @@ def publish_candidate( raise VersionPreparationError("prepared candidate has no bounded changed paths") if self.git.command(worktree, "git", "branch", "--show-current") != branch: raise VersionPreparationError("isolated worktree branch does not bind the operation ID") + if request.expected_target_branch_revision is not None: + target = self.git.command(worktree, "git", "rev-parse", f"origin/{base_branch}") + if target != request.expected_target_branch_revision: + raise VersionPreparationError("target branch revision changed before candidate publication") self.git.command(worktree, "git", "add", "--", *paths) self.git.command(worktree, "git", "commit", "-m", f"build: prepare version operation {request.operation_id}") candidate_sha = self.git.command(worktree, "git", "rev-parse", "HEAD") diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index a7b7a0a8..18b3e206 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -41,6 +41,8 @@ def test_rejects_duplicate_events_and_wrong_source_sha(self) -> None: VersionPreparationRequest.parse(request(source_event_set=["merge:1", "merge:1"])) with self.assertRaisesRegex(VersionPreparationError, "exact SHA"): VersionPreparationRequest.parse(request(expected_source_revision="main")) + with self.assertRaisesRegex(VersionPreparationError, "target branch revision"): + VersionPreparationRequest.parse(request(expected_target_branch_revision="main")) def test_candidate_branch_is_deterministically_bound_to_operation(self) -> None: parsed = VersionPreparationRequest.parse(request()) @@ -108,6 +110,17 @@ def create_or_recover_pull_request(self, branch: str, base: str, title: str, bod with self.assertRaisesRegex(VersionPreparationError, "head changed"): VersionPreparationDelivery(Git(), object()).publish_candidate(Path("."), parsed, prepared, GitHub(), base_branch="main") + def test_publish_rejects_a_target_branch_that_moved_after_admission(self) -> None: + class Git: + def command(self, _root: Path, *args: str) -> str: + if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" + if args[-1] == "origin/main": return "b" * 40 + raise AssertionError(f"candidate must stop before {args!r}") + parsed = VersionPreparationRequest.parse(request(expected_target_branch_revision="a" * 40)) + prepared = {"operation_id": parsed.operation_id, "changed_paths": ("product-version.json",), "prepared_operation_digest": parsed.prepared_operation_digest} + with self.assertRaisesRegex(VersionPreparationError, "target branch revision changed"): + VersionPreparationDelivery(Git(), object()).publish_candidate(Path("."), parsed, prepared, object(), base_branch="main") + def test_existing_worktree_path_is_rejected_before_git_write(self) -> None: class Git: def command(self, *_args: str) -> str: raise AssertionError("must not invoke Git") From ce18576a75b52f68f73ddfe390f86dcfe0fd58e6 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:21:45 +0200 Subject: [PATCH 12/22] fix: validate version preparation identities --- .../version_preparation_delivery.py | 8 ++++++- .../test_version_preparation_delivery.py | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 35a30794..ddacf51d 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -19,6 +19,8 @@ from .execution_repository import GitHubClient _SHA = re.compile(r"^[0-9a-f]{40}$") +_SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$") +_SEMVER = re.compile(r"^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$") _OPERATION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$") _PATH = re.compile(r"^(?:[A-Za-z0-9][A-Za-z0-9._-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*$") _REPOSITORY_PATH = re.compile(r"^(?:[A-Za-z0-9.][A-Za-z0-9._-]*/)*[A-Za-z0-9.][A-Za-z0-9._-]*$") @@ -107,6 +109,10 @@ def text(key: str, optional: bool = False) -> str | None: raise VersionPreparationError("expected source revision must be an exact SHA") if request.expected_target_branch_revision is not None and not _SHA.fullmatch(request.expected_target_branch_revision): raise VersionPreparationError("expected target branch revision must be an exact SHA") + if not _SEMVER.fullmatch(request.expected_version) or not _SEMVER.fullmatch(request.determined_target_version): + raise VersionPreparationError("expected and determined versions must be stable SemVer") + if not _SHA256.fullmatch(request.policy_digest) or not _SHA256.fullmatch(request.prepared_operation_digest): + raise VersionPreparationError("policy and prepared operation digests must be SHA-256 identities") if request.delivery_mode not in {"EXISTING_FEATURE_CANDIDATE", "PROTECTED_VERSION_PREPARATION_CANDIDATE"}: raise VersionPreparationError("unsupported delivery mode") if request.requested_change not in {"patch", "minor", "exact-version"}: @@ -160,7 +166,7 @@ def prepare(self, repository: Path, worktree: Path, request: VersionPreparationR allowed = set(request.allowed_projection_paths) | {receipt} if receipt not in changed or set(changed) - allowed: raise VersionPreparationError("version preparation changed a path outside its declared operation") - digest = hashlib.sha256("\n".join(changed).encode()).hexdigest() + digest = "sha256:" + hashlib.sha256("\n".join(changed).encode()).hexdigest() if digest != request.prepared_operation_digest: raise VersionPreparationError("prepared operation digest does not bind the candidate diff") return {"operation_id": request.operation_id, "changed_paths": changed, "prepared_operation_digest": digest} diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 18b3e206..a11a1a91 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -14,10 +14,10 @@ def request(**overrides: object) -> dict[str, object]: value: dict[str, object] = { "contract_version": "1", "operation_id": "operation-0001", "product_id": "forge", "component_id": "product", "repository_id": "pcvantol/forge", "policy_revision": "v1", - "policy_digest": "sha256:policy", "source_event_set": ["merge:1"], "source_event_policy": "main", + "policy_digest": "sha256:" + "a" * 64, "source_event_set": ["merge:1"], "source_event_policy": "main", "expected_source_revision": "a" * 40, "expected_target_branch_revision": None, "expected_version": "2.3.0", "requested_change": "minor", "determined_target_version": "2.4.0", - "allowed_projection_paths": ["product-version.json"], "prepared_operation_digest": "sha256:diff", + "allowed_projection_paths": ["product-version.json"], "prepared_operation_digest": "sha256:" + "b" * 64, "authorization_reference": "grant:bounded", "delivery_mode": "PROTECTED_VERSION_PREPARATION_CANDIDATE", } value.update(overrides) @@ -44,6 +44,16 @@ def test_rejects_duplicate_events_and_wrong_source_sha(self) -> None: with self.assertRaisesRegex(VersionPreparationError, "target branch revision"): VersionPreparationRequest.parse(request(expected_target_branch_revision="main")) + def test_rejects_noncanonical_versions_and_digests(self) -> None: + with self.assertRaisesRegex(VersionPreparationError, "stable SemVer"): + VersionPreparationRequest.parse(request(expected_version="02.3.0")) + with self.assertRaisesRegex(VersionPreparationError, "stable SemVer"): + VersionPreparationRequest.parse(request(determined_target_version="2.3.00")) + with self.assertRaisesRegex(VersionPreparationError, "SHA-256"): + VersionPreparationRequest.parse(request(policy_digest="sha256:policy")) + with self.assertRaisesRegex(VersionPreparationError, "SHA-256"): + VersionPreparationRequest.parse(request(prepared_operation_digest="sha256:diff")) + def test_candidate_branch_is_deterministically_bound_to_operation(self) -> None: parsed = VersionPreparationRequest.parse(request()) self.assertEqual(VersionPreparationDelivery.branch_name(parsed), "ep/version-preparation/operation-0001") @@ -56,7 +66,7 @@ def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, objec VersionPreparationDelivery.qualify_candidate({"candidate_commit_sha": "a" * 40, "pull_request_id": 9}, GitHub()) def test_delivery_evidence_is_idempotent_and_exact_head_bound(self) -> None: - candidate = {"operation_id": "operation-0001", "prepared_operation_digest": "sha256:diff", "candidate_commit_sha": "a" * 40, "candidate_tree_sha": "c" * 40, "branch": "ep/version-preparation/operation-0001", "pull_request_id": 9, "pull_request_head_sha": "a" * 40, "authorization_reference": "grant:bounded"} + candidate = {"operation_id": "operation-0001", "prepared_operation_digest": "sha256:" + "b" * 64, "candidate_commit_sha": "a" * 40, "candidate_tree_sha": "c" * 40, "branch": "ep/version-preparation/operation-0001", "pull_request_id": 9, "pull_request_head_sha": "a" * 40, "authorization_reference": "grant:bounded"} qualification = {"exact_qualified_sha": "a" * 40, "conclusion": "PASS", "checks": []} with tempfile.TemporaryDirectory() as directory: first = VersionPreparationDelivery.record_delivery_evidence(Path(directory), candidate, qualification) @@ -87,7 +97,7 @@ def create_or_recover_pull_request(self, branch: str, base: str, title: str, bod return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base, head_sha="a" * 40) def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, object]: return {"pull_request_id": number, "exact_qualified_sha": sha, "conclusion": "PASS", "checks": []} - digest = hashlib.sha256(b".version-operations/operation-0001.json\nproduct-version.json").hexdigest() + digest = "sha256:" + hashlib.sha256(b".version-operations/operation-0001.json\nproduct-version.json").hexdigest() parsed = VersionPreparationRequest.parse(request(prepared_operation_digest=digest)) with tempfile.TemporaryDirectory() as directory: Path(directory, ".version-preparation.json").write_text(__import__("json").dumps({"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"}), encoding="utf-8") @@ -145,7 +155,7 @@ def apply(self, _worktree: Path, _request: object) -> None: pass with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / ".version-preparation.json").write_text(__import__("json").dumps({"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"}), encoding="utf-8") - digest = hashlib.sha256(b"other/operation-0001.json\nproduct-version.json").hexdigest() + digest = "sha256:" + hashlib.sha256(b"other/operation-0001.json\nproduct-version.json").hexdigest() with self.assertRaisesRegex(VersionPreparationError, "declared operation"): VersionPreparationDelivery(Git(), Helper()).prepare(root, root, VersionPreparationRequest.parse(request(prepared_operation_digest=digest))) @@ -164,7 +174,7 @@ def git(root: Path, *args: str) -> None: subprocess.run(("git", *args), cwd=root, check=True, text=True, capture_output=True) declaration = {"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"} - digest = hashlib.sha256(b".version-operations/operation-0001.json\nproduct-version.json").hexdigest() + digest = "sha256:" + hashlib.sha256(b".version-operations/operation-0001.json\nproduct-version.json").hexdigest() with tempfile.TemporaryDirectory() as directory: root, candidate = Path(directory, "source"), Path(directory, "candidate") root.mkdir() From bf7435ed828a46fc01fe3e3dfff5c6217aa8d6c2 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:23:00 +0200 Subject: [PATCH 13/22] fix: bind prepared receipts to admitted operations --- .../version_preparation_delivery.py | 24 +++++++++++++++++++ .../test_version_preparation_delivery.py | 17 ++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index ddacf51d..7e93a5fc 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -146,6 +146,29 @@ def _changed_paths(self, worktree: Path) -> tuple[str, ...]: raise VersionPreparationError("version preparation has duplicate changed paths") return tuple(sorted(paths)) + @staticmethod + def _validate_prepared_receipt(path: Path, declaration: ProductHelperDeclaration, request: VersionPreparationRequest) -> None: + """Bind the product-owned receipt without imposing one product schema. + + Product helpers deliberately retain their own detailed receipt forms. + EP validates only the shared operation facts required to publish their + output as one bounded candidate. + """ + try: + receipt = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError, json.JSONDecodeError) as error: + raise VersionPreparationError("prepared operation receipt is unreadable") from error + if not isinstance(receipt, dict): + raise VersionPreparationError("prepared operation receipt must be an object") + if receipt.get("schema_version") not in {1, "1"} or isinstance(receipt.get("schema_version"), bool): + raise VersionPreparationError("prepared operation receipt has an unsupported schema") + if (receipt.get("operation_id") != request.operation_id + or receipt.get("product") != request.product_id + or receipt.get("policy_revision") != declaration.policy_revision + or receipt.get("expected_source_revision") != request.expected_source_revision + or receipt.get("allowed_projection_paths") != list(request.allowed_projection_paths)): + raise VersionPreparationError("prepared operation receipt does not bind the admitted operation") + def prepare(self, repository: Path, worktree: Path, request: VersionPreparationRequest) -> dict[str, object]: head = self.git.command(repository, "git", "rev-parse", "HEAD") if head != request.expected_source_revision: @@ -166,6 +189,7 @@ def prepare(self, repository: Path, worktree: Path, request: VersionPreparationR allowed = set(request.allowed_projection_paths) | {receipt} if receipt not in changed or set(changed) - allowed: raise VersionPreparationError("version preparation changed a path outside its declared operation") + self._validate_prepared_receipt(worktree / receipt, declaration, request) digest = "sha256:" + hashlib.sha256("\n".join(changed).encode()).hexdigest() if digest != request.prepared_operation_digest: raise VersionPreparationError("prepared operation digest does not bind the candidate diff") diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index a11a1a91..3e2d251c 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -6,7 +6,7 @@ import hashlib import subprocess -from engineering_platform.version_preparation_delivery import VersionPreparationDelivery, VersionPreparationError, VersionPreparationRequest +from engineering_platform.version_preparation_delivery import ProductHelperDeclaration, VersionPreparationDelivery, VersionPreparationError, VersionPreparationRequest from engineering_platform.execution_models import PullRequestEvidence @@ -91,7 +91,10 @@ def command(self, _root: Path, *args: str) -> str: if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" return "" class Helper: - def apply(self, _worktree: Path, _request: object) -> None: pass + def apply(self, worktree: Path, operation: VersionPreparationRequest) -> None: + receipt = worktree / ".version-operations" + receipt.mkdir(exist_ok=True) + (receipt / "operation-0001.json").write_text(__import__("json").dumps({"schema_version": 1, "operation_id": operation.operation_id, "product": operation.product_id, "policy_revision": operation.policy_revision, "expected_source_revision": operation.expected_source_revision, "allowed_projection_paths": list(operation.allowed_projection_paths)}), encoding="utf-8") class GitHub: def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base, head_sha="a" * 40) @@ -159,6 +162,14 @@ def apply(self, _worktree: Path, _request: object) -> None: pass with self.assertRaisesRegex(VersionPreparationError, "declared operation"): VersionPreparationDelivery(Git(), Helper()).prepare(root, root, VersionPreparationRequest.parse(request(prepared_operation_digest=digest))) + def test_prepared_receipt_must_bind_the_admitted_product_operation(self) -> None: + declaration = ProductHelperDeclaration("forge", "pcvantol/forge", "scripts/advance_product_version.py", ".version-operations", ("product-version.json",), "v1") + with tempfile.TemporaryDirectory() as directory: + receipt = Path(directory, "operation-0001.json") + receipt.write_text(__import__("json").dumps({"schema_version": 1, "operation_id": "operation-0001", "product": "workspace", "policy_revision": "v1", "expected_source_revision": "a" * 40, "allowed_projection_paths": ["product-version.json"]}), encoding="utf-8") + with self.assertRaisesRegex(VersionPreparationError, "does not bind"): + VersionPreparationDelivery._validate_prepared_receipt(receipt, declaration, VersionPreparationRequest.parse(request())) + def test_isolated_git_worktree_includes_an_untracked_receipt_in_candidate_scope(self) -> None: """A real temporary Git checkout proves receipts cannot be omitted by diff.""" from engineering_platform.providers import GitProvider @@ -168,7 +179,7 @@ def apply(self, worktree: Path, _request: object) -> None: (worktree / "product-version.json").write_text('{"version":"2.4.0"}\n', encoding="utf-8") receipt = worktree / ".version-operations" receipt.mkdir() - (receipt / "operation-0001.json").write_text('{"operation_id":"operation-0001"}\n', encoding="utf-8") + (receipt / "operation-0001.json").write_text(__import__("json").dumps({"schema_version": 1, "operation_id": "operation-0001", "product": "forge", "policy_revision": "v1", "expected_source_revision": sha, "allowed_projection_paths": ["product-version.json"]}), encoding="utf-8") def git(root: Path, *args: str) -> None: subprocess.run(("git", *args), cwd=root, check=True, text=True, capture_output=True) From c8cdcfb944de38433014b82a0d12a4d52ccee509 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:24:04 +0200 Subject: [PATCH 14/22] feat: prepare version candidates in isolated worktrees --- .../version_preparation_delivery.py | 12 ++++++++++++ .../engineering/test_version_preparation_delivery.py | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 7e93a5fc..b390c883 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -211,6 +211,18 @@ def create_isolated_worktree(self, repository: Path, worktree: Path, request: Ve if self.git.command(worktree, "git", "status", "--porcelain", "--untracked-files=all"): raise VersionPreparationError("isolated version preparation worktree is not clean") + def prepare_in_isolated_worktree( + self, repository: Path, worktree: Path, request: VersionPreparationRequest, + ) -> dict[str, object]: + """Create and prepare one candidate without publishing it. + + The worktree is deliberately retained on an apply failure. It has no + commit, push, PR or publication side effect, and retaining it prevents + a retry from silently deriving a new operation from partial files. + """ + self.create_isolated_worktree(repository, worktree, request) + return self.prepare(repository, worktree, request) + def publish_candidate( self, worktree: Path, request: VersionPreparationRequest, prepared: Mapping[str, object], github: GitHubClient, *, base_branch: str, diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 3e2d251c..baf652c1 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -199,9 +199,9 @@ def git(root: Path, *args: str) -> None: sha = subprocess.run(("git", "rev-parse", "HEAD"), cwd=root, check=True, text=True, capture_output=True).stdout.strip() parsed = VersionPreparationRequest.parse(request(expected_source_revision=sha, prepared_operation_digest=digest)) delivery = VersionPreparationDelivery(GitProvider(), Helper()) - delivery.create_isolated_worktree(root, candidate, parsed) - prepared = delivery.prepare(root, candidate, parsed) + prepared = delivery.prepare_in_isolated_worktree(root, candidate, parsed) self.assertEqual(prepared["changed_paths"], (".version-operations/operation-0001.json", "product-version.json")) + self.assertEqual(GitProvider().command(candidate, "git", "rev-parse", "HEAD"), sha) if __name__ == "__main__": From 2deaf5a4a715fc04292ab078c2c39cc4edf9bb98 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:24:36 +0200 Subject: [PATCH 15/22] test: retain failed version preparation worktrees --- .../test_version_preparation_delivery.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index baf652c1..98cf218c 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -203,6 +203,20 @@ def git(root: Path, *args: str) -> None: self.assertEqual(prepared["changed_paths"], (".version-operations/operation-0001.json", "product-version.json")) self.assertEqual(GitProvider().command(candidate, "git", "rev-parse", "HEAD"), sha) + class FailingHelper: + def apply(self, worktree: Path, operation: VersionPreparationRequest) -> None: + (worktree / "product-version.json").write_text('{"version":"2.4.0"}\n', encoding="utf-8") + (worktree / ".version-operations").mkdir() + (worktree / ".version-operations" / f"{operation.operation_id}.json").write_text("not-json", encoding="utf-8") + + failed = Path(directory, "failed-candidate") + failed_request = VersionPreparationRequest.parse(request(operation_id="operation-0002", expected_source_revision=sha, prepared_operation_digest=digest)) + with self.assertRaisesRegex(VersionPreparationError, "receipt is unreadable"): + VersionPreparationDelivery(GitProvider(), FailingHelper()).prepare_in_isolated_worktree(root, failed, failed_request) + self.assertEqual(GitProvider().command(root, "git", "rev-parse", "HEAD"), sha) + self.assertEqual(GitProvider().command(failed, "git", "rev-parse", "HEAD"), sha) + self.assertTrue(GitProvider().command(failed, "git", "status", "--porcelain")) + if __name__ == "__main__": unittest.main() From 0190c5c40de11724f0380993b676f68280b45308 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:25:52 +0200 Subject: [PATCH 16/22] feat: preflight scoped version delivery writers --- .../execution_repository.py | 21 ++++++++++++++++++ .../version_preparation_delivery.py | 5 +++++ .../test_version_preparation_delivery.py | 22 ++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/engineering_platform/execution_repository.py b/src/engineering_platform/execution_repository.py index b7c1d8c8..03edded8 100644 --- a/src/engineering_platform/execution_repository.py +++ b/src/engineering_platform/execution_repository.py @@ -46,6 +46,7 @@ def normalize_markdown_body(self, number: int) -> bool: ... def merge(self, number: int) -> None: ... def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: ... def qualification_for_exact_head(self, number: int, head_sha: str) -> dict[str, object]: ... + def version_preparation_writer(self) -> dict[str, object]: ... class SubprocessRepositoryClient: @@ -194,6 +195,26 @@ def _github(self, *args: str) -> str: scoped = (*args, "--repo", self.repository) if self.repository else args return self.provider.github(*scoped) + def version_preparation_writer(self) -> dict[str, object]: + """Read the configured writer's safe identity and repository scope. + + This is a preflight observation, never a credential issuer or branch + protection bypass. GitHub does not expose a general branch-write + guarantee here, so a protected merge remains GitHub's authority. + """ + if not self.repository or not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", self.repository): + raise RunnerError("Version preparation writer requires one exact GitHub repository scope.") + try: + identity = json.loads(self.provider.github("api", "user")) + repository = json.loads(self._github("api", f"repos/{self.repository}")) + except (RuntimeError, json.JSONDecodeError) as error: + raise RunnerError("Version preparation writer identity could not be read.") from error + actor = identity.get("login") if isinstance(identity, dict) else None + permissions = repository.get("permissions") if isinstance(repository, dict) else None + if not isinstance(actor, str) or not actor or not isinstance(permissions, dict): + raise RunnerError("Version preparation writer identity is incomplete.") + return {"actor": actor, "repository_id": self.repository, "can_push": permissions.get("push") is True} + def pull_request(self, number: int) -> PullRequestEvidence: try: raw = json.loads(self._github("pr", "view", str(number), "--json", "number,state,isDraft,mergeCommit,statusCheckRollup,headRefName,headRefOid,baseRefName,mergeStateStatus")) except RuntimeError as error: raise RunnerError(str(error)) from error diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index b390c883..35c2dc71 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -234,6 +234,11 @@ def publish_candidate( raise VersionPreparationError("prepared candidate has no bounded changed paths") if self.git.command(worktree, "git", "branch", "--show-current") != branch: raise VersionPreparationError("isolated worktree branch does not bind the operation ID") + writer = github.version_preparation_writer() + if (writer.get("repository_id") != request.repository_id + or not isinstance(writer.get("actor"), str) or not writer["actor"] + or writer.get("can_push") is not True): + raise VersionPreparationError("configured GitHub writer is not authorized for this version candidate") if request.expected_target_branch_revision is not None: target = self.git.command(worktree, "git", "rev-parse", f"origin/{base_branch}") if target != request.expected_target_branch_revision: diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 98cf218c..e65bf32b 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -96,6 +96,8 @@ def apply(self, worktree: Path, operation: VersionPreparationRequest) -> None: receipt.mkdir(exist_ok=True) (receipt / "operation-0001.json").write_text(__import__("json").dumps({"schema_version": 1, "operation_id": operation.operation_id, "product": operation.product_id, "policy_revision": operation.policy_revision, "expected_source_revision": operation.expected_source_revision, "allowed_projection_paths": list(operation.allowed_projection_paths)}), encoding="utf-8") class GitHub: + def version_preparation_writer(self) -> dict[str, object]: + return {"actor": "ep-writer", "repository_id": "pcvantol/forge", "can_push": True} def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base, head_sha="a" * 40) def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, object]: @@ -116,6 +118,8 @@ def command(self, _root: Path, *args: str) -> str: if args[-1] == "HEAD": return "a" * 40 return "" class GitHub: + def version_preparation_writer(self) -> dict[str, object]: + return {"actor": "ep-writer", "repository_id": "pcvantol/forge", "can_push": True} def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base, head_sha="b" * 40) parsed = VersionPreparationRequest.parse(request()) @@ -123,16 +127,32 @@ def create_or_recover_pull_request(self, branch: str, base: str, title: str, bod with self.assertRaisesRegex(VersionPreparationError, "head changed"): VersionPreparationDelivery(Git(), object()).publish_candidate(Path("."), parsed, prepared, GitHub(), base_branch="main") + def test_publish_rejects_a_writer_without_exact_repository_push_scope(self) -> None: + class Git: + def command(self, _root: Path, *args: str) -> str: + if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" + raise AssertionError(f"candidate must stop before {args!r}") + class GitHub: + def version_preparation_writer(self) -> dict[str, object]: + return {"actor": "ep-writer", "repository_id": "pcvantol/other", "can_push": True} + parsed = VersionPreparationRequest.parse(request()) + prepared = {"operation_id": parsed.operation_id, "changed_paths": ("product-version.json",), "prepared_operation_digest": parsed.prepared_operation_digest} + with self.assertRaisesRegex(VersionPreparationError, "not authorized"): + VersionPreparationDelivery(Git(), object()).publish_candidate(Path("."), parsed, prepared, GitHub(), base_branch="main") + def test_publish_rejects_a_target_branch_that_moved_after_admission(self) -> None: class Git: def command(self, _root: Path, *args: str) -> str: if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" if args[-1] == "origin/main": return "b" * 40 raise AssertionError(f"candidate must stop before {args!r}") + class GitHub: + def version_preparation_writer(self) -> dict[str, object]: + return {"actor": "ep-writer", "repository_id": "pcvantol/forge", "can_push": True} parsed = VersionPreparationRequest.parse(request(expected_target_branch_revision="a" * 40)) prepared = {"operation_id": parsed.operation_id, "changed_paths": ("product-version.json",), "prepared_operation_digest": parsed.prepared_operation_digest} with self.assertRaisesRegex(VersionPreparationError, "target branch revision changed"): - VersionPreparationDelivery(Git(), object()).publish_candidate(Path("."), parsed, prepared, object(), base_branch="main") + VersionPreparationDelivery(Git(), object()).publish_candidate(Path("."), parsed, prepared, GitHub(), base_branch="main") def test_existing_worktree_path_is_rejected_before_git_write(self) -> None: class Git: From 51b274f32b8b4e9f39dd1c05d6b77a6c4a904bcf Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:28:58 +0200 Subject: [PATCH 17/22] test: cover version delivery writer preflight --- tests/engineering/test_execution_host.py | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/engineering/test_execution_host.py b/tests/engineering/test_execution_host.py index bdc23383..b45c6bee 100644 --- a/tests/engineering/test_execution_host.py +++ b/tests/engineering/test_execution_host.py @@ -1199,6 +1199,36 @@ def github(self, *_: str) -> str: self.assertTrue(evidence.checks_passed) self.assertEqual(evidence.failed_checks, ()) + def test_github_writer_preflight_reads_only_identity_and_scoped_permission(self) -> None: + class Provider: + def __init__(self) -> None: + self.calls: list[tuple[str, ...]] = [] + def github(self, *args: str) -> str: + self.calls.append(args) + if args[:2] == ("api", "user"): + return json.dumps({"login": "ep-delivery-app"}) + if args[:2] == ("api", "repos/pcvantol/forge"): + return json.dumps({"permissions": {"push": True}}) + raise AssertionError(args) + + provider = Provider() + self.assertEqual( + GhCliClient(provider, "pcvantol/forge").version_preparation_writer(), + {"actor": "ep-delivery-app", "repository_id": "pcvantol/forge", "can_push": True}, + ) + self.assertEqual(provider.calls, [("api", "user"), ("api", "repos/pcvantol/forge", "--repo", "pcvantol/forge")]) + + def test_github_writer_preflight_rejects_missing_scope_or_identity(self) -> None: + with self.assertRaisesRegex(RunnerError, "exact GitHub repository"): + GhCliClient(object()).version_preparation_writer() + + class Provider: + def github(self, *args: str) -> str: + return json.dumps({"login": "ep-delivery-app"}) if args[:2] == ("api", "user") else json.dumps({}) + + with self.assertRaisesRegex(RunnerError, "identity is incomplete"): + GhCliClient(Provider(), "pcvantol/forge").version_preparation_writer() + @patch("engineering_platform.execution_host.subprocess.run") def test_codex_client_handles_valid_review_and_invoke_results(self, run: object) -> None: review_message = json.dumps( From a044ee63a3570faf966cffea3eaaf52bb9fee554 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 18:55:24 +0200 Subject: [PATCH 18/22] test: cover version preparation repository paths --- tests/engineering/test_execution_host.py | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/engineering/test_execution_host.py b/tests/engineering/test_execution_host.py index b45c6bee..35f79b33 100644 --- a/tests/engineering/test_execution_host.py +++ b/tests/engineering/test_execution_host.py @@ -1229,6 +1229,67 @@ def github(self, *args: str) -> str: with self.assertRaisesRegex(RunnerError, "identity is incomplete"): GhCliClient(Provider(), "pcvantol/forge").version_preparation_writer() + def test_github_client_recovers_only_one_matching_version_preparation_pr(self) -> None: + evidence = { + "number": 12, "state": "OPEN", "isDraft": False, + "mergeCommit": None, "headRefName": "codex/version-prepare/op", + "headRefOid": "a" * 40, "baseRefName": "main", "mergeStateStatus": "CLEAN", + "statusCheckRollup": [{"name": "validate", "status": "COMPLETED", "conclusion": "SUCCESS"}], + } + + class Provider: + def __init__(self, listing: list[object]) -> None: self.listing, self.calls = listing, [] + def github(self, *args: str) -> str: + self.calls.append(args) + if args[:2] == ("pr", "list"): return json.dumps(self.listing) + if args[:2] == ("pr", "view"): return json.dumps(evidence) + raise AssertionError(args) + + client = GhCliClient(Provider([{"number": 12}])) + recovered = client.create_or_recover_pull_request("codex/version-prepare/op", "main", "title", "body") + self.assertEqual(recovered.number, 12) + self.assertIsNone(GhCliClient(Provider([])).pull_request_for_head_branch("missing")) + with self.assertRaisesRegex(RunnerError, "more than one"): + GhCliClient(Provider([{"number": 12}, {"number": 13}])).pull_request_for_head_branch("ambiguous") + + def test_github_client_create_acknowledgement_and_qualification_are_exact_head_bound(self) -> None: + head = "b" * 40 + evidence = { + "number": 17, "state": "OPEN", "isDraft": False, "mergeCommit": None, + "headRefName": "codex/version-prepare/op", "headRefOid": head, + "baseRefName": "main", "baseRefOid": "c" * 40, "mergeStateStatus": "CLEAN", + "statusCheckRollup": [{"name": "validate", "status": "COMPLETED", "conclusion": "SUCCESS"}], + } + + class Provider: + def github(self, *args: str) -> str: + if args[:2] == ("pr", "list"): return "[]" + if args[:2] == ("pr", "create"): return "https://github.com/pcvantol/forge/pull/17\n" + if args[:2] == ("pr", "view"): return json.dumps(evidence) + raise AssertionError(args) + + client = GhCliClient(Provider()) + self.assertEqual(client.create_or_recover_pull_request("codex/version-prepare/op", "main", "title", "body").number, 17) + qualification = client.qualification_for_exact_head(17, head) + self.assertEqual(qualification["exact_qualified_sha"], head) + with self.assertRaisesRegex(RunnerError, "exact candidate SHA"): + client.qualification_for_exact_head(17, "not-a-sha") + evidence["headRefOid"] = "d" * 40 + with self.assertRaisesRegex(RunnerError, "different pull request head"): + client.qualification_for_exact_head(17, head) + + def test_github_client_rejects_incomplete_or_failed_version_preparation_qualification(self) -> None: + head = "e" * 40 + class Provider: + def __init__(self, checks: list[dict[str, str]]) -> None: self.checks = checks + def github(self, *args: str) -> str: + return json.dumps({"headRefOid": head, "baseRefOid": "f" * 40, "statusCheckRollup": self.checks}) + + with self.assertRaisesRegex(RunnerError, "incomplete"): + GhCliClient(Provider([])).qualification_for_exact_head(1, head) + with self.assertRaisesRegex(RunnerError, "failed: lint"): + GhCliClient(Provider([{"name": "lint", "status": "COMPLETED", "conclusion": "FAILURE"}])).qualification_for_exact_head(1, head) + @patch("engineering_platform.execution_host.subprocess.run") def test_codex_client_handles_valid_review_and_invoke_results(self, run: object) -> None: review_message = json.dumps( From d1ae0de2b8cf1f2c8eb22756f85aaa716080510c Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 19:01:26 +0200 Subject: [PATCH 19/22] feat: bind release classification to version preparation --- .../version_preparation_delivery.py | 11 +++++++++-- .../engineering/test_version_preparation_delivery.py | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 35c2dc71..9f200025 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -24,7 +24,7 @@ _OPERATION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$") _PATH = re.compile(r"^(?:[A-Za-z0-9][A-Za-z0-9._-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*$") _REPOSITORY_PATH = re.compile(r"^(?:[A-Za-z0-9.][A-Za-z0-9._-]*/)*[A-Za-z0-9.][A-Za-z0-9._-]*$") -_REQUEST_KEYS = frozenset({"contract_version", "operation_id", "product_id", "component_id", "repository_id", "policy_revision", "policy_digest", "source_event_set", "source_event_policy", "expected_source_revision", "expected_target_branch_revision", "expected_version", "requested_change", "determined_target_version", "allowed_projection_paths", "prepared_operation_digest", "authorization_reference", "delivery_mode"}) +_REQUEST_KEYS = frozenset({"contract_version", "operation_id", "product_id", "component_id", "repository_id", "policy_revision", "policy_digest", "source_event_set", "source_event_policy", "release_class", "release_rationale", "expected_source_revision", "expected_target_branch_revision", "expected_version", "requested_change", "determined_target_version", "allowed_projection_paths", "prepared_operation_digest", "authorization_reference", "delivery_mode"}) _HELPER_KEYS = frozenset({"contract_version", "product_id", "repository_id", "helper_path", "receipt_directory", "allowed_projection_paths", "policy_revision"}) @@ -70,6 +70,8 @@ class VersionPreparationRequest: policy_digest: str source_event_set: tuple[str, ...] source_event_policy: str + release_class: str + release_rationale: str expected_source_revision: str expected_target_branch_revision: str | None expected_version: str @@ -99,7 +101,7 @@ def text(key: str, optional: bool = False) -> str | None: raise VersionPreparationError("allowed projection paths are invalid") request = cls(*(text(key, key in {"component_id", "expected_target_branch_revision"}) for key in ( "contract_version", "operation_id", "product_id", "component_id", "repository_id", "policy_revision", "policy_digest")), - tuple(events), text("source_event_policy"), text("expected_source_revision"), + tuple(events), text("source_event_policy"), text("release_class"), text("release_rationale"), text("expected_source_revision"), text("expected_target_branch_revision", True), text("expected_version"), text("requested_change"), text("determined_target_version"), tuple(paths), text("prepared_operation_digest"), text("authorization_reference"), text("delivery_mode")) @@ -117,6 +119,11 @@ def text(key: str, optional: bool = False) -> str | None: raise VersionPreparationError("unsupported delivery mode") if request.requested_change not in {"patch", "minor", "exact-version"}: raise VersionPreparationError("unsupported requested change") + if request.release_class not in {"PATCH", "MINOR", "MAJOR", "EXACT", "NO_BUMP"}: + raise VersionPreparationError("unsupported release classification") + expected_class = {"patch": "PATCH", "minor": "MINOR", "exact-version": "EXACT"}[request.requested_change] + if request.release_class != expected_class: + raise VersionPreparationError("release classification does not bind the requested change") return request diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index e65bf32b..8c52f05c 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -14,7 +14,7 @@ def request(**overrides: object) -> dict[str, object]: value: dict[str, object] = { "contract_version": "1", "operation_id": "operation-0001", "product_id": "forge", "component_id": "product", "repository_id": "pcvantol/forge", "policy_revision": "v1", - "policy_digest": "sha256:" + "a" * 64, "source_event_set": ["merge:1"], "source_event_policy": "main", + "policy_digest": "sha256:" + "a" * 64, "source_event_set": ["increment:I-123"], "source_event_policy": "engineering-increment", "release_class": "MINOR", "release_rationale": "capability boundary", "expected_source_revision": "a" * 40, "expected_target_branch_revision": None, "expected_version": "2.3.0", "requested_change": "minor", "determined_target_version": "2.4.0", "allowed_projection_paths": ["product-version.json"], "prepared_operation_digest": "sha256:" + "b" * 64, @@ -54,6 +54,10 @@ def test_rejects_noncanonical_versions_and_digests(self) -> None: with self.assertRaisesRegex(VersionPreparationError, "SHA-256"): VersionPreparationRequest.parse(request(prepared_operation_digest="sha256:diff")) + def test_rejects_a_release_classification_that_disagrees_with_the_operation(self) -> None: + with self.assertRaisesRegex(VersionPreparationError, "classification"): + VersionPreparationRequest.parse(request(release_class="PATCH")) + def test_candidate_branch_is_deterministically_bound_to_operation(self) -> None: parsed = VersionPreparationRequest.parse(request()) self.assertEqual(VersionPreparationDelivery.branch_name(parsed), "ep/version-preparation/operation-0001") From 05f4283e3b8e9d16a8cd5e5190198695da1a916b Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 20:06:08 +0200 Subject: [PATCH 20/22] fix: bind version preparation to candidate bytes --- .../version_preparation_delivery.py | 118 +++++++++++++++--- .../test_version_preparation_delivery.py | 115 +++++++++++++++-- 2 files changed, 206 insertions(+), 27 deletions(-) diff --git a/src/engineering_platform/version_preparation_delivery.py b/src/engineering_platform/version_preparation_delivery.py index 9f200025..476e0e77 100644 --- a/src/engineering_platform/version_preparation_delivery.py +++ b/src/engineering_platform/version_preparation_delivery.py @@ -153,6 +153,56 @@ def _changed_paths(self, worktree: Path) -> tuple[str, ...]: raise VersionPreparationError("version preparation has duplicate changed paths") return tuple(sorted(paths)) + @staticmethod + def _prepared_candidate_digest( + worktree: Path, changed: tuple[str, ...], request: VersionPreparationRequest, + ) -> str: + """Return a canonical identity for the complete, resulting candidate. + + The identity deliberately covers resulting bytes rather than just a + path inventory. It also repeats the operation facts that select the + product-owned mutation. This is evidence for one prepared candidate, + not a substitute for a Git tree identity after it is committed. + """ + entries: list[dict[str, str]] = [] + for relative in changed: + path = worktree / relative + if path.is_symlink() or not path.is_file(): + raise VersionPreparationError("prepared candidate contains a non-regular projection") + try: + content = path.read_bytes() + except OSError as error: + raise VersionPreparationError("prepared candidate projection is unreadable") from error + entries.append({ + "path": relative, + "resulting_sha256": "sha256:" + hashlib.sha256(content).hexdigest(), + }) + operation = { + "operation_id": request.operation_id, + "product_id": request.product_id, + "component_id": request.component_id, + "repository_id": request.repository_id, + "policy_revision": request.policy_revision, + "policy_digest": request.policy_digest, + "source_event_set": list(request.source_event_set), + "source_event_policy": request.source_event_policy, + "release_class": request.release_class, + "release_rationale": request.release_rationale, + "expected_source_revision": request.expected_source_revision, + "expected_target_branch_revision": request.expected_target_branch_revision, + "expected_version": request.expected_version, + "requested_change": request.requested_change, + "determined_target_version": request.determined_target_version, + "allowed_projection_paths": list(request.allowed_projection_paths), + "authorization_reference": request.authorization_reference, + "delivery_mode": request.delivery_mode, + } + payload = json.dumps( + {"schema_version": 1, "operation": operation, "resulting_paths": entries}, + sort_keys=True, separators=(",", ":"), ensure_ascii=True, + ).encode("ascii") + return "sha256:" + hashlib.sha256(payload).hexdigest() + @staticmethod def _validate_prepared_receipt(path: Path, declaration: ProductHelperDeclaration, request: VersionPreparationRequest) -> None: """Bind the product-owned receipt without imposing one product schema. @@ -169,13 +219,47 @@ def _validate_prepared_receipt(path: Path, declaration: ProductHelperDeclaration raise VersionPreparationError("prepared operation receipt must be an object") if receipt.get("schema_version") not in {1, "1"} or isinstance(receipt.get("schema_version"), bool): raise VersionPreparationError("prepared operation receipt has an unsupported schema") - if (receipt.get("operation_id") != request.operation_id - or receipt.get("product") != request.product_id - or receipt.get("policy_revision") != declaration.policy_revision - or receipt.get("expected_source_revision") != request.expected_source_revision - or receipt.get("allowed_projection_paths") != list(request.allowed_projection_paths)): + expected = { + "operation_id": request.operation_id, + "product": request.product_id, + "component_id": request.component_id, + "repository_id": request.repository_id, + "policy_revision": declaration.policy_revision, + "policy_digest": request.policy_digest, + "source_event_set": list(request.source_event_set), + "source_event_policy": request.source_event_policy, + "release_class": request.release_class, + "release_rationale": request.release_rationale, + "expected_source_revision": request.expected_source_revision, + "expected_target_branch_revision": request.expected_target_branch_revision, + "expected_version": request.expected_version, + "requested_change": request.requested_change, + "determined_target_version": request.determined_target_version, + "allowed_projection_paths": list(request.allowed_projection_paths), + "authorization_reference": request.authorization_reference, + "delivery_mode": request.delivery_mode, + } + if any(receipt.get(key) != value for key, value in expected.items()): raise VersionPreparationError("prepared operation receipt does not bind the admitted operation") + def _verify_prepared_candidate( + self, worktree: Path, declaration: ProductHelperDeclaration, request: VersionPreparationRequest, + *, expected_paths: tuple[str, ...] | None = None, + ) -> tuple[tuple[str, ...], str]: + """Re-read every candidate input; never publish a stale preparation.""" + changed = self._changed_paths(worktree) + if expected_paths is not None and changed != expected_paths: + raise VersionPreparationError("prepared candidate changed after verification") + receipt = f"{declaration.receipt_directory}/{request.operation_id}.json" + allowed = set(request.allowed_projection_paths) | {receipt} + if receipt not in changed or set(changed) - allowed: + raise VersionPreparationError("version preparation changed a path outside its declared operation") + self._validate_prepared_receipt(worktree / receipt, declaration, request) + digest = self._prepared_candidate_digest(worktree, changed, request) + if digest != request.prepared_operation_digest: + raise VersionPreparationError("prepared operation digest does not bind the candidate content") + return changed, digest + def prepare(self, repository: Path, worktree: Path, request: VersionPreparationRequest) -> dict[str, object]: head = self.git.command(repository, "git", "rev-parse", "HEAD") if head != request.expected_source_revision: @@ -191,15 +275,7 @@ def prepare(self, repository: Path, worktree: Path, request: VersionPreparationR or tuple(request.allowed_projection_paths) != declaration.allowed_projection_paths): raise VersionPreparationError("product helper declaration does not bind the admitted operation") self.helper.apply(worktree, request) - changed = self._changed_paths(worktree) - receipt = f"{declaration.receipt_directory}/{request.operation_id}.json" - allowed = set(request.allowed_projection_paths) | {receipt} - if receipt not in changed or set(changed) - allowed: - raise VersionPreparationError("version preparation changed a path outside its declared operation") - self._validate_prepared_receipt(worktree / receipt, declaration, request) - digest = "sha256:" + hashlib.sha256("\n".join(changed).encode()).hexdigest() - if digest != request.prepared_operation_digest: - raise VersionPreparationError("prepared operation digest does not bind the candidate diff") + changed, digest = self._verify_prepared_candidate(worktree, declaration, request) return {"operation_id": request.operation_id, "changed_paths": changed, "prepared_operation_digest": digest} @staticmethod @@ -250,7 +326,19 @@ def publish_candidate( target = self.git.command(worktree, "git", "rev-parse", f"origin/{base_branch}") if target != request.expected_target_branch_revision: raise VersionPreparationError("target branch revision changed before candidate publication") - self.git.command(worktree, "git", "add", "--", *paths) + # This is deliberately the final read before staging. A preparation + # is not authority to commit bytes that changed after it was checked. + declared = ProductHelperDeclaration.load(worktree) + if (declared.product_id != request.product_id or declared.repository_id != request.repository_id + or declared.policy_revision != request.policy_revision + or declared.allowed_projection_paths != request.allowed_projection_paths): + raise VersionPreparationError("product helper declaration does not bind the admitted operation") + actual_paths, actual_digest = self._verify_prepared_candidate( + worktree, declared, request, expected_paths=paths, + ) + if actual_digest != prepared.get("prepared_operation_digest"): + raise VersionPreparationError("prepared candidate digest changed after verification") + self.git.command(worktree, "git", "add", "--", *actual_paths) self.git.command(worktree, "git", "commit", "-m", f"build: prepare version operation {request.operation_id}") candidate_sha = self.git.command(worktree, "git", "rev-parse", "HEAD") candidate_tree_sha = self.git.command(worktree, "git", "rev-parse", "HEAD^{tree}") diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 8c52f05c..3153003e 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -24,6 +24,31 @@ def request(**overrides: object) -> dict[str, object]: return value +def receipt_for(operation: VersionPreparationRequest) -> dict[str, object]: + """The cross-repository receipt fields EP admits and re-checks.""" + return { + "schema_version": 1, + "operation_id": operation.operation_id, + "product": operation.product_id, + "component_id": operation.component_id, + "repository_id": operation.repository_id, + "policy_revision": operation.policy_revision, + "policy_digest": operation.policy_digest, + "source_event_set": list(operation.source_event_set), + "source_event_policy": operation.source_event_policy, + "release_class": operation.release_class, + "release_rationale": operation.release_rationale, + "expected_source_revision": operation.expected_source_revision, + "expected_target_branch_revision": operation.expected_target_branch_revision, + "expected_version": operation.expected_version, + "requested_change": operation.requested_change, + "determined_target_version": operation.determined_target_version, + "allowed_projection_paths": list(operation.allowed_projection_paths), + "authorization_reference": operation.authorization_reference, + "delivery_mode": operation.delivery_mode, + } + + class VersionPreparationRequestTest(unittest.TestCase): def test_accepts_exact_bounded_contract(self) -> None: parsed = VersionPreparationRequest.parse(request()) @@ -58,6 +83,10 @@ def test_rejects_a_release_classification_that_disagrees_with_the_operation(self with self.assertRaisesRegex(VersionPreparationError, "classification"): VersionPreparationRequest.parse(request(release_class="PATCH")) + def test_rejects_no_bump_from_the_mutating_prepare_adapter(self) -> None: + with self.assertRaisesRegex(VersionPreparationError, "requested change"): + VersionPreparationRequest.parse(request(requested_change="none", release_class="NO_BUMP")) + def test_candidate_branch_is_deterministically_bound_to_operation(self) -> None: parsed = VersionPreparationRequest.parse(request()) self.assertEqual(VersionPreparationDelivery.branch_name(parsed), "ep/version-preparation/operation-0001") @@ -96,9 +125,10 @@ def command(self, _root: Path, *args: str) -> str: return "" class Helper: def apply(self, worktree: Path, operation: VersionPreparationRequest) -> None: + (worktree / "product-version.json").write_text('{"version":"2.4.0"}\n', encoding="utf-8") receipt = worktree / ".version-operations" receipt.mkdir(exist_ok=True) - (receipt / "operation-0001.json").write_text(__import__("json").dumps({"schema_version": 1, "operation_id": operation.operation_id, "product": operation.product_id, "policy_revision": operation.policy_revision, "expected_source_revision": operation.expected_source_revision, "allowed_projection_paths": list(operation.allowed_projection_paths)}), encoding="utf-8") + (receipt / "operation-0001.json").write_text(__import__("json").dumps(receipt_for(operation)), encoding="utf-8") class GitHub: def version_preparation_writer(self) -> dict[str, object]: return {"actor": "ep-writer", "repository_id": "pcvantol/forge", "can_push": True} @@ -106,11 +136,19 @@ def create_or_recover_pull_request(self, branch: str, base: str, title: str, bod return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base, head_sha="a" * 40) def qualification_for_exact_head(self, number: int, sha: str) -> dict[str, object]: return {"pull_request_id": number, "exact_qualified_sha": sha, "conclusion": "PASS", "checks": []} - digest = "sha256:" + hashlib.sha256(b".version-operations/operation-0001.json\nproduct-version.json").hexdigest() - parsed = VersionPreparationRequest.parse(request(prepared_operation_digest=digest)) with tempfile.TemporaryDirectory() as directory: Path(directory, ".version-preparation.json").write_text(__import__("json").dumps({"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"}), encoding="utf-8") - result = VersionPreparationDelivery(Git(), Helper()).execute(Path(directory), Path(directory), parsed, GitHub(), base_branch="main", evidence_root=Path(directory)) + root = Path(directory) + provisional = VersionPreparationRequest.parse(request()) + (root / "product-version.json").write_text('{"version":"2.4.0"}\n', encoding="utf-8") + (root / ".version-operations").mkdir() + (root / ".version-operations" / "operation-0001.json").write_text(__import__("json").dumps(receipt_for(provisional)), encoding="utf-8") + digest = VersionPreparationDelivery(Git(), Helper())._prepared_candidate_digest(root, (".version-operations/operation-0001.json", "product-version.json"), provisional) + (root / "product-version.json").unlink() + (root / ".version-operations" / "operation-0001.json").unlink() + (root / ".version-operations").rmdir() + parsed = VersionPreparationRequest.parse(request(prepared_operation_digest=digest)) + result = VersionPreparationDelivery(Git(), Helper()).execute(root, root, parsed, GitHub(), base_branch="main", evidence_root=root) self.assertEqual(result["pull_request_id"], 9) self.assertTrue(Path(str(result["delivery_evidence_path"])).is_file()) @@ -118,6 +156,8 @@ def test_publish_rejects_a_pull_request_head_that_raced_the_candidate_push(self) class Git: def command(self, _root: Path, *args: str) -> str: if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" + if args[-2:] == ("-z", "HEAD"): return "product-version.json\0" + if args[-1] == "-z": return ".version-operations/operation-0001.json\0" if args[-1] == "HEAD^{tree}": return "c" * 40 if args[-1] == "HEAD": return "a" * 40 return "" @@ -126,10 +166,46 @@ def version_preparation_writer(self) -> dict[str, object]: return {"actor": "ep-writer", "repository_id": "pcvantol/forge", "can_push": True} def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: return PullRequestEvidence(9, "OPEN", True, True, head_branch=branch, base_branch=base, head_sha="b" * 40) - parsed = VersionPreparationRequest.parse(request()) - prepared = {"operation_id": parsed.operation_id, "changed_paths": ("product-version.json",), "prepared_operation_digest": parsed.prepared_operation_digest} - with self.assertRaisesRegex(VersionPreparationError, "head changed"): - VersionPreparationDelivery(Git(), object()).publish_candidate(Path("."), parsed, prepared, GitHub(), base_branch="main") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / ".version-preparation.json").write_text(__import__("json").dumps({"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"}), encoding="utf-8") + provisional = VersionPreparationRequest.parse(request()) + (root / "product-version.json").write_text('{"version":"2.4.0"}\n', encoding="utf-8") + (root / ".version-operations").mkdir() + (root / ".version-operations" / "operation-0001.json").write_text(__import__("json").dumps(receipt_for(provisional)), encoding="utf-8") + digest = VersionPreparationDelivery(Git(), object())._prepared_candidate_digest(root, (".version-operations/operation-0001.json", "product-version.json"), provisional) + parsed = VersionPreparationRequest.parse(request(prepared_operation_digest=digest)) + prepared = {"operation_id": parsed.operation_id, "changed_paths": (".version-operations/operation-0001.json", "product-version.json"), "prepared_operation_digest": digest} + with self.assertRaisesRegex(VersionPreparationError, "head changed"): + VersionPreparationDelivery(Git(), object()).publish_candidate(root, parsed, prepared, GitHub(), base_branch="main") + + def test_publish_rejects_candidate_bytes_changed_after_prepare(self) -> None: + class Git: + def command(self, _root: Path, *args: str) -> str: + if args[-2:] == ("branch", "--show-current"): return "ep/version-preparation/operation-0001" + if args[-2:] == ("-z", "HEAD"): return "product-version.json\0" + if args[-1] == "-z": return ".version-operations/operation-0001.json\0" + if args[1:3] == ("add", "--"): + raise AssertionError("must not stage mutated candidate bytes") + return "" + class GitHub: + def version_preparation_writer(self) -> dict[str, object]: + return {"actor": "ep-writer", "repository_id": "pcvantol/forge", "can_push": True} + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / ".version-preparation.json").write_text(__import__("json").dumps({"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"}), encoding="utf-8") + provisional = VersionPreparationRequest.parse(request()) + (root / "product-version.json").write_text('{"version":"2.4.0"}\n', encoding="utf-8") + (root / ".version-operations").mkdir() + receipt = root / ".version-operations" / "operation-0001.json" + receipt.write_text(__import__("json").dumps(receipt_for(provisional)), encoding="utf-8") + delivery = VersionPreparationDelivery(Git(), object()) + digest = delivery._prepared_candidate_digest(root, (".version-operations/operation-0001.json", "product-version.json"), provisional) + parsed = VersionPreparationRequest.parse(request(prepared_operation_digest=digest)) + prepared = {"operation_id": parsed.operation_id, "changed_paths": (".version-operations/operation-0001.json", "product-version.json"), "prepared_operation_digest": digest} + (root / "product-version.json").write_text('{"version":"9.9.9"}\n', encoding="utf-8") + with self.assertRaisesRegex(VersionPreparationError, "candidate content"): + delivery.publish_candidate(root, parsed, prepared, GitHub(), base_branch="main") def test_publish_rejects_a_writer_without_exact_repository_push_scope(self) -> None: class Git: @@ -194,22 +270,30 @@ def test_prepared_receipt_must_bind_the_admitted_product_operation(self) -> None with self.assertRaisesRegex(VersionPreparationError, "does not bind"): VersionPreparationDelivery._validate_prepared_receipt(receipt, declaration, VersionPreparationRequest.parse(request())) + def test_prepared_receipt_rejects_changed_version_or_classification(self) -> None: + declaration = ProductHelperDeclaration("forge", "pcvantol/forge", "scripts/advance_product_version.py", ".version-operations", ("product-version.json",), "v1") + parsed = VersionPreparationRequest.parse(request()) + with tempfile.TemporaryDirectory() as directory: + receipt = Path(directory, "operation-0001.json") + receipt.write_text(__import__("json").dumps({"schema_version": 1, "operation_id": parsed.operation_id, "product": parsed.product_id, "policy_revision": parsed.policy_revision, "expected_source_revision": parsed.expected_source_revision, "expected_version": "8.0.0", "requested_change": "exact-version", "release_class": "EXACT", "release_rationale": parsed.release_rationale, "determined_target_version": "9.0.0", "allowed_projection_paths": list(parsed.allowed_projection_paths)}), encoding="utf-8") + with self.assertRaisesRegex(VersionPreparationError, "does not bind"): + VersionPreparationDelivery._validate_prepared_receipt(receipt, declaration, parsed) + def test_isolated_git_worktree_includes_an_untracked_receipt_in_candidate_scope(self) -> None: """A real temporary Git checkout proves receipts cannot be omitted by diff.""" from engineering_platform.providers import GitProvider class Helper: - def apply(self, worktree: Path, _request: object) -> None: + def apply(self, worktree: Path, operation: VersionPreparationRequest) -> None: (worktree / "product-version.json").write_text('{"version":"2.4.0"}\n', encoding="utf-8") receipt = worktree / ".version-operations" receipt.mkdir() - (receipt / "operation-0001.json").write_text(__import__("json").dumps({"schema_version": 1, "operation_id": "operation-0001", "product": "forge", "policy_revision": "v1", "expected_source_revision": sha, "allowed_projection_paths": ["product-version.json"]}), encoding="utf-8") + (receipt / "operation-0001.json").write_text(__import__("json").dumps(receipt_for(operation)), encoding="utf-8") def git(root: Path, *args: str) -> None: subprocess.run(("git", *args), cwd=root, check=True, text=True, capture_output=True) declaration = {"contract_version": "1", "product_id": "forge", "repository_id": "pcvantol/forge", "helper_path": "scripts/advance_product_version.py", "receipt_directory": ".version-operations", "allowed_projection_paths": ["product-version.json"], "policy_revision": "v1"} - digest = "sha256:" + hashlib.sha256(b".version-operations/operation-0001.json\nproduct-version.json").hexdigest() with tempfile.TemporaryDirectory() as directory: root, candidate = Path(directory, "source"), Path(directory, "candidate") root.mkdir() @@ -221,8 +305,15 @@ def git(root: Path, *args: str) -> None: git(root, "add", "product-version.json", ".version-preparation.json") git(root, "commit", "-qm", "baseline") sha = subprocess.run(("git", "rev-parse", "HEAD"), cwd=root, check=True, text=True, capture_output=True).stdout.strip() - parsed = VersionPreparationRequest.parse(request(expected_source_revision=sha, prepared_operation_digest=digest)) delivery = VersionPreparationDelivery(GitProvider(), Helper()) + provisional = VersionPreparationRequest.parse(request(expected_source_revision=sha)) + planned = Path(directory, "planned") + planned.mkdir() + (planned / "product-version.json").write_text('{"version":"2.4.0"}\n', encoding="utf-8") + (planned / ".version-operations").mkdir() + (planned / ".version-operations" / "operation-0001.json").write_text(__import__("json").dumps(receipt_for(provisional)), encoding="utf-8") + digest = delivery._prepared_candidate_digest(planned, (".version-operations/operation-0001.json", "product-version.json"), provisional) + parsed = VersionPreparationRequest.parse(request(expected_source_revision=sha, prepared_operation_digest=digest)) prepared = delivery.prepare_in_isolated_worktree(root, candidate, parsed) self.assertEqual(prepared["changed_paths"], (".version-operations/operation-0001.json", "product-version.json")) self.assertEqual(GitProvider().command(candidate, "git", "rev-parse", "HEAD"), sha) From 78b27ca3f9e0aaae03d71db17d0e8ecf9a69afd0 Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 20:08:00 +0200 Subject: [PATCH 21/22] fix: require protected checks for version qualification --- .../execution_repository.py | 28 +++++++++++++++-- tests/engineering/test_execution_host.py | 30 ++++++++++++++++--- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/engineering_platform/execution_repository.py b/src/engineering_platform/execution_repository.py index 03edded8..81ba68e4 100644 --- a/src/engineering_platform/execution_repository.py +++ b/src/engineering_platform/execution_repository.py @@ -277,22 +277,44 @@ def create_or_recover_pull_request(self, branch: str, base: str, title: str, bod return self.pull_request(int(match.group(1))) def qualification_for_exact_head(self, number: int, head_sha: str) -> dict[str, object]: - """Read real check evidence and reject merge-ref/old-head substitution.""" + """Read the base branch's required checks for this exact candidate.""" if not re.fullmatch(r"[0-9a-f]{40}", head_sha): raise RunnerError("Qualification requires an exact candidate SHA.") try: - raw = json.loads(self._github("pr", "view", str(number), "--json", "headRefOid,baseRefOid,statusCheckRollup")) + raw = json.loads(self._github("pr", "view", str(number), "--json", "headRefOid,baseRefOid,baseRefName,statusCheckRollup")) except (RuntimeError, json.JSONDecodeError) as error: raise RunnerError("Version preparation qualification could not be read.") from error if raw.get("headRefOid") != head_sha: raise RunnerError("Qualification evidence belongs to a different pull request head.") + base = raw.get("baseRefName") + if not self.repository or not isinstance(base, str) or not base: + raise RunnerError("Version preparation qualification lacks an exact protected base branch.") + try: + required = json.loads(self._github( + "api", f"repos/{self.repository}/branches/{base}/protection/required_status_checks", + )) + except (RuntimeError, json.JSONDecodeError) as error: + raise RunnerError("Version preparation required-check policy could not be read.") from error + contexts = required.get("contexts") if isinstance(required, dict) else None + protected_checks = required.get("checks") if isinstance(required, dict) else None + names = set() + if isinstance(contexts, list): + names.update(item for item in contexts if isinstance(item, str) and item) + if isinstance(protected_checks, list): + names.update(item.get("context") for item in protected_checks if isinstance(item, dict) and isinstance(item.get("context"), str) and item["context"]) + if not names: + raise RunnerError("Version preparation qualification has no required checks configured.") checks = [item for item in (raw.get("statusCheckRollup") or []) if isinstance(item, dict) and isinstance(item.get("status"), str)] if not checks or any(item.get("status") != "COMPLETED" for item in checks): raise RunnerError("Version preparation qualification is incomplete.") failed = [str(item.get("name") or "unnamed check") for item in checks if item.get("conclusion") not in {"SUCCESS", "NEUTRAL", "SKIPPED"}] if failed: raise RunnerError("Version preparation qualification failed: " + ", ".join(failed)) - return {"pull_request_id": number, "exact_qualified_sha": head_sha, "base_revision": raw.get("baseRefOid"), "checks": checks, "conclusion": "PASS"} + observed = {item.get("name") for item in checks if isinstance(item.get("name"), str) and item["name"]} + missing = sorted(names - observed) + if missing: + raise RunnerError("Version preparation qualification is missing required checks: " + ", ".join(missing)) + return {"pull_request_id": number, "exact_qualified_sha": head_sha, "base_revision": raw.get("baseRefOid"), "required_checks": sorted(names), "checks": checks, "conclusion": "PASS"} def ready(self, number: int) -> None: try: self._github("pr", "ready", str(number)) except RuntimeError as error: diff --git a/tests/engineering/test_execution_host.py b/tests/engineering/test_execution_host.py index 35f79b33..ce83411f 100644 --- a/tests/engineering/test_execution_host.py +++ b/tests/engineering/test_execution_host.py @@ -1266,9 +1266,11 @@ def github(self, *args: str) -> str: if args[:2] == ("pr", "list"): return "[]" if args[:2] == ("pr", "create"): return "https://github.com/pcvantol/forge/pull/17\n" if args[:2] == ("pr", "view"): return json.dumps(evidence) + if args[:2] == ("api", "repos/pcvantol/forge/branches/main/protection/required_status_checks"): + return json.dumps({"contexts": ["validate"], "checks": []}) raise AssertionError(args) - client = GhCliClient(Provider()) + client = GhCliClient(Provider(), "pcvantol/forge") self.assertEqual(client.create_or_recover_pull_request("codex/version-prepare/op", "main", "title", "body").number, 17) qualification = client.qualification_for_exact_head(17, head) self.assertEqual(qualification["exact_qualified_sha"], head) @@ -1283,12 +1285,32 @@ def test_github_client_rejects_incomplete_or_failed_version_preparation_qualific class Provider: def __init__(self, checks: list[dict[str, str]]) -> None: self.checks = checks def github(self, *args: str) -> str: - return json.dumps({"headRefOid": head, "baseRefOid": "f" * 40, "statusCheckRollup": self.checks}) + if args[:2] == ("pr", "view"): + return json.dumps({"headRefOid": head, "baseRefOid": "f" * 40, "baseRefName": "main", "statusCheckRollup": self.checks}) + if args[:2] == ("api", "repos/pcvantol/forge/branches/main/protection/required_status_checks"): + return json.dumps({"contexts": ["lint"], "checks": []}) + raise AssertionError(args) with self.assertRaisesRegex(RunnerError, "incomplete"): - GhCliClient(Provider([])).qualification_for_exact_head(1, head) + GhCliClient(Provider([]), "pcvantol/forge").qualification_for_exact_head(1, head) with self.assertRaisesRegex(RunnerError, "failed: lint"): - GhCliClient(Provider([{"name": "lint", "status": "COMPLETED", "conclusion": "FAILURE"}])).qualification_for_exact_head(1, head) + GhCliClient(Provider([{"name": "lint", "status": "COMPLETED", "conclusion": "FAILURE"}]), "pcvantol/forge").qualification_for_exact_head(1, head) + + def test_github_client_requires_the_protected_branch_check_set(self) -> None: + head = "e" * 40 + class Provider: + def __init__(self, required: dict[str, object]) -> None: self.required = required + def github(self, *args: str) -> str: + if args[:2] == ("pr", "view"): + return json.dumps({"headRefOid": head, "baseRefOid": "f" * 40, "baseRefName": "main", "statusCheckRollup": [{"name": "unit", "status": "COMPLETED", "conclusion": "SUCCESS"}]}) + if args[:2] == ("api", "repos/pcvantol/forge/branches/main/protection/required_status_checks"): + return json.dumps(self.required) + raise AssertionError(args) + + with self.assertRaisesRegex(RunnerError, "no required checks"): + GhCliClient(Provider({"contexts": [], "checks": []}), "pcvantol/forge").qualification_for_exact_head(1, head) + with self.assertRaisesRegex(RunnerError, "missing required checks: integration"): + GhCliClient(Provider({"contexts": ["unit", "integration"], "checks": []}), "pcvantol/forge").qualification_for_exact_head(1, head) @patch("engineering_platform.execution_host.subprocess.run") def test_codex_client_handles_valid_review_and_invoke_results(self, run: object) -> None: From 9bb956000ce89b8608151d5d12bf4915446bb99a Mon Sep 17 00:00:00 2001 From: pcvantol Date: Tue, 8 Sep 2026 20:09:01 +0200 Subject: [PATCH 22/22] test: exercise version preparation with fixture product --- .../test_version_preparation_delivery.py | 45 +++++++++++++++++++ .../.version-preparation.json | 9 ++++ .../product-version.json | 4 ++ .../scripts/apply_version.py | 38 ++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 tests/fixtures/version_preparation_product/.version-preparation.json create mode 100644 tests/fixtures/version_preparation_product/product-version.json create mode 100644 tests/fixtures/version_preparation_product/scripts/apply_version.py diff --git a/tests/engineering/test_version_preparation_delivery.py b/tests/engineering/test_version_preparation_delivery.py index 3153003e..32dd58e6 100644 --- a/tests/engineering/test_version_preparation_delivery.py +++ b/tests/engineering/test_version_preparation_delivery.py @@ -5,6 +5,9 @@ import tempfile import hashlib import subprocess +import shutil +import sys +from dataclasses import asdict from engineering_platform.version_preparation_delivery import ProductHelperDeclaration, VersionPreparationDelivery, VersionPreparationError, VersionPreparationRequest from engineering_platform.execution_models import PullRequestEvidence @@ -332,6 +335,48 @@ def apply(self, worktree: Path, operation: VersionPreparationRequest) -> None: self.assertEqual(GitProvider().command(failed, "git", "rev-parse", "HEAD"), sha) self.assertTrue(GitProvider().command(failed, "git", "status", "--porcelain")) + def test_external_fixture_product_proves_the_declared_receipt_contract(self) -> None: + """Exercise a real product helper process, not an in-memory fake.""" + from engineering_platform.providers import GitProvider + + class ExternalFixtureHelper: + def apply(self, worktree: Path, operation: VersionPreparationRequest) -> None: + payload = asdict(operation) + payload["source_event_set"] = list(operation.source_event_set) + payload["allowed_projection_paths"] = list(operation.allowed_projection_paths) + subprocess.run( + (sys.executable, str(worktree / "scripts" / "apply_version.py"), str(worktree)), + input=__import__("json").dumps(payload), text=True, check=True, capture_output=True, + ) + + def git(root: Path, *args: str) -> None: + subprocess.run(("git", *args), cwd=root, check=True, text=True, capture_output=True) + + fixture = Path(__file__).parents[1] / "fixtures" / "version_preparation_product" + with tempfile.TemporaryDirectory() as directory: + root, candidate, planned = Path(directory, "source"), Path(directory, "candidate"), Path(directory, "planned") + shutil.copytree(fixture, root) + git(root, "init", "-q") + git(root, "config", "user.email", "test@example.invalid") + git(root, "config", "user.name", "Version Preparation Test") + git(root, "add", ".") + git(root, "commit", "-qm", "fixture baseline") + sha = subprocess.run(("git", "rev-parse", "HEAD"), cwd=root, check=True, text=True, capture_output=True).stdout.strip() + raw = request( + product_id="fixture-product", repository_id="pcvantol/fixture-product", component_id="fixture-product", + policy_revision="fixture-policy-v1", expected_source_revision=sha, + ) + provisional = VersionPreparationRequest.parse(raw) + shutil.copytree(fixture, planned) + ExternalFixtureHelper().apply(planned, provisional) + delivery = VersionPreparationDelivery(GitProvider(), ExternalFixtureHelper()) + digest = delivery._prepared_candidate_digest(planned, (".version-operations/operation-0001.json", "product-version.json"), provisional) + prepared = delivery.prepare_in_isolated_worktree( + root, candidate, VersionPreparationRequest.parse({**raw, "prepared_operation_digest": digest}), + ) + self.assertEqual(prepared["prepared_operation_digest"], digest) + self.assertEqual(__import__("json").loads((candidate / "product-version.json").read_text())["version"], "2.4.0") + if __name__ == "__main__": unittest.main() diff --git a/tests/fixtures/version_preparation_product/.version-preparation.json b/tests/fixtures/version_preparation_product/.version-preparation.json new file mode 100644 index 00000000..9273fee8 --- /dev/null +++ b/tests/fixtures/version_preparation_product/.version-preparation.json @@ -0,0 +1,9 @@ +{ + "contract_version": "1", + "product_id": "fixture-product", + "repository_id": "pcvantol/fixture-product", + "helper_path": "scripts/apply_version.py", + "receipt_directory": ".version-operations", + "allowed_projection_paths": ["product-version.json"], + "policy_revision": "fixture-policy-v1" +} diff --git a/tests/fixtures/version_preparation_product/product-version.json b/tests/fixtures/version_preparation_product/product-version.json new file mode 100644 index 00000000..f94f9611 --- /dev/null +++ b/tests/fixtures/version_preparation_product/product-version.json @@ -0,0 +1,4 @@ +{ + "product": "fixture-product", + "version": "2.3.0" +} diff --git a/tests/fixtures/version_preparation_product/scripts/apply_version.py b/tests/fixtures/version_preparation_product/scripts/apply_version.py new file mode 100644 index 00000000..f38b40ee --- /dev/null +++ b/tests/fixtures/version_preparation_product/scripts/apply_version.py @@ -0,0 +1,38 @@ +"""Minimal external product helper fixture for the EP delivery contract.""" +from __future__ import annotations + +import json +from pathlib import Path +import sys + + +root = Path(sys.argv[1]) +request = json.loads(sys.stdin.read()) +(root / "product-version.json").write_text( + json.dumps({"product": request["product_id"], "version": request["determined_target_version"]}) + "\n", + encoding="utf-8", +) +receipt = { + "schema_version": 1, + "operation_id": request["operation_id"], + "product": request["product_id"], + "component_id": request["component_id"], + "repository_id": request["repository_id"], + "policy_revision": request["policy_revision"], + "policy_digest": request["policy_digest"], + "source_event_set": request["source_event_set"], + "source_event_policy": request["source_event_policy"], + "release_class": request["release_class"], + "release_rationale": request["release_rationale"], + "expected_source_revision": request["expected_source_revision"], + "expected_target_branch_revision": request["expected_target_branch_revision"], + "expected_version": request["expected_version"], + "requested_change": request["requested_change"], + "determined_target_version": request["determined_target_version"], + "allowed_projection_paths": request["allowed_projection_paths"], + "authorization_reference": request["authorization_reference"], + "delivery_mode": request["delivery_mode"], +} +directory = root / ".version-operations" +directory.mkdir(exist_ok=True) +(directory / f"{request['operation_id']}.json").write_text(json.dumps(receipt) + "\n", encoding="utf-8")