"""Shared utilities for the publish-site action.""" import os import shutil import subprocess import sys from pathlib import Path import yaml from jinja2 import Environment, FileSystemLoader APPS_REPO = "fritzlab/apps" GITEA_HOST = "code.fritzlab.net" NAMESPACE = "websites" DEFAULT_S3_ENDPOINT = "http://garage.storage.svc:3900" EXCLUDE_FILES = { ".git", ".gitea", ".gitignore", "site.yaml", "build", "Makefile", "README.md", "CLAUDE.md", "Dockerfile", ".dockerignore", "go.mod", "go.sum", } VALID_TYPES = {"static", "hugo", "mkdocs", "docker"} def k8s_name(name): """Sanitize for DNS-1035 label (dots → dashes).""" return name.replace(".", "-") def env(key, default=None): val = os.environ.get(key, default) if val is None: die(f"Missing required env var: {key}") return val def die(msg): print(f"ERROR: {msg}", file=sys.stderr) sys.exit(1) def run(cmd, **kwargs): print(f" $ {cmd}") return subprocess.run(cmd, shell=True, check=True, **kwargs) def parse_site_yaml(site_dir): path = Path(site_dir) / "site.yaml" if not path.exists(): die("site.yaml not found in repo root") with open(path) as f: cfg = yaml.safe_load(f) if not cfg.get("domain"): die("domain is required in site.yaml") site_type = cfg.get("type", "static") if site_type not in VALID_TYPES: die(f"Unknown site type: {site_type} (valid: {', '.join(sorted(VALID_TYPES))})") site = { "domain": cfg["domain"], "type": site_type, "enabled": cfg.get("enabled", True), "aliases": cfg.get("aliases") or [], } if site_type == "docker": if not cfg.get("image"): die("image is required in site.yaml for type: docker") site["image"] = cfg["image"] site["port"] = cfg.get("port", 8080) site["build_args"] = cfg.get("build_args") or {} site["health_path"] = cfg.get("health_path", "/healthz") site["replicas"] = cfg.get("replicas", 1) else: site["content_dir"] = cfg.get("content_dir", "") site["tidy"] = cfg.get("tidy", True) print("Site config:") for k, v in site.items(): print(f" {k}: {v}") return site def clone_apps(token): user = env("CI_BOT_USER", "ci-bot") apps_dir = Path("/tmp/apps-deploy") if apps_dir.exists(): shutil.rmtree(apps_dir) run(f"git clone --depth 1 https://{user}:{token}@{GITEA_HOST}/{APPS_REPO}.git {apps_dir}") run(f"git -C {apps_dir} config user.name {user}") run(f"git -C {apps_dir} config user.email {user}@fritzlab.net") return apps_dir def render_templates(action_dir, template_vars, app_dir, manifests_dir, site_type): """Render Jinja2 templates, selecting the right set for the site type.""" templates_dir = Path(action_dir) / "templates" jinja_env = Environment( loader=FileSystemLoader(str(templates_dir)), keep_trailing_newline=True, ) if site_type == "docker": service_tmpl = "service-docker.yaml.j2" tmpl_names = ["app.yaml.j2", "certificate.yaml.j2", "ingress.yaml.j2", "kustomization.yaml.j2", service_tmpl, "deployment.yaml.j2"] else: service_tmpl = "service-static.yaml.j2" tmpl_names = ["app.yaml.j2", "certificate.yaml.j2", "ingress.yaml.j2", "kustomization.yaml.j2", service_tmpl] for tmpl_name in tmpl_names: tmpl = jinja_env.get_template(tmpl_name) rendered = tmpl.render(**template_vars) out_name = tmpl_name.replace(".j2", "") # service-docker.yaml / service-static.yaml → service.yaml if out_name.startswith("service-"): out_name = "service.yaml" dest = app_dir / out_name if tmpl_name == "app.yaml.j2" else manifests_dir / out_name dest.write_text(rendered) print(f" Rendered {tmpl_name} -> {dest}") def commit_and_push(apps_dir, message): run(f"git -C {apps_dir} add -A") result = subprocess.run( f"git -C {apps_dir} diff --cached --quiet", shell=True, check=False, ) if result.returncode == 0: print("No manifest changes to commit") return False run(f"git -C {apps_dir} commit -m '{message}'") run(f"git -C {apps_dir} push") print("Manifests pushed — ArgoCD will sync") return True