diff --git a/komodo/symlink/suggester/cli.py b/komodo/symlink/suggester/cli.py index c33a797a..2d87b0f7 100755 --- a/komodo/symlink/suggester/cli.py +++ b/komodo/symlink/suggester/cli.py @@ -16,11 +16,10 @@ logging.basicConfig(level=logging.WARNING) logger = logging.getLogger(__name__) -PR_TEMPLATE = """:robot: Suggesting updating {platform} {mode} to {release} in {sym_file} +PR_TEMPLATE = """:robot: Suggesting updating {mode} to {release} --- ### Description - Release: `{release}` -- Platform: `{platform}` - Link type: `{mode}` - When: `{now}` @@ -39,11 +38,6 @@ def _parse_args(): parser.add_argument("mode", help="stable,testing") parser.add_argument("joburl", help="link to the job that triggered this") parser.add_argument("jobname", help="name of the job") - parser.add_argument( - "--symlink-conf-path", - help="", - default="symlink_configuration/symlink_config.json", - ) parser.add_argument("--git-fork", help="git fork", default="equinor") parser.add_argument("--git-repo", help="git repo", default="komodo-releases") parser.add_argument("--git-ref", help="git ref", default="main") @@ -58,6 +52,12 @@ def _parse_args(): help="Set dry-run, will do everything except making the PR", action="store_true", ) + parser.add_argument("--python-versions", help="e.g. py38, py311", default="py311") + parser.add_argument( + "--config-files", + help="e.g. symlink_config.json, symlink_config_azure.json", + default="symlink_configuration/symlink_config.json", + ) return parser.parse_args() @@ -78,55 +78,58 @@ def suggest_symlink_configuration( """Returns a pull request if the symlink configuration could be updated, or None if no update was possible. """ - try: - sym_conf_content = repo.get_contents(args.symlink_conf_path, ref=args.git_ref) - except UnknownObjectException: - sys.exit(f"Filename {args.symlink_conf_path} is not in repo {repo.full_name}") + config_files = args.config_files.split(",") + python_versions = args.python_versions.split(",") if args.release.startswith("bleeding"): logger.warning("Symlink to bleeding is not allowed") return None - try: - new_symlink_content, updated = configuration.update( - b64decode(sym_conf_content.content), - args.release, - args.mode, - ) - except ValueError as exc: - logger.critical(exc) - sys.exit(1) - - if not updated: - logger.info("Nothing to update") - return None - - platform = "azure" if "azure" in args.symlink_conf_path else "onprem" - - target_branch = f"{args.release}/{platform}-{args.mode}" - + target_branch = f"{args.release}/{args.mode}" from_sha = repo.get_branch(args.git_ref).commit.sha + msg = f"Update {args.mode} symlinks for {args.release}" - msg = f"Update {platform} {args.mode} symlinks for {args.release}" if not dry_run: repo.create_git_ref(ref=f"refs/heads/{target_branch}", sha=from_sha) - repo.update_file( - args.symlink_conf_path, - msg, - new_symlink_content, - sym_conf_content.sha, - branch=target_branch, - ) + + for symlink_config_file in config_files: + symlink_config_file = symlink_config_file.strip() + try: + sym_conf_content = repo.get_contents(symlink_config_file, ref=args.git_ref) + except UnknownObjectException: + sys.exit(f"Filename {symlink_config_file} is not in repo {repo.full_name}") + + try: + new_symlink_content, updated = configuration.update( + b64decode(sym_conf_content.content), + args.release, + args.mode, + python_versions, + ) + except ValueError as exc: + logger.critical(exc) + sys.exit(1) + + if not updated: + logger.info("Nothing to update") + return None + + if not dry_run: + repo.update_file( + symlink_config_file, + msg, + new_symlink_content, + sym_conf_content.sha, + branch=target_branch, + ) body = PR_TEMPLATE.format( - sym_file=args.symlink_conf_path, change=target_branch, release=args.release, mode=args.mode, now=datetime.now(), job_url=args.joburl, job_name=args.jobname, - platform=platform, ) if dry_run: diff --git a/komodo/symlink/suggester/configuration.py b/komodo/symlink/suggester/configuration.py index eb3b4c5f..ecc40d11 100644 --- a/komodo/symlink/suggester/configuration.py +++ b/komodo/symlink/suggester/configuration.py @@ -1,5 +1,5 @@ import json -from typing import Tuple +from typing import List, Tuple from komodo.symlink.suggester.release import Release @@ -18,38 +18,42 @@ def _get_concrete_release(self, link): release = Release(self.links[repr(release)]) return release - def update(self, release, mode): - link = f"{mode}-{release.py_ver()}" - link_exists = link in self.links - linked_release = self._get_concrete_release(link) if link_exists else None - - if mode == "testing": - stable_link = f"stable-{release.py_ver()}" - stable = ( - self._get_concrete_release(stable_link) - if stable_link in self.links - else None - ) - linked = self.links.get(link, None) - - # ripe is when stable is -1 month ago, ours is that the handle - # already points to a release in the same month - # if no stable, then it is ripe - handle_ripe = stable.monthly_diff(release) <= -1 if stable else True - handle_ours = link_exists and linked_release.monthly_diff(release) == 0 - if handle_ripe or handle_ours: - # i.e. if the linked release is a month alias - if linked and not Release(linked).is_concrete(): - self.links[release.month_alias()] = repr(release) - self.links[link] = release.month_alias() - else: - self.links[link] = repr(release) - elif mode == "stable": - self.links[release.month_alias()] = repr(release) - self.links[link] = release.month_alias() - else: - msg = f"Mode {mode} was not recognized" - raise ValueError(msg) + def update(self, release, mode, python_versions: List[str]): + for python_version in python_versions: + python_version = python_version.strip() + release.set_python_version(python_version) + + link = f"{mode}-{python_version}" + link_exists = link in self.links + linked_release = self._get_concrete_release(link) if link_exists else None + + if mode == "testing": + stable_link = f"stable-{python_version}" + stable = ( + self._get_concrete_release(stable_link) + if stable_link in self.links + else None + ) + linked = self.links.get(link, None) + + # ripe is when stable is -1 month ago, ours is that the handle + # already points to a release in the same month + # if no stable, then it is ripe + handle_ripe = stable.monthly_diff(release) <= -1 if stable else True + handle_ours = link_exists and linked_release.monthly_diff(release) == 0 + if handle_ripe or handle_ours: + # i.e. if the linked release is a month alias + if linked and not Release(linked).is_concrete(): + self.links[release.month_alias()] = repr(release) + self.links[link] = release.month_alias() + else: + self.links[link] = repr(release) + elif mode == "stable": + self.links[release.month_alias()] = repr(release) + self.links[link] = release.month_alias() + else: + msg = f"Mode {mode} was not recognized" + raise ValueError(msg) def to_json(self, json_kwargs): return json.dumps(self.conf, **json_kwargs) @@ -59,17 +63,24 @@ def from_json(conf_json_str): return Configuration(json.loads(conf_json_str)) -def update(symlink_configuration, release_id, mode) -> Tuple[str, bool]: +def update( + symlink_configuration, release_id, mode, python_versions: List[str] = None +) -> Tuple[str, bool]: """Return a tuple of a string representing the new symlink config json, - and whether or not an update was made. This function assumes the release_id - is in the yyyy.mm.[part ...]-py[\\d+] format and that symlink_configuration - is a string representing the current symlink config json. + and whether an update was made. This function assumes the release_id + is in the yyyy.mm.[part ...] format and will look for python suffix (-py38, -py311) + if no python_versions are passed. Symlink_configuration shall be a string + representing the current symlink config json. """ json_kwargs = {"sort_keys": True, "indent": 4, "separators": (",", ": ")} - release = Release(release_id) + + id_parts = release_id.split("-") + release = Release(id_parts[0]) + if not python_versions: + python_versions = [part for part in id_parts[1:] if "py" in part][:1] configuration = Configuration.from_json(symlink_configuration) - configuration.update(release, mode) + configuration.update(release, mode, python_versions) new_json_str = configuration.to_json(json_kwargs) old_json_str = json.dumps(json.loads(symlink_configuration), **json_kwargs) diff --git a/komodo/symlink/suggester/release.py b/komodo/symlink/suggester/release.py index 1e3e3447..23a0b142 100644 --- a/komodo/symlink/suggester/release.py +++ b/komodo/symlink/suggester/release.py @@ -14,6 +14,9 @@ def __init__(self, release_id: str) -> None: def __repr__(self) -> str: return self.release_id + def set_python_version(self, python_version: str) -> None: + self.release_id = self.release_id.split("-")[0] + "-" + python_version + def month(self) -> str: return self.release_id[0:7] diff --git a/tests/test_suggester.py b/tests/test_suggester.py index d3c51ed6..8796b05b 100644 --- a/tests/test_suggester.py +++ b/tests/test_suggester.py @@ -251,13 +251,13 @@ def _mock_repo(sym_config): @pytest.mark.parametrize( - ("symlink_file", "branch_name"), + ("symlink_file", "mode"), [ - ("foo.json", "onprem-stable"), - ("foo_azure.json", "azure-stable"), + ("foo.json", "stable"), + ("foo_azure.json", "stable"), ], ) -def test_suggest_symlink_configuration(symlink_file, branch_name): +def test_suggest_symlink_configuration(symlink_file, mode): """Testing whether when updating symlink file the branch gets a corresponding name.""" config = """{"links": { "2050.02-py58": "2050.02.00-py58", @@ -268,22 +268,23 @@ def test_suggest_symlink_configuration(symlink_file, branch_name): args = Namespace( git_ref="master", release="2050.02.01-py58", - mode="stable", - symlink_conf_path=symlink_file, + mode=mode, joburl="http://job", jobname="job", + config_files=symlink_file, + python_versions="py58", ) suggest_symlink_configuration(args, repo) repo.get_contents.assert_called_once_with(symlink_file, ref="master") repo.get_branch.assert_called_once_with("master") repo.create_git_ref.assert_called_once_with( - ref=f"refs/heads/2050.02.01-py58/{branch_name}", + ref=f"refs/heads/2050.02.01-py58/{mode}", sha=ANY, ) repo.update_file.assert_called_once_with( symlink_file, - f"Update {branch_name.split('-')[0]} stable symlinks for 2050.02.01-py58", + f"Update {mode} symlinks for 2050.02.01-py58", """{ "links": { "2050.02-py58": "2050.02.01-py58", @@ -292,12 +293,12 @@ def test_suggest_symlink_configuration(symlink_file, branch_name): } """, ANY, - branch=f"2050.02.01-py58/{branch_name}", + branch=f"2050.02.01-py58/{mode}", ) repo.create_pull.assert_called_once_with( title=ANY, body=ANY, - head=f"2050.02.01-py58/{branch_name}", + head=f"2050.02.01-py58/{mode}", base="master", ) @@ -313,11 +314,86 @@ def test_noop_suggestion(): git_ref="master", release="2050.02.00-py58", mode="stable", - symlink_conf_path="foo.json", joburl="http://job", jobname="job", + config_files="foo.json", + python_versions="py58", ) repo.create_pull.assert_not_called() assert suggest_symlink_configuration(args, repo) is None + + +def test_suggest_symlink_multi_configuration(): + config = """{"links": { +"2050.02-py58": "2050.02.00-py58", +"stable-py58": "2050.02-py58" +}}""" + repo = _mock_repo(config) + mode = "stable" + args = Namespace( + git_ref="master", + release="2050.02.01-py58", + mode=mode, + joburl="http://job", + jobname="job", + config_files="foo.json, foo_azure.json", + python_versions="py38, py311", + ) + suggest_symlink_configuration(args, repo) + + +@pytest.mark.parametrize( + ("json_in", "release_id", "mode", "changed", "json_out"), + [ # testing happy path + ( + """{"links": { + "testing-py38": "1994.12.00.rc0-py38", + "1994.12-py38": "1994.12.00.rc0-py38", + "testing-py311": "1994.12.00.rc0-py311", + "1994.12-py311": "1994.12.00.rc0-py311"}}""", + "1994.12.00.rc1", + "testing", + "changed", + """{ + "links": { + "1994.12-py311": "1994.12.00.rc0-py311", + "1994.12-py38": "1994.12.00.rc0-py38", + "testing-py311": "1994.12.00.rc1-py311", + "testing-py38": "1994.12.00.rc1-py38" + } +} +""", + ), + # testing promotion from previous release + ( + """{"links": { + "2001.11-py38": "2001.11.00-py38", + "2001.11-py311": "2001.11.00-py311", + "stable-py38" : "2001.11-py38", + "testing-py38": "2001.11.rc0-py38", + "stable-py311" : "2001.11-py311", + "testing-py311": "2001.11.rc0-py311"}}""", + "2001.12.rc0", + "testing", + "changed", + """{ + "links": { + "2001.11-py311": "2001.11.00-py311", + "2001.11-py38": "2001.11.00-py38", + "stable-py311": "2001.11-py311", + "stable-py38": "2001.11-py38", + "testing-py311": "2001.12.rc0-py311", + "testing-py38": "2001.12.rc0-py38" + } +} +""", + ), + ], +) +def test_multi_update(json_in, release_id, mode, changed, json_out): + assert update(json_in, release_id, mode, ["py38", "py311"]) == ( + json_out, + changed == "changed", + )