Skip to content

Commit 69627bd

Browse files
authored
Merge pull request #54 from warestack/feat/update-watchflow-rules
chore: update watchflow rules
2 parents 2df6e2b + 8a239b6 commit 69627bd

9 files changed

Lines changed: 56 additions & 63 deletions

File tree

.watchflow/rules.yaml

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,29 @@
11
rules:
2-
# Essential Open Source Rules
3-
- description: "Pull requests must have descriptive titles following conventional commit format"
2+
- description: "PRs must reference a linked issue in the description or title (e.g. Fixes #123)."
43
enabled: true
5-
severity: "medium"
4+
severity: "high"
65
event_types: ["pull_request"]
76
parameters:
8-
title_pattern: "^feat|^fix|^docs|^style|^refactor|^test|^chore|^perf|^ci|^build|^revert"
7+
require_linked_issue: true
98

10-
- description: "New contributors require approval from at least one past contributor"
9+
- description: "When a PR modifies paths with CODEOWNERS, those owners must be added as reviewers."
1110
enabled: true
12-
severity: "medium"
11+
severity: "high"
1312
event_types: ["pull_request"]
1413
parameters:
15-
min_past_contributors: 1
14+
require_code_owner_reviewers: true
1615

17-
- description: "Code changes must include corresponding tests"
16+
- description: "PR total lines changed must not exceed a maximum (e.g. 500 lines)."
1817
enabled: true
1918
severity: "medium"
2019
event_types: ["pull_request"]
2120
parameters:
22-
pattern: "tests/.*\\.py$|test_.*\\.py$"
23-
condition_type: "files_match_pattern"
24-
25-
- description: "Changes to critical files require review from code owners"
26-
enabled: true
27-
severity: "high"
28-
event_types: ["pull_request"]
29-
parameters:
30-
critical_owners: []
21+
max_pr_loc: 500
3122

32-
- description: "When a PR modifies paths with CODEOWNERS, those owners must be added as reviewers"
23+
- description: "PR titles must follow a pattern (e.g. feat:, fix:, docs:) and descriptions must align diff."
3324
enabled: true
34-
severity: "high"
25+
severity: "medium"
3526
event_types: ["pull_request"]
3627
parameters:
37-
require_code_owner_reviewers: true
38-
39-
- description: "No direct pushes to main branch - all changes must go through PRs"
40-
enabled: true
41-
severity: "critical"
42-
event_types: ["push"]
43-
parameters:
44-
no_force_push: true
28+
title_pattern: "^feat|^fix|^docs|^style|^refactor|^test|^chore|^perf|^ci|^build|^revert"
29+
min_description_length: 50

