Files
stack/dev/scripts/gen_config.py
kert bd8cda1931
Some checks failed
CI / skinny-install (aco) (push) Successful in 1m14s
CI / skinny-install (bib) (push) Successful in 5m47s
CI / skinny-install (api) (push) Successful in 37s
CI / skinny-install (bcda) (push) Successful in 37s
CI / skinny-install (bls) (push) Successful in 40s
CI / skinny-install (ccw) (push) Successful in 41s
CI / skinny-install (cli) (push) Successful in 42s
CI / skinny-install (cms) (push) Successful in 39s
CI / skinny-install (conf) (push) Successful in 41s
CI / skinny-install (opps) (push) Successful in 39s
CI / skinny-install (perf) (push) Successful in 40s
CI / skinny-install (pfs) (push) Successful in 50s
CI / skinny-install (rex) (push) Successful in 33s
Deploy / build-scan-report (push) Failing after 12m6s
Infra CI / notebooks (push) Successful in 22s
Infra CI / zotero (push) Successful in 12s
Infra CI / docs (push) Successful in 11s
Infra CI / api (push) Successful in 12s
Infra CI / mc (push) Successful in 22s
CI / lint-test (push) Failing after 41m1s
remove: scrub all woodpecker references from codebase
Clean 17 files across src/, tests/, dev/, stack.toml, deploy.sh:
- api/auth/provision.py: remove WoodpeckerClient, provision_woodpecker,
  _get_woodpecker_token, woodpecker field from ProvisionResult
- api/auth/manifest.py: remove woodpecker from CREDENTIALS + Provisioner
- api/diag: remove woodpecker log fetching
- sem/hooks.py: remove woodpecker sync step
- stack.toml: remove [services.woodpecker] config
- deploy.sh: remove woodpecker deploy steps
- dev/scripts: remove woodpecker from config gen, secrets, readme
- tests: remove all woodpecker assertions and test cases

Zero woodpecker references remain in the codebase.
2026-04-18 18:51:21 -04:00

263 lines
8.3 KiB
Python

