diff options
author | Richard Samuels <richard.l.samuels@gmail.com> | 2021-06-01 17:49:24 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-06-15 01:14:51 +0000 |
commit | 6b74115e627a0764d8914b2a19725655563f80de (patch) | |
tree | 0e5e3ec3be896e7051902b8b500713d116e25863 | |
parent | e2fba06768ba16225951ec0735ec1665e4a549c9 (diff) | |
download | mongo-6b74115e627a0764d8914b2a19725655563f80de.tar.gz |
SERVER-57398 Extract evergreen yaml linter into its own repo
(cherry picked from commit b17e0a99632872e44684700978d55c01be1a7895)
(cherry picked from commit b5980ed90e48d6e7d196384fdf4f5707d1e3fb1d)
(cherry picked from commit 5dbe78f5ecf516be8996d74604479595657b406f)
(cherry picked from commit 9b89e3fe3c66e2641a0a7a5c15df35906ae29c56)
-rw-r--r-- | buildscripts/evglint/__init__.py | 1 | ||||
-rw-r--r-- | buildscripts/evglint/__main__.py | 57 | ||||
-rw-r--r-- | buildscripts/evglint/helpers.py | 236 | ||||
-rw-r--r-- | buildscripts/evglint/model.py | 5 | ||||
-rw-r--r-- | buildscripts/evglint/rules.py | 368 | ||||
-rw-r--r-- | buildscripts/evglint/yamlhandler.py | 39 | ||||
-rw-r--r-- | buildscripts/tests/test_evglint.py | 774 | ||||
-rwxr-xr-x | buildscripts/yamllinters.sh | 2 | ||||
-rw-r--r-- | etc/evergreen.yml | 12 | ||||
-rw-r--r-- | etc/evergreen_lint.yml | 18 | ||||
-rw-r--r-- | etc/pip/components/lint.req | 1 |
11 files changed, 32 insertions, 1481 deletions
diff --git a/buildscripts/evglint/__init__.py b/buildscripts/evglint/__init__.py deleted file mode 100644 index 43b4653ed25..00000000000 --- a/buildscripts/evglint/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""I exist to appease the linter.""" diff --git a/buildscripts/evglint/__main__.py b/buildscripts/evglint/__main__.py deleted file mode 100644 index fac55eb4916..00000000000 --- a/buildscripts/evglint/__main__.py +++ /dev/null @@ -1,57 +0,0 @@ -"""entry point for evglint.""" -import os -import copy -import sys -import itertools -from typing import Callable, List, Iterator, Dict - -import click -from typing_extensions import TypedDict -if not __package__: - newpath = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")) - sys.path.append(newpath) - -# pylint: disable=wrong-import-position -from buildscripts.evglint.rules import RULES -from buildscripts.evglint.model import LintError, LintRule -from buildscripts.evglint.yamlhandler import load_file - -_LINTABLE_YAMLS: List[str] = [ - "evergreen.yml" - # :( - # "perf.yml" - # "system-perf.yml" -] - - -@click.command() -def _main() -> int: - ret = 0 - for yaml_file in _LINTABLE_YAMLS: - yaml_dict = load_file(yaml_file) - errors: Dict[str, List[LintError]] = {} - for rule, fn in RULES.items(): - rule_errors = fn(yaml_dict) - if rule_errors: - errors[rule] = rule_errors - - err_count = 0 - for error_list in errors.values(): - err_count += len(error_list) - - if err_count: - print(f"{err_count} errors found in '{yaml_file}':") - print_nl = False - for rule, error_list in errors.items(): - for error in error_list: - if print_nl: - print("") - print(f"{rule}:", error) - print_nl = True - ret = 1 - - sys.exit(ret) - - -if __name__ == "__main__": - _main() diff --git a/buildscripts/evglint/helpers.py b/buildscripts/evglint/helpers.py deleted file mode 100644 index 53781f028d8..00000000000 --- a/buildscripts/evglint/helpers.py +++ /dev/null @@ -1,236 +0,0 @@ -"""Helpers for iterating over the yaml dictionary.""" -import re -from typing import Generator, Tuple, Union, List, Callable - -_CommandList = List[dict] -_Commands = Union[dict, _CommandList] -_Selector = Callable[[str, _Commands], Generator] - - -def _in_dict_and_truthy(dictionary: dict, key: str) -> bool: - return key in dictionary and dictionary[key] - - -def iterate_commands(yaml_dict: dict) -> Generator[Tuple[str, dict], None, None]: - """Return a Generator that yields commands from the yaml dict. - - :param dict yaml_dict: the parsed yaml dictionary - - Yields a Tuple, 0: is a human friendly description of where the command - block can be found, 1: a dict representing the command - """ - generator = iterate_commands_context(yaml_dict) - for (context, command, _) in generator: - yield (context, command) - - -# pylint: disable=too-many-branches -def _iterator(yaml_dict: dict, selector: _Selector, - skip_blocks: List[str] = None) -> Generator[Tuple[str, dict, dict], None, None]: - def _should_process(yaml_dict: dict, key: str) -> bool: - if skip_blocks and key in skip_blocks: - return False - - return _in_dict_and_truthy(yaml_dict, key) - - if _should_process(yaml_dict, "functions"): - for function, commands in yaml_dict["functions"].items(): - if not commands: - continue - gen = selector(f"Function '{function}'", commands) - for out in gen: - yield out - - if _should_process(yaml_dict, "tasks"): - for task in yaml_dict["tasks"]: - if _in_dict_and_truthy(task, "commands"): - gen = selector(f"Task '{task['name']}'", task["commands"]) - for out in gen: - yield out - if _in_dict_and_truthy(task, "setup_task"): - gen = selector(f"Task '{task['name']}', setup_task", task["setup_task"]) - for out in gen: - yield out - if _in_dict_and_truthy(task, "teardown_task"): - gen = selector(f"Task '{task['name']}', teardown_task", task["teardown_task"]) - for out in gen: - yield out - if _in_dict_and_truthy(task, "setup_group"): - gen = selector(f"Task '{task['name']}', setup_group", task["setup_group"]) - for out in gen: - yield out - if _in_dict_and_truthy(task, "teardown_group"): - gen = selector(f"Task '{task['name']}', teardown_group", task["teardown_group"]) - for out in gen: - yield out - if _in_dict_and_truthy(task, "timeout"): - gen = selector(f"Task '{task['name']}', timeout", task["timeout"]) - for out in gen: - yield out - - if _should_process(yaml_dict, "pre"): - gen = selector("Global pre", yaml_dict["pre"]) - for out in gen: - yield out - if _should_process(yaml_dict, "post"): - gen = selector("Global post", yaml_dict["post"]) - for out in gen: - yield out - if _should_process(yaml_dict, "timeout"): - gen = selector("Global timeout", yaml_dict["timeout"]) - for out in gen: - yield out - - -def iterate_commands_context(yaml_dict: dict, skip_blocks: List[str] = None - ) -> Generator[Tuple[str, dict, dict], None, None]: - """Return a Generator that yields commands from the yaml dict. - - :param dict yaml_dict: the parsed yaml dictionary - :param list skip_blocks: skip root level keys in the yaml dictionary in list - - Yields a Tuple, 0: is a human friendly description of where the command - block can be found, 1: a dict representing the command, 2: a dict or list of - dicts that provides the whole context of where that command is used. For - example, when iterating over commands in a function, this will be the list - of dicts or single dict that makes up the function definition. - - Functions will not be returned. - """ - - def _helper(prefix: str, - commands: _Commands) -> Generator[Tuple[str, _Commands, _CommandList], None, None]: - # commands are either a singular dict (representing one command), or - # a list of dicts - if isinstance(commands, dict): - # never yield functions - if "command" in commands: - yield (f'{prefix}, command', commands, commands) - else: - for idx, command in enumerate(commands): - if "command" in command: - yield (f"{prefix}, command {idx}", command, commands) - - gen = _iterator(yaml_dict, _helper, skip_blocks) - for out in gen: - yield out - - -def iterate_fn_calls_context(yaml_dict: dict) -> Generator[Tuple[str, dict, dict], None, None]: - """Return a Generator that yields function calls from the yaml dict. - - :param dict yaml_dict: the parsed yaml dictionary - - Yields a Tuple, 0: is a human friendly description of where the command - block can be found, 1: a dict representing the command, 2: a dict or list of - dicts that provides the whole context of where that command is used. For - example, when iterating over commands in a function, this will be the list - of dicts or single dict that makes up the function definition. - - Function definitions will not be returned. - """ - - def _helper(prefix: str, commands: Union[dict, List[dict]]): - # commands are either a singular dict (representing one command), or - # a list of dicts - if isinstance(commands, dict): - # only yield functions - if "func" in commands: - yield (f"{prefix}, function call '{commands['func']}'", commands, commands) - else: - for idx, command in enumerate(commands): - if "func" in command: - yield (f"{prefix}, command {idx} (function call: '{command['func']}')", command, - commands) - - gen = _iterator(yaml_dict, _helper) - for out in gen: - yield out - - -EVERGREEN_SCRIPT_RE = re.compile(r".*\/evergreen\/.*\.sh") - - -# Return true for any subprocess exec commands that look like this: -#- command: subprocess.exec -# params: -# with one of : -# args: -# - r"\/evergreen\/.*\.sh" -# or -# command: r"\/evergreen\/.*\.sh" -def match_subprocess_exec(command: dict) -> bool: - """Return True if the command is a subprocess.exec command that consumes an evergreen shell script.""" - if "command" in command and command["command"] != "subprocess.exec": - return False - if "params" not in command: - return False - - params = command["params"] - try: - if "args" in params and not EVERGREEN_SCRIPT_RE.search(params["args"][0]): - return False - elif "command" in params and not EVERGREEN_SCRIPT_RE.search(params["command"]): - return False - except IndexError: - return False - - return True - - -def match_expansions_update(command: dict) -> bool: - """Return True if the command is an expansions.update command.""" - return "command" in command and command["command"] == "expansions.update" - - -def match_timeout_update(command: dict) -> bool: - """Return True if the command is a timeout.update command.""" - return "command" in command and command["command"] == "timeout.update" - - -def match_expansions_write(command: dict) -> bool: - """Return True if the command is a properly formed expansions.write command. - - Properly formed is of the form: - command: expansions.write - params: - file: expansions.yml - redacted: true - """ - if "command" not in command or command["command"] != "expansions.write": - return False - - if "params" not in command: - return False - - params = command["params"] - if "file" not in params or params["file"] != "expansions.yml": - return False - if "redacted" not in params or not params["redacted"]: - return False - - return True - - -def iterate_command_lists(yaml_dict: dict) -> Generator[Tuple[str, _Commands], None, None]: - """Return a Generator that yields every single command list once. - - Command lists are defined as the list of commands found in tasks: - commands, setup_task, setup_group, teardown_task, teardown_group, timeout; - globally: pre, post, timeout, and the definitions of functions. - - :param dict yaml_dict: the parsed yaml dictionary - - Yields a Tuple, 0: is a human friendly description of where the command - block can be found, representing the command, 1: a dict or list of - dicts that provides the whole context of where that command is used. For - example, when iterating over commands in a function, this will be the list - of dicts or single dict that makes up the function definition. - """ - - def _helper(prefix: str, commands: Union[dict, List[dict]]): - yield (prefix, commands) - - gen = _iterator(yaml_dict, _helper) - for out in gen: - yield out diff --git a/buildscripts/evglint/model.py b/buildscripts/evglint/model.py deleted file mode 100644 index d77b13e5991..00000000000 --- a/buildscripts/evglint/model.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Type annotations for evglint.""" -from typing import Callable, List - -LintRule = Callable[[dict], List["LintError"]] -LintError = str diff --git a/buildscripts/evglint/rules.py b/buildscripts/evglint/rules.py deleted file mode 100644 index 52f03a553f7..00000000000 --- a/buildscripts/evglint/rules.py +++ /dev/null @@ -1,368 +0,0 @@ -"""Lint rules.""" -import re -from typing import Dict, List, Set, Optional - -import buildscripts.evglint.helpers as helpers -from buildscripts.evglint.model import LintRule, LintError -from buildscripts.evglint.helpers import iterate_commands, iterate_commands_context, iterate_fn_calls_context, iterate_command_lists - - -def no_keyval_inc(yaml: dict) -> List[LintError]: - """Prevent usage of keyval.inc.""" - - def _out_message(context: str) -> LintError: - return f"{context} includes keyval.inc, which is not permitted. Do not use keyval.inc." - - out: List[LintError] = [] - for context, command in iterate_commands(yaml): - if "command" in command and command["command"] == "keyval.inc": - out.append(_out_message(context)) - - return out - - -def shell_exec_explicit_shell(yaml: dict) -> List[LintError]: - """Require explicitly specifying shell in uses of shell.exec.""" - - def _out_message(context: str) -> LintError: - return f"{context} is a shell.exec command without an explicitly declared shell. You almost certainly want to add 'shell: bash' to the parameters list." - - out: List[LintError] = [] - for context, command in iterate_commands(yaml): - if "command" in command and command["command"] == "shell.exec": - if "params" not in command or "shell" not in command["params"]: - out.append(_out_message(context)) - - return out - - -SHELL_COMMANDS = ["subprocess.exec", "subprocess.scripting", "shell.exec"] - - -def no_working_dir_on_shell(yaml: dict) -> List[LintError]: - """Do not allow working_dir to be set on shell.exec, subprocess.*.""" - - def _out_message(context: str, cmd: str) -> LintError: - return f"{context} is a {cmd} command with a working_dir parameter. Do not set working_dir, instead `cd` into the directory in the shell script." - - out: List[LintError] = [] - for context, command in iterate_commands(yaml): - if "command" in command and command["command"] in SHELL_COMMANDS: - if "params" in command and "working_dir" in command["params"]: - out.append(_out_message(context, command["command"])) - - return out - - -FUNCTION_NAME = "^f_[a-z][A-Za-z0-9_]*" -FUNCTION_NAME_RE = re.compile(FUNCTION_NAME) - - -def invalid_function_name(yaml: dict) -> List[LintError]: - """Enforce naming convention on functions.""" - - def _out_message(context: str) -> LintError: - return f"Function '{context}' must have a name matching '{FUNCTION_NAME}'" - - if "functions" not in yaml: - return [] - - out: List[LintError] = [] - for fname in yaml["functions"].keys(): - if not FUNCTION_NAME_RE.fullmatch(fname): - out.append(_out_message(fname)) - - return out - - -def no_shell_exec(yaml: dict) -> List[LintError]: - """Do not allow shell.exec. Users should use subprocess.exec instead.""" - - def _out_message(context: str) -> LintError: - return (f"{context} is a shell.exec command, which is forbidden. " - "Extract your shell script out of the YAML and into a .sh file " - "in directory 'evergreen', and use subprocess.exec instead.") - - out: List[LintError] = [] - for context, command in iterate_commands(yaml): - if "command" in command and command["command"] == "shell.exec": - out.append(_out_message(context)) - return out - - -def no_multiline_expansions_update(yaml: dict) -> List[LintError]: - """Forbid multi-line values in expansion.updates parameters.""" - - def _out_message(context: str, idx: int) -> LintError: - return (f"{context}, key-value pair {idx} is an expansions.update " - "command with multi-line values embedded in the yaml, which is" - " forbidden. For long-form values, use the files parameter of " - "expansions.update.") - - out: List[LintError] = [] - for context, command in iterate_commands(yaml): - if "command" in command and command["command"] == "expansions.update": - if "params" in command and "updates" in command["params"]: - for idx, item in enumerate(command["params"]["updates"]): - if "value" in item and "\n" in item["value"]: - out.append(_out_message(context, idx)) - return out - - -BUILD_PARAMETER = "[a-z][a-z0-9_]*" -BUILD_PARAMETER_RE = re.compile(BUILD_PARAMETER) - - -def invalid_build_parameter(yaml: dict) -> List[LintError]: - """Require that parameters obey a naming convention and have a description.""" - - def _out_message_key(idx: int) -> LintError: - return f"Build parameter, pair {idx}, key must match '{BUILD_PARAMETER}'." - - def _out_message_description(idx: int) -> LintError: - return f"Build parameter, pair {idx}, must have a description." - - if "parameters" not in yaml: - return [] - - out: List[LintError] = [] - for idx, param in enumerate(yaml["parameters"]): - if "key" not in param or not BUILD_PARAMETER_RE.fullmatch(param["key"]): - out.append(_out_message_key(idx)) - if "description" not in param or not param["description"]: - out.append(_out_message_description(idx)) - return out - - -# pylint: disable=too-many-branches,too-many-locals,too-many-statements -def required_expansions_write(yaml: dict) -> List[LintError]: - """Require that subprocess.exec functions that consume evergreen scripts correctly bootstrap the prelude.""" - - # This logic is well and truly awful. - # Here's the problem: - # 1. Evergreen functions can have a dict or list definition. Functions - # with a dict definition, that is: - # "f_my_fn": &f_my_fn - # command: shell.exec - # can called either as "- func: f_my_fn" or as *f_my_fn. The former syntax - # can have arguments passed into it. The latter syntax, - # i.e. the use of the YAML anchor, is used to workaround Evergreen not - # allowing functions to call other functions. Arguments cannot be passed in - # when the YAML anchor syntax is used. - - # This poses a problem: if the user calls a function with a dict definition, - # and that function has arguments passed in, and that function calls - # subprocess.exec, then the function will not have its expansions populated - # in the external script as expected. It must be defined as a list, and - # incorporate an expansions.write call, or not be called with args. - # See Resolution 1. - - # Functions with a list definition, that is: - # "f_my_fn2": &f_my_fn2 - # - command: shell.exec - # - command: shell.exec - # cannot use the YAML anchor syntax (this is an evergreen validation - # error). If these functions call subprocess.exec, then they will only work - # if expansions.write is called before subprocess.exec. See Resolution 2. - # - # 2. Expansions can be defined/changed at any step of the way by Evergreen, - # function arguments, build variants, Evergreen projects, tasks. Expansions - # written to disk MUST be up to date. See Resolution 2 - # 3. Expansions be updated after expansions.update, but before - # subprocess.exec. See Resolution 3. - - # Resolutions: - # Given the above, here are the checks we need to perform - # 1. Any function defined with a dict definition that calls subprocess.exec - # MUST NOT be called with arguments. - # 2. expansions.write MUST be called prior to invoking subprocess.exec for - # the first time in any command list. - # (i.e. in tasks: commands, setup_task, setup_group, teardown_task, - # teardown_group, timeout,, globally: pre, post, timeout, functions with - # list definitions) - # When paired with Resolution 3, this works all the time (but does - # sometimes require redundant expansions.write calls) - # 3. Every invocation of expansions.update MUST be immediately followed by - # expansions.write - - # 4. It makes sense to place your expansions.write call in a dict - # definition function, that way you can call it with either syntax. So, in - # the above passage where I've mentioned "expansions.write", we must not - # only check for a properly formed expansions.write call, we must also - # check for a function that calls expansions.write. For reference, a - # properly formed expansions.write call looks like this: - # command: expansions.write - # params: - # file: expansions.yml - # redacted: true - # A function containing the above as a dict definition is treated as - # equivalent to calling expansions.write - # - # 5. Furthermore, above mentions of subprocess.exec MUST be applied only to - # subprocess.exec invocations that call scripts in the evergreen directory. - # - # 6. And because that's not complicated enough, any functions that are - # dict-defined with subprocess.exec must be treated as equivalent to - # subprocess.exec on its own. - # 7. Functions that call expansions.update, and are dict-defined MUST - # require an expansions.update call after they are called, regardless of - # syntax - # 8. timeout.update can also affect expansion values, and all of the rules - # above need to applied equally to timeout.update - - # These are functions that invoke expansions.write in a dict defintion, - # as described in Resolution 4. - expansions_write_fns: Set[str] = set() - # These are functions whose invocations must be checked for Resolution 1. - subprocess_exec_fns: Set[str] = set() - # And these are for Resolution 7 - expansions_update_fns: Set[str] = set() - # and Resolution 8 - timeout_update_fns: Set[str] = set() - - def _out_message_dangerous_function(context: str) -> LintError: - return f"{context} cannot safely take arguments. Call expansions.write with params: file: expansions.yml; redacted: true, (or use one of these functions: {list(expansions_write_fns)}) in the function, or do not pass arguments to it." - - def _out_message_expansions_update(context: str, fname: str = "expansions.update") -> LintError: - return f"{context} is an {fname} command that is not immediately followed by an expansions.write call. Always call expansions.write with params: file: expansions.yml; redacted: true, (or use one of these functions: {list(expansions_write_fns)}) after calling {fname}." - - def _out_message_subprocess(context: str) -> LintError: - return f"{context} calls an evergreen shell script without a preceding expansions.write call. Always call expansions.write with params: file: expansions.yml; redacted: true, (or use one of these functions: {list(expansions_write_fns)}) before calling an evergreen shell script via subprocess.exec." - - def _is_expansions_write_or_fn(command: dict) -> bool: - return helpers.match_expansions_write(command) or ("func" in command and - command["func"] in expansions_write_fns) - - def _is_subprocess_exec_or_fn(command: dict) -> bool: - return helpers.match_subprocess_exec(command) or ("func" in command and - command["func"] in subprocess_exec_fns) - - def _is_expansions_update_or_fn(command: dict) -> bool: - return helpers.match_expansions_update(command) or ( - "func" in command and command["func"] in expansions_update_fns) - - def _is_timeout_update_or_fn(command: dict) -> bool: - return helpers.match_timeout_update(command) or ("func" in command - and command["func"] in timeout_update_fns) - - def _context_add_fn(context: str, command: Optional[dict]) -> str: - if command and "func" in command: - return f'{context}, (function call: {command["func"]})' - - return context - - def _check_command_list(context: str, commands: List[dict]) -> List[LintError]: - out: List[LintError] = [] - first_subprocess: int = None - first_subprocess_cmd: dict = None - first_exp_write: int = None - warned_subprocess = False - for idx, command in enumerate(commands): - if first_subprocess is None and _is_subprocess_exec_or_fn(command): - first_subprocess = idx - first_subprocess_cmd = command - - elif first_exp_write is None and _is_expansions_write_or_fn(command): - first_exp_write = idx - - if not warned_subprocess and _is_subprocess_exec_or_fn( - command) and first_exp_write is None: - out.append( - _out_message_subprocess(_context_add_fn(f"{context}, command {idx}", command))) - # only warn for the first instance of this per command list. - # Once you resolve the first instance, the Resolution 3 and - # Resolution 8 checks below handle the rest of the errors. - warned_subprocess = True - - if (_is_expansions_update_or_fn(command) or _is_timeout_update_or_fn(command)): - # Resolution 3 and Resolution 8 - if len(commands) <= idx + 1 or not _is_expansions_write_or_fn(commands[idx + 1]): - if _is_expansions_update_or_fn(command): - out.append( - _out_message_expansions_update( - _context_add_fn(f"{context}, command {idx}", command))) - elif _is_timeout_update_or_fn(command): - out.append( - _out_message_expansions_update( - _context_add_fn(f"{context}, command {idx}", command), - "timeout.update")) - - if not warned_subprocess and first_subprocess is not None and first_exp_write is not None and first_subprocess < first_exp_write: - out.append( - _out_message_subprocess( - _context_add_fn(f"{context}, command {first_subprocess}", - first_subprocess_cmd))) - - return out - - out: List[LintError] = [] - if "functions" in yaml: - for fname, body in yaml["functions"].items(): - if isinstance(body, dict): - # assemble the list of functions whose bodies are the expected - # expansions.write call. (Resolution 4) - if helpers.match_expansions_write(body): - expansions_write_fns.add(fname) - # assemble the list of functions that must never be called - # with arguments (Resolution 1) - elif helpers.match_subprocess_exec(body): - subprocess_exec_fns.add(fname) - # Resolution 7 - elif helpers.match_expansions_update(body): - expansions_update_fns.add(fname) - # Resolution 8 - elif helpers.match_timeout_update(body): - timeout_update_fns.add(fname) - - for context, commands in iterate_command_lists(yaml): - if isinstance(commands, dict): - continue - out += _check_command_list(context, commands) - - for context, command, _ in iterate_fn_calls_context(yaml): - if command["func"] in subprocess_exec_fns and "vars" in command and command["vars"]: - out.append(_out_message_dangerous_function(context)) - - return out - - -RULES: Dict[str, LintRule] = { - #"invalid-function-name": invalid_function_name, - # TODO: after SERVER-54315 - #"no-keyval-inc": no_keyval_inc, - "no-working-dir-on-shell": no_working_dir_on_shell, - "no-shell-exec": no_shell_exec, - "no-multiline-expansions-update": no_multiline_expansions_update, - "invalid-build-parameter": invalid_build_parameter, - "required-expansions-write": required_expansions_write, -} -# Thoughts on Writing Rules -# - see .helpers for reliable iteration helpers -# - Do not assume a key exists, unless it's been mentioned here -# - Do not allow exceptions to percolate outside of the rule function -# - YAML anchors are not available. Unless you want to write your own yaml -# parser, or fork adrienverge/yamllint, abandon all hope on that idea you have. -# - Anchors are basically copy and paste, so you might see "duplicate" errors -# that originate from the same anchor, but are reported in multiple locations - -# Evergreen YAML Root Structure Reference -# Unless otherwise mentioned, the key is optional. You can infer the -# substructure by reading etc/evergreen.yml - -# Function blocks: are dicts with the key 'func', which maps to a string, -# the name of the function -# Command blocks: are dicts with the key 'command', which maps to a string, -# the Evergreen command to run - -# variables: List[dict]. These can be any valid yaml and it's very difficult -# to infer anything -# functions: Dict[str, Union[dict, List[dict]]]. The key is the name of the -# function, the value is either a dict, or list of dicts, with each dict -# representing a command -# pre, post, and timeout: List[dict] representing commands or functions to -# be run before/after/on timeout condition respectively -# tasks: List[dict], each dict is a task definition, key is always present -# task_groups: List[dict] -# modules: List[dict] -# buildvariants: List[dict], key is always present -# parameters: List[dict] diff --git a/buildscripts/evglint/yamlhandler.py b/buildscripts/evglint/yamlhandler.py deleted file mode 100644 index fff1e472d79..00000000000 --- a/buildscripts/evglint/yamlhandler.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Yaml handling helpers for evglint.""" -import io -import os -from typing import Union -import yaml - - -class ReadOnlyDict(dict): - """RO dictionary wrapper to prevent modifications to the yaml dict.""" - - # pylint: disable=no-self-use - def __readonly__(self, *args, **kwargs): - raise RuntimeError("Rules must not modify the yaml dictionary") - - __setitem__ = __readonly__ - __delitem__ = __readonly__ - pop = __readonly__ - popitem = __readonly__ - clear = __readonly__ - update = __readonly__ - setdefault = __readonly__ - del __readonly__ - - -def _etc_dir() -> str: - self_dir = os.path.dirname(os.path.realpath(__file__)) - return os.path.abspath(os.path.join(self_dir, "..", "..", "etc")) - - -def load_file(yaml_file: Union[str, os.PathLike]) -> dict: - """Load yaml from a file on disk.""" - with open(os.path.join(_etc_dir(), yaml_file)) as fh: - return load(fh) - - -def load(data: Union[io.TextIOWrapper, str, bytes]) -> dict: - """Given a file handle or buffer, load yaml.""" - yaml_dict = yaml.safe_load(data) - return ReadOnlyDict(yaml_dict) diff --git a/buildscripts/tests/test_evglint.py b/buildscripts/tests/test_evglint.py deleted file mode 100644 index 6e4cc5a09f5..00000000000 --- a/buildscripts/tests/test_evglint.py +++ /dev/null @@ -1,774 +0,0 @@ -"""evglint tests.""" -import unittest -from io import StringIO -from unittest.mock import MagicMock, patch -from typing import List - -import yaml -from typing_extensions import TypedDict - -from buildscripts.evglint.yamlhandler import load -from buildscripts.evglint import rules -from buildscripts.evglint.model import LintRule, LintError -import buildscripts.evglint.helpers as h - - -class TestRulebreaker(unittest.TestCase): - """Attempt to raise exceptions in evglint rules.""" - - # the Evergreen YAML freely allows for lists of dicts or just a single - # dict for commands, which can painfully lead to exceptions. - # Additionally, the rule author cannot safely assume that any parameters - # are defined, so we generate an even larger list of parameter-less - # commands that will raise exceptions in any rule. - RULEBREAKER = """ - functions: - "single command": &a1 - # this is, surprisingly, a valid evergreen command - command: shell.exec - - "list of commands": &a2 - - command: shell.exec - params: - script: /bin/true - - command: shell.exec - params: - script: /bin/true - - "deliberately empty kv pair": - "inject here": -{inject_here} - "anchor cheese": - - *a1 - - *a1 - - timeout: - - *a1 - pre: - - *a1 - post: - - *a1 - tasks: - - name: empty - - name: clang_tidy - setup_task: - - *a1 - teardown_task: - - *a1 - teardown_group: - - *a1 - setup_group: - - *a1 - timeout: - - *a1 - - commands: - - func: "single command" - - func: "anchor cheese" - - command: shell.exec - """ - - @classmethod - def _gen_rule_breaker(cls) -> dict: - # List from https://github.com/evergreen-ci/evergreen/wiki/Project-Commands - commands = [ - "keyval.inc", - "archive.targz_extract", - "archive.targz_pack", - "attach.artifacts", - "attach.results", - "attach.xunit_results", - "expansions.update", - "expansions.write", - "generate.tasks", - "git.get_project", - "gotest.parse_files", - "host.create", - "host.list", - "json.send", - "manifest.load", - "perf.send", - "s3.get", - "s3.put", - "s3.push", - "s3.pull", - "s3Copy.copy", - "shell.exec", - "subprocess.exec", - "subprocess.scripting", - "timeout.update", - ] - buf = StringIO() - for cmd in commands: - buf.write(f" - command: {cmd}\n") - - gen_commands = TestRulebreaker.RULEBREAKER.format(inject_here=buf.getvalue()) - return load(gen_commands) - - def test_break_rules(self): - """test that rules don't raise exceptions.""" - yaml_dict = self._gen_rule_breaker() - for rule_name, rule in rules.RULES.items(): - try: - rule(yaml_dict) - except Exception as ex: # pylint: disable=broad-except - self.fail(f"{rule_name} raised an exception, but must not. " - "The rule is likely accessing a key without " - "verifying that it exists first. Write a more " - "thorough rule.\n" - f"Exception: {ex}") - - -class TestHelpers(unittest.TestCase): - """Test .helpers module.""" - - def test_iterate_commands(self): - """test iterate_commands.""" - yaml_dict = load(TestRulebreaker.RULEBREAKER.format(inject_here="")) - gen = h.iterate_commands(yaml_dict) - count = 0 - for _ in gen: - count = count + 1 - self.assertEqual(count, 14) - - I_CANT_BELIEVE_THAT_VALIDATES = """ -tasks: -- name: test - """ - - def test_iterate_commands_no_commands(self): - """Test iterate_commands when the yaml has no commands.""" - yaml_dict = load(TestHelpers.I_CANT_BELIEVE_THAT_VALIDATES) - gen = h.iterate_commands(yaml_dict) - count = 0 - for _ in gen: - count = count + 1 - self.assertEqual(count, 0) - - def test_iterate_command_lists(self): - """test iterate_command_lists.""" - yaml_dict = load(TestRulebreaker.RULEBREAKER.format(inject_here="")) - gen = h.iterate_command_lists(yaml_dict) - count = 0 - for _ in gen: - count = count + 1 - self.assertEqual(count, 12) - - def test_iterate_command_lists_no_commands(self): - """Test iterate_command_lists when the yaml has no commands.""" - yaml_dict = load(TestHelpers.I_CANT_BELIEVE_THAT_VALIDATES) - gen = h.iterate_command_lists(yaml_dict) - count = 0 - for _ in gen: - count = count + 1 - self.assertEqual(count, 0) - - def test_match_expansions_write(self): - """Test match_expansions_write.""" - cmd = {} - self.assertFalse(h.match_expansions_write(cmd)) - cmd = { - "command": "expansions.write", "params": {"file": "expansions.yml", "redacted": True} - } - self.assertTrue(h.match_expansions_write(cmd)) - - def test_iterate_fn_calls_context(self): - """Test iterate_fn_calls_context.""" - yaml_dict = load(TestRulebreaker.RULEBREAKER.format(inject_here="")) - gen = h.iterate_fn_calls_context(yaml_dict) - count = 0 - for _ in gen: - count = count + 1 - self.assertEqual(count, 2) - - def test_match_subprocess_exec(self): - """Test match_subprocess_exec.""" - cmd = {} - self.assertFalse(h.match_subprocess_exec(cmd)) - cmd = { - "command": "subprocess.exec", - "params": {"binary": "bash", "args": ["./src/evergreen/something.sh"]} - } - self.assertTrue(h.match_subprocess_exec(cmd)) - - -class _RuleExpect(TypedDict): - raw_yaml: str - errors: List[LintError] - - -class _BaseTestClasses: - # this extra class prevents unittest from running the base class as a test - # suite - - class RuleTest(unittest.TestCase): - """Test a rule.""" - - @staticmethod - def _whine(_: dict) -> LintRule: - raise RuntimeError("Programmer error: func was not set") - - def __init__(self, *args, **kwargs): - self.table: List[_RuleExpect] = [] - self.func: LintRule = self._whine - super().__init__(*args, **kwargs) - self.maxDiff = None # pylint: disable=invalid-name - - def test_rule(self): - """Test self.func with the yamls listed in self.table, and compare results.""" - - for expectation in self.table: - yaml_dict = load(expectation["raw_yaml"]) - errors = self.func(yaml_dict) - # a discrepancy on this assert means that your rule isn't working - # as expected - self.assertListEqual(errors, expectation["errors"]) - - -class TestNoKeyvalInc(_BaseTestClasses.RuleTest): - """Test no-keyval-inc.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.func = rules.no_keyval_inc - self.table = [ - { - "raw_yaml": - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: shell.exec -tasks: -- name: test - """, "errors": [] - }, - { - 'raw_yaml': - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: keyval.inc -tasks: -- name: test - """, - "errors": [ - "Function 'cat i'm a kitty cat, and i test test test and i test test test', command 0 includes keyval.inc, which is not permitted. Do not use keyval.inc." - ] - }, - ] - - -class TestShellExecExplicitShell(_BaseTestClasses.RuleTest): - """Test shell-exec-explicit-shell.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.func = rules.shell_exec_explicit_shell - self.table = [ - { - "raw_yaml": - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: shell.exec - params: - shell: bash -tasks: -- name: test - """, "errors": [] - }, - { - 'raw_yaml': - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: shell.exec -tasks: -- name: test - """, - "errors": [ - "Function 'cat i'm a kitty cat, and i test test test and i test test test', command 0 is a shell.exec command without an explicitly declared shell. You almost certainly want to add 'shell: bash' to the parameters list." - ] - }, - ] - - -class TestNoWorkingDirOnShell(_BaseTestClasses.RuleTest): - """Test no-working-dir-on-shell.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.func = rules.no_working_dir_on_shell - self.table = [ - { - "raw_yaml": - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: subprocess.exec -tasks: -- name: test - """, "errors": [] - }, - { - 'raw_yaml': - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: shell.exec - params: - working_dir: somewhere -tasks: -- name: test - """, - "errors": [( - "Function 'cat i'm a kitty cat, and i test test test and i test test test', command 0 is a shell.exec command with a working_dir parameter. Do not set working_dir, instead `cd` into the directory in the shell script." - )] - }, - ] - - -class TestInvalidFunctionName(_BaseTestClasses.RuleTest): - """Test invalid-function-name.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.func = rules.invalid_function_name - self.table = [ - { - 'raw_yaml': - """ -functions: - "f_cat_im_a_kitty_cat_and_i_test_test_test_and_i_test_test_test": - - command: shell.exec -tasks: -- name: test - """, "errors": [] - }, - { - "raw_yaml": - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: subprocess.exec -tasks: -- name: test - """, - "errors": [( - "Function 'cat i'm a kitty cat, and i test test test and i test test test' must have a name matching '^f_[a-z][A-Za-z0-9_]*'" - )] - }, - ] - - -class TestNoShellExec(_BaseTestClasses.RuleTest): - """Test no-shell-exec.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.func = rules.no_shell_exec - self.table = [ - { - 'raw_yaml': - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: subprocess.exec -tasks: -- name: test - """, "errors": [] - }, - { - "raw_yaml": - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: shell.exec -tasks: -- name: test - """, - "errors": [( - "Function 'cat i'm a kitty cat, and i test test test and i test test test', command 0 is a shell.exec command, which is forbidden. Extract your shell script out of the YAML and into a .sh file in directory 'evergreen', and use subprocess.exec instead." - )] - }, - ] - - -class TestNoMultilineExpansionsUpdate(_BaseTestClasses.RuleTest): - """Test no-multiline-expansions-update.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.func = rules.no_multiline_expansions_update - self.table = [ - { - 'raw_yaml': - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: expansions.update - params: - updates: - - key: test - value: a single line value \n -tasks: -- name: test - """, "errors": [] - }, - { - "raw_yaml": - """ -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: expansions.update - params: - updates: - - key: test - value: | - a - multiline - value - -tasks: -- name: test - """, - "errors": - [("Function 'cat i'm a kitty cat, and i test test test and i test test test', " - "command 0, key-value pair 0 is an expansions.update command with multi-line " - "values embedded in the yaml, which is forbidden. For long-form values, use " - "the files parameter of expansions.update.")] - }, - ] - - -class TestInvalidBuildParameter(_BaseTestClasses.RuleTest): - """Test invalid-build-parameter.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.func = rules.invalid_build_parameter - self.table = [ - { - 'raw_yaml': - """ -parameters: - - key: num_kitties - description: "number of kitties" - -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: shell.exec -tasks: -- name: test - """, "errors": [] - }, - { - "raw_yaml": - """ -parameters: - - key: numberOfKitties - description: "number of kitties" - - key: number_of_kitties - - key: number_of_kitties2 - description: "" - -functions: - "cat i'm a kitty cat, and i test test test and i test test test": - - command: shell.exec -tasks: -- name: test - """, "errors": [ - "Build parameter, pair 0, key must match '[a-z][a-z0-9_]*'.", - "Build parameter, pair 1, must have a description.", - "Build parameter, pair 2, must have a description." - ] - }, - ] - - -class TestRequiredExpansionsWrite(_BaseTestClasses.RuleTest): - """Test required-expansions-write.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.func = rules.required_expansions_write - self.table = [ - { - 'raw_yaml': - """ -functions: - # this function can serve in lieu of an expansions.write call - "f_expansions_write": &f_expansions_write - command: expansions.write - params: - file: expansions.yml - redacted: true - - # this function cannot, because redacted is not True - "f_expansions_write2": &f_expansions_write2 - command: expansions.write - params: - file: expansions.yml - - "dangerous_fn": &dangerous_fn - # will not generate errors because this is a dict defintion. Errors - # will be generated if this function is called with arguments - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" - - "dangerous_fn2": &dangerous_fn2 - # will not generate errors because this is a dict defintion. - command: expansions.update - - "test1": - # needs expansions.write - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" - # only ONE of these should generate an error - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" - "test2a": - # correct - - func: "f_expansions_write" - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" - "test2b": - # correct - - *f_expansions_write - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" - "test2c": - # function isn't a compatible substitution - - *f_expansions_write2 - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" - "test3": - # correct because the subprocess.exec call is a script outside - # of evergreen - - command: subprocess.exec - params: - binary: bash - args: - - "somewhere/else/do_something.sh" - "test3a": - # needs expansions.write - - command: subprocess.exec - params: - binary: bash - args: - - "somewhere/else/do_something.sh" - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" - "test4a": - # need an expansions.write call after expansions.update - - command: expansions.update - "test4b": - # need an expansions.write call after expansions.update - - command: expansions.update - - command: shell.exec - - *f_expansions_write - "test4c": - # no errors - - command: expansions.update - - *f_expansions_write - "test4d": - # errors, because an incompatible function is called - - *f_expansions_write2 - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" - "test5": - # errors, because an incompatible function is called - - command: expansions.update - - func: f_expansions_write2 - - "test6": - # error because this needs an expansions write call after dangerous_fn2 - - *f_expansions_write - - func: "dangerous_fn2" - vars: - test: test - - "test7": - # error, needs an expansion.write at the end - - command: expansions.update - - *f_expansions_write - - command: timeout.update - - "test8": - # no errors - - command: expansions.update - - *f_expansions_write - - command: timeout.update - - *f_expansions_write - - command: subprocess.exec - params: - binary: bash - args: - - "somewhere/else/do_something.sh" - - command: timeout.update - - *f_expansions_write - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" - - "test8b": - - command: expansions.update - - command: timeout.update - - *f_expansions_write - - command: subprocess.exec - params: - binary: bash - args: - - "somewhere/else/do_something.sh" - - command: timeout.update - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" - -tasks: -- name: test - commands: - # need an expansions.write call here - - command: shell.exec - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" -- name: test1 - commands: - - func: "dangerous_fn" - vars: - test: true - -- name: test2 - commands: - # need an expansions.write call after expansions.update - - command: expansions.update - - command: shell.exec - - command: subprocess.exec - params: - binary: bash - args: - - "src/evergreen/do_something.sh" -- name: test3 - commands: - # an expansions.write call is required here. - - func: dangerous_fn - """, - "errors": [ - "Function 'test1', command 0 calls an evergreen shell script without a " - 'preceding expansions.write call. Always call expansions.write with params: ' - 'file: expansions.yml; redacted: true, (or use one of these functions: ' - "['f_expansions_write']) before calling an evergreen shell script via " - 'subprocess.exec.', - "Function 'test2c', command 1 calls an evergreen shell script without a " - 'preceding expansions.write call. Always call expansions.write with params: ' - 'file: expansions.yml; redacted: true, (or use one of these functions: ' - "['f_expansions_write']) before calling an evergreen shell script via " - 'subprocess.exec.', - "Function 'test3a', command 1 calls an evergreen shell script without a " - 'preceding expansions.write call. Always call expansions.write with params: ' - 'file: expansions.yml; redacted: true, (or use one of these functions: ' - "['f_expansions_write']) before calling an evergreen shell script via " - 'subprocess.exec.', - "Function 'test4a', command 0 is an expansions.update command that is not " - 'immediately followed by an expansions.write call. Always call ' - 'expansions.write with params: file: expansions.yml; redacted: true, (or use ' - "one of these functions: ['f_expansions_write']) after calling " - 'expansions.update.', - "Function 'test4b', command 0 is an expansions.update command that is not " - 'immediately followed by an expansions.write call. Always call ' - 'expansions.write with params: file: expansions.yml; redacted: true, (or use ' - "one of these functions: ['f_expansions_write']) after calling " - 'expansions.update.', - "Function 'test4d', command 1 calls an evergreen shell script without a " - 'preceding expansions.write call. Always call expansions.write with params: ' - 'file: expansions.yml; redacted: true, (or use one of these functions: ' - "['f_expansions_write']) before calling an evergreen shell script via " - 'subprocess.exec.', - "Function 'test5', command 0 is an expansions.update command that is not " - 'immediately followed by an expansions.write call. Always call ' - 'expansions.write with params: file: expansions.yml; redacted: true, (or use ' - "one of these functions: ['f_expansions_write']) after calling " - 'expansions.update.', - "Function 'test6', command 1, (function call: dangerous_fn2) is an " - 'expansions.update command that is not immediately followed by an ' - 'expansions.write call. Always call expansions.write with params: file: ' - 'expansions.yml; redacted: true, (or use one of these functions: ' - "['f_expansions_write']) after calling expansions.update.", - "Function 'test7', command 2 is an timeout.update command that is not " - 'immediately followed by an expansions.write call. Always call ' - 'expansions.write with params: file: expansions.yml; redacted: true, (or use ' - "one of these functions: ['f_expansions_write']) after calling " - 'timeout.update.', - "Function 'test8b', command 0 is an expansions.update command that is not " - 'immediately followed by an expansions.write call. Always call ' - 'expansions.write with params: file: expansions.yml; redacted: true, (or use ' - "one of these functions: ['f_expansions_write']) after calling " - 'expansions.update.', - "Function 'test8b', command 4 is an timeout.update command that is not " - 'immediately followed by an expansions.write call. Always call ' - 'expansions.write with params: file: expansions.yml; redacted: true, (or use ' - "one of these functions: ['f_expansions_write']) after calling " - 'timeout.update.', - "Task 'test', command 1 calls an evergreen shell script without a preceding " - 'expansions.write call. Always call expansions.write with params: file: ' - 'expansions.yml; redacted: true, (or use one of these functions: ' - "['f_expansions_write']) before calling an evergreen shell script via " - 'subprocess.exec.', - "Task 'test1', command 0, (function call: dangerous_fn) calls an evergreen " - 'shell script without a preceding expansions.write call. Always call ' - 'expansions.write with params: file: expansions.yml; redacted: true, (or use ' - "one of these functions: ['f_expansions_write']) before calling an evergreen " - 'shell script via subprocess.exec.', - "Task 'test2', command 0 is an expansions.update command that is not " - 'immediately followed by an expansions.write call. Always call ' - 'expansions.write with params: file: expansions.yml; redacted: true, (or use ' - "one of these functions: ['f_expansions_write']) after calling " - 'expansions.update.', - "Task 'test2', command 2 calls an evergreen shell script without a preceding " - 'expansions.write call. Always call expansions.write with params: file: ' - 'expansions.yml; redacted: true, (or use one of these functions: ' - "['f_expansions_write']) before calling an evergreen shell script via " - 'subprocess.exec.', - "Task 'test3', command 0, (function call: dangerous_fn) calls an evergreen " - 'shell script without a preceding expansions.write call. Always call ' - 'expansions.write with params: file: expansions.yml; redacted: true, (or use ' - "one of these functions: ['f_expansions_write']) before calling an evergreen " - 'shell script via subprocess.exec.', - "Task 'test1', command 0 (function call: 'dangerous_fn') cannot safely take " - 'arguments. Call expansions.write with params: file: expansions.yml; ' - "redacted: true, (or use one of these functions: ['f_expansions_write']) in " - 'the function, or do not pass arguments to it.' - ] - }, - ] diff --git a/buildscripts/yamllinters.sh b/buildscripts/yamllinters.sh index 1450014a774..37ad1cab454 100755 --- a/buildscripts/yamllinters.sh +++ b/buildscripts/yamllinters.sh @@ -4,4 +4,4 @@ BASEDIR=$(dirname "$0") cd "$BASEDIR/../" find buildscripts etc jstests -name '*.y*ml' -exec yamllint -c etc/yamllint_config.yml {} + -python3 buildscripts/evglint +python -m evergreen_lint -c ./etc/evergreen_lint.yml lint diff --git a/etc/evergreen.yml b/etc/evergreen.yml index af29201c4a1..b1c6544cc23 100644 --- a/etc/evergreen.yml +++ b/etc/evergreen.yml @@ -1,4 +1,12 @@ ##################################################### +# YAML Conventions # +##################################################### +# Please see our conventions document at +# https://wiki.corp.mongodb.com/pages/viewpage.action?pageId=133273894 +# for help navigating this document, or for help with our lint rules. + + +##################################################### # A note on expansions # ##################################################### @@ -7213,10 +7221,12 @@ task_groups: - func: "set up win mount script" - func: "generate compile expansions" teardown_group: + - func: "f_expansions_write" - func: "umount shared scons directory" - func: "cleanup environment" setup_task: - func: "apply compile expansions" + - func: "f_expansions_write" - func: "set task expansion macros" - func: "f_expansions_write" teardown_task: @@ -7241,6 +7251,7 @@ task_groups: - func: "umount shared scons directory" setup_task: - func: "set task expansion macros" + - func: "f_expansions_write" - func: "apply compile expansions" - func: "f_expansions_write" teardown_task: @@ -7266,6 +7277,7 @@ task_groups: - func: "umount shared scons directory" setup_task: - func: "set task expansion macros" + - func: "f_expansions_write" - func: "apply compile expansions" - func: "f_expansions_write" teardown_task: diff --git a/etc/evergreen_lint.yml b/etc/evergreen_lint.yml new file mode 100644 index 00000000000..1414f4259b4 --- /dev/null +++ b/etc/evergreen_lint.yml @@ -0,0 +1,18 @@ +# These paths are relative to the directory containing this configuration file +files: + - evergreen.yml +help_url: https://wiki.corp.mongodb.com/pages/viewpage.action?pageId=133273894 + +rules: + # this is a list of all rules available, their parameters, and their + # default values. Comment out a rule to disable it + - rule: "limit-keyval-inc" + # the maximum number of keyval.inc commands to allow in your YAML + limit: 4 + - rule: "shell-exec-explicit-shell" + - rule: "no-working-dir-on-shell" + - rule: "no-shell-exec" + - rule: "no-multiline-expansions-update" + - rule: "invalid-build-parameter" + - rule: "required-expansions-write" + regex: .*\/evergreen\/.*\.sh diff --git a/etc/pip/components/lint.req b/etc/pip/components/lint.req index fe366186b49..d8f159290a2 100644 --- a/etc/pip/components/lint.req +++ b/etc/pip/components/lint.req @@ -8,3 +8,4 @@ structlog ~= 19.2.0 typing yamllint == 1.15.0 yapf == 0.26.0 +evergreen-lint == 0.1.2 |