Skip to content

Commit 6561baa

Browse files
authored
Merge pull request #11 from warestack/refactor/rules-status-check-and-test-support
Improve missing rules file status and messaging
2 parents 24b9215 + c58519b commit 6561baa

3 files changed

Lines changed: 133 additions & 58 deletions

File tree

src/event_processors/pull_request.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from src.agents.engine_agent.agent import RuleEngineAgent
77
from src.event_processors.base import BaseEventProcessor, ProcessingResult
8+
from src.rules.github_provider import RulesFileNotFoundError
89
from src.tasks.task_queue import Task
910

1011
logger = logging.getLogger(__name__)
@@ -45,8 +46,25 @@ async def process(self, task: Task) -> ProcessingResult:
4546
api_calls += 1
4647

4748
# Fetch rules
48-
rules = await self.rule_provider.get_rules(task.repo_full_name, task.installation_id)
49-
api_calls += 1
49+
try:
50+
rules = await self.rule_provider.get_rules(task.repo_full_name, task.installation_id)
51+
api_calls += 1
52+
except RulesFileNotFoundError as e:
53+
logger.warning(f"Rules file not found: {e}")
54+
# Create a neutral check run for missing rules file with helpful guidance
55+
await self._create_check_run(
56+
task,
57+
[],
58+
conclusion="neutral",
59+
error="Rules not configured. Please create `.watchflow/rules.yaml` in your repository.",
60+
)
61+
return ProcessingResult(
62+
success=True, # Not a failure, just needs setup
63+
violations=[],
64+
api_calls_made=api_calls,
65+
processing_time_ms=int((time.time() - start_time) * 1000),
66+
error="Rules not configured",
67+
)
5068

