#!/usr/bin/env python3 """Environment-neutral deployment main-flow sequencing.""" from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Any, Protocol @dataclass(frozen=True) class CandidateRuntime: metadata: dict[str, object] bundle_layout: dict[str, object] downstream_build_root: Path downstream_source_root: Path upstream_root: Path nexuscrm_worktree: Path app_packs_root: Path final_pack_dir: Path connect_artifacts: dict[str, object] frontend_app_packs_root: Path | None frontend_app_packs_ref: str def bind_candidate_runtime( candidate: dict[str, object], envv: dict[str, str], ) -> CandidateRuntime: metadata = candidate.get("metadata") layout = candidate.get("bundle_layout") refs = candidate.get("refs") if not isinstance(metadata, dict) or not isinstance(layout, dict): raise ValueError("invalid deployment Candidate content") if not isinstance(refs, dict): raise ValueError("invalid deployment Candidate refs") extras = layout.get("extras") manifest = layout.get("manifest") source = manifest.get("source") if isinstance(manifest, dict) else None files = manifest.get("files") if isinstance(manifest, dict) else None if ( not isinstance(extras, dict) or not isinstance(source, dict) or not isinstance(files, list) ): raise ValueError("invalid deployment bundle content") required_extras = ( "bcrypt_hash_binary", "pack_runtime_metadata_binary", "nexuscrm_runtime", ) missing = [name for name in required_extras if name not in extras] if missing: raise ValueError( "deployment bundle is missing runtime extras: " + ", ".join(missing) ) sha_by_path = { str(item.get("path", "")): str(item.get("sha256", "")) for item in files if isinstance(item, dict) } runtime_cli_key = ( "runtime_connect_cli" if "runtime_connect_cli" in extras else "" ) runtime_plugin_key = ( "runtime_connect_plugin" if "runtime_connect_plugin" in extras else "" ) connect_cli = ( str(extras[runtime_cli_key]) if runtime_cli_key else str(layout["connect_cli"]) ) connect_plugin = ( str(extras[runtime_plugin_key]) if runtime_plugin_key else str(layout["connect_plugin"]) ) connect_cli_manifest_path = ( f"extras/{runtime_cli_key}" if runtime_cli_key else "connect/cli.tgz" ) connect_plugin_manifest_path = ( f"extras/{runtime_plugin_key}" if runtime_plugin_key else "connect/plugin.dxt" ) frontend_app_packs_root = ( Path(str(extras["frontend_app_packs"])).resolve() if str(extras.get("frontend_app_packs", "")).strip() else None ) envv.update( { "DOWNSTREAM_REF": str(refs["downstream"]), "UPSTREAM_REF": str(refs["upstream"]), "OFFICIAL_APP_PACKS_REF": str(refs["official_app_packs"]), "NEXUSCRM_OFFICIAL_APP_PACKS_ROOT": str(layout["official_app_packs_root"]), "NEXUSCRM_RECOVERY_RELEASE_PACK_ROOT": str( layout["downstream_source_root"] ), "NEXUSCRM_SETUP_BIN": str(layout["setup_binary"]), "NEXUSCRM_BACKEND_BIN": str(layout["backend_binary"]), "NEXUSCRM_BCRYPT_HASH_BIN": str(extras["bcrypt_hash_binary"]), "NEXUSCRM_PACK_RUNTIME_METADATA_BIN": str( extras["pack_runtime_metadata_binary"] ), } ) return CandidateRuntime( metadata={ **metadata, "frontend_dist": str(layout["frontend_dist"]), "final_pack_hash": str(source.get("final_pack_hash", "")), "patch_dir": str(extras.get("patch_bundle", "")), }, bundle_layout=layout, downstream_build_root=Path(str(layout["downstream_root"])).resolve(), downstream_source_root=Path(str(layout["downstream_source_root"])).resolve(), upstream_root=Path(str(layout["upstream_root"])).resolve(), nexuscrm_worktree=Path(str(extras["nexuscrm_runtime"])).resolve(), app_packs_root=Path(str(layout["official_app_packs_root"])).resolve(), final_pack_dir=Path(str(layout["final_pack_dir"])).resolve(), connect_artifacts={ "version": str(layout.get("connect_version", "")), "cli": { "path": connect_cli, "sha256": sha_by_path.get(connect_cli_manifest_path, ""), }, "plugin": { "path": connect_plugin, "sha256": sha_by_path.get(connect_plugin_manifest_path, ""), }, }, frontend_app_packs_root=frontend_app_packs_root, frontend_app_packs_ref=str(refs["official_app_packs"]), ) def preflight_candidate_runtime_files(runtime: CandidateRuntime) -> None: if not runtime.downstream_source_root.is_dir(): raise ValueError( "deployment Candidate downstream source authority is missing: " f"{runtime.downstream_source_root}" ) framework_dir = runtime.downstream_build_root / "deployments" / "offline" required_files = ( framework_dir / "systemd" / "nexuscrm-backend.service.template", framework_dir / "systemd" / "nexuscrm-hubspot-options-adapter.service.template", framework_dir / "runtime" / "hubspot_workflow_options_adapter.py", runtime.downstream_build_root / "scripts" / "apply_deploy_seed.py", ) for path in required_files: if not path.is_file(): raise ValueError(f"deployment Candidate runtime file is missing: {path}") class StartupOperations(Protocol): def rebind_runtime_packs( self, envv: dict[str, str], downstream_build_root: Path, ) -> None: ... def sync_runtime_pack_metadata( self, envv: dict[str, str], downstream_build_root: Path, nexuscrm_worktree: Path, **kwargs: object, ) -> None: ... def sync_runtime_pack_dependency_declarations( self, envv: dict[str, str], downstream_build_root: Path, ) -> None: ... class CandidateLifecycle(Protocol): def prepare_for_setup(self, candidate: dict[str, object]) -> None: ... def converge_candidate_state(self, candidate: dict[str, object]) -> None: ... def start_candidate(self, candidate: dict[str, object]) -> None: ... def technical_acceptance(self, candidate: dict[str, object]) -> None: ... def commit(self, candidate: dict[str, object]) -> None: ... def environment_post_hooks(self, candidate: dict[str, object]) -> None: ... def write_deploy_result(self, candidate: dict[str, object]) -> dict[str, Any]: ... def prepare_candidate_startup_state( operations: StartupOperations, envv: dict[str, str], downstream_build_root: Path, nexuscrm_worktree: Path, ) -> None: operations.rebind_runtime_packs(envv, downstream_build_root) operations.sync_runtime_pack_metadata( envv, downstream_build_root, nexuscrm_worktree, include_runtime_declarations=False, mark_applied=False, ) operations.sync_runtime_pack_dependency_declarations( envv, downstream_build_root, ) def run_deployment_main_flow( candidate: dict[str, object], lifecycle: CandidateLifecycle, ) -> dict[str, Any]: lifecycle.prepare_for_setup(candidate) lifecycle.converge_candidate_state(candidate) lifecycle.start_candidate(candidate) lifecycle.technical_acceptance(candidate) lifecycle.commit(candidate) lifecycle.environment_post_hooks(candidate) return lifecycle.write_deploy_result(candidate)