#!/usr/bin/env python3 """Merge service variables into the server env file (runs on the server, stdlib only). env_merge.py TARGET SOURCE Keys from SOURCE overwrite / add to TARGET; keys only in TARGET stay (server-generated secrets: DB passwords, PSEUDO_KEY, PUBLIC_URL). Protected keys are never taken from SOURCE. Prints changed key NAMES only (no values). Exit code: 0 = changed, 3 = nothing changed. """ import os import sys import tempfile PROTECTED = { "POSTGRES_PASSWORD", "IDENTITY_DB_PASSWORD", "CLINICAL_DB_PASSWORD", "PSEUDO_KEY", "PUBLIC_URL", "IDENTITY_DB_URL", "CLINICAL_DB_URL", "API_BASE_URL", } def parse(path: str) -> list[tuple[str | None, str]]: """Lines as (key, raw_line); comments/blank lines have key None. Keeps order and comments.""" out: list[tuple[str | None, str]] = [] with open(path, encoding="utf-8") as f: for raw in f.read().splitlines(): line = raw.strip() key = line.split("=", 1)[0].strip() if line and not line.startswith("#") and "=" in line else None out.append((key, raw)) return out def merge(target: str, source: str) -> list[str]: lines = parse(target) incoming = { k: raw for k, raw in parse(source) if k and k not in PROTECTED and not k.startswith("GITEA_") and raw.split("=", 1)[1].strip() } changed: list[str] = [] seen: set[str] = set() for i, (k, raw) in enumerate(lines): if k in incoming: seen.add(k) if raw != incoming[k]: lines[i] = (k, incoming[k]) changed.append(k) for k, raw in incoming.items(): if k not in seen: lines.append((k, raw)) changed.append(k) if changed: fd, tmp = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(target))) with os.fdopen(fd, "w", encoding="utf-8") as f: f.write("\n".join(raw for _, raw in lines) + "\n") st = os.stat(target) os.chmod(tmp, 0o600) if os.geteuid() == 0: os.chown(tmp, st.st_uid, st.st_gid) os.replace(tmp, target) return changed def main() -> int: changed = merge(sys.argv[1], sys.argv[2]) if not changed: print("env: no changes") return 3 print("env: updated " + ", ".join(sorted(changed))) return 0 if __name__ == "__main__": sys.exit(main())