up
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Build Test Deploy / build-test (push) Has been cancelled
				
			
		
			
				
	
				Build Test Deploy / authority-container (push) Has been cancelled
				
			
		
			
				
	
				Build Test Deploy / docs (push) Has been cancelled
				
			
		
			
				
	
				Build Test Deploy / deploy (push) Has been cancelled
				
			
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Build Test Deploy / build-test (push) Has been cancelled
				
			Build Test Deploy / authority-container (push) Has been cancelled
				
			Build Test Deploy / docs (push) Has been cancelled
				
			Build Test Deploy / deploy (push) Has been cancelled
				
			Docs CI / lint-and-preview (push) Has been cancelled
				
			This commit is contained in:
		| @@ -1,254 +1,254 @@ | ||||
| #!/usr/bin/env python3 | ||||
| """Render Markdown documentation under docs/ into a static HTML bundle. | ||||
|  | ||||
| The script converts every Markdown file into a standalone HTML document, | ||||
| mirroring the original folder structure under the output directory. A | ||||
| `manifest.json` file is also produced to list the generated documents and | ||||
| surface basic metadata (title, source path, output path). | ||||
|  | ||||
| Usage: | ||||
|     python scripts/render_docs.py --source docs --output build/docs-site | ||||
|  | ||||
| Dependencies: | ||||
|     pip install markdown pygments | ||||
| """ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import argparse | ||||
| import json | ||||
| import logging | ||||
| import os | ||||
| import shutil | ||||
| from dataclasses import dataclass | ||||
| from datetime import datetime, timezone | ||||
| from pathlib import Path | ||||
| from typing import Iterable, List | ||||
|  | ||||
| import markdown | ||||
|  | ||||
| # Enable fenced code blocks, tables, and definition lists. These cover the | ||||
| # Markdown constructs heavily used across the documentation set. | ||||
| MD_EXTENSIONS = [ | ||||
|     "fenced_code", | ||||
|     "codehilite", | ||||
|     "tables", | ||||
|     "toc", | ||||
|     "def_list", | ||||
|     "admonition", | ||||
| ] | ||||
|  | ||||
| HTML_TEMPLATE = """<!DOCTYPE html> | ||||
| <html lang=\"en\"> | ||||
| <head> | ||||
|   <meta charset=\"utf-8\" /> | ||||
|   <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> | ||||
|   <title>{title}</title> | ||||
|   <style> | ||||
|     :root {{ | ||||
|       color-scheme: light dark; | ||||
|       font-family: system-ui, -apple-system, Segoe UI, sans-serif; | ||||
|       line-height: 1.6; | ||||
|     }} | ||||
|     body {{ | ||||
|       margin: 2.5rem auto; | ||||
|       padding: 0 1.5rem; | ||||
|       max-width: 70ch; | ||||
|       background: var(--background, #1118270d); | ||||
|     }} | ||||
|     pre {{ | ||||
|       overflow: auto; | ||||
|       padding: 1rem; | ||||
|       background: #11182714; | ||||
|       border-radius: 0.5rem; | ||||
|     }} | ||||
|     code {{ | ||||
|       font-family: SFMono-Regular, Consolas, 'Liberation Mono', monospace; | ||||
|       font-size: 0.95em; | ||||
|     }} | ||||
|     table {{ | ||||
|       width: 100%; | ||||
|       border-collapse: collapse; | ||||
|       margin: 1rem 0; | ||||
|     }} | ||||
|     th, td {{ | ||||
|       border: 1px solid #4b5563; | ||||
|       padding: 0.5rem; | ||||
|       text-align: left; | ||||
|     }} | ||||
|     a {{ | ||||
|       color: #2563eb; | ||||
|     }} | ||||
|     footer {{ | ||||
|       margin-top: 3rem; | ||||
|       font-size: 0.85rem; | ||||
|       color: #6b7280; | ||||
|     }} | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
|   <main> | ||||
| {body} | ||||
|   </main> | ||||
|   <footer> | ||||
|     <p>Generated on {generated_at} UTC · Source: {source}</p> | ||||
|   </footer> | ||||
| </body> | ||||
| </html> | ||||
| """ | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class DocEntry: | ||||
|     source: Path | ||||
|     output: Path | ||||
|     title: str | ||||
|  | ||||
|     def to_manifest(self) -> dict[str, str]: | ||||
|         return { | ||||
|             "source": self.source.as_posix(), | ||||
|             "output": self.output.as_posix(), | ||||
|             "title": self.title, | ||||
|         } | ||||
|  | ||||
|  | ||||
| def discover_markdown_files(source_root: Path) -> Iterable[Path]: | ||||
|     for path in source_root.rglob("*.md"): | ||||
|         if path.is_file(): | ||||
|             yield path | ||||
|  | ||||
|  | ||||
| def read_title(markdown_text: str, fallback: str) -> str: | ||||
|     for raw_line in markdown_text.splitlines(): | ||||
|         line = raw_line.strip() | ||||
|         if line.startswith("#"): | ||||
|             return line.lstrip("#").strip() or fallback | ||||
|     return fallback | ||||
|  | ||||
|  | ||||
| def convert_markdown(path: Path, source_root: Path, output_root: Path) -> DocEntry: | ||||
|     relative = path.relative_to(source_root) | ||||
|     output_path = output_root / relative.with_suffix(".html") | ||||
|     output_path.parent.mkdir(parents=True, exist_ok=True) | ||||
|  | ||||
|     text = path.read_text(encoding="utf-8") | ||||
|     html_body = markdown.markdown(text, extensions=MD_EXTENSIONS) | ||||
|  | ||||
|     title = read_title(text, fallback=relative.stem.replace("_", " ")) | ||||
|     generated_at = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") | ||||
|  | ||||
|     output_path.write_text( | ||||
|         HTML_TEMPLATE.format( | ||||
|             title=title, | ||||
|             body=html_body, | ||||
|             generated_at=generated_at, | ||||
|             source=relative.as_posix(), | ||||
|         ), | ||||
|         encoding="utf-8", | ||||
|     ) | ||||
|  | ||||
|     return DocEntry(source=relative, output=output_path.relative_to(output_root), title=title) | ||||
|  | ||||
|  | ||||
| def copy_static_assets(source_root: Path, output_root: Path) -> None: | ||||
|     for path in source_root.rglob("*"): | ||||
|         if path.is_dir() or path.suffix.lower() == ".md": | ||||
|             # Skip Markdown (already rendered separately). | ||||
|             continue | ||||
|         relative = path.relative_to(source_root) | ||||
|         destination = output_root / relative | ||||
|         destination.parent.mkdir(parents=True, exist_ok=True) | ||||
|         destination.write_bytes(path.read_bytes()) | ||||
|         logging.info("Copied asset %s", relative) | ||||
|  | ||||
|  | ||||
| def write_manifest(entries: Iterable[DocEntry], output_root: Path) -> None: | ||||
|     manifest_path = output_root / "manifest.json" | ||||
|     manifest = [entry.to_manifest() for entry in entries] | ||||
|     manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8") | ||||
|     logging.info("Wrote manifest with %d entries", len(manifest)) | ||||
|  | ||||
|  | ||||
| def write_index(entries: List[DocEntry], output_root: Path) -> None: | ||||
|     index_path = output_root / "index.html" | ||||
|     generated_at = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") | ||||
|  | ||||
|     items = "\n".join( | ||||
|         f"      <li><a href='{entry.output.as_posix()}'>{entry.title}</a>" f" · <code>{entry.source.as_posix()}</code></li>" | ||||
|         for entry in sorted(entries, key=lambda e: e.title.lower()) | ||||
|     ) | ||||
|  | ||||
|     html = f"""<!DOCTYPE html> | ||||
| <html lang=\"en\"> | ||||
| <head> | ||||
|   <meta charset=\"utf-8\" /> | ||||
|   <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> | ||||
|   <title>Stella Ops Documentation Index</title> | ||||
|   <style> | ||||
|     body {{ | ||||
|       margin: 2.5rem auto; | ||||
|       padding: 0 1.5rem; | ||||
|       max-width: 70ch; | ||||
|       font-family: system-ui, -apple-system, 'Segoe UI', sans-serif; | ||||
|       line-height: 1.6; | ||||
|     }} | ||||
|     h1 {{ font-size: 2.25rem; margin-bottom: 1rem; }} | ||||
|     ul {{ list-style: none; padding: 0; }} | ||||
|     li {{ margin-bottom: 0.75rem; }} | ||||
|     code {{ background: #11182714; padding: 0.2rem 0.35rem; border-radius: 0.35rem; }} | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
|   <h1>Stella Ops Documentation</h1> | ||||
|   <p>Generated on {generated_at} UTC</p> | ||||
|   <ul> | ||||
| {items} | ||||
|   </ul> | ||||
| </body> | ||||
| </html> | ||||
| """ | ||||
|     index_path.write_text(html, encoding="utf-8") | ||||
|     logging.info("Wrote HTML index with %d entries", len(entries)) | ||||
|  | ||||
|  | ||||
| def parse_args() -> argparse.Namespace: | ||||
|     parser = argparse.ArgumentParser(description="Render documentation bundle") | ||||
|     parser.add_argument("--source", default="docs", type=Path, help="Directory containing Markdown sources") | ||||
|     parser.add_argument("--output", default=Path("build/docs-site"), type=Path, help="Directory for rendered output") | ||||
|     parser.add_argument("--clean", action="store_true", help="Remove the output directory before rendering") | ||||
|     return parser.parse_args() | ||||
|  | ||||
|  | ||||
| def main() -> int: | ||||
|     logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") | ||||
|     args = parse_args() | ||||
|  | ||||
|     source_root: Path = args.source.resolve() | ||||
|     output_root: Path = args.output.resolve() | ||||
|  | ||||
|     if not source_root.exists(): | ||||
|         logging.error("Source directory %s does not exist", source_root) | ||||
|         return os.EX_NOINPUT | ||||
|  | ||||
|     if args.clean and output_root.exists(): | ||||
|         logging.info("Cleaning existing output directory %s", output_root) | ||||
|         shutil.rmtree(output_root) | ||||
|  | ||||
|     output_root.mkdir(parents=True, exist_ok=True) | ||||
|  | ||||
|     entries: List[DocEntry] = [] | ||||
|     for md_file in discover_markdown_files(source_root): | ||||
|         entry = convert_markdown(md_file, source_root, output_root) | ||||
|         entries.append(entry) | ||||
|         logging.info("Rendered %s -> %s", entry.source, entry.output) | ||||
|  | ||||
|     write_manifest(entries, output_root) | ||||
|     write_index(entries, output_root) | ||||
|     copy_static_assets(source_root, output_root) | ||||
|  | ||||
|     logging.info("Documentation bundle available at %s", output_root) | ||||
|     return os.EX_OK | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     raise SystemExit(main()) | ||||
| #!/usr/bin/env python3 | ||||
| """Render Markdown documentation under docs/ into a static HTML bundle. | ||||
|  | ||||
| The script converts every Markdown file into a standalone HTML document, | ||||
| mirroring the original folder structure under the output directory. A | ||||
| `manifest.json` file is also produced to list the generated documents and | ||||
| surface basic metadata (title, source path, output path). | ||||
|  | ||||
| Usage: | ||||
|     python scripts/render_docs.py --source docs --output build/docs-site | ||||
|  | ||||
| Dependencies: | ||||
|     pip install markdown pygments | ||||
| """ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import argparse | ||||
| import json | ||||
| import logging | ||||
| import os | ||||
| import shutil | ||||
| from dataclasses import dataclass | ||||
| from datetime import datetime, timezone | ||||
| from pathlib import Path | ||||
| from typing import Iterable, List | ||||
|  | ||||
| import markdown | ||||
|  | ||||
| # Enable fenced code blocks, tables, and definition lists. These cover the | ||||
| # Markdown constructs heavily used across the documentation set. | ||||
| MD_EXTENSIONS = [ | ||||
|     "fenced_code", | ||||
|     "codehilite", | ||||
|     "tables", | ||||
|     "toc", | ||||
|     "def_list", | ||||
|     "admonition", | ||||
| ] | ||||
|  | ||||
| HTML_TEMPLATE = """<!DOCTYPE html> | ||||
| <html lang=\"en\"> | ||||
| <head> | ||||
|   <meta charset=\"utf-8\" /> | ||||
|   <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> | ||||
|   <title>{title}</title> | ||||
|   <style> | ||||
|     :root {{ | ||||
|       color-scheme: light dark; | ||||
|       font-family: system-ui, -apple-system, Segoe UI, sans-serif; | ||||
|       line-height: 1.6; | ||||
|     }} | ||||
|     body {{ | ||||
|       margin: 2.5rem auto; | ||||
|       padding: 0 1.5rem; | ||||
|       max-width: 70ch; | ||||
|       background: var(--background, #1118270d); | ||||
|     }} | ||||
|     pre {{ | ||||
|       overflow: auto; | ||||
|       padding: 1rem; | ||||
|       background: #11182714; | ||||
|       border-radius: 0.5rem; | ||||
|     }} | ||||
|     code {{ | ||||
|       font-family: SFMono-Regular, Consolas, 'Liberation Mono', monospace; | ||||
|       font-size: 0.95em; | ||||
|     }} | ||||
|     table {{ | ||||
|       width: 100%; | ||||
|       border-collapse: collapse; | ||||
|       margin: 1rem 0; | ||||
|     }} | ||||
|     th, td {{ | ||||
|       border: 1px solid #4b5563; | ||||
|       padding: 0.5rem; | ||||
|       text-align: left; | ||||
|     }} | ||||
|     a {{ | ||||
|       color: #2563eb; | ||||
|     }} | ||||
|     footer {{ | ||||
|       margin-top: 3rem; | ||||
|       font-size: 0.85rem; | ||||
|       color: #6b7280; | ||||
|     }} | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
|   <main> | ||||
| {body} | ||||
|   </main> | ||||
|   <footer> | ||||
|     <p>Generated on {generated_at} UTC · Source: {source}</p> | ||||
|   </footer> | ||||
| </body> | ||||
| </html> | ||||
| """ | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class DocEntry: | ||||
|     source: Path | ||||
|     output: Path | ||||
|     title: str | ||||
|  | ||||
|     def to_manifest(self) -> dict[str, str]: | ||||
|         return { | ||||
|             "source": self.source.as_posix(), | ||||
|             "output": self.output.as_posix(), | ||||
|             "title": self.title, | ||||
|         } | ||||
|  | ||||
|  | ||||
| def discover_markdown_files(source_root: Path) -> Iterable[Path]: | ||||
|     for path in source_root.rglob("*.md"): | ||||
|         if path.is_file(): | ||||
|             yield path | ||||
|  | ||||
|  | ||||
| def read_title(markdown_text: str, fallback: str) -> str: | ||||
|     for raw_line in markdown_text.splitlines(): | ||||
|         line = raw_line.strip() | ||||
|         if line.startswith("#"): | ||||
|             return line.lstrip("#").strip() or fallback | ||||
|     return fallback | ||||
|  | ||||
|  | ||||
| def convert_markdown(path: Path, source_root: Path, output_root: Path) -> DocEntry: | ||||
|     relative = path.relative_to(source_root) | ||||
|     output_path = output_root / relative.with_suffix(".html") | ||||
|     output_path.parent.mkdir(parents=True, exist_ok=True) | ||||
|  | ||||
|     text = path.read_text(encoding="utf-8") | ||||
|     html_body = markdown.markdown(text, extensions=MD_EXTENSIONS) | ||||
|  | ||||
|     title = read_title(text, fallback=relative.stem.replace("_", " ")) | ||||
|     generated_at = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") | ||||
|  | ||||
|     output_path.write_text( | ||||
|         HTML_TEMPLATE.format( | ||||
|             title=title, | ||||
|             body=html_body, | ||||
|             generated_at=generated_at, | ||||
|             source=relative.as_posix(), | ||||
|         ), | ||||
|         encoding="utf-8", | ||||
|     ) | ||||
|  | ||||
|     return DocEntry(source=relative, output=output_path.relative_to(output_root), title=title) | ||||
|  | ||||
|  | ||||
| def copy_static_assets(source_root: Path, output_root: Path) -> None: | ||||
|     for path in source_root.rglob("*"): | ||||
|         if path.is_dir() or path.suffix.lower() == ".md": | ||||
|             # Skip Markdown (already rendered separately). | ||||
|             continue | ||||
|         relative = path.relative_to(source_root) | ||||
|         destination = output_root / relative | ||||
|         destination.parent.mkdir(parents=True, exist_ok=True) | ||||
|         destination.write_bytes(path.read_bytes()) | ||||
|         logging.info("Copied asset %s", relative) | ||||
|  | ||||
|  | ||||
| def write_manifest(entries: Iterable[DocEntry], output_root: Path) -> None: | ||||
|     manifest_path = output_root / "manifest.json" | ||||
|     manifest = [entry.to_manifest() for entry in entries] | ||||
|     manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8") | ||||
|     logging.info("Wrote manifest with %d entries", len(manifest)) | ||||
|  | ||||
|  | ||||
| def write_index(entries: List[DocEntry], output_root: Path) -> None: | ||||
|     index_path = output_root / "index.html" | ||||
|     generated_at = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") | ||||
|  | ||||
|     items = "\n".join( | ||||
|         f"      <li><a href='{entry.output.as_posix()}'>{entry.title}</a>" f" · <code>{entry.source.as_posix()}</code></li>" | ||||
|         for entry in sorted(entries, key=lambda e: e.title.lower()) | ||||
|     ) | ||||
|  | ||||
|     html = f"""<!DOCTYPE html> | ||||
| <html lang=\"en\"> | ||||
| <head> | ||||
|   <meta charset=\"utf-8\" /> | ||||
|   <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> | ||||
|   <title>Stella Ops Documentation Index</title> | ||||
|   <style> | ||||
|     body {{ | ||||
|       margin: 2.5rem auto; | ||||
|       padding: 0 1.5rem; | ||||
|       max-width: 70ch; | ||||
|       font-family: system-ui, -apple-system, 'Segoe UI', sans-serif; | ||||
|       line-height: 1.6; | ||||
|     }} | ||||
|     h1 {{ font-size: 2.25rem; margin-bottom: 1rem; }} | ||||
|     ul {{ list-style: none; padding: 0; }} | ||||
|     li {{ margin-bottom: 0.75rem; }} | ||||
|     code {{ background: #11182714; padding: 0.2rem 0.35rem; border-radius: 0.35rem; }} | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
|   <h1>Stella Ops Documentation</h1> | ||||
|   <p>Generated on {generated_at} UTC</p> | ||||
|   <ul> | ||||
| {items} | ||||
|   </ul> | ||||
| </body> | ||||
| </html> | ||||
| """ | ||||
|     index_path.write_text(html, encoding="utf-8") | ||||
|     logging.info("Wrote HTML index with %d entries", len(entries)) | ||||
|  | ||||
|  | ||||
| def parse_args() -> argparse.Namespace: | ||||
|     parser = argparse.ArgumentParser(description="Render documentation bundle") | ||||
|     parser.add_argument("--source", default="docs", type=Path, help="Directory containing Markdown sources") | ||||
|     parser.add_argument("--output", default=Path("build/docs-site"), type=Path, help="Directory for rendered output") | ||||
|     parser.add_argument("--clean", action="store_true", help="Remove the output directory before rendering") | ||||
|     return parser.parse_args() | ||||
|  | ||||
|  | ||||
| def main() -> int: | ||||
|     logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") | ||||
|     args = parse_args() | ||||
|  | ||||
|     source_root: Path = args.source.resolve() | ||||
|     output_root: Path = args.output.resolve() | ||||
|  | ||||
|     if not source_root.exists(): | ||||
|         logging.error("Source directory %s does not exist", source_root) | ||||
|         return os.EX_NOINPUT | ||||
|  | ||||
|     if args.clean and output_root.exists(): | ||||
|         logging.info("Cleaning existing output directory %s", output_root) | ||||
|         shutil.rmtree(output_root) | ||||
|  | ||||
|     output_root.mkdir(parents=True, exist_ok=True) | ||||
|  | ||||
|     entries: List[DocEntry] = [] | ||||
|     for md_file in discover_markdown_files(source_root): | ||||
|         entry = convert_markdown(md_file, source_root, output_root) | ||||
|         entries.append(entry) | ||||
|         logging.info("Rendered %s -> %s", entry.source, entry.output) | ||||
|  | ||||
|     write_manifest(entries, output_root) | ||||
|     write_index(entries, output_root) | ||||
|     copy_static_assets(source_root, output_root) | ||||
|  | ||||
|     logging.info("Documentation bundle available at %s", output_root) | ||||
|     return os.EX_OK | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     raise SystemExit(main()) | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| Param( | ||||
|     [Parameter(ValueFromRemainingArguments = $true)] | ||||
|     [string[]] $RestArgs | ||||
| ) | ||||
|  | ||||
| $Root = Split-Path -Parent $PSScriptRoot | ||||
| $env:UPDATE_GOLDENS = "1" | ||||
|  | ||||
| dotnet test (Join-Path $Root "src/StellaOps.Feedser.Models.Tests/StellaOps.Feedser.Models.Tests.csproj") @RestArgs | ||||
| Param( | ||||
|     [Parameter(ValueFromRemainingArguments = $true)] | ||||
|     [string[]] $RestArgs | ||||
| ) | ||||
|  | ||||
| $Root = Split-Path -Parent $PSScriptRoot | ||||
| $env:UPDATE_GOLDENS = "1" | ||||
|  | ||||
| dotnet test (Join-Path $Root "src/StellaOps.Feedser.Models.Tests/StellaOps.Feedser.Models.Tests.csproj") @RestArgs | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| #!/usr/bin/env bash | ||||
| set -euo pipefail | ||||
|  | ||||
| ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||||
|  | ||||
| export UPDATE_GOLDENS=1 | ||||
|  | ||||
| dotnet test "$ROOT_DIR/src/StellaOps.Feedser.Models.Tests/StellaOps.Feedser.Models.Tests.csproj" "$@" | ||||
| #!/usr/bin/env bash | ||||
| set -euo pipefail | ||||
|  | ||||
| ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||||
|  | ||||
| export UPDATE_GOLDENS=1 | ||||
|  | ||||
| dotnet test "$ROOT_DIR/src/StellaOps.Feedser.Models.Tests/StellaOps.Feedser.Models.Tests.csproj" "$@" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user