diff options
| author | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-11 15:07:35 -0300 |
|---|---|---|
| committer | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-11 15:07:35 -0300 |
| commit | 4cb8841196d0625dfa3825aa326f071cd27c7b8b (patch) | |
| tree | 1682a647d4463397c119183369ae6f750d5fdcff /buildscripts/validate_commit_message.py | |
| parent | aa03c6362cbaa767638e6eed9b031d86dd2643d1 (diff) | |
| parent | 8f0827553e09872941945a093b647a4211a9db7f (diff) | |
Update upstream source from tag 'upstream/6.0.0'master
Update to upstream version '6.0.0'
with Debian dir 5604a80ec1c96ca76f25f40d78e6ef855abec322
Diffstat (limited to 'buildscripts/validate_commit_message.py')
| -rwxr-xr-x | buildscripts/validate_commit_message.py | 253 |
1 files changed, 230 insertions, 23 deletions
diff --git a/buildscripts/validate_commit_message.py b/buildscripts/validate_commit_message.py index da01fa75a62..3aff60e89c1 100755 --- a/buildscripts/validate_commit_message.py +++ b/buildscripts/validate_commit_message.py @@ -28,46 +28,253 @@ # """Validate that the commit message is ok.""" import argparse +import logging +import os import re import sys -import logging +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. + +Commit message should start with a Public Jira ticket, an "Import" for wiredtiger +or tools, or a "Revert" message. + +{error_msg} on '{branch}': +'{commit_message}' +################################################################################ +""" + +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 format.""" + +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 format.""" + +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 -def main(argv=None): +def new_patch_description(pattern: str) -> str: + """ + Wrap the pattern to conform to the new commit queue patch description format. + + Add the commit queue prefix and suffix to the pattern. The format looks like: + + Commit Queue Merge: '<commit message>' into '<owner>/<repo>:<branch>' + + :param pattern: The pattern to wrap. + :return: A pattern to match the new format for the patch description. + """ + return (r"""^((?P<commitqueue>Commit\sQueue\sMerge:)\s')""" + f"{pattern}" + # r"""('\s(?P<into>into\s'((?P<owner>[^/]+))/((?P<repo>[^:]+)):((?P<branch>[^']+))'))""" + ) + + +def old_patch_description(pattern: str) -> str: + """ + Wrap the pattern to conform to the new commit queue patch description format. + + Just add a start anchor. The format looks like: + + <commit message> + + :param pattern: The pattern to wrap. + :return: A pattern to match the old format for the patch description. + """ + 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, + ), + 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, + ), + re.compile( + old_patch_description(COMMON_PRIVATE_PATTERN), + re.MULTILINE | re.DOTALL | re.VERBOSE, + ), +] +"""private patterns.""" + + +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. " "It validates the latest message when no arguments are provided.") parser.add_argument( - "message", - metavar="commit message", - nargs="*", - help="The commit message to validate", + "version_id", + 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.", ) 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) - if not args.message: - LOGGER.error("Must specify non-empty value for --message") - return STATUS_ERROR - message = " ".join(args.message) - - # Valid values look like: - # 1. SERVER-\d+ - # 2. Revert "SERVER-\d+ - # 3. Import wiredtiger - # 4. Revert "Import wiredtiger - valid_pattern = re.compile(r'(Revert ")?(SERVER-[0-9]+|Import wiredtiger)') - - if valid_pattern.match(message): - return STATUS_OK - else: - LOGGER.error(f"Found a commit without a ticket\n{message}") # pylint: disable=logging-fstring-interpolation - return STATUS_ERROR + return orchestrator.validate_commit_messages(args.version_id) if __name__ == "__main__": |
