#!/usr/bin/env bash # TOML syntax + structural sanity for every manifest in this repo. # Used by `make validate` and by .gitea/workflows/ci.yaml. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" python3 - "$ROOT" <<'PY' import sys, tomllib, pathlib root = pathlib.Path(sys.argv[1]) errs = [] count = 0 # Cluster → expected node names. Each cluster ships its own VM(s); a service's # placement.node must be one of the cluster's declared VMs. CLUSTER_NODES = { 'breakpilot-edge': {'vm-edge'}, 'breakpilot-control': {'vm-control'}, 'breakpilot-app': {'vm-app-prod', 'vm-app-stage'}, } for p in sorted(root.glob('clusters/*/services/**/*.toml')): count += 1 try: data = tomllib.load(open(p, 'rb')) except Exception as e: errs.append(f'{p}: TOML parse: {e}') continue svcs = data.get('service') if not svcs: errs.append(f'{p}: no [[service]] block') continue # cluster name = first path component under clusters/ rel = p.relative_to(root) cluster = rel.parts[1] # clusters//services/... expected_nodes = CLUSTER_NODES.get(cluster) if expected_nodes is None: errs.append(f'{p}: unknown cluster "{cluster}" (expected one of {sorted(CLUSTER_NODES)})') continue for svc in svcs: for required in ('name', 'image'): if required not in svc: errs.append(f'{p}: service missing required field "{required}"') # forbidden nesting bugs for sub in ('placement', 'resources', 'env', 'volume'): if isinstance(svc.get(sub), dict): for fb in ('depends_on', 'extra_ports', 'cmd', 'mounts'): if fb in svc[sub]: errs.append(f'{p}: "{fb}" nested under [service.{sub}] — must be at [[service]] level') node = (svc.get('placement') or {}).get('node') if not node: errs.append(f'{p}: missing placement.node') elif node not in expected_nodes: errs.append(f'{p}: placement.node "{node}" not valid for cluster "{cluster}" (expected one of {sorted(expected_nodes)})') mem = (svc.get('resources') or {}).get('memory') if not mem: errs.append(f'{p}: missing resources.memory (mandatory per §8 rule 5)') # Validate overlays parse too for p in sorted(root.glob('overlays/*/overlay.toml')): count += 1 try: tomllib.load(open(p, 'rb')) except Exception as e: errs.append(f'{p}: TOML parse: {e}') print(f'checked {count} files') for e in errs: print(' ', e) sys.exit(1 if errs else 0) PY