1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages
DocForge2/tools/check_release_identity.py

308 lines
12 KiB
Python
Raw Normal View History

"""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())