"""Shared utilities for the site-publish 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-s3.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_DEPRECATION_MSG = """\ type: docker is no longer supported by action/site-publish. site-publish handles only static-content sites (static, hugo, mkdocs) that ship to Garage S3. For containerized web apps, use the standard image-producer chain: - uses: action/image-build@v1 # build + smoke-test - uses: action/image-push@v1 # push + prune - uses: action/image-deploy@v1 # apps repo image-pin Hand-author your apps-repo manifests once (Deployment, Service, Ingress, Certificate, kustomization with images: block) under sjc001/websites//manifests/. image-deploy will pin the tag on every CI run. See action/image-deploy README and sjc001/websites/rainsounds.vino.network/manifests/ for the canonical example.\ """ 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 == "docker": die(DOCKER_DEPRECATION_MSG) if site_type not in VALID_TYPES: die(f"Unknown site type: {site_type} (valid: {', '.join(sorted(VALID_TYPES))})") excludes = cfg.get("excludes") or [] if not isinstance(excludes, list) or any(not isinstance(p, str) for p in excludes): die("excludes must be a list of string patterns") site = { "domain": cfg["domain"], "type": site_type, "enabled": cfg.get("enabled", True), "aliases": cfg.get("aliases") or [], "content_dir": cfg.get("content_dir", ""), "tidy": cfg.get("tidy", True), "excludes": excludes, } 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): """Render Jinja2 templates for a static-content site.""" templates_dir = Path(action_dir) / "templates" jinja_env = Environment( loader=FileSystemLoader(str(templates_dir)), keep_trailing_newline=True, ) tmpl_names = ["app.yaml.j2", "certificate.yaml.j2", "ingress.yaml.j2", "kustomization.yaml.j2", "service.yaml.j2"] for tmpl_name in tmpl_names: tmpl = jinja_env.get_template(tmpl_name) rendered = tmpl.render(**template_vars) out_name = tmpl_name.replace(".j2", "") 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