478 lines
14 KiB
Python
478 lines
14 KiB
Python
#!/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 <version> <codename> [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()
|