|
1 | 1 | import logging |
| 2 | +from typing import Any |
2 | 3 |
|
3 | 4 | import yaml |
4 | 5 |
|
|
12 | 13 |
|
13 | 14 | async def validate_rules_yaml_from_repo(repo_full_name: str, installation_id: int, pr_number: int): |
14 | 15 | 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"]: |
17 | 18 | await github_client.create_pull_request_comment( |
18 | 19 | repo=repo_full_name, |
19 | 20 | pr_number=pr_number, |
20 | | - comment=validation_result, |
| 21 | + comment=validation_result["message"], |
21 | 22 | installation_id=installation_id, |
22 | 23 | ) |
23 | 24 | logger.info(f"Posted validation result to PR #{pr_number} in {repo_full_name}") |
24 | 25 |
|
25 | 26 |
|
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]: |
27 | 28 | try: |
28 | 29 | file_content = await github_client.get_file_content(repo, ".watchflow/rules.yaml", installation_id) |
29 | 30 | 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 | + } |
40 | 45 | try: |
41 | 46 | rules_data = yaml.safe_load(file_content) |
42 | 47 | 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 | + } |
51 | 59 | 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 | + } |
58 | 69 | 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 | + } |
65 | 79 | 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 | + } |
71 | 88 | for i, rule_data in enumerate(rules_data["rules"]): |
72 | 89 | try: |
73 | 90 | Rule.model_validate(rule_data) |
74 | 91 | 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 | + } |
82 | 105 | 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