src/presentation/github_formatter.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ def format_check_run_output(
106106
text += f"## {emoji} {severity.title()} Severity\n\n"
107107

108108
for violation in severity_groups[severity]:
109-
text += f"### {violation.rule_description or 'Unknown Rule'}\n"
109+
text += f"### {violation.rule_description or 'Unknown Rule'}\n\n"
110+
text += f"{violation.message}\n\n"
110111
text += f"Rule validation failed with severity: **{violation.severity}**\n"
111112
if violation.how_to_fix:
112113
text += f"**How to fix:** {violation.how_to_fix}\n"
@@ -169,7 +170,8 @@ def format_violations_comment(violations: list[Violation]) -> str:
169170
comment += f"### {emoji} {severity.title()} Severity\n\n"
170171

171172
for violation in severity_groups[severity]:
172-
comment += f"**{violation.rule_description or 'Unknown Rule'}**\n"
173+
comment += f"### {violation.rule_description or 'Unknown Rule'}\n\n"
174+
comment += f"{violation.message}\n\n"
173175
comment += f"Rule validation failed with severity: **{violation.severity}**\n"
174176
if violation.how_to_fix:
175177
comment += f"**How to fix:** {violation.how_to_fix}\n"

src/rules/acknowledgment.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,17 @@ class RuleID(StrEnum):
3838

3939
# Mapping from violation text patterns to RuleID
4040
VIOLATION_TEXT_TO_RULE_MAPPING: dict[str, RuleID] = {
41-
"Pull request does not have the minimum required": RuleID.MIN_PR_APPROVALS,
42-
"Pull request is missing required label": RuleID.REQUIRED_LABELS,
43-
"Pull request title does not match the required pattern": RuleID.PR_TITLE_PATTERN,
44-
"Pull request description is too short": RuleID.PR_DESCRIPTION_REQUIRED,
45-
"Individual files cannot exceed": RuleID.FILE_SIZE_LIMIT,
46-
"Pull request exceeds maximum lines changed": RuleID.MAX_PR_LOC,
41+
"approvals, requires": RuleID.MIN_PR_APPROVALS,
42+
"Missing required labels": RuleID.REQUIRED_LABELS,
43+
"does not match required pattern": RuleID.PR_TITLE_PATTERN,
44+
"PR description is too short": RuleID.PR_DESCRIPTION_REQUIRED,
45+
"exceed size limit of": RuleID.FILE_SIZE_LIMIT,
46+
"exceeds maximum lines changed": RuleID.MAX_PR_LOC,
4747
"does not reference a linked issue": RuleID.REQUIRE_LINKED_ISSUE,
48-
"Force pushes are not allowed": RuleID.NO_FORCE_PUSH,
49-
"Direct pushes to main/master branches": RuleID.PROTECTED_BRANCH_PUSH,
50-
"Paths without a code owner in CODEOWNERS": RuleID.PATH_HAS_CODE_OWNER,
51-
"Code owners for modified paths must be added as reviewers": RuleID.REQUIRE_CODE_OWNER_REVIEWERS,
48+
"Force push detected": RuleID.NO_FORCE_PUSH,
49+
"targets protected branch": RuleID.PROTECTED_BRANCH_PUSH,
50+
"Paths without a code owner": RuleID.PATH_HAS_CODE_OWNER,
51+
"Code owners for modified paths": RuleID.REQUIRE_CODE_OWNER_REVIEWERS,
5252
}
5353

5454
# Mapping from RuleID to human-readable descriptions

src/rules/conditions/access_control.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ def _required_code_owner_reviewers(event: dict[str, Any]) -> tuple[list[str], li
298298
requested_logins = {u.get("login") for u in requested_users if u.get("login")}
299299
requested_slugs = {t.get("slug") for t in requested_teams if t.get("slug")}
300300

301+
# The PR author is inherently a reviewer of their own code
302+
author_login = pr.get("user", {}).get("login")
303+
if author_login:
304+
requested_logins.add(author_login)
305+
301306
# Owner can be a user (login) or a team (slug or org/slug). Match user by login, team by slug.
302307
requested_identifiers = requested_logins | requested_slugs
303308

src/rules/conditions/filesystem.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,9 @@ class MaxPrLocCondition(BaseCondition):
225225

226226
name = "max_pr_loc"
227227
description = "Validates that total lines changed (additions + deletions) in a PR do not exceed a maximum; enforces a maximum LOC per pull request."
228-
parameter_patterns = ["max_lines"]
228+
parameter_patterns = ["max_lines", "max_pr_loc", "max_changed_lines"]
229229
event_types = ["pull_request"]
230-
examples = [{"max_lines": 500}, {"max_lines": 1000}]
230+
examples = [{"max_lines": 500}, {"max_pr_loc": 1000}]
231231

232232
async def evaluate(self, context: Any) -> list[Violation]:
233233
"""Evaluate max PR LOC condition.
@@ -241,7 +241,9 @@ async def evaluate(self, context: Any) -> list[Violation]:
241241
parameters = context.get("parameters", {})
242242
event = context.get("event", {})
243243

244-
max_lines = parameters.get("max_lines", 0)
244+
max_lines = (
245+
parameters.get("max_lines") or parameters.get("max_pr_loc") or parameters.get("max_changed_lines") or 0
246+
)
245247
if not max_lines:
246248
logger.debug("MaxPrLocCondition: No max_lines specified, skipping validation")
247249
return []
@@ -266,7 +268,9 @@ async def evaluate(self, context: Any) -> list[Violation]:
266268

267269
async def validate(self, parameters: dict[str, Any], event: dict[str, Any]) -> bool:
268270
"""Legacy validation interface for backward compatibility."""
269-
max_lines = parameters.get("max_lines", 0)
271+
max_lines = (
272+
parameters.get("max_lines") or parameters.get("max_pr_loc") or parameters.get("max_changed_lines") or 0
273+
)
270274
if not max_lines:
271275
return True
272276

src/rules/loaders/github_loader.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,6 @@ def _parse_rule(rule_data: dict[str, Any]) -> Rule:
9494
# Get parameters (strip internal "validator" key; engine infers validator from parameter names)
9595
parameters = dict(rule_data.get("parameters", {}))
9696
parameters.pop("validator", None)
97-
# Normalize aliases so conditions match (e.g. max_changed_lines -> max_lines for MaxPrLocCondition)
98-
if "max_changed_lines" in parameters and "max_lines" not in parameters:
99-
parameters["max_lines"] = parameters["max_changed_lines"]
10097

10198
# Instantiate conditions using Registry (matches on parameter keys, e.g. max_lines, require_linked_issue)
10299
conditions = ConditionRegistry.get_conditions_for_parameters(parameters)

tests/unit/event_processors/pull_request/test_enricher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ async def test_enrich_event_data(enricher, mock_task, mock_github_client):
6565
async def test_fetch_acknowledgments(enricher, mock_github_client):
6666
mock_github_client.get_issue_comments.return_value = [
6767
{
68-
"body": "🚨 Watchflow Rule Violations Detected\n\n**Reason:** valid reason\n\n---\nThe following violations have been overridden:\n**Rule** - Pull request does not have the minimum required approvals\n",
68+
"body": "🚨 Watchflow Rule Violations Detected\n\n**Reason:** valid reason\n\n---\nThe following violations have been overridden:\nPR has 1 approvals, requires 2\n",
6969
"user": {"login": "reviewer"},
7070
}
7171
]

tests/unit/presentation/test_github_formatter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ def test_format_violations_comment_groups_by_severity():
1919
assert "## 🚨 Watchflow Rule Violations Detected" in comment
2020
assert "### 🔴 Critical Severity" in comment
2121
assert "### 🟠 High Severity" in comment
22-
assert "**Rule 2**" in comment
23-
assert "**Rule 1**" in comment
24-
assert "**Rule 3**" in comment
22+
assert "### Rule 2" in comment
23+
assert "### Rule 1" in comment
24+
assert "### Rule 3" in comment
2525
assert "Fix 1" in comment
2626
assert "Fix 2" in comment
2727

tests/unit/rules/test_acknowledgment.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -140,18 +140,18 @@ class TestMapViolationTextToRuleId:
140140
@pytest.mark.parametrize(
141141
"text,expected_rule_id",
142142
[
143-
("Pull request does not have the minimum required approvals", RuleID.MIN_PR_APPROVALS),
144-
("Pull request is missing required label: security", RuleID.REQUIRED_LABELS),
145-
("Pull request title does not match the required pattern", RuleID.PR_TITLE_PATTERN),
146-
("Pull request description is too short (20 chars)", RuleID.PR_DESCRIPTION_REQUIRED),
147-
("Individual files cannot exceed 10MB limit", RuleID.FILE_SIZE_LIMIT),
143+
("PR has 1 approvals, requires 2", RuleID.MIN_PR_APPROVALS),
144+
("Missing required labels: security", RuleID.REQUIRED_LABELS),
145+
("PR title 'foo' does not match required pattern '^feat'", RuleID.PR_TITLE_PATTERN),
146+
("PR description is too short", RuleID.PR_DESCRIPTION_REQUIRED),
147+
("Files exceed size limit of 10MB", RuleID.FILE_SIZE_LIMIT),
148148
("Pull request exceeds maximum lines changed (1234 > 500)", RuleID.MAX_PR_LOC),
149149
(
150150
"PR does not reference a linked issue (e.g. #123 or closes #123 in body/title)",
151151
RuleID.REQUIRE_LINKED_ISSUE,
152152
),
153-
("Force pushes are not allowed on this branch", RuleID.NO_FORCE_PUSH),
154-
("Direct pushes to main/master branches prohibited", RuleID.PROTECTED_BRANCH_PUSH),
153+
("Force push detected on protected branch", RuleID.NO_FORCE_PUSH),
154+
("PR targets protected branch 'main'", RuleID.PROTECTED_BRANCH_PUSH),
155155
("Paths without a code owner in CODEOWNERS: src/bar.py", RuleID.PATH_HAS_CODE_OWNER),
156156
(
157157
"Code owners for modified paths must be added as reviewers: alice",
@@ -175,7 +175,7 @@ class TestMapViolationTextToRuleDescription:
175175

176176
def test_maps_to_description(self):
177177
"""Should map violation text to human-readable description."""
178-
text = "Pull request does not have the minimum required approvals"
178+
text = "PR has 1 approvals, requires 2"
179179
description = map_violation_text_to_rule_description(text)
180180
assert description == "Pull requests require at least 2 approvals"
181181

@@ -193,7 +193,7 @@ def test_parses_single_violation(self):
193193
**Reason:** Emergency fix
194194
195195
The following violations have been overridden:
196-
Pull request does not have the minimum required approvals
196+
PR has 1 approvals, requires 2
197197
198198
---
199199
*This acknowledgment was validated.*"""
@@ -211,8 +211,8 @@ def test_parses_multiple_violations(self):
211211
**Reason:** Sprint deadline
212212
213213
The following violations have been overridden:
214-
Pull request does not have the minimum required approvals
215-
Pull request is missing required label: review
214+
PR has 1 approvals, requires 2
215+
Missing required labels: review
216216
217217
---"""
218218

@@ -230,7 +230,7 @@ def test_empty_comment_returns_empty_list(self):
230230
def test_returns_acknowledgment_models(self):
231231
"""Should return proper Acknowledgment model instances."""
232232
comment = """The following violations have been overridden:
233-
• Force pushes are not allowed"""
233+
• Force push detected on protected branch"""
234234

235235
acknowledgments = parse_acknowledgment_comment(comment, "admin")
236236

@@ -240,7 +240,7 @@ def test_returns_acknowledgment_models(self):
240240
def test_stops_at_section_delimiter(self):
241241
"""Should stop parsing when hitting section delimiters."""
242242
comment = """The following violations have been overridden:
243-
Pull request title does not match the required pattern
243+
PR title 'foo' does not match required pattern '^feat'
244244
---
245245
⚠️ Other content that should be ignored
246246
• Some other bullet that is NOT a violation"""

0 commit comments

Comments
 (0)