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)
|