260 lines
7.4 KiB
Python
260 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
collect_versions.py - Collect service versions for suite release
|
|
|
|
Sprint: CI/CD Enhancement - Suite Release Pipeline
|
|
Gathers all service versions from Directory.Versions.props and service-versions.json.
|
|
|
|
Usage:
|
|
python collect_versions.py [options]
|
|
python collect_versions.py --format json
|
|
python collect_versions.py --format yaml --output versions.yaml
|
|
|
|
Options:
|
|
--format FMT Output format: json, yaml, markdown, env (default: json)
|
|
--output FILE Output file (defaults to stdout)
|
|
--include-unreleased Include services with no Docker tag
|
|
--registry URL Container registry URL
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass, asdict
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
|
|
# Repository paths
|
|
SCRIPT_DIR = Path(__file__).parent
|
|
REPO_ROOT = SCRIPT_DIR.parent.parent.parent
|
|
VERSIONS_FILE = REPO_ROOT / "src" / "Directory.Versions.props"
|
|
MANIFEST_FILE = REPO_ROOT / "devops" / "releases" / "service-versions.json"
|
|
|
|
# Default registry
|
|
DEFAULT_REGISTRY = "git.stella-ops.org/stella-ops.org"
|
|
|
|
|
|
@dataclass
|
|
class ServiceVersion:
|
|
name: str
|
|
version: str
|
|
docker_tag: Optional[str] = None
|
|
released_at: Optional[str] = None
|
|
git_sha: Optional[str] = None
|
|
image: Optional[str] = None
|
|
|
|
|
|
def read_versions_from_props() -> Dict[str, str]:
|
|
"""Read versions from Directory.Versions.props."""
|
|
if not VERSIONS_FILE.exists():
|
|
print(f"Warning: {VERSIONS_FILE} not found", file=sys.stderr)
|
|
return {}
|
|
|
|
content = VERSIONS_FILE.read_text(encoding="utf-8")
|
|
versions = {}
|
|
|
|
# Pattern: <StellaOps{Service}Version>X.Y.Z</StellaOps{Service}Version>
|
|
pattern = r"<StellaOps(\w+)Version>(\d+\.\d+\.\d+)</StellaOps\1Version>"
|
|
|
|
for match in re.finditer(pattern, content):
|
|
service_name = match.group(1)
|
|
version = match.group(2)
|
|
versions[service_name.lower()] = version
|
|
|
|
return versions
|
|
|
|
|
|
def read_manifest() -> Dict[str, dict]:
|
|
"""Read service metadata from manifest file."""
|
|
if not MANIFEST_FILE.exists():
|
|
print(f"Warning: {MANIFEST_FILE} not found", file=sys.stderr)
|
|
return {}
|
|
|
|
try:
|
|
manifest = json.loads(MANIFEST_FILE.read_text(encoding="utf-8"))
|
|
return manifest.get("services", {})
|
|
except json.JSONDecodeError as e:
|
|
print(f"Warning: Failed to parse {MANIFEST_FILE}: {e}", file=sys.stderr)
|
|
return {}
|
|
|
|
|
|
def collect_all_versions(
|
|
registry: str = DEFAULT_REGISTRY,
|
|
include_unreleased: bool = False,
|
|
) -> List[ServiceVersion]:
|
|
"""Collect all service versions."""
|
|
props_versions = read_versions_from_props()
|
|
manifest_services = read_manifest()
|
|
|
|
services = []
|
|
|
|
# Merge data from both sources
|
|
all_service_keys = set(props_versions.keys()) | set(manifest_services.keys())
|
|
|
|
for key in sorted(all_service_keys):
|
|
version = props_versions.get(key, "0.0.0")
|
|
manifest = manifest_services.get(key, {})
|
|
|
|
docker_tag = manifest.get("dockerTag")
|
|
released_at = manifest.get("releasedAt")
|
|
git_sha = manifest.get("gitSha")
|
|
|
|
# Skip unreleased if not requested
|
|
if not include_unreleased and not docker_tag:
|
|
continue
|
|
|
|
# Build image reference
|
|
if docker_tag:
|
|
image = f"{registry}/{key}:{docker_tag}"
|
|
else:
|
|
image = f"{registry}/{key}:{version}"
|
|
|
|
service = ServiceVersion(
|
|
name=manifest.get("name", key.title()),
|
|
version=version,
|
|
docker_tag=docker_tag,
|
|
released_at=released_at,
|
|
git_sha=git_sha,
|
|
image=image,
|
|
)
|
|
|
|
services.append(service)
|
|
|
|
return services
|
|
|
|
|
|
def format_json(services: List[ServiceVersion]) -> str:
|
|
"""Format as JSON."""
|
|
data = {
|
|
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
|
"services": [asdict(s) for s in services],
|
|
}
|
|
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
|
|
|
|
def format_yaml(services: List[ServiceVersion]) -> str:
|
|
"""Format as YAML."""
|
|
lines = [
|
|
"# Service Versions",
|
|
f"# Generated: {datetime.now(timezone.utc).isoformat()}",
|
|
"",
|
|
"services:",
|
|
]
|
|
|
|
for s in services:
|
|
lines.extend([
|
|
f" {s.name.lower()}:",
|
|
f" name: {s.name}",
|
|
f" version: \"{s.version}\"",
|
|
])
|
|
if s.docker_tag:
|
|
lines.append(f" dockerTag: \"{s.docker_tag}\"")
|
|
if s.image:
|
|
lines.append(f" image: \"{s.image}\"")
|
|
if s.released_at:
|
|
lines.append(f" releasedAt: \"{s.released_at}\"")
|
|
if s.git_sha:
|
|
lines.append(f" gitSha: \"{s.git_sha}\"")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def format_markdown(services: List[ServiceVersion]) -> str:
|
|
"""Format as Markdown table."""
|
|
lines = [
|
|
"# Service Versions",
|
|
"",
|
|
f"Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
|
"",
|
|
"| Service | Version | Docker Tag | Released |",
|
|
"|---------|---------|------------|----------|",
|
|
]
|
|
|
|
for s in services:
|
|
released = s.released_at[:10] if s.released_at else "-"
|
|
docker_tag = f"`{s.docker_tag}`" if s.docker_tag else "-"
|
|
lines.append(f"| {s.name} | {s.version} | {docker_tag} | {released} |")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def format_env(services: List[ServiceVersion]) -> str:
|
|
"""Format as environment variables."""
|
|
lines = [
|
|
"# Service Versions as Environment Variables",
|
|
f"# Generated: {datetime.now(timezone.utc).isoformat()}",
|
|
"",
|
|
]
|
|
|
|
for s in services:
|
|
name_upper = s.name.upper().replace(" ", "_")
|
|
lines.append(f"STELLAOPS_{name_upper}_VERSION={s.version}")
|
|
if s.docker_tag:
|
|
lines.append(f"STELLAOPS_{name_upper}_DOCKER_TAG={s.docker_tag}")
|
|
if s.image:
|
|
lines.append(f"STELLAOPS_{name_upper}_IMAGE={s.image}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Collect service versions for suite release",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--format",
|
|
choices=["json", "yaml", "markdown", "env"],
|
|
default="json",
|
|
help="Output format",
|
|
)
|
|
parser.add_argument("--output", "-o", help="Output file")
|
|
parser.add_argument(
|
|
"--include-unreleased",
|
|
action="store_true",
|
|
help="Include services without Docker tags",
|
|
)
|
|
parser.add_argument(
|
|
"--registry",
|
|
default=DEFAULT_REGISTRY,
|
|
help="Container registry URL",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Collect versions
|
|
services = collect_all_versions(
|
|
registry=args.registry,
|
|
include_unreleased=args.include_unreleased,
|
|
)
|
|
|
|
if not services:
|
|
print("No services found", file=sys.stderr)
|
|
if not args.include_unreleased:
|
|
print("Hint: Use --include-unreleased to show all services", file=sys.stderr)
|
|
sys.exit(0)
|
|
|
|
# Format output
|
|
formatters = {
|
|
"json": format_json,
|
|
"yaml": format_yaml,
|
|
"markdown": format_markdown,
|
|
"env": format_env,
|
|
}
|
|
|
|
output = formatters[args.format](services)
|
|
|
|
# Write output
|
|
if args.output:
|
|
Path(args.output).write_text(output, encoding="utf-8")
|
|
print(f"Versions written to: {args.output}", file=sys.stderr)
|
|
else:
|
|
print(output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|