import os.path import re import subprocess import sys from collections import defaultdict import structlog import typer from typing_extensions import Annotated if __name__ == "__main__" and __package__ is None: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from buildscripts.ciconfig.evergreen import find_evergreen_binary LOGGER = structlog.get_logger(__name__) DEFAULT_EVG_PROJECT_NAME = "mongodb-mongo-master" DEFAULT_EVG_NIGHTLY_PROJECT_NAME = "mongodb-mongo-master-nightly" DEFAULT_EVG_PROJECT_CONFIG = "etc/evergreen.yml" DEFAULT_EVG_NIGHTLY_PROJECT_CONFIG = "etc/evergreen_nightly.yml" # SET TO TRUE IN RAPID RELEASE BRANCHES - see docs/branching/README.md RELEASE_BRANCH = False UNMATCHED_REGEXES = [ re.compile(r".*buildvariant .+ has unmatched selector: .+"), re.compile(r".*buildvariant .+ has unmatched criteria: .+"), ] ALLOWABLE_EVG_VALIDATE_MESSAGE_REGEXES = [ # These regex match any number of repeated criteria that look like '.tag1 !.tag2' # unless they do not start with a dot or exclamation mark (meaning they are not # tag-based selectors) re.compile(r".*buildvariant .+ has unmatched selector: (('[!.][^']*?'),?\s?)+$"), re.compile(r".*buildvariant .+ has unmatched criteria: (('[!.][^']*?'),?\s?)+$"), re.compile( r".*task 'select_multiversion_binaries' defined but not used by any variants; consider using or disabling.*" ), # this task is added to variants only alongside multiversion generated tasks ] ALLOWABLE_IF_NOT_IN_ALL_PROJECTS_EVG_VALIDATE_MESSAGE_REGEXES = [ re.compile(r".*task .+ defined but not used by any variants; consider using or disabling.*"), ] HORIZONTAL_LINE = "-" * 100 def messages_to_report(messages, num_of_projects): shared_evg_validate_messages = [] error_on_evg_validate_messages = [] for message in messages: if any(regex.match(message) for regex in ALLOWABLE_EVG_VALIDATE_MESSAGE_REGEXES): continue if num_of_projects > 1 and any( regex.match(message) for regex in ALLOWABLE_IF_NOT_IN_ALL_PROJECTS_EVG_VALIDATE_MESSAGE_REGEXES ): shared_evg_validate_messages.append(message) continue error_on_evg_validate_messages.append(message) return (error_on_evg_validate_messages, shared_evg_validate_messages) def default_evg_config(): config_locations = [ os.path.join(os.getcwd(), ".evergreen.yml"), os.path.expanduser("~/.evergreen.yml"), ] for candidate in config_locations: if os.path.exists(candidate): return candidate LOGGER.error(f"No evergreen config exists at any of {config_locations}.") sys.exit(1) def main( evg_project_name: Annotated[ str, typer.Option(help="Evergreen project name") ] = DEFAULT_EVG_PROJECT_NAME, evg_auth_config: Annotated[str, typer.Option(help="Evergreen auth config file")] = None, ): os.chdir(os.environ.get("BUILD_WORKSPACE_DIRECTORY", ".")) if not evg_auth_config: evg_auth_config = default_evg_config() evg_project_config_map = {evg_project_name: DEFAULT_EVG_NIGHTLY_PROJECT_CONFIG} if evg_project_name == DEFAULT_EVG_PROJECT_NAME: evg_project_config_map = { DEFAULT_EVG_NIGHTLY_PROJECT_NAME: DEFAULT_EVG_NIGHTLY_PROJECT_CONFIG, } if RELEASE_BRANCH: for _, project_config in evg_project_config_map.items(): cmd = [ evergreen_bin, "--config", evg_auth_config, "evaluate", "--path", project_config, ] LOGGER.info(f"Running command: {cmd}") subprocess.run(cmd, capture_output=True, text=True, check=True) sys.exit(0) if evg_project_name == DEFAULT_EVG_PROJECT_NAME: evg_project_config_map[DEFAULT_EVG_PROJECT_NAME] = DEFAULT_EVG_PROJECT_CONFIG shared_evg_validate_messages = [] error_on_evg_validate_messages = defaultdict(list) exit_code = 0 num_of_projects = len(evg_project_config_map) evergreen_bin = find_evergreen_binary("evergreen") for project, project_config in evg_project_config_map.items(): cmd = [ evergreen_bin, "--config", evg_auth_config, "validate", "--project", project, "--path", project_config, ] LOGGER.info(f"Running command: {cmd}") result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode: LOGGER.error( f"Command failed with return code {result.returncode}.\nstdout:{result.stdout}stderr:{result.stderr}" ) exit_code = 1 interesting_messages = result.stdout.strip().split("\n")[:-1] (error_on_evg_validate_messages[project], allowed_if_not_shared) = messages_to_report( interesting_messages, num_of_projects ) shared_evg_validate_messages.extend(allowed_if_not_shared) error_on_shared_evg_validate_messages = [] for message in set(shared_evg_validate_messages): if shared_evg_validate_messages.count(message) == num_of_projects: error_on_shared_evg_validate_messages.append(message) all_configs = list(evg_project_config_map.values()) all_projects = list(evg_project_config_map.keys()) for project, errors in error_on_evg_validate_messages.items(): if len(errors) > 0: exit_code = 1 project_config = evg_project_config_map[project] LOGGER.info(HORIZONTAL_LINE) LOGGER.error(f"Config '{project_config}' for '{project}' evergreen project has errors:") for error in errors: LOGGER.error(error) if any(regex.match(error) for regex in UNMATCHED_REGEXES): LOGGER.info( "Unmatched selector/criteria are allowed if they are tagged based (using '!' or '.'), but not if they directly name a task/task group" ) if len(error_on_shared_evg_validate_messages) > 0: exit_code = 1 LOGGER.info(HORIZONTAL_LINE) LOGGER.error( f"Configs {all_configs} for evergreen projects {all_projects} have errors" f" (they can be fixed in either config):" ) for error in error_on_shared_evg_validate_messages: LOGGER.error(error) if exit_code == 0: LOGGER.info(HORIZONTAL_LINE) LOGGER.info( f"Config(s) {all_configs} for evergreen project(s) {all_projects} is(are) valid" ) sys.exit(exit_code) if __name__ == "__main__": typer.run(main)