#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RELEASE_ROOT="${DOMAINCHECK_RELEASE_ROOT:-${ROOT_DIR}/release}" PACKAGE_PATH="${1:-}" HASH_PATH="${2:-}" python3 - "${RELEASE_ROOT}" "${PACKAGE_PATH}" "${HASH_PATH}" <<'PY' import hashlib import json import sys import tarfile import zipfile from pathlib import Path release_root = Path(sys.argv[1]) package_path_arg = sys.argv[2].strip() hash_path_arg = sys.argv[3].strip() def resolve_latest_package(root: Path) -> Path: candidates = sorted( [ item for item in root.glob("*") if item.is_file() and (item.name.endswith(".tar.gz") or item.name.endswith(".zip")) ], key=lambda item: item.stat().st_mtime, reverse=True, ) if not candidates: raise SystemExit("No release archive found") return candidates[0] def derive_hash_path(root: Path, package_path: Path) -> Path: if package_path.name.endswith(".tar.gz"): base_name = package_path.name[:-7] else: base_name = package_path.stem candidate = root / f"{base_name}.sha256.txt" if candidate.exists(): return candidate candidates = sorted(root.glob("*.sha256.txt"), key=lambda item: item.stat().st_mtime, reverse=True) if not candidates: raise SystemExit("No hash file found") return candidates[0] def compute_sha256(path: Path) -> str: sha = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): sha.update(chunk) return sha.hexdigest().lower() def normalize_entry_names(names: list[str]) -> list[str]: normalized: list[str] = [] for name in names: item = name.replace("\\", "/") while item.startswith("./"): item = item[2:] item = item.strip("/") if item: normalized.append(item) return normalized def read_archive_entries(path: Path) -> tuple[list[str], dict]: if path.name.endswith(".zip"): with zipfile.ZipFile(path, "r") as archive: entry_names = normalize_entry_names(archive.namelist()) manifest = {} if "release_manifest.json" in entry_names: with archive.open("release_manifest.json", "r") as handle: manifest = json.loads(handle.read().decode("utf-8")) return entry_names, manifest with tarfile.open(path, "r:*") as archive: entry_names = normalize_entry_names(archive.getnames()) manifest = {} if "release_manifest.json" in entry_names: manifest_member = archive.extractfile("./release_manifest.json") or archive.extractfile("release_manifest.json") if manifest_member is not None: manifest = json.loads(manifest_member.read().decode("utf-8")) return entry_names, manifest package_path = Path(package_path_arg) if package_path_arg else resolve_latest_package(release_root) if not package_path.exists(): raise SystemExit(f"archive not found: {package_path}") hash_path = Path(hash_path_arg) if hash_path_arg else derive_hash_path(release_root, package_path) if not hash_path.exists(): raise SystemExit(f"hash file not found: {hash_path}") expected_hash = "" expected_file = "" for line in hash_path.read_text(encoding="utf-8").splitlines(): if line.startswith("sha256="): expected_hash = line.split("=", 1)[1].strip().lower() elif line.startswith("file="): expected_file = line.split("=", 1)[1].strip() if not expected_hash: raise SystemExit(f"sha256 entry missing in {hash_path}") actual_hash = compute_sha256(package_path) entry_names, manifest = read_archive_entries(package_path) entry_set = set(entry_names) required_entries = [ "README_RELEASE.txt", "release_manifest.json", "domain-api/README.md", "domain-web/README.md", "domainCheck/detect_worker.py", "domainCheck/app/config.py", "domainCheck/detect/register.py", "domainCheck/.env.example", "scripts/package_domain_release.ps1", "scripts/verify_domain_release.ps1", "scripts/package_domain_release.sh", "scripts/verify_domain_release.sh", "scripts/smoke_test_stack.ps1", ] missing_entries = [item for item in required_entries if item not in entry_set] smoke_report = str(((manifest.get("smoke_test") or {}).get("report") or "")).strip() if smoke_report and smoke_report not in entry_set: missing_entries.append(smoke_report) ok = ( actual_hash == expected_hash and not missing_entries and (not expected_file or expected_file == package_path.name) ) report = { "ok": ok, "archive": str(package_path), "hash_file": str(hash_path), "expected_file": expected_file, "actual_file": package_path.name, "expected_sha256": expected_hash, "actual_sha256": actual_hash, "missing_entries": missing_entries, "package_type": "tar.gz" if package_path.name.endswith(".tar.gz") else "zip", } print(json.dumps(report, ensure_ascii=False, indent=2)) raise SystemExit(0 if ok else 1) PY