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