From 0ab4a11928cd720adf951716db8dfe78318a655e Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Thu, 13 Jun 2024 09:12:55 +0200 Subject: [PATCH 1/5] Add logic to render minor automerge allowlist regex pattern list --- commodore/component/template.py | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/commodore/component/template.py b/commodore/component/template.py index 9e75322d..ca053493 100644 --- a/commodore/component/template.py +++ b/commodore/component/template.py @@ -22,6 +22,7 @@ class ComponentTemplater(Templater): _matrix_tests: bool _automerge_patch_blocklist: set[str] _automerge_patch_v0_allowlist: set[str] + _automerge_minor_allowlist: set[str] def __init__( self, @@ -42,6 +43,7 @@ def __init__( ) self._automerge_patch_blocklist = set() self._automerge_patch_v0_allowlist = set() + self._automerge_minor_allowlist = set() @classmethod def from_existing(cls, config: Config, path: Path): @@ -120,6 +122,13 @@ def _initialize_from_cookiecutter_args(self, cookiecutter_args: dict[str, str]): self._automerge_patch_v0_allowlist = set(args_patch_v0_allowlist.split(";")) else: self._automerge_patch_v0_allowlist = set() + args_minor_allowlist = cookiecutter_args.get( + "automerge_minor_regexp_allowlist", "" + ) + if args_minor_allowlist: + self._automerge_minor_allowlist = set(args_minor_allowlist.split(";")) + else: + self._automerge_minor_allowlist = set() return update_cruft_json @@ -137,6 +146,9 @@ def cookiecutter_args(self) -> dict[str, str]: args["automerge_patch_v0_regexp_allowlist"] = ";".join( sorted(self._automerge_patch_v0_allowlist) ) + args["automerge_minor_regexp_allowlist"] = ";".join( + sorted(self._automerge_minor_allowlist) + ) return args @property @@ -257,6 +269,52 @@ def remove_automerge_patch_v0_allow_depname(self, name: str): + "patch v0 allowlist" ) + def add_automerge_minor_allow_pattern(self, pattern: str): + """Add pattern to the minor automerge allowlist. + + `pattern` is expected to be a valid regex pattern. + + See `add_automerge_minor_allow_depname()` for a variant of this method which + will generate an anchored regex pattern for a particular dependency name. + """ + self._automerge_minor_allowlist.add(pattern) + + def remove_automerge_minor_allow_pattern(self, pattern: str): + """Remove the given pattern from the minor allowlist.""" + try: + self._automerge_minor_allowlist.remove(pattern) + except KeyError: + if self.config.verbose: + click.echo( + f" > Pattern '{pattern}' isn't present in the automerge " + + "minor allowlist" + ) + + def add_automerge_minor_allow_depname(self, name: str): + """Add dependency to the minor automerge allowlist. + + This method generates an anchored regex pattern for the provided name and adds + that pattern to the allow list. See `add_automerge_minor_allow_pattern()` for a + variant which allows providing regex patterns directly. + """ + self._automerge_minor_allowlist.add(f"^{name}$") + + def remove_automerge_minor_allow_depname(self, name: str): + """Remove the given dependency name from the minor allowlist. + + The function converts the dependency name into an anchored pattern to match the + pattern that's added `add_automerge_minor_allow_depname()` for the same value of + `name`. + """ + try: + self._automerge_minor_allowlist.remove(f"^{name}$") + except KeyError: + if self.config.verbose: + click.echo( + f" > Dependency name '{name}' isn't present in the automerge " + + "minor allowlist" + ) + @property def deptype(self) -> str: return "component" From 2c751ff457a97b22cfd204437ebb2461e39969ac Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Thu, 13 Jun 2024 09:16:30 +0200 Subject: [PATCH 2/5] Add command line options to configure minor automerge allowlist --- commodore/cli/component.py | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/commodore/cli/component.py b/commodore/cli/component.py index 6a3e80e8..5ff10a98 100644 --- a/commodore/cli/component.py +++ b/commodore/cli/component.py @@ -65,6 +65,14 @@ def _generate_automerge_pattern_help(level: str, remove: bool = False) -> str: + f"See '--{op}-automerge-patch-v0-allow-depname' for a variant of " + "this flag which allows specifying dependency names." ) + if level == "minor": + return ( + f"{opc} regex pattern for dependencies for which minor updates should be " + + "automerged. Can be repeated. Commodore will deduplicate patterns. " + + removenote + + f"See '--{op}-automerge-minor-allow-depname' for a variant of " + + "this flag which allows specifying dependency names." + ) raise ValueError( f"Expected 'level' to be one of ['patch', 'patch_v0', 'minor'], got {level}" @@ -105,6 +113,15 @@ def _generate_automerge_depname_help(level: str, remove: bool = False) -> str: + "flag which allows specifying regex patterns. " + implnote ) + if level == "minor": + return ( + f"{opc} dependency name for which minor updates should be automerged. " + + "Can be repeated. Commodore will deduplicate dependency names. " + + removenote + + f"See '--{op}-automerge-minor-allow-pattern' for a variant of " + + "this flag which allows specifying regex patterns. " + + implnote + ) raise ValueError( f"Expected 'level' to be one of ['patch', 'patch_v0', 'minor'], got {level}" @@ -124,6 +141,22 @@ def new_update_options(new_cmd: bool): add_text, test_case_help = _generate_option_text_snippets(new_cmd) def decorator(cmd): + click.option( + "--add-automerge-minor-allow-pattern", + metavar="PATTERN", + default=[], + show_default=True, + multiple=True, + help=_generate_automerge_pattern_help(level="minor"), + )(cmd) + click.option( + "--add-automerge-minor-allow-depname", + metavar="NAME", + default=[], + show_default=True, + multiple=True, + help=_generate_automerge_depname_help(level="minor"), + )(cmd) click.option( "--add-automerge-patch-v0-allow-pattern", metavar="PATTERN", @@ -281,6 +314,8 @@ def component_new( add_automerge_patch_block_pattern: Iterable[str], add_automerge_patch_v0_allow_depname: Iterable[str], add_automerge_patch_v0_allow_pattern: Iterable[str], + add_automerge_minor_allow_depname: Iterable[str], + add_automerge_minor_allow_pattern: Iterable[str], ): config.update_verbosity(verbose) t = ComponentTemplater( @@ -303,6 +338,10 @@ def component_new( t.add_automerge_patch_v0_allow_depname(name) for pattern in add_automerge_patch_v0_allow_pattern: t.add_automerge_patch_v0_allow_pattern(pattern) + for name in add_automerge_minor_allow_depname: + t.add_automerge_minor_allow_depname(name) + for pattern in add_automerge_minor_allow_pattern: + t.add_automerge_minor_allow_pattern(pattern) t.create() @@ -359,6 +398,22 @@ def component_new( multiple=True, help=_generate_automerge_pattern_help(level="patch_v0", remove=True), ) +@click.option( + "--remove-automerge-minor-allow-depname", + metavar="NAME", + default=[], + show_default=True, + multiple=True, + help=_generate_automerge_depname_help(level="minor", remove=True), +) +@click.option( + "--remove-automerge-minor-allow-pattern", + metavar="PATTERN", + default=[], + show_default=True, + multiple=True, + help=_generate_automerge_pattern_help(level="minor", remove=True), +) @click.option( "--commit / --no-commit", is_flag=True, @@ -386,10 +441,14 @@ def component_update( add_automerge_patch_block_pattern: Iterable[str], add_automerge_patch_v0_allow_depname: Iterable[str], add_automerge_patch_v0_allow_pattern: Iterable[str], + add_automerge_minor_allow_depname: Iterable[str], + add_automerge_minor_allow_pattern: Iterable[str], remove_automerge_patch_block_depname: Iterable[str], remove_automerge_patch_block_pattern: Iterable[str], remove_automerge_patch_v0_allow_depname: Iterable[str], remove_automerge_patch_v0_allow_pattern: Iterable[str], + remove_automerge_minor_allow_depname: Iterable[str], + remove_automerge_minor_allow_pattern: Iterable[str], ): """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 @@ -433,6 +492,10 @@ def component_update( t.add_automerge_patch_v0_allow_depname(name) for pattern in add_automerge_patch_v0_allow_pattern: t.add_automerge_patch_v0_allow_pattern(pattern) + for name in add_automerge_minor_allow_depname: + t.add_automerge_minor_allow_depname(name) + for pattern in add_automerge_minor_allow_pattern: + t.add_automerge_minor_allow_pattern(pattern) for name in remove_automerge_patch_block_depname: t.remove_automerge_patch_block_depname(name) @@ -442,6 +505,10 @@ def component_update( t.remove_automerge_patch_v0_allow_depname(name) for pattern in remove_automerge_patch_v0_allow_pattern: t.remove_automerge_patch_v0_allow_pattern(pattern) + for name in remove_automerge_minor_allow_depname: + t.remove_automerge_minor_allow_depname(name) + for pattern in remove_automerge_minor_allow_pattern: + t.remove_automerge_minor_allow_pattern(pattern) t.update(commit=commit) From b4bfbf8b493fb6b4fe8e8854da55f02bc877d1f7 Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Thu, 13 Jun 2024 15:18:02 +0200 Subject: [PATCH 3/5] Add tests for template minor automerge allowlist --- tests/test_component_template.py | 241 +++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) diff --git a/tests/test_component_template.py b/tests/test_component_template.py index 3dc9d12e..d31b58f7 100644 --- a/tests/test_component_template.py +++ b/tests/test_component_template.py @@ -79,8 +79,10 @@ def _validate_renovatejson_packagerules( check_patterns: bool = False, package_rules_count: int = 0, patch_v0_allowlist_ruleidx: int = -1, + minor_allowlist_ruleidx: int = -1, patch_blocklist: list[str] = [], patch_v0_allowlist: list[str] = [], + minor_allowlist: list[str] = [], ): with open(component_path / "renovate.json") as renovatejson: renovateconfig = json.load(renovatejson) @@ -143,6 +145,22 @@ def _validate_renovatejson_packagerules( assert patch_v0_rule["matchCurrentVersion"] == "/^v?0\\./" assert patch_v0_rule["matchPackagePatterns"] == patch_v0_allowlist + if minor_allowlist_ruleidx >= 0: + minor_rule = package_rules[minor_allowlist_ruleidx] + expected_keys = { + "matchUpdateTypes", + "matchPackagePatterns", + "automerge", + "platformAutomerge", + "labels", + } + assert set(minor_rule.keys()) == expected_keys + assert minor_rule["matchUpdateTypes"] == ["minor"] + assert minor_rule["automerge"] is True + assert minor_rule["platformAutomerge"] is False + assert minor_rule["labels"] == ["dependency", "automerge", "bump:minor"] + assert minor_rule["matchPackagePatterns"] == minor_allowlist + def _validate_rendered_component( tmp_path: P, @@ -663,6 +681,103 @@ def test_run_component_new_automerge_patch_v0_selective_only( ) +@pytest.mark.parametrize( + "automerge_minor_allowlist,expected_minor_allowlist", + [ + ( + ["--add-automerge-minor-allow-pattern=^bar"], + ["^bar"], + ), + ( + ["--add-automerge-minor-allow-depname=foo"], + ["^foo$"], + ), + ( + [ + "--add-automerge-minor-allow-depname=foo", + "--add-automerge-minor-allow-pattern=^bar", + ], + ["^bar", "^foo$"], + ), + ], +) +def test_run_component_new_automerge_minor_allowlist( + tmp_path: P, + cli_runner: RunnerFunc, + automerge_minor_allowlist: list[str], + expected_minor_allowlist: list[str], +): + call_component_new( + tmp_path, + cli_runner, + golden="--golden-tests", + matrix="--matrix-tests", + automerge_patch="--automerge-patch", + extra_args=automerge_minor_allowlist, + ) + + _validate_rendered_component( + tmp_path, + "test-component", + False, + False, + True, + True, + True, + False, + ) + _validate_renovatejson_packagerules( + tmp_path / "dependencies" / "test-component", + True, + False, + check_patterns=True, + package_rules_count=2 if len(expected_minor_allowlist) > 0 else 1, + minor_allowlist_ruleidx=1, + minor_allowlist=expected_minor_allowlist, + ) + + +def test_run_component_new_automerge_all_options( + tmp_path: P, + cli_runner: RunnerFunc, +): + call_component_new( + tmp_path, + cli_runner, + golden="--golden-tests", + matrix="--matrix-tests", + automerge_patch="--automerge-patch", + extra_args=[ + "--add-automerge-patch-block-depname=foo", + "--add-automerge-patch-v0-allow-pattern=^bar", + "--add-automerge-minor-allow-depname=baz", + ], + ) + + _validate_rendered_component( + tmp_path, + "test-component", + False, + False, + True, + True, + True, + False, + ) + _validate_renovatejson_packagerules( + tmp_path / "dependencies" / "test-component", + True, + False, + check_patterns=True, + package_rules_count=3, + patch_v0_allowlist_ruleidx=1, + minor_allowlist_ruleidx=2, + patch_blocklist=["^foo$"], + patch_v0_allowlist=["^bar"], + minor_allowlist=["^baz$"], + ) + + @pytest.mark.parametrize( "test_input", [ @@ -1227,6 +1342,124 @@ def test_component_update_patch_v0_automerge_allowlist( ) +@pytest.mark.parametrize( + "initial_args,expected_initial,update_args,expected_update", + [ + ([], [], [], []), + (["--add-automerge-minor-allow-depname=foo"], ["^foo$"], [], ["^foo$"]), + ( + ["--add-automerge-minor-allow-depname=foo"], + ["^foo$"], + ["--add-automerge-minor-allow-pattern=^foo"], + ["^foo", "^foo$"], + ), + ( + ["--add-automerge-minor-allow-depname=foo"], + ["^foo$"], + ["--add-automerge-minor-allow-pattern=^foo$"], + ["^foo$"], + ), + ( + ["--add-automerge-minor-allow-depname=foo"], + ["^foo$"], + ["--remove-automerge-minor-allow-depname=foo"], + [], + ), + ( + ["--add-automerge-minor-allow-depname=foo"], + ["^foo$"], + ["--remove-automerge-minor-allow-pattern=^foo"], + ["^foo$"], + ), + ( + ["--add-automerge-minor-allow-depname=foo"], + ["^foo$"], + ["--remove-automerge-minor-allow-pattern=^foo$"], + [], + ), + ( + [], + [], + ["--remove-automerge-minor-allow-pattern=^foo$"], + [], + ), + ( + [], + [], + [ + "--remove-automerge-minor-allow-pattern=^foo$", + "--add-automerge-minor-allow-depname=foo", + ], + [], + ), + # validate that minor rule is still created even if patch automerge is disabled + ( + [], + [], + [ + "--no-automerge-patch", + "--add-automerge-minor-allow-depname=foo", + ], + ["^foo$"], + ), + ], +) +def test_component_update_minor_automerge_allowlist( + tmp_path: P, + cli_runner: RunnerFunc, + initial_args: list[str], + expected_initial: list[str], + update_args: list[str], + expected_update: list[str], +): + component_name = "test-component" + result = call_component_new( + tmp_path, + cli_runner, + component_name, + golden="--golden-tests", + matrix="--matrix-tests", + automerge_patch="--automerge-patch", + extra_args=initial_args, + ) + assert result.exit_code == 0 + + component_path = tmp_path / "dependencies" / component_name + + _validate_renovatejson_packagerules( + component_path, + True, + False, + check_patterns=True, + package_rules_count=2 if len(expected_initial) > 0 else 1, + minor_allowlist_ruleidx=1 if len(expected_initial) > 0 else -1, + minor_allowlist=expected_initial, + ) + + result = cli_runner(["component", "update", str(component_path)] + update_args) + + assert result.exit_code == 0 + + has_automerge_patch = "--no-automerge-patch" not in update_args + if has_automerge_patch and len(expected_update) > 0: + minor_allowlist_ruleidx = 1 + elif not has_automerge_patch and len(expected_update) > 0: + minor_allowlist_ruleidx = 0 + else: + minor_allowlist_ruleidx = -1 + _validate_renovatejson_packagerules( + component_path, + has_automerge_patch, + False, + check_patterns=True, + package_rules_count=( + 2 if has_automerge_patch and len(expected_update) > 0 else 1 + ), + minor_allowlist_ruleidx=minor_allowlist_ruleidx, + minor_allowlist=expected_update, + ) + + @pytest.mark.parametrize( "remove_args,expected", [ @@ -1246,6 +1479,14 @@ def test_component_update_patch_v0_automerge_allowlist( ["--remove-automerge-patch-v0-allow-pattern=^foo"], "Pattern '^foo' isn't present in the automerge patch v0 allowlist", ), + ( + ["--remove-automerge-minor-allow-depname=foo"], + "Dependency name 'foo' isn't present in the automerge minor allowlist", + ), + ( + ["--remove-automerge-minor-allow-pattern=^foo"], + "Pattern '^foo' isn't present in the automerge minor allowlist", + ), ], ) def test_component_update_remove_patch_automerge_pattern_verbose( From 4ba195c6e1374209ce64e7d5039fb18c7c345e8f Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Thu, 13 Jun 2024 16:40:34 +0200 Subject: [PATCH 4/5] Update documentation --- docs/modules/ROOT/pages/reference/cli.adoc | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/modules/ROOT/pages/reference/cli.adoc b/docs/modules/ROOT/pages/reference/cli.adoc index b2ae70f5..913dc485 100644 --- a/docs/modules/ROOT/pages/reference/cli.adoc +++ b/docs/modules/ROOT/pages/reference/cli.adoc @@ -244,6 +244,19 @@ NOTE: Enabling automerging of patch-level dependency PRs for v0.x dependencies i Commodore will deduplicate patterns. See `--add-automerge-patch-v0-allow- depname` for a variant of this flag which allows specifying dependency names. +*--add-automerge-minor-allow-depname* NAME:: + Add dependency name for which minor updates should be automerged. + Can be repeated. + Commodore will deduplicate dependency names. + See `--add-automerge-minor-allow-pattern` for a variant of this flag which allows specifying regex patterns. + Commodore will convert the provided dependency names into a list of anchored regex patterns. + +*--add-automerge-minor-allow-pattern* PATTERN:: + Add regex pattern for dependencies for which minor updates should be automerged. + Can be repeated. + Commodore will deduplicate patterns. + See `--add-automerge-minor-allow-depname` for a variant of this flag which allows specifying dependency names. + *--help*:: Show component new usage and options then exit. @@ -357,6 +370,34 @@ NOTE: Enabling automerging of patch-level dependency PRs for v0.x dependencies i This flag has no effect if the provided pattern isn't part of the currently configured patterns. See `--remove-automerge-patch-v0-allow-depname` for a variant of this flag which allows specifying dependency names. +*--add-automerge-minor-allow-depname* NAME:: + Add dependency name for which minor updates should be automerged. + Can be repeated. + Commodore will deduplicate dependency names. + See `--add-automerge-minor-allow-pattern` for a variant of this flag which allows specifying regex patterns. + Commodore will convert the provided dependency names into a list of anchored regex patterns. + +*--add-automerge-minor-allow-pattern* PATTERN:: + Add regex pattern for dependencies for which minor updates should be automerged. + Can be repeated. + Commodore will deduplicate patterns. + See `--add-automerge-minor-allow-depname` for a variant of this flag which allows specifying dependency names. + +*--remove-automerge-minor-allow-depname* NAME:: + Remove dependency name for which minor updates should be automerged. + Can be repeated. + Commodore will deduplicate dependency names. + This flag has no effect if the provided name isn't part of the currently configured dependency names. + See `-- remove-automerge-minor-allow-pattern` for a variant of this flag which allows specifying regex patterns. + Commodore will convert the provided dependency names into a list of anchored regex patterns. + +*--remove-automerge-minor-allow-pattern* PATTERN:: + Remove regex pattern for dependencies for which minor updates should be automerged. + Can be repeated. + Commodore will deduplicate patterns. + This flag has no effect if the provided pattern isn't part of the currently configured patterns. + See `--remove-automerge-minor-allow-depname` for a variant of this flag which allows specifying dependency names. + *--help*:: Show component new usage and options then exit. From 137d757a8b0e374f1bdbfbad01cf622bb8cc482b Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Thu, 13 Jun 2024 16:49:49 +0200 Subject: [PATCH 5/5] Reduce complexity of `ComponentTemplater._initialize_from_cookiecutter_args()` --- commodore/component/template.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/commodore/component/template.py b/commodore/component/template.py index ca053493..70e401d1 100644 --- a/commodore/component/template.py +++ b/commodore/component/template.py @@ -108,6 +108,16 @@ def _initialize_from_cookiecutter_args(self, cookiecutter_args: dict[str, str]): 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" + + self._initialize_automerge_pattern_lists_from_cookiecutter_args( + cookiecutter_args + ) + + return update_cruft_json + + def _initialize_automerge_pattern_lists_from_cookiecutter_args( + self, cookiecutter_args: dict[str, str] + ): args_patch_blocklist = cookiecutter_args.get( "automerge_patch_regexp_blocklist", "" ) @@ -130,8 +140,6 @@ def _initialize_from_cookiecutter_args(self, cookiecutter_args: dict[str, str]): else: self._automerge_minor_allowlist = set() - return update_cruft_json - @property def cookiecutter_args(self) -> dict[str, str]: args = super().cookiecutter_args