5169
# Convert rules to the new format expected by the agent
5270
formatted_rules = self._convert_rules_to_new_format(rules)
@@ -242,7 +260,8 @@ async def _create_check_run(
242260
# Determine check run status
243261
if error:
244262
status = "completed"
245-
conclusion = "failure"
263+
# Use provided conclusion or default to failure
264+
conclusion = conclusion or "failure"
246265
elif violations:
247266
status = "completed"
248267
conclusion = "failure"
@@ -274,11 +293,29 @@ async def _create_check_run(
274293
def _format_check_run_output(self, violations: list[dict[str, Any]], error: str | None = None) -> dict[str, Any]:
275294
"""Format violations for check run output."""
276295
if error:
277-
return {
278-
"title": "Error processing rules",
279-
"summary": f"❌ Error: {error}",
280-
"text": f"An error occurred while processing rules:\n\n```\n{error}\n```\n\nPlease check the logs for more details.",
281-
}
296+
# Check if it's a missing rules file error
297+
if "rules not configured" in error.lower() or "rules file not found" in error.lower():
298+
return {
299+
"title": "Rules not configured",
300+
"summary": "⚙️ Watchflow rules setup required",
301+
"text": (
302+
"**Watchflow rules not configured**\n\n"
303+
"No rules file found in your repository. Watchflow can help enforce governance rules for your team.\n\n"
304+
"**How to set up rules:**\n"
305+
"1. Create a file at `.watchflow/rules.yaml` in your repository root\n"
306+
"2. Add your rules in the following format:\n"
307+
" ```yaml\n rules:\n - id: pr-approval-required\n name: PR Approval Required\n description: All pull requests must have at least 2 approvals\n enabled: true\n severity: high\n event_types: [pull_request]\n parameters:\n min_approvals: 2\n ```\n\n"
308+
"**Note:** Rules are currently read from the main branch only.\n\n"
309+
"📖 [Read the documentation for more examples](https://github.com/warestack/watchflow/blob/main/docs/getting-started/configuration.md)\n\n"
310+
"After adding the file, push your changes to re-run validation."
311+
),
312+
}
313+
else:
314+
return {
315+
"title": "Error processing rules",
316+
"summary": f"❌ Error: {error}",
317+
"text": f"An error occurred while processing rules:\n\n```\n{error}\n```\n\nPlease check the logs for more details.",
318+
}
282319

283320
if not violations:
284321
return {

src/rules/github_provider.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
logger = logging.getLogger(__name__)
1313

1414

15+
class RulesFileNotFoundError(Exception):
16+
"""Raised when the rules file is not found in the repository."""
17+
18+
pass
19+
20+
1521
class GitHubRuleLoader(RuleLoader):
1622
"""
1723
Loads rules from a GitHub repository's rules yaml file.
@@ -30,11 +36,13 @@ async def get_rules(self, repository: str, installation_id: int) -> list[Rule]:
3036
content = await self.github_client.get_file_content(repository, rules_file_path, installation_id)
3137
if not content:
3238
logger.warning(f"No rules.yaml file found in {repository}")
33-
return []
39+
raise RulesFileNotFoundError(f"Rules file not found: {rules_file_path}")
40+
3441
rules_data = yaml.safe_load(content)
3542
if not rules_data or "rules" not in rules_data:
3643
logger.warning(f"No rules found in {repository}/{rules_file_path}")
3744
return []
45+
3846
rules = []
3947
for rule_data in rules_data["rules"]:
4048
try:
@@ -44,8 +52,12 @@ async def get_rules(self, repository: str, installation_id: int) -> list[Rule]:
4452
except Exception as e:
4553
logger.error(f"Error parsing rule {rule_data.get('id', 'unknown')}: {e}")
4654
continue
55+
4756
logger.info(f"Successfully loaded {len(rules)} rules from {repository}")
4857
return rules
58+
except RulesFileNotFoundError:
59+
# Re-raise this specific exception
60+
raise
4961
except Exception as e:
5062
logger.error(f"Error fetching rules for {repository}: {e}")
5163
raise

src/rules/utils.py

Lines changed: 75 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from typing import Any
23

34
import yaml
45

@@ -12,74 +13,99 @@
1213

1314
async def validate_rules_yaml_from_repo(repo_full_name: str, installation_id: int, pr_number: int):
1415
validation_result = await _validate_rules_yaml(repo_full_name, installation_id)
15-
# Only post a comment if the result is not a success (i.e., does not start with the green checkmark)
16-
if not validation_result.strip().startswith("✅"):
16+
# Only post a comment if the result is not a success
17+
if not validation_result["success"]:
1718
await github_client.create_pull_request_comment(
1819
repo=repo_full_name,
1920
pr_number=pr_number,
20-
comment=validation_result,
21+
comment=validation_result["message"],
2122
installation_id=installation_id,
2223
)
2324
logger.info(f"Posted validation result to PR #{pr_number} in {repo_full_name}")
2425

2526

26-
async def _validate_rules_yaml(repo: str, installation_id: int) -> str:
27+
async def _validate_rules_yaml(repo: str, installation_id: int) -> dict[str, Any]:
2728
try:
2829
file_content = await github_client.get_file_content(repo, ".watchflow/rules.yaml", installation_id)
2930
if file_content is None:
30-
return (
31-
"❌ **Watchflow rules file not found**\n\n"
32-
"The file `.watchflow/rules.yaml` is missing from your repository.\n\n"
33-
"**How to fix:**\n"
34-
"1. Create a file at `.watchflow/rules.yaml` in your repository root.\n"
35-
"2. Add your rules in the following format:\n"
36-
" ```yaml\n rules:\n - id: example-rule\n description: Example rule description\n ...\n ```\n"
37-
f"3. [Read the documentation for more details.]({DOCS_URL})\n\n"
38-
"After adding the file, push your changes to re-run validation."
39-
)
31+
return {
32+
"success": False,
33+
"message": (
34+
"⚙️ **Watchflow rules not configured**\n\n"
35+
"No rules file found in your repository. Watchflow can help enforce governance rules for your team.\n\n"
36+
"**How to set up rules:**\n"
37+
"1. Create a file at `.watchflow/rules.yaml` in your repository root\n"
38+
"2. Add your rules in the following format:\n"
39+
" ```yaml\n rules:\n - id: pr-approval-required\n name: PR Approval Required\n description: All pull requests must have at least 2 approvals\n enabled: true\n severity: high\n event_types: [pull_request]\n parameters:\n min_approvals: 2\n ```\n\n"
40+
"**Note:** Rules are currently read from the main branch only.\n\n"
41+
"📖 [Read the documentation for more examples](https://github.com/warestack/watchflow/blob/main/docs/getting-started/configuration.md)\n\n"
42+
"After adding the file, push your changes to re-run validation."
43+
),
44+
}
4045
try:
4146
rules_data = yaml.safe_load(file_content)
4247
except Exception as e:
43-
return (
44-
"❌ **Failed to parse `.watchflow/rules.yaml`**\n\n"
45-
f"Error details: `{e}`\n\n"
46-
"**How to fix:**\n"
47-
"- Ensure your YAML is valid. You can use an online YAML validator.\n"
48-
"- Check for indentation, missing colons, or invalid syntax.\n\n"
49-
f"[See configuration docs.]({DOCS_URL})"
50-
)
48+
return {
49+
"success": False,
50+
"message": (
51+
"❌ **Failed to parse `.watchflow/rules.yaml`**\n\n"
52+
f"Error details: `{e}`\n\n"
53+
"**How to fix:**\n"
54+
"- Ensure your YAML is valid. You can use an online YAML validator.\n"
55+
"- Check for indentation, missing colons, or invalid syntax.\n\n"
56+
f"[See configuration docs.]({DOCS_URL})"
57+
),
58+
}
5159
if not isinstance(rules_data, dict) or "rules" not in rules_data:
52-
return (
53-
"❌ **Invalid `.watchflow/rules.yaml`: missing top-level `rules:` key**\n\n"
54-
"Your file must start with a `rules:` key, like:\n"
55-
"```yaml\nrules:\n - id: ...\n```\n"
56-
f"[See configuration docs.]({DOCS_URL})"
57-
)
60+
return {
61+
"success": False,
62+
"message": (
63+
"❌ **Invalid `.watchflow/rules.yaml`: missing top-level `rules:` key**\n\n"
64+
"Your file must start with a `rules:` key, like:\n"
65+
"```yaml\nrules:\n - id: ...\n```\n"
66+
f"[See configuration docs.]({DOCS_URL})"
67+
),
68+
}
5869
if not isinstance(rules_data["rules"], list):
59-
return (
60-
"❌ **Invalid `.watchflow/rules.yaml`: `rules` must be a list**\n\n"
61-
"Example:\n"
62-
"```yaml\nrules:\n - id: my-rule\n description: ...\n```\n"
63-
f"[See configuration docs.]({DOCS_URL})"
64-
)
70+
return {
71+
"success": False,
72+
"message": (
73+
"❌ **Invalid `.watchflow/rules.yaml`: `rules` must be a list**\n\n"
74+
"Example:\n"
75+
"```yaml\nrules:\n - id: my-rule\n description: ...\n```\n"
76+
f"[See configuration docs.]({DOCS_URL})"
77+
),
78+
}
6579
if not rules_data["rules"]:
66-
return (
67-
"✅ **`.watchflow/rules.yaml` is valid but contains no rules.**\n\n"
68-
"You can add rules at any time. [See documentation for examples.]"
69-
f"({DOCS_URL})"
70-
)
80+
return {
81+
"success": True,
82+
"message": (
83+
"✅ **`.watchflow/rules.yaml` is valid but contains no rules.**\n\n"
84+
"You can add rules at any time. [See documentation for examples.]"
85+
f"({DOCS_URL})"
86+
),
87+
}
7188
for i, rule_data in enumerate(rules_data["rules"]):
7289
try:
7390
Rule.model_validate(rule_data)
7491
except Exception as e:
75-
return (
76-
f"❌ **Rule #{i + 1} (`{rule_data.get('id', 'N/A')}`) failed validation**\n\n"
77-
f"Error: `{e}`\n\n"
78-
"Please check your rule definition and fix the error above.\n\n"
79-
f"[See rule schema docs.]({DOCS_URL})"
80-
)
81-
return f"✅ **`.watchflow/rules.yaml` is valid and contains {len(rules_data['rules'])} rules.**\n\nNo action needed."
92+
return {
93+
"success": False,
94+
"message": (
95+
f"❌ **Rule #{i + 1} (`{rule_data.get('id', 'N/A')}`) failed validation**\n\n"
96+
f"Error: `{e}`\n\n"
97+
"Please check your rule definition and fix the error above.\n\n"
98+
f"[See rule schema docs.]({DOCS_URL})"
99+
),
100+
}
101+
return {
102+
"success": True,
103+
"message": f"✅ **`.watchflow/rules.yaml` is valid and contains {len(rules_data['rules'])} rules.**\n\nNo action needed.",
104+
}
82105
except Exception as e:
83-
return (
84-
f"❌ **Error validating `.watchflow/rules.yaml`**\n\nError: `{e}`\n\n[See configuration docs.]({DOCS_URL})"
85-
)
106+
return {
107+
"success": False,
108+
"message": (
109+
f"❌ **Error validating `.watchflow/rules.yaml`**\n\nError: `{e}`\n\n[See configuration docs.]({DOCS_URL})"
110+
),
111+
}

0 commit comments

Comments
 (0)