mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
feat(ci): enforce reusable workload plans (#8393)
* feat(ci): enforce reusable workload plans * fix(ci): skip incompatible convergence upgrades
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render strict text templates owned by GitHub automation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
TEMPLATES_ROOT = Path(__file__).resolve().parents[1] / "templates"
|
||||
ALLOWED_SUFFIXES = {".md", ".txt"}
|
||||
PLACEHOLDER = re.compile(r"{{\s*([a-z][a-z0-9_]*)\s*}}")
|
||||
|
||||
|
||||
class TemplateError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def resolve_template(path_value: str) -> Path:
|
||||
relative = Path(path_value)
|
||||
if relative.is_absolute():
|
||||
raise TemplateError("template path must be relative to .github/templates")
|
||||
path = (TEMPLATES_ROOT / relative).resolve()
|
||||
try:
|
||||
path.relative_to(TEMPLATES_ROOT.resolve())
|
||||
except ValueError as error:
|
||||
raise TemplateError("template path must stay within .github/templates") from error
|
||||
if path.suffix not in ALLOWED_SUFFIXES:
|
||||
raise TemplateError("template must use a .md or .txt extension")
|
||||
if not path.is_file():
|
||||
raise TemplateError(f"template does not exist: {path_value}")
|
||||
return path
|
||||
|
||||
|
||||
def template_parameters(source: str) -> set[str]:
|
||||
parameters = set(PLACEHOLDER.findall(source))
|
||||
remainder = PLACEHOLDER.sub("", source)
|
||||
if "{{" in remainder or "}}" in remainder:
|
||||
raise TemplateError("template contains an invalid placeholder")
|
||||
return parameters
|
||||
|
||||
|
||||
def render_template(path_value: str, params: dict[str, str]) -> str:
|
||||
source = resolve_template(path_value).read_text(encoding="utf-8")
|
||||
expected = template_parameters(source)
|
||||
missing = sorted(expected - params.keys())
|
||||
unknown = sorted(params.keys() - expected)
|
||||
if missing:
|
||||
raise TemplateError(f"missing template parameters: {', '.join(missing)}")
|
||||
if unknown:
|
||||
raise TemplateError(f"unknown template parameters: {', '.join(unknown)}")
|
||||
return PLACEHOLDER.sub(lambda match: params[match.group(1)], source)
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> tuple[str, dict[str, str], Path | None]:
|
||||
if not argv or argv[0] in {"-h", "--help"}:
|
||||
print(
|
||||
"usage: template.py <path> [--params-<name> <value>]... [--output <path>]",
|
||||
file=sys.stdout,
|
||||
)
|
||||
raise SystemExit(0)
|
||||
|
||||
path_value = argv[0]
|
||||
params: dict[str, str] = {}
|
||||
output: Path | None = None
|
||||
index = 1
|
||||
while index < len(argv):
|
||||
argument = argv[index]
|
||||
if "=" in argument:
|
||||
option, value = argument.split("=", 1)
|
||||
consumed = 1
|
||||
else:
|
||||
option = argument
|
||||
if index + 1 >= len(argv):
|
||||
raise TemplateError(f"missing value for {option}")
|
||||
value = argv[index + 1]
|
||||
consumed = 2
|
||||
|
||||
if option == "--output":
|
||||
if output is not None:
|
||||
raise TemplateError("--output may only be provided once")
|
||||
output = Path(value)
|
||||
elif option.startswith("--params-"):
|
||||
name = option.removeprefix("--params-").replace("-", "_")
|
||||
if not re.fullmatch(r"[a-z][a-z0-9_]*", name):
|
||||
raise TemplateError(f"invalid template parameter option: {option}")
|
||||
if name in params:
|
||||
raise TemplateError(f"duplicate template parameter: {name}")
|
||||
params[name] = value
|
||||
else:
|
||||
raise TemplateError(f"unknown option: {option}")
|
||||
index += consumed
|
||||
return path_value, params, output
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
try:
|
||||
path_value, params, output = parse_args(sys.argv[1:] if argv is None else argv)
|
||||
rendered = render_template(path_value, params)
|
||||
if output is None:
|
||||
sys.stdout.write(rendered)
|
||||
else:
|
||||
output.write_text(rendered, encoding="utf-8")
|
||||
return 0
|
||||
except TemplateError as error:
|
||||
print(f"template error: {error}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user