"""Generate all derived config files from stack.toml.
Reads [platform], [services], [images], and [ci] from stack.toml.
Dispatches to the active CI backend emitter (gitea or github)
for pipeline YAML, plus generates coredns config.
Usage::
uv run python dev/scripts/gen_config.py # generate all
uv run python dev/scripts/gen_config.py --check # verify up-to-date
uv run python dev/scripts/gen_config.py --backend X # override backend
"""
from __future__ import annotations
import argparse
import sys
from conf import ROOT, cfg
# ── Platform values ──────────────────────────────────────────────
DOMAIN = cfg.platform.domain
HOST_IP = cfg.platform.host_ip
def _platform_dict() -> dict:
return {
"registry": cfg.platform.registry,
"org": cfg.platform.repo.split("/")[0],
"image_prefix": cfg.platform.image_prefix,
"domain": DOMAIN,
"host_ip": HOST_IP,
"repo": cfg.platform.repo,
}
# ── Image definitions ────────────────────────────────────────────
def load_images() -> list[dict]:
"""Read [images] from stack.toml, merge each with defaults."""
raw = dict(cfg._data.get("images", {}))
defaults = dict(raw.pop("defaults", {}))
images = []
for name, overrides in raw.items():
merged = {**defaults, **overrides, "name": name}
images.append(merged)
return images
def scannable(images: list[dict]) -> list[dict]:
return [i for i in images if i.get("scan", True)]
# ── CoreDNS ──────────────────────────────────────────────────────
def gen_coredns() -> dict[str, str]:
subdomains = cfg._data.get("platform", {}).get("subdomains", [])
lines = [f"{HOST_IP} {DOMAIN}"] # bare domain always first
for sub in subdomains:
lines.append(f"{HOST_IP} {sub}.{DOMAIN}")
hosts = "\n".join(lines) + "\n"
corefile = f"""{DOMAIN} {{
hosts /etc/coredns/hosts {{
fallthrough
}}
log
}}
. {{
forward . 1.1.1.1 8.8.8.8
cache 300
}}
"""
return {"infra/coredns/hosts": hosts, "infra/coredns/Corefile": corefile}
# ── Backend dispatch ─────────────────────────────────────────────
STALE_DIRS: dict[str, list[str]] = {
"github": [".gitea/workflows"],
"gitea": [".github/workflows"],
}
def _emit_backend(backend: str) -> dict[str, str]:
"""Dispatch to the correct CI backend emitter."""
images = load_images()
scans = scannable(images)
platform = _platform_dict()
ci_data = cfg._data.get("ci", {})
ci_cfg = ci_data.get(backend, {})
# Propagate top-level coverage_threshold so each backend can read it.
ci_cfg.setdefault("coverage_threshold", ci_data.get("coverage_threshold", 99))
if backend == "github":
from backends.github import emit
elif backend == "gitea":
from backends.gitea import emit
else:
print(f"Unknown CI backend: {backend}", file=sys.stderr)
sys.exit(1)
return emit(images, scans, platform, ci_cfg)
def _clean_stale(backend: str, generated_files: dict[str, str]) -> list[str]:
"""Return stale files from inactive backend directories."""
stale_dirs = STALE_DIRS.get(backend, [])
removed = []
for stale_dir in stale_dirs:
stale_path = ROOT / stale_dir
if not stale_path.exists():
continue
for f in sorted(stale_path.glob("*.yml")):
rel = str(f.relative_to(ROOT))
if rel not in generated_files:
content = f.read_text()
if "generated by gen_config.py" in content:
removed.append(rel)
return removed
# ── Main ─────────────────────────────────────────────────────────
def _emit_dab(*, sql: bool = False) -> dict[str, str]:
"""Generate databricks.yml (and optionally SQL/DDL) from pipeline registry.
When *sql* is False (default), only databricks.yml is emitted — fast
enough for --check in CI. When True, also transpiles all pipeline
expressions to SQL and generates DDL (1500+ files, ~4 minutes).
"""
if "databricks" not in cfg._data:
return {}
from backends.databricks import emit
return emit(cfg._data, sql=sql)
def generate(backend: str | None = None, *, dab_sql: bool = False) -> dict[str, str]:
if backend is None:
backend = cfg._data.get("ci", {}).get("backend", "gitea")
files: dict[str, str] = {}
files.update(gen_coredns())
files.update(_emit_backend(backend))
files.update(_emit_dab(sql=dab_sql))
return files
def write_all(files: dict[str, str]) -> None:
for relpath, content in sorted(files.items()):
out = ROOT / relpath
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(content)
print(f" wrote {relpath}")
def check_all(files: dict[str, str]) -> list[str]:
diffs = []
for relpath, expected in sorted(files.items()):
out = ROOT / relpath
if not out.exists():
diffs.append(f"{relpath}: missing")
elif out.read_text() != expected:
diffs.append(f"{relpath}: out of date")
return diffs
def check_modules() -> list[str]:
"""Verify module-name and optional-dependencies stay in sync."""
import tomllib
pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text())
module_names = set(
pyproject.get("tool", {})
.get("uv", {})
.get("build-backend", {})
.get("module-name", [])
)
opt_deps = set(pyproject.get("project", {}).get("optional-dependencies", {}).keys())
# Groups that are aggregates or cloud providers, not Python modules
meta_groups = {"all", "lake", "dev", "aws", "gcp", "azure"}
errors = []
for mod in sorted(module_names):
if mod not in opt_deps and mod not in meta_groups:
errors.append(
f"module '{mod}' in [tool.uv.build-backend].module-name "
f"has no matching [project.optional-dependencies] group"
)
for group in sorted(opt_deps - meta_groups):
if group not in module_names:
errors.append(
f"optional-dep group '{group}' has no matching "
f"module in [tool.uv.build-backend].module-name"
)
return errors
def main() -> int:
parser = argparse.ArgumentParser(
description="Generate derived config from stack.toml [ci] backend"
)
parser.add_argument(
"--check",
action="store_true",
help="Verify files are up-to-date (exit 1 if not)",
)
parser.add_argument(
"--backend",
default=None,
help="Override CI backend (gitea|github)",
)
parser.add_argument(
"--dab-sql",
action="store_true",
help="Also generate bundle/sql/ and bundle/ddl/ (slow, ~4 min)",
)
args = parser.parse_args()
files = generate(args.backend, dab_sql=args.dab_sql)
if args.check:
diffs = check_all(files)
mod_errors = check_modules()
all_errors = diffs + mod_errors
if all_errors:
if diffs:
print("Files out of date:")
for d in diffs:
print(f" {d}")
if mod_errors:
print("Module/dependency sync errors:")
for e in mod_errors:
print(f" {e}")
print("\nRun: uv run python dev/scripts/gen_config.py")
return 1
print("All generated files are up-to-date.")
print("Module names and optional-dependencies are in sync.")
return 0
write_all(files)
# Clean stale files from inactive backend
backend = args.backend or cfg._data.get("ci", {}).get("backend", "gitea")
stale = _clean_stale(backend, files)
for s in stale:
(ROOT / s).unlink()
print(f" removed stale {s}")
print(f"\nGenerated {len(files)} files from stack.toml (backend: {backend})")
return 0
if __name__ == "__main__":
sys.exit(main())