From 7b79d65daed077d4a4ca9c925eb42284081804c3 Mon Sep 17 00:00:00 2001 From: Andraxion Date: Wed, 29 Jul 2026 15:49:46 -0400 Subject: [PATCH] Add reproducible release artifact proof --- pyproject.toml | 6 +- tests/test_release_identity.py | 30 ++++ tools/check_release_identity.py | 307 ++++++++++++++++++++++++++++++++ 3 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 tests/test_release_identity.py create mode 100644 tools/check_release_identity.py diff --git a/pyproject.toml b/pyproject.toml index 64c98cd..1d63e5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,13 +8,17 @@ dynamic = ["version"] description = "Project-scoped documentation indexing and context service" readme = "README.md" requires-python = ">=3.12" -license = { text = "MIT" } +license = "MIT" authors = [{ name = "Worldforge contributors" }] dependencies = [ "markdown-it-py>=4.2,<5", "mcp>=1.28,<2", ] +[project.urls] +Repository = "https://repo.andraxion.net/administrator/DocForge2" +Issues = "https://repo.andraxion.net/administrator/DocForge2/issues" + [project.optional-dependencies] javascript = [ "tree-sitter>=0.25,<0.26", diff --git a/tests/test_release_identity.py b/tests/test_release_identity.py new file mode 100644 index 0000000..dc3f9f6 --- /dev/null +++ b/tests/test_release_identity.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import unittest + +from tools.check_release_identity import build_release_identity_evidence + + +class ReleaseIdentityTests(unittest.TestCase): + def test_smoke_identity_is_consistent_before_tagging(self) -> None: + evidence = build_release_identity_evidence( + mode="smoke", + require_clean=False, + tag_state="absent", + ) + + self.assertEqual(1, evidence["schema_version"]) + self.assertEqual("1.4.0", evidence["version"]) + self.assertEqual( + { + "docforge.cli": "docforge 1.4.0", + "docforge.mcp_server": "docforge-mcp 1.4.0", + "docforge.reference_mcp": "python -m docforge.reference_mcp 1.4.0", + "docforge.viewer_manager": "docforge-viewer-manager 1.4.0", + }, + evidence["surfaces"], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/check_release_identity.py b/tools/check_release_identity.py new file mode 100644 index 0000000..70b8599 --- /dev/null +++ b/tools/check_release_identity.py @@ -0,0 +1,307 @@ +"""Validate one authoritative version and reproducible DocForge release artifacts.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import subprocess +import sys +import tarfile +import tempfile +import tomllib +import zipfile +from email.message import Message +from email.parser import BytesParser +from email.policy import default +from pathlib import Path +from typing import cast + +ROOT = Path(__file__).resolve().parents[1] +VERSION_PATH = ROOT / "src/docforge/_version.py" +PYPROJECT_PATH = ROOT / "pyproject.toml" +VERSION_PATTERN = re.compile(r'^__version__ = "([0-9]+\.[0-9]+\.[0-9]+)"$', re.MULTILINE) +EXPECTED_ENTRY_POINTS = { + "docforge": "docforge.cli:main", + "docforge-mcp": "docforge.mcp_server:main", + "docforge-viewer-manager": "docforge.viewer_manager:main", +} +VERSION_SURFACES = { + "docforge.cli": "docforge {version}\n", + "docforge.mcp_server": "docforge-mcp {version}\n", + "docforge.reference_mcp": "python -m docforge.reference_mcp {version}\n", + "docforge.viewer_manager": "docforge-viewer-manager {version}\n", +} + + +class ReleaseIdentityError(RuntimeError): + """Release identity or artifact evidence is inconsistent.""" + + +def _run(arguments: list[str], *, environment: dict[str, str] | None = None) -> str: + completed = subprocess.run( + arguments, + cwd=ROOT, + env=environment, + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0: + raise ReleaseIdentityError( + f"Command failed ({' '.join(arguments)}): {completed.stderr.strip()}" + ) + return completed.stdout + + +def _source_version() -> str: + match = VERSION_PATTERN.search(VERSION_PATH.read_text(encoding="utf-8")) + if match is None: + raise ReleaseIdentityError("The authoritative version file is not a fixed semantic version") + return match.group(1) + + +def _configuration(version: str) -> dict[str, object]: + document = tomllib.loads(PYPROJECT_PATH.read_text(encoding="utf-8")) + project = cast(dict[str, object], document.get("project")) + if project.get("name") != "docforge": + raise ReleaseIdentityError("Distribution name is not docforge") + if project.get("dynamic") != ["version"] or "version" in project: + raise ReleaseIdentityError("Project version is not exclusively dynamic") + tool = cast(dict[str, object], document.get("tool")) + hatch = cast(dict[str, object], tool.get("hatch")) + hatch_version = cast(dict[str, object], hatch.get("version")) + if hatch_version.get("path") != "src/docforge/_version.py": + raise ReleaseIdentityError("Hatch does not use the authoritative version file") + if project.get("license") != "MIT": + raise ReleaseIdentityError("Project metadata does not use the MIT SPDX expression") + scripts = cast(dict[str, object], project.get("scripts")) + if scripts != EXPECTED_ENTRY_POINTS: + raise ReleaseIdentityError("Distribution entry points changed") + urls = cast(dict[str, object], project.get("urls")) + repository = urls.get("Repository") + if repository != "https://repo.andraxion.net/administrator/DocForge2": + raise ReleaseIdentityError("Release repository URL is not the public successor") + if version != _source_version(): + raise ReleaseIdentityError("Release version changed during configuration inspection") + return project + + +def _surface_versions(version: str) -> dict[str, str]: + evidence: dict[str, str] = {} + for module_name, expected_template in VERSION_SURFACES.items(): + output = _run([sys.executable, "-m", module_name, "--version"]) + expected = expected_template.format(version=version) + if output != expected: + raise ReleaseIdentityError( + f"{module_name} reported {output.strip()!r}, expected {expected.strip()!r}" + ) + evidence[module_name] = output.strip() + return evidence + + +def _git_evidence(tag_state: str, version: str, *, require_clean: bool) -> dict[str, object]: + commit = _run(["git", "rev-parse", "HEAD"]).strip() + status = _run(["git", "status", "--porcelain"]) + if require_clean and status: + raise ReleaseIdentityError("Release artifact proof requires a clean working tree") + tag_name = f"v{version}" + tag_result = subprocess.run( + ["git", "rev-parse", "-q", "--verify", f"refs/tags/{tag_name}^{{commit}}"], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + tag_commit = tag_result.stdout.strip() if tag_result.returncode == 0 else None + if tag_state == "absent" and tag_commit is not None: + raise ReleaseIdentityError(f"{tag_name} already exists") + if tag_state == "head" and tag_commit != commit: + raise ReleaseIdentityError(f"{tag_name} does not identify the release commit") + return { + "commit": commit, + "clean": not bool(status), + "tag": tag_name, + "tag_commit": tag_commit, + } + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _metadata(wheel: Path, version: str) -> dict[str, object]: + with zipfile.ZipFile(wheel) as archive: + members = set(archive.namelist()) + prefix = f"docforge-{version}.dist-info" + metadata_name = f"{prefix}/METADATA" + entry_points_name = f"{prefix}/entry_points.txt" + if metadata_name not in members or entry_points_name not in members: + raise ReleaseIdentityError("Wheel metadata or entry points are missing") + if f"{prefix}/licenses/LICENSE" not in members: + raise ReleaseIdentityError("Wheel does not contain the MIT license") + if "docforge/_version.py" not in members: + raise ReleaseIdentityError("Wheel does not contain the authoritative version module") + message = cast(Message, BytesParser(policy=default).parsebytes(archive.read(metadata_name))) + entry_points = archive.read(entry_points_name).decode("utf-8") + if message["Name"] != "docforge" or message["Version"] != version: + raise ReleaseIdentityError("Wheel name or version metadata is inconsistent") + if message["License-Expression"] != "MIT": + raise ReleaseIdentityError("Wheel does not publish the MIT SPDX expression") + for name, target in EXPECTED_ENTRY_POINTS.items(): + if f"{name} = {target}" not in entry_points: + raise ReleaseIdentityError(f"Wheel entry point {name} is inconsistent") + project_urls = sorted(message.get_all("Project-URL", [])) + if "Repository, https://repo.andraxion.net/administrator/DocForge2" not in project_urls: + raise ReleaseIdentityError("Wheel repository metadata is inconsistent") + return { + "name": message["Name"], + "version": message["Version"], + "license_expression": message["License-Expression"], + "project_urls": project_urls, + "license_member": f"{prefix}/licenses/LICENSE", + } + + +def _sdist(sdist: Path, version: str) -> dict[str, object]: + prefix = f"docforge-{version}" + with tarfile.open(sdist, mode="r:gz") as archive: + members = {member.name for member in archive.getmembers()} + license_name = f"{prefix}/LICENSE" + version_name = f"{prefix}/src/docforge/_version.py" + if license_name not in members or version_name not in members: + raise ReleaseIdentityError("Source distribution omits release identity or license files") + return { + "license_member": license_name, + "version_member": version_name, + "member_count": len(members), + } + + +def _artifact_files(directory: Path, version: str) -> tuple[Path, Path]: + wheel = directory / f"docforge-{version}-py3-none-any.whl" + sdist = directory / f"docforge-{version}.tar.gz" + if not wheel.is_file() or not sdist.is_file(): + raise ReleaseIdentityError( + "Build did not create the expected wheel and source distribution" + ) + allowed = {wheel.name, sdist.name, ".gitignore"} + if {path.name for path in directory.iterdir()} != allowed: + raise ReleaseIdentityError("Build output contains an unexpected artifact") + return wheel, sdist + + +def _build_evidence(version: str) -> dict[str, object]: + epoch = _run(["git", "show", "-s", "--format=%ct", "HEAD"]).strip() + if not epoch.isdigit(): + raise ReleaseIdentityError("Release commit timestamp is invalid") + environment = dict(os.environ) + environment["SOURCE_DATE_EPOCH"] = epoch + with tempfile.TemporaryDirectory(prefix="docforge-release-proof-") as directory_name: + root = Path(directory_name) + first = root / "first" + second = root / "second" + first.mkdir() + second.mkdir() + _run(["uv", "build", "--out-dir", str(first)], environment=environment) + _run(["uv", "build", "--out-dir", str(second)], environment=environment) + first_wheel, first_sdist = _artifact_files(first, version) + second_wheel, second_sdist = _artifact_files(second, version) + hashes = { + "wheel": _sha256(first_wheel), + "sdist": _sha256(first_sdist), + } + repeated = { + "wheel": _sha256(second_wheel), + "sdist": _sha256(second_sdist), + } + if hashes != repeated: + raise ReleaseIdentityError("Repeated release builds are not byte-for-byte reproducible") + wheel_metadata = _metadata(first_wheel, version) + sdist_metadata = _sdist(first_sdist, version) + sizes = { + "wheel": first_wheel.stat().st_size, + "sdist": first_sdist.stat().st_size, + } + return { + "source_date_epoch": int(epoch), + "reproducible": True, + "sha256": hashes, + "bytes": sizes, + "wheel": wheel_metadata, + "sdist": sdist_metadata, + } + + +def build_release_identity_evidence( + *, + mode: str, + require_clean: bool, + tag_state: str, +) -> dict[str, object]: + """Return validated, deterministic release identity evidence.""" + + version = _source_version() + _configuration(version) + evidence: dict[str, object] = { + "schema_version": 1, + "version": version, + "git": _git_evidence(tag_state, version, require_clean=require_clean), + "surfaces": _surface_versions(version), + } + if mode == "full": + evidence["artifacts"] = _build_evidence(version) + return evidence + + +def _publish_output(path: Path, payload: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, temporary_name = tempfile.mkstemp(prefix=".release-identity-", dir=path.parent) + temporary = Path(temporary_name) + try: + with os.fdopen(descriptor, "wb") as handle: + handle.write(payload) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, path) + directory_fd = os.open(path.parent, os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + except Exception: + temporary.unlink(missing_ok=True) + raise + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--mode", choices=("smoke", "full"), default="smoke") + parser.add_argument("--output", type=Path) + parser.add_argument("--require-clean", action="store_true") + parser.add_argument("--tag-state", choices=("ignore", "absent", "head"), default="ignore") + arguments = parser.parse_args() + try: + evidence = build_release_identity_evidence( + mode=arguments.mode, + require_clean=arguments.require_clean, + tag_state=arguments.tag_state, + ) + except (OSError, ReleaseIdentityError, subprocess.SubprocessError) as error: + print(str(error), file=sys.stderr) + return 2 + payload = json.dumps(evidence, sort_keys=True, separators=(",", ":")).encode("utf-8") + b"\n" + if arguments.output is not None: + _publish_output(arguments.output, payload) + print(payload.decode("utf-8"), end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())