Skip to main content

scripts-generate-release-manifest

#!/usr/bin/env python3 """

title: "Platform configuration" component_type: script version: "1.0.0" audience: contributor status: stable summary: "Generate release manifest JSON for binary distribution." keywords: ['generate', 'git', 'manifest', 'release', 'validation'] tokens: ~500 created: 2025-12-22 updated: 2025-12-22 script_name: "generate-release-manifest.py" language: python executable: true usage: "python3 scripts/generate-release-manifest.py [options]" python_version: "3.10+" dependencies: [] modifies_files: false network_access: false requires_auth: false

Generate release manifest JSON for binary distribution.

This script creates a manifest.json file containing version information, download URLs, SHA256 checksums, and file sizes for all platform binaries. The manifest is used by postinstall scripts and native installers for secure binary downloads.

Usage: python generate-release-manifest.py --version 1.0.0
--binaries-dir ./dist --cdn-base https://dist.coditect.ai
--output manifest.json

python generate-release-manifest.py --version 1.0.0 \
--binaries-dir ./dist --bucket gs://coditect-dist \
--output manifest.json

Output manifest.json: { "name": "@az1/coditect-core", "version": "1.0.0", "released": "2025-12-08T00:00:00Z", "platforms": { "darwin-x64": { "url": "https://dist.coditect.ai/v1.0.0/coditect-darwin-x64.tar.gz", "sha256": "abc123...", "size": 12345678 }, ... } } """

import argparse import hashlib import json import sys from datetime import datetime, timezone from pathlib import Path from typing import Dict, Any, Optional

Platform configuration

PLATFORMS = { "darwin-x64": {"os": "darwin", "arch": "x64", "ext": ".tar.gz"}, "darwin-arm64": {"os": "darwin", "arch": "arm64", "ext": ".tar.gz"}, "linux-x64": {"os": "linux", "arch": "x64", "ext": ".tar.gz"}, "linux-arm64": {"os": "linux", "arch": "arm64", "ext": ".tar.gz"}, "linux-x64-musl": {"os": "linux", "arch": "x64", "ext": ".tar.gz"}, "win32-x64": {"os": "win32", "arch": "x64", "ext": ".zip"}, }

def calculate_sha256(file_path: Path, chunk_size: int = 8192) -> str: """ Calculate SHA256 checksum of a file.

Args:
file_path: Path to file
chunk_size: Read buffer size (default 8KB)

Returns:
Lowercase hex digest (64 characters)
"""
sha256 = hashlib.sha256()

with open(file_path, 'rb') as f:
while chunk := f.read(chunk_size):
sha256.update(chunk)

return sha256.hexdigest()

def find_binary_file( binaries_dir: Path, binary_name: str, platform: str, extensions: list[str] ) -> Optional[Path]: """ Find binary file for a platform.

Searches for files matching common naming patterns:
- {binary_name}-{platform}{ext}
- {binary_name}_{platform}{ext}
- {platform}/{binary_name}{ext}

Args:
binaries_dir: Directory containing binaries
binary_name: Base name of the binary
platform: Platform key (e.g., 'darwin-arm64')
extensions: List of file extensions to try

Returns:
Path to binary file or None if not found
"""
patterns = [
f"{binary_name}-{platform}",
f"{binary_name}_{platform}",
f"{platform}/{binary_name}",
]

for pattern in patterns:
for ext in extensions:
# Check directly in binaries_dir
path = binaries_dir / f"{pattern}{ext}"
if path.exists():
return path

# Check in subdirectories
for subdir in binaries_dir.iterdir():
if subdir.is_dir():
path = subdir / f"{pattern}{ext}"
if path.exists():
return path

return None

def generate_url( cdn_base: str, version: str, binary_name: str, platform: str, extension: str ) -> str: """ Generate download URL for a platform binary.

