#!/usr/bin/env python3 """Build and verify the exact stopped-upgrade tools carried by a D3 candidate.""" from __future__ import annotations import argparse import hashlib import json import os import platform import re import shutil import subprocess import sys import tempfile from pathlib import Path from typing import Any REPOSITORY_ROOT = Path(__file__).resolve().parents[1] RUNTIME_HELPERS = REPOSITORY_ROOT / "deployments" / "offline" / "runtime" if str(RUNTIME_HELPERS) not in sys.path: sys.path.insert(0, str(RUNTIME_HELPERS)) import common # noqa: E402 LEGACY_SCHEMA = "nexuscrm.serviceification-deployment-tools/v1" SCHEMA = "nexuscrm.serviceification-deployment-tools/v2" COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") SHA256_RE = re.compile(r"^sha256:[0-9a-f]{64}$") CORE_APP_SOURCE_PATH = Path("backend/distributions/appsource/source.go") CORE_APP_SOURCE_NAME = "OfficialAppPacksSourceRevision" CORE_APP_SOURCE_VALUE_RE = re.compile(r'^"([0-9a-f]{40})"$') BUILDER_IMAGE = ( "docker.io/library/golang:1.25.0-bookworm@sha256:" "81dc45d05a7444ead8c92a389621fafabc8e40f8fd1a19d7e5df14e61e98bc1a" ) FILES = { "setup": "nexuscrm-setup", "pack_runtime_metadata": "nexuscrm-pack-runtime-metadata", } REQUIRED_SETUP_COMMANDS = ( "apply-offline-candidate", "ensure-l0-schema-ddl", ) class ToolError(RuntimeError): pass def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as source: for chunk in iter(lambda: source.read(1024 * 1024), b""): digest.update(chunk) return "sha256:" + digest.hexdigest() def canonical_json(payload: Any) -> str: return json.dumps(payload, indent=2, sort_keys=True) + "\n" def require_exact_checkout(root: Path, revision: str, label: str) -> Path: if not COMMIT_RE.fullmatch(revision): raise ToolError(f"{label} revision must be one exact commit") if root.is_symlink() or not root.is_dir(): raise ToolError(f"{label} checkout is unavailable") resolved = root.resolve() top = Path( subprocess.check_output( ["git", "-C", str(resolved), "rev-parse", "--show-toplevel"], text=True ).strip() ).resolve() if top != resolved: raise ToolError(f"{label} root is not the exact checkout root") head = subprocess.check_output( ["git", "-C", str(resolved), "rev-parse", "HEAD"], text=True ).strip() if head != revision: raise ToolError(f"{label} checkout revision mismatch") status = subprocess.check_output( ["git", "-C", str(resolved), "status", "--porcelain=v1", "--untracked-files=all"], text=True, ).strip() if status: raise ToolError(f"{label} checkout is not pristine") return resolved def go_tokens(source: str) -> list[tuple[str, str]]: tokens: list[tuple[str, str]] = [] index = 0 while index < len(source): char = source[index] if char.isspace(): index += 1 continue if source.startswith("//", index): newline = source.find("\n", index + 2) index = len(source) if newline < 0 else newline + 1 continue if source.startswith("/*", index): end = source.find("*/", index + 2) if end < 0: raise ToolError("Core official AppPacks source has an unterminated comment") index = end + 2 continue if char.isalpha() or char == "_": end = index + 1 while end < len(source) and (source[end].isalnum() or source[end] == "_"): end += 1 tokens.append(("identifier", source[index:end])) index = end continue if char in {'"', "'", "`"}: delimiter = char end = index + 1 while end < len(source): if delimiter != "`" and source[end] == "\\": end += 2 continue if source[end] == delimiter: end += 1 break end += 1 else: raise ToolError("Core official AppPacks source has an unterminated literal") kind = "string" if delimiter == '"' else "literal" tokens.append((kind, source[index:end])) index = end continue tokens.append(("symbol", char)) index += 1 return tokens def core_official_app_packs_revision(core_root: Path) -> str: try: core_root = core_root.resolve(strict=True) except OSError as exc: raise ToolError( "Core official AppPacks source declaration is unavailable" ) from exc source_path = core_root / CORE_APP_SOURCE_PATH try: resolved_source = source_path.resolve(strict=True) except OSError as exc: raise ToolError( "Core official AppPacks source declaration is unavailable" ) from exc if ( source_path.is_symlink() or not source_path.is_file() or resolved_source != source_path or not resolved_source.is_relative_to(core_root) ): raise ToolError("Core official AppPacks source declaration is unavailable") tokens = go_tokens(source_path.read_text(encoding="utf-8")) declarations: list[str] = [] name_indexes = [ index for index, (kind, value) in enumerate(tokens) if kind == "identifier" and value == CORE_APP_SOURCE_NAME ] for index in name_indexes: if ( index >= 1 and index + 2 < len(tokens) and tokens[index - 1] == ("identifier", "const") and tokens[index + 1] == ("symbol", "=") and tokens[index + 2][0] == "string" and (match := CORE_APP_SOURCE_VALUE_RE.fullmatch(tokens[index + 2][1])) is not None ): declarations.append(match.group(1)) if len(name_indexes) != 1 or len(declarations) != 1: raise ToolError( "Core must declare exactly one immutable OfficialAppPacksSourceRevision" ) return declarations[0] def inspect_core_app_source(*, core_root: Path, core_commit: str) -> str: core_root = require_exact_checkout(core_root, core_commit, "Core") return core_official_app_packs_revision(core_root) def materialize_writable_checkout(source: Path, revision: str, target: Path) -> Path: """Clone one already-verified source tree for build-time generated files.""" if target.exists() or target.is_symlink(): raise ToolError("writable Core build checkout already exists") subprocess.run( ["git", "clone", "--quiet", "--no-hardlinks", str(source), str(target)], check=True, ) subprocess.run( ["git", "-C", str(target), "checkout", "--quiet", "--detach", revision], check=True, ) return require_exact_checkout(target, revision, "writable Core build") def setup_vcs_settings(path: Path) -> dict[str, str]: completed = subprocess.run( ["go", "version", "-m", str(path)], check=True, capture_output=True, text=True, ) settings: dict[str, str] = {} for raw_line in completed.stdout.splitlines(): line = raw_line.strip() if not line.startswith("build\t") or "=" not in line: continue key, value = line.removeprefix("build\t").split("=", 1) settings[key] = value return settings def require_setup_commands(path: Path) -> list[str]: completed = subprocess.run( [str(path)], check=False, capture_output=True, text=True, ) usage = completed.stdout + "\n" + completed.stderr missing = [ command for command in REQUIRED_SETUP_COMMANDS if f"\n {command}" not in usage ] if missing: raise ToolError( "setup binary is incompatible with the D3 deployer; missing commands: " + ", ".join(missing) ) return list(REQUIRED_SETUP_COMMANDS) def build_tools( *, core_root: Path, controlplane_root: Path, app_source_root: Path, app_release_root: Path, core_commit: str, controlplane_commit: str, app_source_commit: str, app_release_commit: str, builder_image: str, output: Path, ) -> dict[str, Any]: if sys.platform != "linux" or platform.machine().lower() not in {"x86_64", "amd64"}: raise ToolError("D3 deployment tools must be built on linux/amd64") core_root = require_exact_checkout(core_root, core_commit, "Core") controlplane_root = require_exact_checkout( controlplane_root, controlplane_commit, "Controlplane" ) app_source_root = require_exact_checkout( app_source_root, app_source_commit, "explicit application source" ) app_release_root = require_exact_checkout( app_release_root, app_release_commit, "AppRelease" ) token = os.environ.get("GH_TOKEN", "").strip() if not token: raise ToolError("GH_TOKEN is required to build private exact dependencies") if builder_image != BUILDER_IMAGE: raise ToolError("deployment-tools builder must be the pinned Go 1.25.0 image") output = output.expanduser().resolve() if output.is_symlink() or output.exists(): raise ToolError("deployment-tools output must not already exist") output.parent.mkdir(parents=True, exist_ok=True) staging = Path(tempfile.mkdtemp(prefix=".deployment-tools-", dir=output.parent)) try: setup = staging / FILES["setup"] metadata = staging / FILES["pack_runtime_metadata"] build_env = { "GH_TOKEN": token, "UPSTREAM_REF": core_commit, "BACKEND_GO_BUILD_TAGS": "crm", "NEXUSCRM_OFFICIAL_APP_PACKS_ROOT": str(app_source_root), "NEXUSCRM_SETUP_BIN": str(setup), "NEXUSCRM_SETUP_CONTROLPLANE_ROOT": str(controlplane_root), "NEXUSCRM_SETUP_CONTROLPLANE_REF": controlplane_commit, } if common.prepare_backend_setup_binary( build_env, core_root, app_source_root ) != setup: raise ToolError("official setup builder returned the wrong output") writable_core = materialize_writable_checkout( core_root, core_commit, staging / "metadata-core" ) if common.prepare_pack_runtime_metadata_binary( build_env, writable_core, app_source_root, metadata ) != metadata: raise ToolError("pack metadata builder returned the wrong output") shutil.rmtree(writable_core) for binary in (setup, metadata): if binary.is_symlink() or not binary.is_file(): raise ToolError(f"deployment tool was not built: {binary.name}") binary.chmod(0o555) setup_commands = require_setup_commands(setup) vcs = setup_vcs_settings(setup) if vcs.get("vcs.revision") != core_commit or vcs.get("vcs.modified") != "false": raise ToolError("setup binary VCS receipt does not match exact clean Core source") manifest = { "schema": SCHEMA, "platform": "linux/amd64", "builderImage": builder_image, "coreRevision": core_commit, "controlplaneRevision": controlplane_commit, "appSourceRevision": app_source_commit, "appReleaseRevision": app_release_commit, "setupCommands": setup_commands, "files": { key: { "path": name, "sha256": sha256_file(staging / name), "size": (staging / name).stat().st_size, } for key, name in FILES.items() }, } manifest_path = staging / "manifest.json" manifest_path.write_text(canonical_json(manifest), encoding="utf-8") manifest_path.chmod(0o444) os.replace(staging, output) return manifest except BaseException: shutil.rmtree(staging, ignore_errors=True) raise def verify_tools( *, root: Path, resolved_bom: Path, require_executable: bool = True, ) -> dict[str, Any]: if root.is_symlink() or not root.is_dir(): raise ToolError("deployment-tools root is unavailable") root = root.resolve() manifest_path = root / "manifest.json" if manifest_path.is_symlink() or not manifest_path.is_file(): raise ToolError("deployment-tools manifest is unavailable") manifest = json.loads(manifest_path.read_text(encoding="utf-8")) # Every revision below is provenance for the deployment-tool bytes, not a # compatibility declaration for an independently published artifact. In # particular, nexuscrm-setup and Catalog reconcile are different executables # with different lifecycles even though they are built from the same L0 # repository. Binding their Git commits would prevent a stopped deployment # from validating a setup-only repair before publishing a new Catalog image. bom = json.loads(resolved_bom.read_text(encoding="utf-8")) # v1 bundles built before executable L1 source was recorded used the # AppRelease publication revision for both facts. Keep those run-owned # bundles readable for recovery, while every new build records both facts. app_source_revision = manifest.get( "appSourceRevision", manifest.get("appReleaseRevision") ) schema = manifest.get("schema") controlplane_revision = manifest.get("controlplaneRevision") if ( schema not in {LEGACY_SCHEMA, SCHEMA} or manifest.get("platform") != "linux/amd64" or manifest.get("builderImage") != BUILDER_IMAGE or not COMMIT_RE.fullmatch(str(manifest.get("coreRevision", ""))) or not COMMIT_RE.fullmatch(str(app_source_revision or "")) or not COMMIT_RE.fullmatch(str(manifest.get("appReleaseRevision", ""))) or manifest.get("setupCommands") != list(REQUIRED_SETUP_COMMANDS) ): raise ToolError("deployment-tools capability manifest is invalid") if schema == SCHEMA and not COMMIT_RE.fullmatch(str(controlplane_revision or "")): raise ToolError("deployment-tools exact Controlplane revision is missing") expected_entries = {"manifest.json", *FILES.values()} if {entry.name for entry in root.iterdir()} != expected_entries: raise ToolError("deployment-tools bundle contains an unexpected file set") file_receipts = manifest.get("files") if not isinstance(file_receipts, dict) or set(file_receipts) != set(FILES): raise ToolError("deployment-tools file receipts are incomplete") for key, name in FILES.items(): receipt = file_receipts[key] path = root / name if ( path.is_symlink() or not path.is_file() or (require_executable and not os.access(path, os.X_OK)) ): raise ToolError(f"deployment tool is unavailable or not executable: {name}") if ( not isinstance(receipt, dict) or receipt.get("path") != name or not SHA256_RE.fullmatch(str(receipt.get("sha256", ""))) or receipt.get("sha256") != sha256_file(path) or receipt.get("size") != path.stat().st_size ): raise ToolError(f"deployment tool receipt mismatch: {name}") return manifest def restore_tools(*, root: Path, resolved_bom: Path) -> dict[str, Any]: """Normalize modes lost by GitHub artifact transport after byte verification.""" manifest = verify_tools( root=root, resolved_bom=resolved_bom, require_executable=False, ) for name in FILES.values(): (root / name).chmod(0o555) (root / "manifest.json").chmod(0o444) verified = verify_tools(root=root, resolved_bom=resolved_bom) if verified != manifest: raise ToolError("deployment-tools manifest changed during mode normalization") return verified def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command", required=True) build = subparsers.add_parser("build") build.add_argument("--core-root", required=True, type=Path) build.add_argument("--controlplane-root", required=True, type=Path) build.add_argument("--app-source-root", required=True, type=Path) build.add_argument("--app-release-root", required=True, type=Path) build.add_argument("--core-commit", required=True) build.add_argument("--controlplane-commit", required=True) build.add_argument("--app-source-commit", required=True) build.add_argument("--app-release-commit", required=True) build.add_argument("--builder-image", required=True) build.add_argument("--output", required=True, type=Path) verify = subparsers.add_parser("verify") verify.add_argument("--root", required=True, type=Path) verify.add_argument("--resolved-bom", required=True, type=Path) restore = subparsers.add_parser("restore") restore.add_argument("--root", required=True, type=Path) restore.add_argument("--resolved-bom", required=True, type=Path) inspect = subparsers.add_parser("inspect-core-app-source") inspect.add_argument("--core-root", required=True, type=Path) inspect.add_argument("--core-commit", required=True) return parser.parse_args() def main() -> int: args = parse_args() try: if args.command == "build": result = build_tools( core_root=args.core_root, controlplane_root=args.controlplane_root, app_source_root=args.app_source_root, app_release_root=args.app_release_root, core_commit=args.core_commit, controlplane_commit=args.controlplane_commit, app_source_commit=args.app_source_commit, app_release_commit=args.app_release_commit, builder_image=args.builder_image, output=args.output, ) elif args.command == "verify": result = verify_tools(root=args.root, resolved_bom=args.resolved_bom) elif args.command == "restore": result = restore_tools(root=args.root, resolved_bom=args.resolved_bom) else: result = {"commit": inspect_core_app_source( core_root=args.core_root, core_commit=args.core_commit, )} except (ToolError, common.DeployError, OSError, subprocess.SubprocessError, json.JSONDecodeError, KeyError) as exc: print(f"serviceification deployment tools failed: {exc}", file=sys.stderr) return 1 print(canonical_json(result), end="") return 0 if __name__ == "__main__": raise SystemExit(main())