diff --git a/commodore/cli/component.py b/commodore/cli/component.py index bbe500ee..f0f66a64 100644 --- a/commodore/cli/component.py +++ b/commodore/cli/component.py @@ -5,7 +5,7 @@ from collections.abc import Iterable from datetime import timedelta from pathlib import Path -from typing import Optional +from typing import Optional, Tuple import click @@ -18,6 +18,23 @@ import commodore.cli.options as options +def _generate_option_text_snippets(new_cmd: bool) -> Tuple[str, str]: + if new_cmd: + test_case_help = ( + "Additional test cases to generate in the new component. " + + "Can be repeated. Test case `defaults` will always be generated. " + + "Commodore will deduplicate test cases by name." + ) + else: + test_case_help = ( + "Additional test cases to add to the component. Can be repeated. " + + "Commodore will deduplicate test cases by name." + ) + add_text = "Add" if new_cmd else "Add or remove" + + return add_text, test_case_help + + def new_update_options(new_cmd: bool): """Shared command options for component new and component update. @@ -28,18 +45,22 @@ def new_update_options(new_cmd: bool): unchanged by default by `component update`. """ + add_text, test_case_help = _generate_option_text_snippets(new_cmd) + def decorator(cmd): - if new_cmd: - test_case_help = ( - "Additional test cases to generate in the new component. " - + "Can be repeated. Test case `defaults` will always be generated. " - + "Commodore will deduplicate test cases by name." - ) - else: - test_case_help = ( - "Additional test cases to add to the component. Can be repeated. " - + "Commodore will deduplicate test cases by name." - ) + click.option( + "--automerge-patch-v0 / --no-automerge-patch-v0", + is_flag=True, + default=False if new_cmd else None, + help="Enable automerging of patch-level dependency PRs " + + "for v0.x dependencies.", + )(cmd) + click.option( + "--automerge-patch / --no-automerge-patch", + is_flag=True, + default=True if new_cmd else None, + help="Enable automerging of patch-level dependency PRs.", + )(cmd) click.option( "--additional-test-case", "-t", @@ -49,7 +70,6 @@ def decorator(cmd): multiple=True, help=test_case_help, )(cmd) - add_text = "Add" if new_cmd else "Add or remove" click.option( "--matrix-tests/--no-matrix-tests", default=True if new_cmd else None, @@ -147,6 +167,8 @@ def component_new( template_url: str, template_version: str, additional_test_case: Iterable[str], + automerge_patch: bool, + automerge_patch_v0: bool, ): config.update_verbosity(verbose) t = ComponentTemplater( @@ -159,6 +181,8 @@ def component_new( t.golden_tests = golden_tests t.matrix_tests = matrix_tests t.test_cases = ["defaults"] + list(additional_test_case) + t.automerge_patch = automerge_patch + t.automerge_patch_v0 = automerge_patch_v0 t.create() @@ -204,6 +228,8 @@ def component_update( additional_test_case: Iterable[str], remove_test_case: Iterable[str], commit: bool, + automerge_patch: Optional[bool], + automerge_patch_v0: Optional[bool], ): """This command updates the component at COMPONENT_PATH to the latest version of the template which was originally used to create it, if the template version is given as @@ -230,6 +256,10 @@ def component_update( t.library = lib if pp is not None: t.post_process = pp + if automerge_patch is not None: + t.automerge_patch = automerge_patch + if automerge_patch_v0 is not None: + t.automerge_patch_v0 = automerge_patch_v0 test_cases = t.test_cases test_cases.extend(additional_test_case) diff --git a/commodore/component/template.py b/commodore/component/template.py index 68739ed1..01aeb360 100644 --- a/commodore/component/template.py +++ b/commodore/component/template.py @@ -16,6 +16,8 @@ class ComponentTemplater(Templater): library: bool post_process: bool + _automerge_patch: bool + automerge_patch_v0: bool _matrix_tests: bool @classmethod @@ -79,6 +81,8 @@ def _initialize_from_cookiecutter_args(self, cookiecutter_args: dict[str, str]): self.library = cookiecutter_args["add_lib"] == "y" self.post_process = cookiecutter_args["add_pp"] == "y" self.matrix_tests = cookiecutter_args["add_matrix"] == "y" + self.automerge_patch = cookiecutter_args["automerge_patch"] == "y" + self.automerge_patch_v0 = cookiecutter_args["automerge_patch_v0"] == "y" return update_cruft_json @@ -88,8 +92,24 @@ def cookiecutter_args(self) -> dict[str, str]: args["add_lib"] = "y" if self.library else "n" args["add_pp"] = "y" if self.post_process else "n" args["add_matrix"] = "y" if self.matrix_tests else "n" + args["automerge_patch"] = "y" if self.automerge_patch else "n" + args["automerge_patch_v0"] = "y" if self.automerge_patch_v0 else "n" return args + @property + def automerge_patch(self) -> bool: + if self.automerge_patch_v0: + click.echo( + " > Forcing automerging of patch dependencies to be enabled " + + "when automerging of v0.x patch dependencies is requested" + ) + return True + return self._automerge_patch + + @automerge_patch.setter + def automerge_patch(self, automerge_patch: bool) -> None: + self._automerge_patch = automerge_patch + @property def matrix_tests(self) -> bool: if len(self.test_cases) > 1: diff --git a/docs/modules/ROOT/pages/reference/cli.adoc b/docs/modules/ROOT/pages/reference/cli.adoc index e41cea3e..a9b78f14 100644 --- a/docs/modules/ROOT/pages/reference/cli.adoc +++ b/docs/modules/ROOT/pages/reference/cli.adoc @@ -208,6 +208,14 @@ Defaults to `--no-force`. Test case `defaults` will always be generated. Commodore will deduplicate the provided test cases. +*--automerge-patch / --no-automerge-patch*:: + Enable automerging of patch-level dependency PRs. + +*--automerge-patch-v0 / --no-automerge-patch-v0*:: + Enable automerging of patch-level dependency PRs for v0.x dependencies. ++ +NOTE: Enabling automerging of patch-level dependency PRs for v0.x dependencies implicitly enables automerging of all patch-level dependency PRs. + *--help*:: Show component new usage and options then exit. @@ -254,6 +262,14 @@ Defaults to `--no-force`. *--commit / --no-commit*:: Whether to commit the rendered template changes. +*--automerge-patch / --no-automerge-patch*:: + Enable automerging of patch-level dependency PRs. + +*--automerge-patch-v0 / --no-automerge-patch-v0*:: + Enable automerging of patch-level dependency PRs for v0.x dependencies. ++ +NOTE: Enabling automerging of patch-level dependency PRs for v0.x dependencies implicitly enables automerging of all patch-level dependency PRs. + *--help*:: Show component new usage and options then exit. diff --git a/tests/test_component_template.py b/tests/test_component_template.py index 685cb304..e8740faa 100644 --- a/tests/test_component_template.py +++ b/tests/test_component_template.py @@ -32,13 +32,17 @@ def call_component_new( pp="--no-pp", golden="--no-golden-tests", matrix="--no-matrix-tests", + automerge_patch="--no-automerge-patch", + automerge_patch_v0="--no-automerge-patch-v0", output_dir="", extra_args: list[str] = [], ): args = ["-d", str(tmp_path), "component", "new"] if output_dir: args.extend(["--output-dir", str(output_dir)]) - args.extend([component_name, lib, pp, golden, matrix]) + args.extend( + [component_name, lib, pp, golden, matrix, automerge_patch, automerge_patch_v0] + ) args.extend(extra_args) result = cli_runner(args) assert result.exit_code == 0 @@ -52,6 +56,8 @@ def _validate_rendered_component( has_pp: bool, has_golden: bool, has_matrix: bool, + has_automerge_patch: bool, + has_automerge_patch_v0: bool, test_cases: list[str] = ["defaults"], ): expected_files = [ @@ -188,6 +194,29 @@ def _validate_rendered_component( cmd = renovateconfig["postUpgradeTasks"]["commands"][0] expected_cmd = "make gen-golden-all" if has_matrix else "make gen-golden" assert cmd == expected_cmd + assert "packageRules" in renovateconfig + package_rules = renovateconfig["packageRules"] + assert (len(package_rules) == 1) == ( + has_automerge_patch or has_automerge_patch_v0 + ) + if has_automerge_patch or has_automerge_patch_v0: + patch_rule = package_rules[0] + expected_keys = { + "matchUpdateTypes", + "automerge", + "platformAutomerge", + "labels", + } + if not has_automerge_patch_v0 and has_automerge_patch: + expected_keys.add("matchCurrentVersion") + + assert set(patch_rule.keys()) == expected_keys + assert patch_rule["matchUpdateTypes"] == ["patch"] + assert patch_rule["automerge"] is True + assert patch_rule["platformAutomerge"] is False + assert patch_rule["labels"] == ["dependency", "automerge"] + if "matchCurrentVersion" in patch_rule: + assert patch_rule["matchCurrentVersion"] == "!/^v?0\\./" def _format_test_case_args(flag: str, test_cases: list[str]) -> list[str]: @@ -233,7 +262,14 @@ def test_run_component_new_command( ) _validate_rendered_component( - tmp_path, component_name, has_lib, has_pp, has_golden, has_matrix + tmp_path, + component_name, + has_lib, + has_pp, + has_golden, + has_matrix, + False, + False, ) @@ -264,7 +300,15 @@ def test_run_component_new_with_additional_test_cases( not in result.stdout ) _validate_rendered_component( - tmp_path, component_name, False, False, True, True, ["defaults"] + test_cases + tmp_path, + component_name, + False, + False, + True, + True, + False, + False, + ["defaults"] + test_cases, ) @@ -284,7 +328,15 @@ def test_run_component_new_force_matrix_additional_test_cases( assert " > Forcing matrix tests when multiple test cases requested" in result.stdout _validate_rendered_component( - tmp_path, component_name, False, False, True, True, ["defaults", "foo"] + tmp_path, + component_name, + False, + False, + True, + True, + False, + False, + ["defaults", "foo"], ) @@ -335,6 +387,54 @@ def test_run_component_new_command_with_name(tmp_path: P): assert cruftjson["context"]["cookiecutter"]["name"] == component_name +@pytest.mark.parametrize( + "automerge_patch", + [ + "--automerge-patch", + "--no-automerge-patch", + ], +) +@pytest.mark.parametrize( + "automerge_patch_v0", + [ + "--automerge-patch-v0", + "--no-automerge-patch-v0", + ], +) +def test_run_component_new_automerge_options( + tmp_path: P, cli_runner: RunnerFunc, automerge_patch, automerge_patch_v0 +): + result = call_component_new( + tmp_path, + cli_runner, + golden="--golden-tests", + matrix="--matrix-tests", + automerge_patch=automerge_patch, + automerge_patch_v0=automerge_patch_v0, + ) + + has_automerge_patch_v0 = automerge_patch_v0 == "--automerge-patch-v0" + has_automerge_patch = ( + automerge_patch == "--automerge-patch" or has_automerge_patch_v0 + ) + if automerge_patch == "--no-automerge-patch" and has_automerge_patch_v0: + assert ( + " > Forcing automerging of patch dependencies to be enabled " + + "when automerging of v0.x patch dependencies is requested" + ) in result.stdout + + _validate_rendered_component( + tmp_path, + "test-component", + False, + False, + True, + True, + has_automerge_patch, + has_automerge_patch_v0, + ) + + @pytest.mark.parametrize( "test_input", [ @@ -440,6 +540,14 @@ def test_check_golden_diff(tmp_path: P): ([], ["--matrix-tests"]), (["--matrix-tests"], ["--no-matrix-tests"]), ([], ["--golden-tests", "--matrix-tests"]), + ([], ["--automerge-patch"]), + ([], ["--automerge-patch-v0"]), + ([], ["--automerge-patch", "--automerge-patch-v0"]), + (["--automerge-patch"], ["--no-automerge-patch"]), + (["--automerge-patch-v0"], ["--no-automerge-patch-v0"]), + (["--no-automerge-patch"], ["--automerge-patch-v0"]), + (["--automerge-patch-v0"], ["--no-automerge-patch"]), + (["--automerge-patch-v0"], ["--no-automerge-patch-v0"]), ], ) def test_component_update_bool_flags( @@ -455,6 +563,8 @@ def test_component_update_bool_flags( "--no-pp", "--no-golden-tests", "--no-matrix-tests", + "--no-automerge-patch", + "--no-automerge-patch-v0", component_name, ] @@ -462,12 +572,23 @@ def test_component_update_bool_flags( has_pp = "--pp" in new_args has_golden = "--golden-tests" in new_args has_matrix = "--matrix-tests" in new_args + has_automerge_patch = ( + "--automerge-patch" in new_args or "--automerge-patch-v0" in new_args + ) + has_automerge_patch_v0 = "--automerge-patch-v0" in new_args result = cli_runner(new_cmd + new_args) assert result.exit_code == 0 _validate_rendered_component( - tmp_path, component_name, has_lib, has_pp, has_golden, has_matrix + tmp_path, + component_name, + has_lib, + has_pp, + has_golden, + has_matrix, + has_automerge_patch, + has_automerge_patch_v0, ) update_cmd = [ @@ -481,12 +602,32 @@ def test_component_update_bool_flags( has_pp = "--pp" in update_args has_golden = "--golden-tests" in update_args has_matrix = "--matrix-tests" in update_args + # updated component has automerge_patch_v0 if we enable it in the update call or if + # it was enabled originally, and we haven't explicitly disabled it. + has_automerge_patch_v0_update = "--automerge-patch-v0" in update_args or ( + has_automerge_patch_v0 and "--no-automerge-patch-v0" not in update_args + ) + # updated component has automerge_patch if we enable it in the update call, the + # component had automerge_patch originally, and we haven't disabled it explicitly, + # or if the updated component has automerge_patch_v0 enabled. + has_automerge_patch_update = ( + "--automerge-patch" in update_args + or (has_automerge_patch and "--no-automerge-patch" not in update_args) + or has_automerge_patch_v0_update + ) result = cli_runner(update_cmd + update_args) assert result.exit_code == 0 _validate_rendered_component( - tmp_path, component_name, has_lib, has_pp, has_golden, has_matrix + tmp_path, + component_name, + has_lib, + has_pp, + has_golden, + has_matrix, + has_automerge_patch_update, + has_automerge_patch_v0_update, ) @@ -615,7 +756,7 @@ def test_component_update_test_cases( orig_cases = ["defaults"] + initial_cases _validate_rendered_component( - tmp_path, component_name, False, False, True, True, orig_cases + tmp_path, component_name, False, False, True, True, False, False, orig_cases ) update_args = _format_test_case_args("--additional-test-case", additional_cases) @@ -639,7 +780,7 @@ def test_component_update_test_cases( final_cases = updated_cases _validate_rendered_component( - tmp_path, component_name, False, False, True, True, final_cases + tmp_path, component_name, False, False, True, True, False, False, final_cases ) @@ -687,6 +828,8 @@ def test_cookiecutter_args_no_cruft_json(tmp_path: P, config: Config): t.post_process = False t.copyright_holder = "" t.github_owner = "projectsyn" + t.automerge_patch = True + t.automerge_patch_v0 = False templater_cookiecutter_args = t.cookiecutter_args