summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Samuels <richard.l.samuels@gmail.com>2021-06-01 17:49:24 -0400
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2021-06-15 01:14:51 +0000
commit6b74115e627a0764d8914b2a19725655563f80de (patch)
tree0e5e3ec3be896e7051902b8b500713d116e25863
parente2fba06768ba16225951ec0735ec1665e4a549c9 (diff)
downloadmongo-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__.py1
-rw-r--r--buildscripts/evglint/__main__.py57
-rw-r--r--buildscripts/evglint/helpers.py236
-rw-r--r--buildscripts/evglint/model.py5
-rw-r--r--buildscripts/evglint/rules.py368
-rw-r--r--buildscripts/evglint/yamlhandler.py39
-rw-r--r--buildscripts/tests/test_evglint.py774
-rwxr-xr-xbuildscripts/yamllinters.sh2
-rw-r--r--etc/evergreen.yml12
-rw-r--r--etc/evergreen_lint.yml18
-rw-r--r--etc/pip/components/lint.req1
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