#!/usr/bin/env python3 """ generate_suite_docs.py - Generate suite release documentation Sprint: CI/CD Enhancement - Suite Release Pipeline Creates the docs/releases/YYYY.MM/ documentation structure. Usage: python generate_suite_docs.py [options] python generate_suite_docs.py 2026.04 Nova --channel lts python generate_suite_docs.py 2026.10 Orion --changelog CHANGELOG.md Arguments: version Suite version (YYYY.MM format) codename Release codename Options: --channel CH Release channel: edge, stable, lts --changelog FILE Pre-generated changelog file --output-dir DIR Output directory (default: docs/releases/YYYY.MM) --registry URL Container registry URL --previous VERSION Previous version for upgrade guide """ import argparse import json import os import re import subprocess import sys 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" # Support timeline SUPPORT_TIMELINE = { "edge": "3 months", "stable": "9 months", "lts": "5 years", } def get_git_sha() -> str: """Get current git HEAD SHA.""" try: result = subprocess.run( ["git", "rev-parse", "HEAD"], capture_output=True, text=True, cwd=REPO_ROOT, check=True, ) return result.stdout.strip()[:12] except subprocess.CalledProcessError: return "unknown" def read_service_versions() -> Dict[str, dict]: """Read service versions from manifest.""" if not MANIFEST_FILE.exists(): return {} try: manifest = json.loads(MANIFEST_FILE.read_text(encoding="utf-8")) return manifest.get("services", {}) except json.JSONDecodeError: return {} def generate_readme( version: str, codename: str, channel: str, registry: str, services: Dict[str, dict], ) -> str: """Generate README.md for the release.""" now = datetime.now(timezone.utc) support_period = SUPPORT_TIMELINE.get(channel, "unknown") lines = [ f"# StellaOps {version} \"{codename}\"", "", f"**Release Date:** {now.strftime('%B %d, %Y')}", f"**Channel:** {channel.upper()}", f"**Support Period:** {support_period}", "", "## Overview", "", f"StellaOps {version} \"{codename}\" is a {'Long-Term Support (LTS)' if channel == 'lts' else channel} release ", "of the StellaOps container security platform.", "", "## Quick Start", "", "### Docker Compose", "", "```bash", f"curl -O https://git.stella-ops.org/stella-ops.org/releases/{version}/docker-compose.yml", "docker compose up -d", "```", "", "### Helm", "", "```bash", f"helm repo add stellaops https://charts.stella-ops.org", f"helm install stellaops stellaops/stellaops --version {version}", "```", "", "## Included Services", "", "| Service | Version | Image |", "|---------|---------|-------|", ] for key, svc in sorted(services.items()): name = svc.get("name", key.title()) ver = svc.get("version", "1.0.0") tag = svc.get("dockerTag", ver) image = f"`{registry}/{key}:{tag}`" lines.append(f"| {name} | {ver} | {image} |") lines.extend([ "", "## Documentation", "", "- [CHANGELOG.md](./CHANGELOG.md) - Detailed list of changes", "- [services.md](./services.md) - Service version details", "- [upgrade-guide.md](./upgrade-guide.md) - Upgrade instructions", "- [docker-compose.yml](./docker-compose.yml) - Docker Compose configuration", "", "## Support", "", f"This release is supported until **{calculate_eol(now, channel)}**.", "", "For issues and feature requests, please visit:", "https://git.stella-ops.org/stella-ops.org/git.stella-ops.org/issues", "", "---", "", f"Generated: {now.isoformat()}", f"Git SHA: {get_git_sha()}", ]) return "\n".join(lines) def calculate_eol(release_date: datetime, channel: str) -> str: """Calculate end-of-life date based on channel.""" from dateutil.relativedelta import relativedelta periods = { "edge": relativedelta(months=3), "stable": relativedelta(months=9), "lts": relativedelta(years=5), } try: eol = release_date + periods.get(channel, relativedelta(months=9)) return eol.strftime("%B %Y") except ImportError: # Fallback without dateutil return f"See {channel} support policy" def generate_services_doc( version: str, codename: str, registry: str, services: Dict[str, dict], ) -> str: """Generate services.md with detailed service information.""" lines = [ f"# Services - StellaOps {version} \"{codename}\"", "", "This document lists all services included in this release with their versions,", "Docker images, and configuration details.", "", "## Service Matrix", "", "| Service | Version | Docker Tag | Released | Git SHA |", "|---------|---------|------------|----------|---------|", ] for key, svc in sorted(services.items()): name = svc.get("name", key.title()) ver = svc.get("version", "1.0.0") tag = svc.get("dockerTag") or "-" released = svc.get("releasedAt", "-") if released != "-": released = released[:10] sha = svc.get("gitSha") or "-" lines.append(f"| {name} | {ver} | `{tag}` | {released} | `{sha}` |") lines.extend([ "", "## Container Images", "", "All images are available from the StellaOps registry:", "", "```", f"Registry: {registry}", "```", "", "### Pull Commands", "", "```bash", ]) for key, svc in sorted(services.items()): tag = svc.get("dockerTag") or svc.get("version", "latest") lines.append(f"docker pull {registry}/{key}:{tag}") lines.extend([ "```", "", "## Service Descriptions", "", ]) service_descriptions = { "authority": "Authentication and authorization service with OAuth/OIDC support", "attestor": "in-toto/DSSE attestation generation and verification", "concelier": "Vulnerability advisory ingestion and merge engine", "scanner": "Container scanning with SBOM generation", "policy": "Policy engine with K4 lattice logic", "signer": "Cryptographic signing operations", "excititor": "VEX document ingestion and export", "gateway": "API gateway with routing and transport abstraction", "scheduler": "Job scheduling and queue management", "cli": "Command-line interface", "orchestrator": "Workflow orchestration and task coordination", "notify": "Notification delivery (Email, Slack, Teams, Webhooks)", } for key, svc in sorted(services.items()): name = svc.get("name", key.title()) desc = service_descriptions.get(key, "StellaOps service") lines.extend([ f"### {name}", "", desc, "", f"- **Version:** {svc.get('version', '1.0.0')}", f"- **Image:** `{registry}/{key}:{svc.get('dockerTag', 'latest')}`", "", ]) return "\n".join(lines) def generate_upgrade_guide( version: str, codename: str, previous_version: Optional[str], ) -> str: """Generate upgrade-guide.md.""" lines = [ f"# Upgrade Guide - StellaOps {version} \"{codename}\"", "", ] if previous_version: lines.extend([ f"This guide covers upgrading from StellaOps {previous_version} to {version}.", "", ]) else: lines.extend([ "This guide covers upgrading to this release from a previous version.", "", ]) lines.extend([ "## Before You Begin", "", "1. **Backup your data** - Ensure all databases and configuration are backed up", "2. **Review changelog** - Check [CHANGELOG.md](./CHANGELOG.md) for breaking changes", "3. **Check compatibility** - Verify your environment meets the requirements", "", "## Upgrade Steps", "", "### Docker Compose", "", "```bash", "# Pull new images", "docker compose pull", "", "# Stop services", "docker compose down", "", "# Start with new version", "docker compose up -d", "", "# Verify health", "docker compose ps", "```", "", "### Helm", "", "```bash", "# Update repository", "helm repo update stellaops", "", "# Upgrade release", f"helm upgrade stellaops stellaops/stellaops --version {version}", "", "# Verify status", "helm status stellaops", "```", "", "## Database Migrations", "", "Database migrations are applied automatically on service startup.", "For manual migration control, set `AUTO_MIGRATE=false` and run:", "", "```bash", "stellaops-cli db migrate", "```", "", "## Configuration Changes", "", "Review the following configuration changes:", "", "| Setting | Previous | New | Notes |", "|---------|----------|-----|-------|", "| (No breaking changes) | - | - | - |", "", "## Rollback Procedure", "", "If issues occur, rollback to the previous version:", "", "### Docker Compose", "", "```bash", "# Edit docker-compose.yml to use previous image tags", "docker compose down", "docker compose up -d", "```", "", "### Helm", "", "```bash", "helm rollback stellaops", "```", "", "## Support", "", "For upgrade assistance, contact support or open an issue at:", "https://git.stella-ops.org/stella-ops.org/git.stella-ops.org/issues", ]) return "\n".join(lines) def generate_manifest_yaml( version: str, codename: str, channel: str, services: Dict[str, dict], ) -> str: """Generate manifest.yaml for the release.""" now = datetime.now(timezone.utc) lines = [ "apiVersion: stellaops.org/v1", "kind: SuiteRelease", "metadata:", f" version: \"{version}\"", f" codename: \"{codename}\"", f" channel: \"{channel}\"", f" date: \"{now.isoformat()}\"", f" gitSha: \"{get_git_sha()}\"", "spec:", " services:", ] for key, svc in sorted(services.items()): lines.append(f" {key}:") lines.append(f" version: \"{svc.get('version', '1.0.0')}\"") if svc.get("dockerTag"): lines.append(f" dockerTag: \"{svc['dockerTag']}\"") if svc.get("gitSha"): lines.append(f" gitSha: \"{svc['gitSha']}\"") return "\n".join(lines) def main(): parser = argparse.ArgumentParser( description="Generate suite release documentation", ) parser.add_argument("version", help="Suite version (YYYY.MM format)") parser.add_argument("codename", help="Release codename") parser.add_argument( "--channel", choices=["edge", "stable", "lts"], default="stable", help="Release channel", ) parser.add_argument("--changelog", help="Pre-generated changelog file") parser.add_argument("--output-dir", help="Output directory") parser.add_argument( "--registry", default=DEFAULT_REGISTRY, help="Container registry URL", ) parser.add_argument("--previous", help="Previous version for upgrade guide") args = parser.parse_args() # Determine output directory if args.output_dir: output_dir = Path(args.output_dir) else: output_dir = REPO_ROOT / "docs" / "releases" / args.version output_dir.mkdir(parents=True, exist_ok=True) print(f"Output directory: {output_dir}", file=sys.stderr) # Read service versions services = read_service_versions() if not services: print("Warning: No service versions found in manifest", file=sys.stderr) # Generate README.md readme = generate_readme( args.version, args.codename, args.channel, args.registry, services ) (output_dir / "README.md").write_text(readme, encoding="utf-8") print("Generated: README.md", file=sys.stderr) # Copy or generate CHANGELOG.md if args.changelog and Path(args.changelog).exists(): changelog = Path(args.changelog).read_text(encoding="utf-8") else: # Generate basic changelog changelog = f"# Changelog - StellaOps {args.version} \"{args.codename}\"\n\n" changelog += "See git history for detailed changes.\n" (output_dir / "CHANGELOG.md").write_text(changelog, encoding="utf-8") print("Generated: CHANGELOG.md", file=sys.stderr) # Generate services.md services_doc = generate_services_doc( args.version, args.codename, args.registry, services ) (output_dir / "services.md").write_text(services_doc, encoding="utf-8") print("Generated: services.md", file=sys.stderr) # Generate upgrade-guide.md upgrade_guide = generate_upgrade_guide( args.version, args.codename, args.previous ) (output_dir / "upgrade-guide.md").write_text(upgrade_guide, encoding="utf-8") print("Generated: upgrade-guide.md", file=sys.stderr) # Generate manifest.yaml manifest = generate_manifest_yaml( args.version, args.codename, args.channel, services ) (output_dir / "manifest.yaml").write_text(manifest, encoding="utf-8") print("Generated: manifest.yaml", file=sys.stderr) print(f"\nSuite documentation generated in: {output_dir}", file=sys.stderr) if __name__ == "__main__": main()