Coverage for src / rhiza_hooks / check_rhiza_config.py: 100%
110 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 08:53 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 08:53 +0000
1#!/usr/bin/env python3
2"""Check that .rhiza/template.yml is valid and well-formed."""
4from __future__ import annotations
6import argparse
7import sys
8from pathlib import Path
10import yaml
12REQUIRED_KEYS = {"template-repository", "template-branch"}
13OPTIONAL_KEYS = {"include", "exclude", "templates"}
14VALID_KEYS = REQUIRED_KEYS | OPTIONAL_KEYS
17def _load_config(filepath: Path) -> dict | list[str]:
18 """Load configuration from YAML file.
20 Returns:
21 Config dict on success, or list of error messages on failure
22 """
23 try:
24 with filepath.open() as f:
25 config = yaml.safe_load(f)
26 except yaml.YAMLError as e:
27 return [f"Invalid YAML: {e}"]
28 except FileNotFoundError:
29 return [f"File not found: {filepath}"]
31 if config is None:
32 return ["Configuration file is empty"]
34 if not isinstance(config, dict):
35 return ["Configuration must be a YAML mapping"]
37 return config
40def _validate_required_keys(config: dict) -> list[str]:
41 """Validate required keys are present."""
42 errors = []
43 for key in REQUIRED_KEYS:
44 if key not in config:
45 errors.append(f"Missing required key: {key}")
46 return errors
49def _validate_unknown_keys(config: dict) -> list[str]:
50 """Check for unknown keys."""
51 errors = []
52 for key in config:
53 if key not in VALID_KEYS:
54 errors.append(f"Unknown key: {key}")
55 return errors
58def _validate_include_or_templates(config: dict) -> list[str]:
59 """Ensure at least one of 'include' or 'templates' is present."""
60 if "include" not in config and "templates" not in config:
61 return ["At least one of 'include' or 'templates' must be present"]
62 return []
65def _validate_template_repository(config: dict) -> list[str]:
66 """Validate template-repository field."""
67 errors = []
68 if "template-repository" in config:
69 repo = config["template-repository"]
70 if not isinstance(repo, str):
71 errors.append("template-repository must be a string")
72 elif "/" not in repo:
73 errors.append(f"template-repository should be in 'owner/repo' format, got: {repo}")
74 return errors
77def _validate_template_branch(config: dict) -> list[str]:
78 """Validate template-branch field."""
79 errors = []
80 if "template-branch" in config:
81 branch = config["template-branch"]
82 if not isinstance(branch, str):
83 errors.append("template-branch must be a string")
84 elif not branch:
85 errors.append("template-branch cannot be empty")
86 return errors
89def _validate_include_field(config: dict) -> list[str]:
90 """Validate include field."""
91 errors = []
92 if "include" in config:
93 include = config["include"]
94 if not isinstance(include, list):
95 errors.append("include must be a list")
96 elif not include:
97 errors.append("include list cannot be empty")
98 return errors
101def _validate_templates_field(config: dict) -> list[str]:
102 """Validate templates field."""
103 errors = []
104 if "templates" in config:
105 templates = config["templates"]
106 if not isinstance(templates, list):
107 errors.append("templates must be a list")
108 elif not templates:
109 errors.append("templates list cannot be empty")
110 return errors
113def _validate_exclude_field(config: dict) -> list[str]:
114 """Validate exclude field."""
115 errors = []
116 if "exclude" in config:
117 exclude = config["exclude"]
118 if exclude is not None and not isinstance(exclude, list):
119 errors.append("exclude must be a list or null")
120 return errors
123def validate_rhiza_config(filepath: Path) -> list[str]:
124 """Validate a rhiza configuration file.
126 Args:
127 filepath: Path to the .rhiza/template.yml file
129 Returns:
130 List of error messages (empty if valid)
131 """
132 # Load configuration
133 config = _load_config(filepath)
134 if isinstance(config, list):
135 return config
137 # Validate all aspects
138 errors = []
139 errors.extend(_validate_required_keys(config))
140 errors.extend(_validate_unknown_keys(config))
141 errors.extend(_validate_include_or_templates(config))
142 errors.extend(_validate_template_repository(config))
143 errors.extend(_validate_template_branch(config))
144 errors.extend(_validate_include_field(config))
145 errors.extend(_validate_templates_field(config))
146 errors.extend(_validate_exclude_field(config))
148 return errors
151def main(argv: list[str] | None = None) -> int:
152 """Main entry point for the hook."""
153 parser = argparse.ArgumentParser(description="Validate .rhiza/template.yml configuration")
154 parser.add_argument(
155 "filenames",
156 nargs="*",
157 help="Filenames to check",
158 )
159 args = parser.parse_args(argv)
161 retval = 0
162 for filename in args.filenames:
163 filepath = Path(filename)
164 errors = validate_rhiza_config(filepath)
165 if errors:
166 print(f"{filename}:")
167 for error in errors:
168 print(f" - {error}")
169 retval = 1
171 return retval
174if __name__ == "__main__":
175 sys.exit(main())