validate_pr_description.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import sys
  2. import re
  3. from typing import Tuple
  4. issue_patterns = [
  5. r"https://github.com/ydb-platform/[a-z\-]+/issues/\d+",
  6. r"https://st.yandex-team.ru/[a-zA-Z]+-\d+",
  7. r"#\d+",
  8. r"[a-zA-Z]+-\d+"
  9. ]
  10. pull_request_template = """
  11. ### Changelog entry <!-- a user-readable short description of the changes that goes to CHANGELOG.md and Release Notes -->
  12. ...
  13. ### Changelog category <!-- remove all except one -->
  14. * New feature
  15. * Experimental feature
  16. * Improvement
  17. * Performance improvement
  18. * User Interface
  19. * Bugfix
  20. * Backward incompatible change
  21. * Documentation (changelog entry is not required)
  22. * Not for changelog (changelog entry is not required)
  23. """
  24. def validate_pr_description(description, is_not_for_cl_valid=True) -> bool:
  25. try:
  26. result, _ = check_pr_description(description, is_not_for_cl_valid)
  27. return result
  28. except Exception as e:
  29. print(f"::error::Error during validation: {e}")
  30. return False
  31. def check_pr_description(description, is_not_for_cl_valid=True) -> Tuple[bool, str]:
  32. if not description.strip():
  33. txt = "PR description is empty. Please fill it out."
  34. print(f"::warning::{txt}")
  35. return False, txt
  36. if "### Changelog category" not in description and "### Changelog entry" not in description:
  37. return is_not_for_cl_valid, "Changelog category and entry sections are not found."
  38. if pull_request_template.strip() in description.strip():
  39. return is_not_for_cl_valid, "Pull request template as is."
  40. # Extract changelog category section
  41. category_section = re.search(r"### Changelog category.*?\n(.*?)(\n###|$)", description, re.DOTALL)
  42. if not category_section:
  43. txt = "Changelog category section not found."
  44. print(f"::warning::{txt}")
  45. return False, txt
  46. categories = [line.strip('* ').strip() for line in category_section.group(1).splitlines() if line.strip()]
  47. if len(categories) != 1:
  48. txt = "Only one category can be selected at a time."
  49. print(f"::warning::{txt}")
  50. return False, txt
  51. category = categories[0]
  52. for_cl_categories = [
  53. "New feature",
  54. "Experimental feature",
  55. "User Interface",
  56. "Improvement",
  57. "Performance improvement",
  58. "Bugfix",
  59. "Backward incompatible change"
  60. ]
  61. not_for_cl_categories = [
  62. "Documentation (changelog entry is not required)",
  63. "Not for changelog (changelog entry is not required)"
  64. ]
  65. valid_categories = for_cl_categories + not_for_cl_categories
  66. if not any(cat.startswith(category) for cat in valid_categories):
  67. txt = f"Invalid Changelog category: {category}"
  68. print(f"::warning::{txt}")
  69. return False, txt
  70. if not is_not_for_cl_valid and any(cat.startswith(category) for cat in not_for_cl_categories):
  71. txt = f"Category is not for changelog: {category}"
  72. print(f"::notice::{txt}")
  73. return False, txt
  74. if not any(cat.startswith(category) for cat in not_for_cl_categories):
  75. entry_section = re.search(r"### Changelog entry.*?\n(.*?)(\n###|$)", description, re.DOTALL)
  76. if not entry_section or len(entry_section.group(1).strip()) < 20:
  77. txt = "The changelog entry is less than 20 characters or missing."
  78. print(f"::warning::{txt}")
  79. return False, txt
  80. if category == "Bugfix":
  81. def check_issue_pattern(issue_pattern):
  82. return re.search(issue_pattern, description)
  83. if not any(check_issue_pattern(issue_pattern) for issue_pattern in issue_patterns):
  84. txt = "Bugfix requires a linked issue in the changelog entry"
  85. print(f"::warning::{txt}")
  86. return False, txt
  87. print("PR description is valid.")
  88. return True, "PR description is valid."
  89. def validate_pr_description_from_file(file_path) -> Tuple[bool, str]:
  90. try:
  91. with open(file_path, 'r') as file:
  92. description = file.read()
  93. return check_pr_description(description)
  94. except Exception as e:
  95. txt = f"Failed to validate PR description: {e}"
  96. print(f"::error::{txt}")
  97. return False, txt
  98. if __name__ == "__main__":
  99. if len(sys.argv) != 2:
  100. print("Usage: validate_pr_description.py <path_to_pr_description_file>")
  101. sys.exit(1)
  102. file_path = sys.argv[1]
  103. is_valid, txt = validate_pr_description_from_file(file_path)
  104. from post_status_to_github import post
  105. post(is_valid, txt)
  106. if not is_valid:
  107. sys.exit(1)