diff options
Diffstat (limited to 'buildscripts/validate_commit_message.py')
-rwxr-xr-x | buildscripts/validate_commit_message.py | 207 |
1 files changed, 151 insertions, 56 deletions
diff --git a/buildscripts/validate_commit_message.py b/buildscripts/validate_commit_message.py index 61fcce06f85..3aff60e89c1 100755 --- a/buildscripts/validate_commit_message.py +++ b/buildscripts/validate_commit_message.py @@ -28,19 +28,27 @@ # """Validate that the commit message is ok.""" import argparse +import logging import os import re import sys -import logging -from evergreen import RetryingEvergreenApi, EvergreenApi +from typing import List, Optional + +from evergreen import EvergreenApi, RetryingEvergreenApi +from buildscripts.client.jiraclient import JiraAuth, JiraClient, SecurityLevel + +JIRA_SERVER = "https://jira.mongodb.org" EVG_CONFIG_FILE = "~/.evergreen.yml" +SERVER_TICKET_PREFIX = "SERVER-" +PUBLIC_PROJECT_PREFIX = "mongodb-mongo-" + LOGGER = logging.getLogger(__name__) ERROR_MSG = """ ################################################################################ Encountered an invalid commit message. Please correct to the commit message to -continue. +continue. Commit message should start with a Public Jira ticket, an "Import" for wiredtiger or tools, or a "Revert" message. @@ -50,35 +58,33 @@ or tools, or a "Revert" message. ################################################################################ """ -COMMON_PUBLIC_PATTERN = r''' +COMMON_PUBLIC_PATTERN = r""" ((?P<revert>Revert)\s+[\"\']?)? # Revert (optional) ((?P<ticket>(?:EVG|SERVER|WT)-[0-9]+)[\"\']?\s*) # ticket identifier (?P<body>(?:(?!\(cherry\spicked\sfrom).)*)? # To also capture the body (?P<backport>\(cherry\spicked\sfrom.*)? # back port (optional) - ''' + """ """Common Public pattern format.""" -COMMON_LINT_PATTERN = r'(?P<lint>Fix\slint)' +COMMON_LINT_PATTERN = r"(?P<lint>Fix\slint)" """Common Lint pattern format.""" -COMMON_IMPORT_PATTERN = r'(?P<imported>Import\s(wiredtiger|tools):\s.*)' +COMMON_IMPORT_PATTERN = r"(?P<imported>Import\s(wiredtiger|tools):\s.*)" """Common Import pattern format.""" -COMMON_REVERT_IMPORT_PATTERN = r'Revert\s+[\"\']?(?P<imported>Import\s(wiredtiger|tools):\s.*)' +COMMON_REVERT_IMPORT_PATTERN = (r"Revert\s+[\"\']?(?P<imported>Import\s(wiredtiger|tools):\s.*)") """Common revert Import pattern format.""" -COMMON_PRIVATE_PATTERN = r''' +COMMON_PRIVATE_PATTERN = r""" ((?P<revert>Revert)\s+[\"\']?)? # Revert (optional) ((?P<ticket>[A-Z]+-[0-9]+)[\"\']?\s*) # ticket identifier (?P<body>(?:(?!('\s(into\s'(([^/]+))/(([^:]+)):(([^']+))'))).)*)? # To also capture the body -''' +""" """Common Private pattern format.""" STATUS_OK = 0 STATUS_ERROR = 1 -GIT_SHOW_COMMAND = ["git", "show", "-1", "-s", "--format=%s"] - def new_patch_description(pattern: str) -> str: """ @@ -92,7 +98,7 @@ def new_patch_description(pattern: str) -> str: :return: A pattern to match the new format for the patch description. """ return (r"""^((?P<commitqueue>Commit\sQueue\sMerge:)\s')""" - f'{pattern}' + f"{pattern}" # r"""('\s(?P<into>into\s'((?P<owner>[^/]+))/((?P<repo>[^:]+)):((?P<branch>[^']+))'))""" ) @@ -108,63 +114,146 @@ def old_patch_description(pattern: str) -> str: :param pattern: The pattern to wrap. :return: A pattern to match the old format for the patch description. """ - return r'^' f'{pattern}' + return r"^" f"{pattern}" # NOTE: re.VERBOSE is for visibility / debugging. As such significant white space must be # escaped (e.g ' ' to \s). VALID_PATTERNS = [ - re.compile(new_patch_description(COMMON_PUBLIC_PATTERN), re.MULTILINE | re.DOTALL | re.VERBOSE), - re.compile(old_patch_description(COMMON_PUBLIC_PATTERN), re.MULTILINE | re.DOTALL | re.VERBOSE), - re.compile(new_patch_description(COMMON_LINT_PATTERN), re.MULTILINE | re.DOTALL | re.VERBOSE), - re.compile(old_patch_description(COMMON_LINT_PATTERN), re.MULTILINE | re.DOTALL | re.VERBOSE), - re.compile(new_patch_description(COMMON_IMPORT_PATTERN), re.MULTILINE | re.DOTALL | re.VERBOSE), - re.compile(old_patch_description(COMMON_IMPORT_PATTERN), re.MULTILINE | re.DOTALL | re.VERBOSE), re.compile( - new_patch_description(COMMON_REVERT_IMPORT_PATTERN), re.MULTILINE | re.DOTALL | re.VERBOSE), + new_patch_description(COMMON_PUBLIC_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), + re.compile( + old_patch_description(COMMON_PUBLIC_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), + re.compile( + new_patch_description(COMMON_LINT_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), re.compile( - old_patch_description(COMMON_REVERT_IMPORT_PATTERN), re.MULTILINE | re.DOTALL | re.VERBOSE), + old_patch_description(COMMON_LINT_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), + re.compile( + new_patch_description(COMMON_IMPORT_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), + re.compile( + old_patch_description(COMMON_IMPORT_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), + re.compile( + new_patch_description(COMMON_REVERT_IMPORT_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), + re.compile( + old_patch_description(COMMON_REVERT_IMPORT_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), ] """valid public patterns.""" PRIVATE_PATTERNS = [ re.compile( - new_patch_description(COMMON_PRIVATE_PATTERN), re.MULTILINE | re.DOTALL | re.VERBOSE), + new_patch_description(COMMON_PRIVATE_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), re.compile( - old_patch_description(COMMON_PRIVATE_PATTERN), re.MULTILINE | re.DOTALL | re.VERBOSE), + old_patch_description(COMMON_PRIVATE_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), ] """private patterns.""" -def validate_commit_messages(version_id: str, evg_api: EvergreenApi) -> int: - """ - Validate the commit messages for the given build. - - :param version_id: ID of version to validate. - :param evg_api: Evergreen API client. - :return: True if all commit messages were valid. - """ - found_error = False - code_changes = evg_api.patch_by_id(version_id).module_code_changes - for change in code_changes: - for message in change.commit_messages: - if any(valid_pattern.match(message) for valid_pattern in VALID_PATTERNS): - continue - elif any(private_pattern.match(message) for private_pattern in PRIVATE_PATTERNS): - print( - ERROR_MSG.format(error_msg="Reference to a private project", - branch=change.branch_name, commit_message=message)) - found_error = True - else: - print( - ERROR_MSG.format(error_msg="Commit without a ticket", branch=change.branch_name, - commit_message=message)) - found_error = True - - return STATUS_ERROR if found_error else STATUS_OK - - -def main(argv=None): +class CommitMessageValidationOrchestrator: + """An orchestrator to validate that commit messages are valid.""" + + def __init__(self, evg_api: EvergreenApi, jira_client: JiraClient) -> None: + """ + Initialize the orchestrator. + + :param evg_api: Evergreen API client. + :param jira_client: Client to Jira API. + """ + self.evg_api = evg_api + self.jira_client = jira_client + + def validate_ticket(self, ticket: str, project: str) -> bool: + """ + Check that the given Jira ticket has a proper security level. + + Commits targeting a public project should not have a defined security level (these are + public by default). + + :param ticket: Ticket to check. + :param project: Project commit is targeting. + :return: True if ticket is valid. + """ + if ticket.startswith(SERVER_TICKET_PREFIX) and project.startswith(PUBLIC_PROJECT_PREFIX): + security_level = self.jira_client.get_ticket_security_level(ticket) + return security_level == SecurityLevel.NONE + return True + + def validate_msg(self, message: str, project: str) -> bool: + """ + Check that the given message is valid. + + :param message: Commit message to validate. + :param project: Project commit is targeting. + :return: True if the message is valid. + """ + valid_matches = [valid_pattern.match(message) for valid_pattern in VALID_PATTERNS] + if any(valid_matches): + ticket_matches = [pattern.match(message) for pattern in VALID_PATTERNS[0:2]] + for match in [ticket_match for ticket_match in ticket_matches if ticket_match]: + if not self.validate_ticket(match.group("ticket"), project): + print( + ERROR_MSG.format( + error_msg="Reference to a internal Jira Ticket", + branch=project, + commit_message=message, + )) + return False + return True + elif any(private_pattern.match(message) for private_pattern in PRIVATE_PATTERNS): + print( + ERROR_MSG.format( + error_msg="Reference to a private project", + branch=project, + commit_message=message, + )) + return False + else: + print( + ERROR_MSG.format( + error_msg="Commit without a ticket", + branch=project, + commit_message=message, + )) + return False + + def validate_commit_messages(self, version_id: str) -> int: + """ + Validate the commit messages for the given build. + + :param version_id: ID of version to validate. + :param evg_api: Evergreen API client. + :return: True if all commit messages were valid. + """ + found_error = False + code_changes = self.evg_api.patch_by_id(version_id).module_code_changes + for change in code_changes: + for message in change.commit_messages: + is_valid = self.validate_msg(message, change.branch_name) + found_error = found_error or not is_valid + + return STATUS_ERROR if found_error else STATUS_OK + + +def main(argv: Optional[List[str]] = None) -> int: """Execute Main function to validate commit messages.""" parser = argparse.ArgumentParser( usage="Validate the commit message. " @@ -174,12 +263,18 @@ def main(argv=None): metavar="version id", help="The id of the version to validate", ) - parser.add_argument("--evg-config-file", default=EVG_CONFIG_FILE, - help="Path to evergreen configuration file containing auth information.") + parser.add_argument( + "--evg-config-file", + default=EVG_CONFIG_FILE, + help="Path to evergreen configuration file containing auth information.", + ) args = parser.parse_args(argv) evg_api = RetryingEvergreenApi.get_api(config_file=os.path.expanduser(args.evg_config_file)) + jira_auth = JiraAuth() + jira_client = JiraClient(JIRA_SERVER, jira_auth) + orchestrator = CommitMessageValidationOrchestrator(evg_api, jira_client) - return validate_commit_messages(args.version_id, evg_api) + return orchestrator.validate_commit_messages(args.version_id) if __name__ == "__main__": |