save progress
This commit is contained in:
259
.gitea/scripts/release/collect_versions.py
Normal file
259
.gitea/scripts/release/collect_versions.py
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user