Files
mongo/buildscripts/validate_evg_project_config.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

181 lines
6.6 KiB
Python
Raw Normal View History

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)