summaryrefslogtreecommitdiffstats
path: root/.github/actions/validate_pr_description/validate_pr_description.py
blob: 61e5b40c6ddc0be6b4ddce394af208d5c80f89c1 (plain) (blame)
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
import sys
import re
from typing import Tuple
from pr_template import (
    ISSUE_PATTERNS,
    PULL_REQUEST_TEMPLATE,
    NOT_FOR_CHANGELOG_CATEGORIES,
    ALL_CATEGORIES
)

def validate_pr_description(description, is_not_for_cl_valid=True) -> bool:
    try:
        result, _ = check_pr_description(description, is_not_for_cl_valid)
        return result
    except Exception as e:
        print(f"::error::Error during validation: {e}")
        return False

def check_pr_description(description, is_not_for_cl_valid=True) -> Tuple[bool, str]:
    if not description.strip():
        txt = "PR description is empty. Please fill it out."
        print(f"::warning::{txt}")
        return False, txt

    if "### Changelog category" not in description and "### Changelog entry" not in description:
        return is_not_for_cl_valid, "Changelog category and entry sections are not found."

    if PULL_REQUEST_TEMPLATE.strip() in description.strip():
        return is_not_for_cl_valid, "Pull request template as is."

    # Extract changelog category section
    category_section = re.search(r"### Changelog category.*?\n(.*?)(\n###|$)", description, re.DOTALL)
    if not category_section:
        txt = "Changelog category section not found."
        print(f"::warning::{txt}")
        return False, txt

    categories = [line.strip('* ').strip() for line in category_section.group(1).splitlines() if line.strip()]

    if len(categories) != 1:
        txt = "Only one category can be selected at a time."
        print(f"::warning::{txt}")
        return False, txt

    category = categories[0]
    category_lower = category.lower()

    # Check if category matches any valid category using startswith for flexible matching
    def category_matches(cat):
        """Check if category matches a valid category (supports variants like 'Not for changelog' vs 'Not for changelog (...)')"""
        base = cat.lower().split('(')[0].strip()
        return category_lower.startswith(base) or base.startswith(category_lower)
    
    if not any(category_matches(cat) for cat in ALL_CATEGORIES):
        txt = f"Invalid Changelog category: {category}"
        print(f"::warning::{txt}")
        return False, txt

    is_not_for_changelog = any(category_matches(cat) for cat in NOT_FOR_CHANGELOG_CATEGORIES)
    if not is_not_for_cl_valid and is_not_for_changelog:
        txt = f"Category is not for changelog: {category}"
        print(f"::notice::{txt}")
        return False, txt

    if not is_not_for_changelog:
        entry_section = re.search(r"### Changelog entry.*?\n(.*?)(\n###|$)", description, re.DOTALL)
        if not entry_section or len(entry_section.group(1).strip()) < 20:
            txt = "The changelog entry is less than 20 characters or missing."
            print(f"::warning::{txt}")
            return False, txt

        if category == "Bugfix":
            def check_issue_pattern(issue_pattern):
                return re.search(issue_pattern, description)

            if not any(check_issue_pattern(issue_pattern) for issue_pattern in ISSUE_PATTERNS):
                txt = "Bugfix requires a linked issue in the changelog entry"
                print(f"::warning::{txt}")
                return False, txt

    print("PR description is valid.")
    return True, "PR description is valid."

def validate_pr_description_from_file(file_path) -> Tuple[bool, str]:
    try:
        if file_path:
            with open(file_path, 'r') as file:
                description = file.read()
        else:
            description = sys.stdin.read()
        return check_pr_description(description)
    except Exception as e:
        txt = f"Failed to validate PR description: {e}"
        print(f"::error::{txt}")
        return False, txt

if __name__ == "__main__":
    is_valid, txt = validate_pr_description_from_file(sys.argv[1] if len(sys.argv) > 1 else None)
    from post_status_to_github import post
    post(is_valid, txt)
    if not is_valid:
        sys.exit(1)