feat: ship blog platform admin and deploy stack

This commit is contained in:
2026-03-31 21:48:39 +08:00
parent a9a05aa105
commit 313f174fbc
210 changed files with 25476 additions and 5803 deletions

View File

@@ -0,0 +1,122 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import Any
try:
import yaml
except ImportError as exc: # pragma: no cover
raise SystemExit(
"Missing dependency: PyYAML. Install it with `python -m pip install pyyaml`."
) from exc
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Render docker compose .env from deploy config.yaml"
)
parser.add_argument(
"--input",
default="deploy/docker/config.yaml",
help="Path to config.yaml (default: deploy/docker/config.yaml)",
)
parser.add_argument(
"--output",
default="deploy/docker/.env",
help="Output dotenv file path (default: deploy/docker/.env)",
)
parser.add_argument(
"--section",
default="compose_env",
help="Top-level mapping section to export (default: compose_env)",
)
parser.add_argument(
"--stdout",
action="store_true",
help="Print rendered dotenv to stdout instead of writing file",
)
return parser.parse_args()
def load_config(path: Path) -> dict[str, Any]:
if not path.exists():
raise SystemExit(f"Config file not found: {path}")
data = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise SystemExit("config.yaml root must be a mapping/object")
return data
def encode_env_value(value: Any) -> str:
if value is None:
return '""'
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return str(value)
if not isinstance(value, str):
raise SystemExit(f"compose_env only supports scalar values, got: {type(value).__name__}")
if value == "":
return '""'
needs_quotes = any(ch in value for ch in [' ', '#', '"', "'", '\t', '\n', '\r']) or value.startswith('$')
if not needs_quotes:
return value
escaped = (
value.replace('\\', '\\\\')
.replace('"', '\\"')
.replace('\n', '\\n')
.replace('\r', '\\r')
.replace('\t', '\\t')
)
return f'"{escaped}"'
def render_env(section_name: str, values: dict[str, Any], source_path: Path) -> str:
lines = [
f"# Generated from {source_path.as_posix()}::{section_name}",
"# Do not edit this file directly; edit config.yaml and re-render.",
"",
]
for key, value in values.items():
if not isinstance(key, str) or not key:
raise SystemExit(f"Invalid env key: {key!r}")
lines.append(f"{key}={encode_env_value(value)}")
lines.append("")
return "\n".join(lines)
def main() -> int:
args = parse_args()
source_path = Path(args.input)
output_path = Path(args.output)
data = load_config(source_path)
section = data.get(args.section)
if not isinstance(section, dict):
raise SystemExit(
f"Section `{args.section}` must exist in config.yaml and must be a mapping/object"
)
rendered = render_env(args.section, section, source_path)
if args.stdout:
sys.stdout.write(rendered)
return 0
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8", newline="\n")
print(f"Wrote {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())