-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathgithub_loader.py
More file actions
153 lines (124 loc) · 5.95 KB
/
Copy pathgithub_loader.py
File metadata and controls
153 lines (124 loc) · 5.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""
GitHub-based rule loader.
Loads rules from GitHub repository files, implementing the RuleLoader interface.
"""
import logging
from typing import Any
import yaml
from src.core.config import config
from src.core.models import EventType
from src.integrations.github import GitHubClient, github_client
from src.rules.interface import RuleLoader
from src.rules.models import Rule, RuleAction, RuleSeverity, RuleWhen
from src.rules.registry import CONDITION_CLASS_TO_RULE_ID, ConditionRegistry
logger = logging.getLogger(__name__)
class RulesFileNotFoundError(Exception):
"""Raised when the rules file is not found in the repository."""
pass
class GitHubRuleLoader(RuleLoader):
"""
Loads rules from a GitHub repository's rules yaml file.
This loader maps parameters to condition types using the ConditionRegistry.
"""
def __init__(self, client: GitHubClient):
self.github_client = client
async def get_rules(self, repository: str, installation_id: int) -> list[Rule]:
try:
# Construct the rules file path using config
rules_file_path = f"{config.repo_config.base_path}/{config.repo_config.rules_file}"
logger.info(f"Fetching rules for repository: {repository} (installation: {installation_id})")
content = await self.github_client.get_file_content(repository, rules_file_path, installation_id)
if not content:
logger.warning(f"No rules.yaml file found in {repository}")
raise RulesFileNotFoundError(f"Rules file not found: {rules_file_path}")
rules_data = yaml.safe_load(content)
if not isinstance(rules_data, dict) or "rules" not in rules_data:
logger.warning(f"No rules found in {repository}/{rules_file_path}")
return []
rules = []
if not isinstance(rules_data["rules"], list):
logger.warning(f"Rules key is not a list in {repository}/{rules_file_path}")
return []
for rule_data in rules_data["rules"]:
try:
if not isinstance(rule_data, dict):
continue
# Skip disabled rules
if str(rule_data.get("enabled", True)).lower() == "false":
logger.info(f"Skipping disabled rule: {rule_data.get('description', 'unknown')}")
continue
rule = GitHubRuleLoader._parse_rule(rule_data)
if rule:
rules.append(rule)
except Exception as e:
rule_description = rule_data.get("description", "unknown")
logger.error(f"Error parsing rule {rule_description}: {e}")
continue
logger.info(f"Successfully loaded {len(rules)} rules from {repository}")
return rules
except RulesFileNotFoundError:
# Re-raise this specific exception
raise
except Exception as e:
logger.error(f"Error fetching rules for {repository}: {e}")
raise
@staticmethod
def _parse_rule(rule_data: dict[str, Any]) -> Rule:
# Validate required fields
if "description" not in rule_data:
raise ValueError("Rule must have 'description' field")
event_types = []
if "event_types" in rule_data:
for event_type_str in rule_data["event_types"]:
try:
event_type = EventType(event_type_str)
event_types.append(event_type)
except ValueError:
logger.warning(f"Unknown event type: {event_type_str}")
# Get parameters (strip internal "validator" key; engine infers validator from parameter names)
parameters = dict(rule_data.get("parameters", {}))
parameters.pop("validator", None)
# Instantiate conditions using Registry (matches on parameter keys, e.g. max_lines, require_linked_issue)
conditions = ConditionRegistry.get_conditions_for_parameters(parameters)
# Set rule_id from first condition that has a RuleID (for acknowledgment lookup).
# Multi-condition rules use the first mapped ID; conditions not in CONDITION_CLASS_TO_RULE_ID yield None.
rule_id_val: str | None = None
for cond in conditions:
rid = CONDITION_CLASS_TO_RULE_ID.get(type(cond))
if rid is not None:
rule_id_val = rid.value
break
# Parse optional `when:` block (structured predicates controlling rule applicability).
when_block: RuleWhen | None = None
when_data = rule_data.get("when")
if when_data is not None:
if isinstance(when_data, dict):
try:
when_block = RuleWhen(**when_data)
except Exception as e:
logger.warning(
f"Invalid `when` block in rule '{rule_data.get('description', 'unknown')}': {e} — ignoring"
)
else:
logger.warning(
f"`when` block in rule '{rule_data.get('description', 'unknown')}' is not a mapping — ignoring"
)
# Actions are optional and not mapped
actions = []
if "actions" in rule_data:
for action_data in rule_data["actions"]:
action = RuleAction(type=action_data["type"], parameters=action_data.get("parameters", {}))
actions.append(action)
rule = Rule(
description=rule_data["description"],
enabled=rule_data.get("enabled", True),
severity=RuleSeverity(rule_data.get("severity", "medium")),
event_types=event_types,
conditions=conditions,
actions=actions,
parameters=parameters,
rule_id=rule_id_val,
when=when_block,
)
return rule
github_rule_loader = GitHubRuleLoader(github_client)