Args:
cdn_base: CDN base URL (e.g., https://dist.coditect.ai)
version: Version string (e.g., 1.0.0)
binary_name: Base name of the binary
platform: Platform key
extension: File extension

Returns:
Full download URL
"""
# Remove trailing slash from cdn_base
cdn_base = cdn_base.rstrip('/')

filename = f"{binary_name}-{platform}{extension}"
return f"{cdn_base}/v{version}/{filename}"

def generate_manifest( name: str, version: str, binaries_dir: Path, binary_name: str, cdn_base: str, include_missing: bool = False ) -> Dict[str, Any]: """ Generate release manifest with checksums for all platforms.

Args:
name: Package name (e.g., @az1/coditect-core)
version: Version string
binaries_dir: Directory containing binary files
binary_name: Base name of binaries
cdn_base: CDN base URL
include_missing: Include platforms even if binary not found

Returns:
Manifest dictionary
"""
manifest = {
"name": name,
"version": version,
"released": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"algorithm": "sha256",
"platforms": {}
}

for platform, config in PLATFORMS.items():
extension = config["ext"]

# Try to find binary file
binary_path = find_binary_file(
binaries_dir, binary_name, platform,
[extension, ".tar.gz", ".zip", ".tgz", ""]
)

if binary_path:
checksum = calculate_sha256(binary_path)
size = binary_path.stat().st_size

manifest["platforms"][platform] = {
"url": generate_url(cdn_base, version, binary_name, platform, extension),
"sha256": checksum,
"size": size,
"filename": binary_path.name
}
print(f" ✓ {platform}: {binary_path.name} ({size:,} bytes)")

elif include_missing:
manifest["platforms"][platform] = {
"url": generate_url(cdn_base, version, binary_name, platform, extension),
"sha256": "PLACEHOLDER_CHECKSUM",
"size": 0,
"filename": f"{binary_name}-{platform}{extension}"
}
print(f" ⚠ {platform}: binary not found (placeholder added)")

else:
print(f" ⚠ {platform}: binary not found (skipped)")

return manifest

def validate_manifest(manifest: Dict[str, Any]) -> list[str]: """ Validate manifest structure and content.

Args:
manifest: Manifest dictionary to validate

Returns:
List of validation errors (empty if valid)
"""
errors = []

# Check required fields
required_fields = ["name", "version", "released", "platforms"]
for field in required_fields:
if field not in manifest:
errors.append(f"Missing required field: {field}")

# Check version format (semver-ish)
version = manifest.get("version", "")
if not version or not all(c.isdigit() or c == '.' for c in version):
errors.append(f"Invalid version format: {version}")

# Check platforms
platforms = manifest.get("platforms", {})
if not platforms:
errors.append("No platforms defined")

for platform, info in platforms.items():
if platform not in PLATFORMS:
errors.append(f"Unknown platform: {platform}")

if not info.get("url"):
errors.append(f"Missing URL for {platform}")

checksum = info.get("sha256", "")
if len(checksum) != 64:
errors.append(f"Invalid SHA256 checksum for {platform}: {checksum[:20]}...")

return errors

def main(): parser = argparse.ArgumentParser( description="Generate release manifest JSON for binary distribution", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=doc )

parser.add_argument(
"--name", default="@az1/coditect-core",
help="Package name (default: @az1/coditect-core)"
)
parser.add_argument(
"--version", required=True,
help="Release version (e.g., 1.0.0)"
)
parser.add_argument(
"--binary-name", default="coditect",
help="Binary base name (default: coditect)"
)
parser.add_argument(
"--binaries-dir", type=Path, required=True,
help="Directory containing binary files"
)
parser.add_argument(
"--cdn-base", default="https://dist.coditect.ai",
help="CDN base URL (default: https://dist.coditect.ai)"
)
parser.add_argument(
"--output", "-o", type=Path, default=Path("manifest.json"),
help="Output manifest file (default: manifest.json)"
)
parser.add_argument(
"--include-missing", action="store_true",
help="Include platforms even if binary not found (with placeholder)"
)
parser.add_argument(
"--validate", action="store_true",
help="Validate existing manifest instead of generating"
)
parser.add_argument(
"--pretty", action="store_true", default=True,
help="Pretty-print JSON output (default: true)"
)
parser.add_argument(
"--dry-run", action="store_true",
help="Print manifest without writing to file"
)

args = parser.parse_args()

# Validate mode
if args.validate:
if not args.output.exists():
print(f"Error: Manifest file not found: {args.output}")
sys.exit(1)

with open(args.output) as f:
manifest = json.load(f)

errors = validate_manifest(manifest)
if errors:
print("Validation errors:")
for error in errors:
print(f" ✗ {error}")
sys.exit(1)
else:
print(f"✓ Manifest is valid: {args.output}")
sys.exit(0)

# Generate mode
if not args.binaries_dir.exists():
print(f"Error: Binaries directory not found: {args.binaries_dir}")
sys.exit(1)

print(f"Generating manifest for {args.name}@{args.version}")
print(f"Binaries directory: {args.binaries_dir}")
print(f"CDN base: {args.cdn_base}")
print()

manifest = generate_manifest(
name=args.name,
version=args.version,
binaries_dir=args.binaries_dir,
binary_name=args.binary_name,
cdn_base=args.cdn_base,
include_missing=args.include_missing
)

# Validate generated manifest
errors = validate_manifest(manifest)
if errors:
print()
print("Warning: Generated manifest has issues:")
for error in errors:
print(f" ⚠ {error}")

# Output
indent = 2 if args.pretty else None
manifest_json = json.dumps(manifest, indent=indent)

if args.dry_run:
print()
print("Generated manifest (dry run):")
print(manifest_json)
else:
with open(args.output, 'w') as f:
f.write(manifest_json)
f.write('\n')

print()
print(f"✓ Manifest written to: {args.output}")
print(f" Platforms: {len(manifest['platforms'])}")

# Summary
platform_count = len(manifest["platforms"])
total_size = sum(p.get("size", 0) for p in manifest["platforms"].values())

print()
print(f"Summary:")
print(f" Platforms: {platform_count}/{len(PLATFORMS)}")
print(f" Total size: {total_size:,} bytes ({total_size / 1024 / 1024:.1f} MB)")

if name == "main": main()