1111from typing import Any
1212
1313import yaml
14+ from jsonschema import FormatChecker , validate
15+ from jsonschema .exceptions import ValidationError
1416
1517# Global files with settings for the app and org, e.g. GitHub token and org name
1618ORG_CONFIG_FILE = r"org\.ya?ml"
1719APP_CONFIG_FILE = r"app\.ya?ml"
1820TEAM_CONFIG_DIR = "teams"
1921TEAM_CONFIG_FILES = r".+\.ya?ml"
2022
23+ # Schemas for config validation
24+ APP_CONFIG_SCHEMA = {
25+ "type" : "object" ,
26+ "properties" : {
27+ "github_token" : {"type" : "string" },
28+ "github_app_id" : {"type" : "integer" },
29+ "github_app_private_key" : {"type" : "string" },
30+ "remove_members_without_team" : {"type" : "boolean" },
31+ "delete_unconfigured_teams" : {"type" : "boolean" },
32+ },
33+ "additionalProperties" : False ,
34+ }
35+ ORG_CONFIG_SCHEMA = {
36+ "type" : "object" ,
37+ "properties" : {
38+ "org_name" : {"type" : "string" },
39+ "org_owners" : {
40+ "type" : "array" ,
41+ "items" : {"type" : "string" },
42+ "minItems" : 1 ,
43+ },
44+ "defaults" : {
45+ "type" : "object" ,
46+ "properties" : {
47+ "team" : {
48+ "type" : "object" ,
49+ "properties" : {
50+ "description" : {"type" : "string" },
51+ "privacy" : {"type" : "string" , "enum" : ["secret" , "closed" ]},
52+ "notification_setting" : {
53+ "type" : "string" ,
54+ "enum" : ["notifications_enabled" , "notifications_disabled" ],
55+ },
56+ },
57+ "additionalProperties" : False ,
58+ }
59+ },
60+ "additionalProperties" : False ,
61+ },
62+ },
63+ "additionalProperties" : False ,
64+ "required" : ["org_name" , "org_owners" ],
65+ }
66+ TEAM_CONFIG_SCHEMA = {
67+ "type" : "object" ,
68+ "patternProperties" : {
69+ "^[a-zA-Z0-9 _\\ -]+$" : {
70+ "type" : "object" ,
71+ "properties" : {
72+ "description" : {"type" : "string" },
73+ "privacy" : {"type" : "string" , "enum" : ["secret" , "closed" ]},
74+ "notification_setting" : {
75+ "type" : "string" ,
76+ "enum" : ["notifications_enabled" , "notifications_disabled" ],
77+ },
78+ "maintainer" : {"type" : "array" , "items" : {"type" : "string" }},
79+ "member" : {"type" : "array" , "items" : {"type" : "string" }},
80+ "parent" : {"type" : "string" },
81+ "repos" : {
82+ "type" : "object" ,
83+ "propertyNames" : {"type" : "string" },
84+ "additionalProperties" : {
85+ "type" : "string" ,
86+ "enum" : ["pull" , "triage" , "push" , "maintain" , "admin" ],
87+ },
88+ },
89+ },
90+ "additionalProperties" : False ,
91+ }
92+ },
93+ "additionalProperties" : False ,
94+ "required" : [],
95+ }
96+
2197
2298def _find_matching_files (directory : str , pattern : str , only_one : bool = False ) -> list [str ]:
2399 """
@@ -85,6 +161,16 @@ def _read_config_file(file: str) -> dict:
85161 return config
86162
87163
164+ def _validate_config_schema (file : str , cfg : dict , schema : dict ) -> None :
165+ """Validate the config against a JSON schema"""
166+ try :
167+ validate (instance = cfg , schema = schema , format_checker = FormatChecker ())
168+ except ValidationError as e :
169+ logging .critical ("Config validation of file %s failed: %s" , file , e .message )
170+ raise ValueError (e ) from None
171+ logging .debug ("Config in file %s validated successfully against schema." , file )
172+
173+
88174def parse_config_files (path : str ) -> tuple [dict [str , str | dict [str , str ]], dict , dict ]:
89175 """Parse all relevant files in the configuration directory. Returns a tuple
90176 of org config, app config, and merged teams config"""
@@ -95,14 +181,17 @@ def parse_config_files(path: str) -> tuple[dict[str, str | dict[str, str]], dict
95181
96182 # Read and parse config files for app and org
97183 cfg_app = _read_config_file (cfg_app_files [0 ])
184+ _validate_config_schema (file = cfg_app_files [0 ], cfg = cfg_app , schema = APP_CONFIG_SCHEMA )
98185 cfg_org = _read_config_file (cfg_org_files [0 ])
186+ _validate_config_schema (file = cfg_org_files [0 ], cfg = cfg_org , schema = ORG_CONFIG_SCHEMA )
99187
100188 # For the teams config files, we parse and combine them as there may be multiple
101189 cfg_teams : dict [str , Any ] = {}
102190 # For this, merge the resulting dicts of the previously read files, and the current file
103191 # Compare their keys (team names). They must not be defined multiple times!
104192 for cfg_team_file in cfg_teams_files :
105193 cfg = _read_config_file (cfg_team_file )
194+ _validate_config_schema (file = cfg_team_file , cfg = cfg , schema = TEAM_CONFIG_SCHEMA )
106195 if overlap := set (cfg_teams .keys ()) & set (cfg .keys ()):
107196 logging .critical (
108197 "The config file '%s' contains keys that are also defined in "
0 commit comments