From 4d4c798feb6e9eee2c88fdbda1282d8c109b8eb5 Mon Sep 17 00:00:00 2001 From: Joe Wang <106995533+JoeWang1127@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:43:40 -0400 Subject: [PATCH] chore: migrate config change functions to `common` module (#3311) In this PR: - Migrate config change functions to `common` module. - Library generation will accept library names directly. --- .github/scripts/action.yaml | 2 - .../scripts/hermetic_library_generation.sh | 10 +- .../common/cli/get_changed_libraries.py | 83 +++++++ .../model/config_change.py | 42 ++-- hermetic_build/common/requirements.in | 1 + hermetic_build/common/requirements.txt | 12 + hermetic_build/common/tests/cli/__init__.py | 0 .../tests/cli/config_change_unit_tests.py | 71 ++++++ .../tests/model/config_change_unit_tests.py | 6 +- .../tests/resources/cli/empty_config.yaml | 0 hermetic_build/common/tests/utils/__init__.py | 0 ...generation_config_comparator_unit_tests.py | 4 +- .../utils/proto_path_utils_unit_tests.py | 39 ++++ .../utils/generation_config_comparator.py | 8 +- .../common/utils/proto_path_utils.py | 33 +++ .../library_generation/cli/entry_point.py | 108 +++------ .../library_generation/requirements.in | 1 - .../library_generation/requirements.txt | 12 - .../tests/cli/entry_point_unit_tests.py | 213 ++---------------- .../tests/integration_tests.py | 12 +- .../utils/proto_path_utils_unit_tests.py | 20 +- .../utils/proto_path_utils.py | 18 -- .../cli/generate_release_note.py | 2 +- .../commit_message_formatter.py | 2 +- .../generate_pr_description.py | 4 +- .../commit_message_formatter_unit_tests.py | 2 +- .../generate_pr_description_unit_tests.py | 2 +- 27 files changed, 335 insertions(+), 372 deletions(-) create mode 100644 hermetic_build/common/cli/get_changed_libraries.py rename hermetic_build/{library_generation => common}/model/config_change.py (86%) create mode 100644 hermetic_build/common/tests/cli/__init__.py create mode 100644 hermetic_build/common/tests/cli/config_change_unit_tests.py rename hermetic_build/{library_generation => common}/tests/model/config_change_unit_tests.py (98%) create mode 100644 hermetic_build/common/tests/resources/cli/empty_config.yaml create mode 100644 hermetic_build/common/tests/utils/__init__.py rename hermetic_build/{library_generation => common}/tests/utils/generation_config_comparator_unit_tests.py (99%) create mode 100644 hermetic_build/common/tests/utils/proto_path_utils_unit_tests.py rename hermetic_build/{library_generation => common}/utils/generation_config_comparator.py (97%) create mode 100644 hermetic_build/common/utils/proto_path_utils.py diff --git a/.github/scripts/action.yaml b/.github/scripts/action.yaml index 7c4ae22f4e..5e8b55660f 100644 --- a/.github/scripts/action.yaml +++ b/.github/scripts/action.yaml @@ -60,8 +60,6 @@ runs: cd "${GITHUB_WORKSPACE}" pip install --require-hashes -r hermetic_build/common/requirements.txt pip install hermetic_build/common - pip install --require-hashes -r hermetic_build/library_generation/requirements.txt - pip install hermetic_build/library_generation pip install --require-hashes -r hermetic_build/release_note_generation/requirements.txt pip install hermetic_build/release_note_generation - name: Generate changed libraries diff --git a/.github/scripts/hermetic_library_generation.sh b/.github/scripts/hermetic_library_generation.sh index 8f863451eb..43b764fe34 100755 --- a/.github/scripts/hermetic_library_generation.sh +++ b/.github/scripts/hermetic_library_generation.sh @@ -92,6 +92,12 @@ pushd "${api_def_dir}" git checkout "${googleapis_commitish}" popd +# get changed library list. +changed_libraries=$(python hermetic_build/common/cli/get_changed_libraries.py create \ + --baseline-generation-config-path="${baseline_generation_config}" \ + --current-generation-config-path="${generation_config}") +echo "Changed libraries are: ${changed_libraries:-"No changed library"}." + # run hermetic code generation docker image. docker run \ --rm \ @@ -101,8 +107,8 @@ docker run \ -v "${api_def_dir}:${workspace_name}/googleapis" \ -e GENERATOR_VERSION="${image_tag}" \ gcr.io/cloud-devrel-public-resources/java-library-generation:"${image_tag}" \ - --baseline-generation-config-path="${workspace_name}/${baseline_generation_config}" \ - --current-generation-config-path="${workspace_name}/${generation_config}" \ + --generation-config-path="${workspace_name}/${generation_config}" \ + --library-names="${changed_libraries}" \ --api-definitions-path="${workspace_name}/googleapis" python hermetic_build/release_note_generation/cli/generate_release_note.py generate \ diff --git a/hermetic_build/common/cli/get_changed_libraries.py b/hermetic_build/common/cli/get_changed_libraries.py new file mode 100644 index 0000000000..cf92cf0853 --- /dev/null +++ b/hermetic_build/common/cli/get_changed_libraries.py @@ -0,0 +1,83 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import click as click + +from common.model.generation_config import from_yaml +from common.utils.generation_config_comparator import compare_config + + +@click.group(invoke_without_command=False) +@click.pass_context +@click.version_option(message="%(version)s") +def main(ctx): + pass + + +@main.command() +@click.option( + "--baseline-generation-config-path", + required=True, + type=str, + help=""" + Absolute or relative path to a generation_config.yaml. + This config file is used for computing changed library list. + """, +) +@click.option( + "--current-generation-config-path", + required=True, + type=str, + help=""" + Absolute or relative path to a generation_config.yaml that contains the + metadata about library generation. + """, +) +def create( + baseline_generation_config_path: str, + current_generation_config_path: str, +) -> None: + """ + Compares baseline generation config with current generation config and + generates changed library names (a comma separated string) based on current + generation config. + """ + baseline_generation_config_path = os.path.abspath(baseline_generation_config_path) + if not os.path.isfile(baseline_generation_config_path): + raise FileNotFoundError( + f"{baseline_generation_config_path} does not exist. " + "A valid generation config has to be passed in as " + "baseline-generation-config-path." + ) + current_generation_config_path = os.path.abspath(current_generation_config_path) + if not os.path.isfile(current_generation_config_path): + raise FileNotFoundError( + f"{current_generation_config_path} does not exist. " + "A valid generation config has to be passed in as " + "current-generation-config-path." + ) + config_change = compare_config( + baseline_config=from_yaml(baseline_generation_config_path), + current_config=from_yaml(current_generation_config_path), + ) + changed_libraries = config_change.get_changed_libraries() + if changed_libraries is None: + print("No changed library.") + return + click.echo(",".join(config_change.get_changed_libraries())) + + +if __name__ == "__main__": + main() diff --git a/hermetic_build/library_generation/model/config_change.py b/hermetic_build/common/model/config_change.py similarity index 86% rename from hermetic_build/library_generation/model/config_change.py rename to hermetic_build/common/model/config_change.py index 018aee8ccd..7ddc338448 100644 --- a/hermetic_build/library_generation/model/config_change.py +++ b/hermetic_build/common/model/config_change.py @@ -11,17 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import os -import shutil +import tempfile from enum import Enum from typing import Optional from git import Commit, Repo - from common.model.gapic_inputs import parse_build_str from common.model.generation_config import GenerationConfig from common.model.library_config import LibraryConfig -from library_generation.utils.utilities import sh_util -from library_generation.utils.proto_path_utils import find_versioned_proto_path +from common.utils.proto_path_utils import find_versioned_proto_path INSERTIONS = "insertions" LINES = "lines" @@ -109,25 +106,22 @@ def get_qualified_commits( :param repo_url: the repository contains the commit history. :return: QualifiedCommit objects. """ - tmp_dir = sh_util("get_output_folder") - shutil.rmtree(tmp_dir, ignore_errors=True) - os.mkdir(tmp_dir) - # we only need commit history, thus shadow clone is enough. - repo = Repo.clone_from(url=repo_url, to_path=tmp_dir, filter=["blob:none"]) - commit = repo.commit(self.current_config.googleapis_commitish) - proto_paths = self.current_config.get_proto_path_to_library_name() - qualified_commits = [] - while str(commit.hexsha) != self.baseline_config.googleapis_commitish: - qualified_commit = ConfigChange.__create_qualified_commit( - proto_paths=proto_paths, commit=commit - ) - if qualified_commit is not None: - qualified_commits.append(qualified_commit) - commit_parents = commit.parents - if len(commit_parents) == 0: - break - commit = commit_parents[0] - shutil.rmtree(tmp_dir, ignore_errors=True) + with tempfile.TemporaryDirectory() as tmp_dir: + # we only need commit history, thus a shadow clone is enough. + repo = Repo.clone_from(url=repo_url, to_path=tmp_dir, filter=["blob:none"]) + commit = repo.commit(self.current_config.googleapis_commitish) + proto_paths = self.current_config.get_proto_path_to_library_name() + qualified_commits = [] + while str(commit.hexsha) != self.baseline_config.googleapis_commitish: + qualified_commit = ConfigChange.__create_qualified_commit( + proto_paths=proto_paths, commit=commit + ) + if qualified_commit is not None: + qualified_commits.append(qualified_commit) + commit_parents = commit.parents + if len(commit_parents) == 0: + break + commit = commit_parents[0] return qualified_commits def __get_library_names_from_qualified_commits(self) -> list[str]: diff --git a/hermetic_build/common/requirements.in b/hermetic_build/common/requirements.in index a34205e5fa..21607220f6 100644 --- a/hermetic_build/common/requirements.in +++ b/hermetic_build/common/requirements.in @@ -1,3 +1,4 @@ black==24.8.0 +GitPython==3.1.43 parameterized==0.9.0 PyYAML==6.0.2 \ No newline at end of file diff --git a/hermetic_build/common/requirements.txt b/hermetic_build/common/requirements.txt index d952506bb5..9b79817c1b 100644 --- a/hermetic_build/common/requirements.txt +++ b/hermetic_build/common/requirements.txt @@ -32,6 +32,14 @@ click==8.1.7 \ --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de # via black +gitdb==4.0.11 \ + --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ + --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b + # via gitpython +gitpython==3.1.43 \ + --hash=sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c \ + --hash=sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff + # via -r hermetic_build/common/requirements.in mypy-extensions==1.0.0 \ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 @@ -107,3 +115,7 @@ pyyaml==6.0.2 \ --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 # via -r hermetic_build/common/requirements.in +smmap==5.0.1 \ + --hash=sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62 \ + --hash=sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da + # via gitdb diff --git a/hermetic_build/common/tests/cli/__init__.py b/hermetic_build/common/tests/cli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hermetic_build/common/tests/cli/config_change_unit_tests.py b/hermetic_build/common/tests/cli/config_change_unit_tests.py new file mode 100644 index 0000000000..e3bdd753de --- /dev/null +++ b/hermetic_build/common/tests/cli/config_change_unit_tests.py @@ -0,0 +1,71 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +from click.testing import CliRunner +import unittest + +from common.cli.get_changed_libraries import create + +script_dir = os.path.dirname(os.path.realpath(__file__)) +test_resource_dir = os.path.join(script_dir, "..", "resources", "cli") + + +class GetChangedLibrariesTest(unittest.TestCase): + def test_entry_point_without_baseline_config_raise_system_exception(self): + os.chdir(script_dir) + runner = CliRunner() + # noinspection PyTypeChecker + result = runner.invoke(create) + self.assertEqual(2, result.exit_code) + self.assertEqual(SystemExit, result.exc_info[0]) + + def test_entry_point_without_current_config_raise_system_exception(self): + os.chdir(script_dir) + runner = CliRunner() + # noinspection PyTypeChecker + result = runner.invoke( + create, ["--baseline-generation-config-path=/invalid/path/file"] + ) + self.assertEqual(2, result.exit_code) + self.assertEqual(SystemExit, result.exc_info[0]) + + def test_entry_point_with_invalid_baseline_config_raise_file_exception(self): + os.chdir(script_dir) + runner = CliRunner() + # noinspection PyTypeChecker + result = runner.invoke( + create, + [ + "--baseline-generation-config-path=/invalid/path/file", + "--current-generation-config-path=/invalid/path/file", + ], + ) + self.assertEqual(1, result.exit_code) + self.assertEqual(FileNotFoundError, result.exc_info[0]) + self.assertRegex(result.exception.args[0], "baseline-generation-config-path") + + def test_entry_point_with_invalid_current_config_raise_file_exception(self): + os.chdir(script_dir) + runner = CliRunner() + # noinspection PyTypeChecker + result = runner.invoke( + create, + [ + f"--baseline-generation-config-path={test_resource_dir}/empty_config.yaml", + "--current-generation-config-path=/invalid/path/file", + ], + ) + self.assertEqual(1, result.exit_code) + self.assertEqual(FileNotFoundError, result.exc_info[0]) + self.assertRegex(result.exception.args[0], "current-generation-config-path") diff --git a/hermetic_build/library_generation/tests/model/config_change_unit_tests.py b/hermetic_build/common/tests/model/config_change_unit_tests.py similarity index 98% rename from hermetic_build/library_generation/tests/model/config_change_unit_tests.py rename to hermetic_build/common/tests/model/config_change_unit_tests.py index 6e0a088e75..3abc603141 100644 --- a/hermetic_build/library_generation/tests/model/config_change_unit_tests.py +++ b/hermetic_build/common/tests/model/config_change_unit_tests.py @@ -13,9 +13,9 @@ # limitations under the License. import unittest -from library_generation.model.config_change import ChangeType -from library_generation.model.config_change import ConfigChange -from library_generation.model.config_change import LibraryChange +from common.model.config_change import ChangeType +from common.model.config_change import ConfigChange +from common.model.config_change import LibraryChange from common.model.gapic_config import GapicConfig from common.model.generation_config import GenerationConfig from common.model.library_config import LibraryConfig diff --git a/hermetic_build/common/tests/resources/cli/empty_config.yaml b/hermetic_build/common/tests/resources/cli/empty_config.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hermetic_build/common/tests/utils/__init__.py b/hermetic_build/common/tests/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hermetic_build/library_generation/tests/utils/generation_config_comparator_unit_tests.py b/hermetic_build/common/tests/utils/generation_config_comparator_unit_tests.py similarity index 99% rename from hermetic_build/library_generation/tests/utils/generation_config_comparator_unit_tests.py rename to hermetic_build/common/tests/utils/generation_config_comparator_unit_tests.py index f88f71d40e..00edb511eb 100644 --- a/hermetic_build/library_generation/tests/utils/generation_config_comparator_unit_tests.py +++ b/hermetic_build/common/tests/utils/generation_config_comparator_unit_tests.py @@ -16,8 +16,8 @@ from common.model.gapic_config import GapicConfig from common.model.generation_config import GenerationConfig from common.model.library_config import LibraryConfig -from library_generation.utils.generation_config_comparator import ChangeType -from library_generation.utils.generation_config_comparator import compare_config +from common.utils.generation_config_comparator import ChangeType +from common.utils.generation_config_comparator import compare_config class GenerationConfigComparatorTest(unittest.TestCase): diff --git a/hermetic_build/common/tests/utils/proto_path_utils_unit_tests.py b/hermetic_build/common/tests/utils/proto_path_utils_unit_tests.py new file mode 100644 index 0000000000..90b3dd3f55 --- /dev/null +++ b/hermetic_build/common/tests/utils/proto_path_utils_unit_tests.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import unittest +from pathlib import Path +from common.utils.proto_path_utils import find_versioned_proto_path + +script_dir = os.path.dirname(os.path.realpath(__file__)) +resources_dir = os.path.join(script_dir, "..", "resources") +test_config_dir = Path(os.path.join(resources_dir, "test-config")).resolve() + + +class ProtoPathsUtilsTest(unittest.TestCase): + def test_find_versioned_proto_path_nested_version_success(self): + proto_path = "google/cloud/aiplatform/v1/schema/predict/params/image_classification.proto" + expected = "google/cloud/aiplatform/v1" + self.assertEqual(expected, find_versioned_proto_path(proto_path)) + + def test_find_versioned_proto_path_success(self): + proto_path = "google/cloud/asset/v1p2beta1/assets.proto" + expected = "google/cloud/asset/v1p2beta1" + self.assertEqual(expected, find_versioned_proto_path(proto_path)) + + def test_find_versioned_proto_without_version_return_itself(self): + proto_path = "google/type/color.proto" + expected = "google/type/color.proto" + self.assertEqual(expected, find_versioned_proto_path(proto_path)) diff --git a/hermetic_build/library_generation/utils/generation_config_comparator.py b/hermetic_build/common/utils/generation_config_comparator.py similarity index 97% rename from hermetic_build/library_generation/utils/generation_config_comparator.py rename to hermetic_build/common/utils/generation_config_comparator.py index d0851c7f31..f41299ddc2 100644 --- a/hermetic_build/library_generation/utils/generation_config_comparator.py +++ b/hermetic_build/common/utils/generation_config_comparator.py @@ -15,10 +15,10 @@ from typing import Any from typing import Dict from typing import List -from library_generation.model.config_change import ChangeType -from library_generation.model.config_change import ConfigChange -from library_generation.model.config_change import LibraryChange -from library_generation.model.config_change import HashLibrary +from common.model.config_change import ChangeType +from common.model.config_change import ConfigChange +from common.model.config_change import LibraryChange +from common.model.config_change import HashLibrary from common.model.gapic_config import GapicConfig from common.model.generation_config import GenerationConfig from common.model.library_config import LibraryConfig diff --git a/hermetic_build/common/utils/proto_path_utils.py b/hermetic_build/common/utils/proto_path_utils.py new file mode 100644 index 0000000000..49a86dcbd2 --- /dev/null +++ b/hermetic_build/common/utils/proto_path_utils.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + + +def find_versioned_proto_path(proto_path: str) -> str: + """ + Returns a versioned proto_path from a given proto_path; or proto_path itself + if it doesn't contain a versioned proto_path. + :param proto_path: a proto file path + :return: the versioned proto_path + """ + version_regex = re.compile(r"^v[1-9].*") + directories = proto_path.split("/") + for directory in directories: + result = version_regex.search(directory) + if result: + version = result[0] + idx = proto_path.find(version) + return proto_path[:idx] + version + return proto_path diff --git a/hermetic_build/library_generation/cli/entry_point.py b/hermetic_build/library_generation/cli/entry_point.py index b15880f06d..e568f831ab 100644 --- a/hermetic_build/library_generation/cli/entry_point.py +++ b/hermetic_build/library_generation/cli/entry_point.py @@ -16,9 +16,7 @@ from typing import Optional import click as click from library_generation.generate_repo import generate_from_yaml -from library_generation.model.config_change import ConfigChange -from common.model.generation_config import from_yaml -from library_generation.utils.generation_config_comparator import compare_config +from common.model.generation_config import from_yaml, GenerationConfig @click.group(invoke_without_command=False) @@ -30,18 +28,7 @@ def main(ctx): @main.command() @click.option( - "--baseline-generation-config-path", - required=False, - default=None, - type=str, - help=""" - Absolute or relative path to a generation_config.yaml. - This config file is used for commit history generation, not library - generation. - """, -) -@click.option( - "--current-generation-config-path", + "--generation-config-path", required=False, default=None, type=str, @@ -85,8 +72,7 @@ def main(ctx): """, ) def generate( - baseline_generation_config_path: str, - current_generation_config_path: str, + generation_config_path: Optional[str], library_names: Optional[str], repository_path: str, api_definitions_path: str, @@ -115,103 +101,63 @@ def generate( Raise FileNotFoundError if the default config does not exist. """ - __generate_repo_and_pr_description_impl( - baseline_generation_config_path=baseline_generation_config_path, - current_generation_config_path=current_generation_config_path, + __generate_repo_impl( + generation_config_path=generation_config_path, library_names=library_names, repository_path=repository_path, api_definitions_path=api_definitions_path, ) -def __generate_repo_and_pr_description_impl( - baseline_generation_config_path: str, - current_generation_config_path: str, +def __generate_repo_impl( + generation_config_path: Optional[str], library_names: Optional[str], repository_path: str, api_definitions_path: str, ): """ Implementation method for generate(). - The decoupling of generate and __generate_repo_and_pr_description_impl is + The decoupling of generate and __generate_repo_impl is meant to allow testing of this implementation function. """ default_generation_config_path = f"{os.getcwd()}/generation_config.yaml" - - if ( - baseline_generation_config_path is None - and current_generation_config_path is None - ): - if not os.path.isfile(default_generation_config_path): - raise FileNotFoundError( - f"{default_generation_config_path} does not exist. " - "A valid generation config has to be passed in as " - "current_generation_config or exist in the current working " - "directory." - ) - current_generation_config_path = default_generation_config_path - elif current_generation_config_path is None: + if generation_config_path is None: + generation_config_path = default_generation_config_path + generation_config_path = os.path.abspath(generation_config_path) + if not os.path.isfile(generation_config_path): raise FileNotFoundError( - "current_generation_config is not specified when " - "baseline_generation_config is specified. " - "current_generation_config should be the source of truth of " - "library generation." + f"Generation config {generation_config_path} does not exist." ) - - current_generation_config_path = os.path.abspath(current_generation_config_path) repository_path = os.path.abspath(repository_path) api_definitions_path = os.path.abspath(api_definitions_path) - include_library_names = _parse_library_name_from(library_names) - - if not baseline_generation_config_path: - # Execute selective generation based on current_generation_config if - # baseline_generation_config is not specified. - generate_from_yaml( - config=from_yaml(current_generation_config_path), - repository_path=repository_path, - api_definitions_path=api_definitions_path, - target_library_names=include_library_names, - ) - return - - # Compare two generation configs to get changed libraries. - baseline_generation_config_path = os.path.abspath(baseline_generation_config_path) - config_change = compare_config( - baseline_config=from_yaml(baseline_generation_config_path), - current_config=from_yaml(current_generation_config_path), - ) - # Pass None if we want to fully generate the repository. - changed_library_names = ( - config_change.get_changed_libraries() - if not _needs_full_repo_generation(config_change=config_change) - else None - ) - # Include library names takes preference if specified. - target_library_names = ( - include_library_names - if include_library_names is not None - else changed_library_names + generation_config = from_yaml(generation_config_path) + include_library_names = _parse_library_name_from( + includes=library_names, generation_config=generation_config ) generate_from_yaml( - config=config_change.current_config, + config=generation_config, repository_path=repository_path, api_definitions_path=api_definitions_path, - target_library_names=target_library_names, + target_library_names=include_library_names, ) -def _needs_full_repo_generation(config_change: ConfigChange) -> bool: +def _needs_full_repo_generation(generation_config: GenerationConfig) -> bool: """ Whether you should need a full repo generation, i.e., generate all libraries in the generation configuration. """ - current_config = config_change.current_config - return not current_config.is_monorepo() or current_config.contains_common_protos() + return ( + not generation_config.is_monorepo() + or generation_config.contains_common_protos() + ) -def _parse_library_name_from(includes: str) -> Optional[list[str]]: - if includes is None: +def _parse_library_name_from( + includes: Optional[str], generation_config: GenerationConfig +) -> Optional[list[str]]: + if includes is None or _needs_full_repo_generation(generation_config): return None return [library_name.strip() for library_name in includes.split(",")] diff --git a/hermetic_build/library_generation/requirements.in b/hermetic_build/library_generation/requirements.in index 4f8ad9709b..7ab992ea62 100644 --- a/hermetic_build/library_generation/requirements.in +++ b/hermetic_build/library_generation/requirements.in @@ -1,6 +1,5 @@ attrs==24.2.0 click==8.1.7 -GitPython==3.1.43 jinja2==3.1.4 lxml==5.3.0 PyYAML==6.0.2 diff --git a/hermetic_build/library_generation/requirements.txt b/hermetic_build/library_generation/requirements.txt index 87ac0ba921..ef3d97bedc 100644 --- a/hermetic_build/library_generation/requirements.txt +++ b/hermetic_build/library_generation/requirements.txt @@ -123,14 +123,6 @@ click==8.1.7 \ --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de # via -r hermetic_build/library_generation/requirements.in -gitdb==4.0.11 \ - --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ - --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b - # via gitpython -gitpython==3.1.43 \ - --hash=sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c \ - --hash=sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff - # via -r hermetic_build/library_generation/requirements.in idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 @@ -407,10 +399,6 @@ requests-mock==1.12.1 \ --hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \ --hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401 # via -r hermetic_build/library_generation/requirements.in -smmap==5.0.1 \ - --hash=sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62 \ - --hash=sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da - # via gitdb urllib3==2.2.3 \ --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 diff --git a/hermetic_build/library_generation/tests/cli/entry_point_unit_tests.py b/hermetic_build/library_generation/tests/cli/entry_point_unit_tests.py index b7e7088cf9..82f6ec1c13 100644 --- a/hermetic_build/library_generation/tests/cli/entry_point_unit_tests.py +++ b/hermetic_build/library_generation/tests/cli/entry_point_unit_tests.py @@ -18,7 +18,7 @@ from library_generation.cli.entry_point import ( generate, validate_generation_config, - __generate_repo_and_pr_description_impl as generate_impl, + __generate_repo_impl as generate_impl, ) from common.model.generation_config import from_yaml @@ -27,34 +27,27 @@ class EntryPointTest(unittest.TestCase): - def test_entry_point_without_config_raise_file_exception(self): + def test_entry_point_without_default_config_raise_file_exception(self): os.chdir(script_dir) runner = CliRunner() # noinspection PyTypeChecker - result = runner.invoke(generate, ["--repository-path=."]) + result = runner.invoke(generate) self.assertEqual(1, result.exit_code) self.assertEqual(FileNotFoundError, result.exc_info[0]) self.assertRegex( result.exception.args[0], "generation_config.yaml does not exist." ) - def test_entry_point_with_baseline_without_current_raise_file_exception(self): + def test_entry_point_with_invalid_config_raise_file_exception(self): + os.chdir(script_dir) runner = CliRunner() # noinspection PyTypeChecker result = runner.invoke( - generate, - [ - "--baseline-generation-config-path=path/to/config.yaml", - "--repository-path=.", - ], + generate, ["--generation-config-path=/non-existent/file"] ) self.assertEqual(1, result.exit_code) self.assertEqual(FileNotFoundError, result.exc_info[0]) - self.assertRegex( - result.exception.args[0], - "current_generation_config is not specified when " - "baseline_generation_config is specified.", - ) + self.assertRegex(result.exception.args[0], "/non-existent/file does not exist.") def test_validate_generation_config_succeeds( self, @@ -86,7 +79,7 @@ def test_validate_generation_config_with_duplicate_library_name_raise_file_excep ) @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_non_monorepo_without_changes_triggers_full_generation( + def test_generate_non_monorepo_without_library_names_full_generation( self, generate_from_yaml, ): @@ -100,8 +93,7 @@ def test_generate_non_monorepo_without_changes_triggers_full_generation( # we call the implementation method directly since click # does special handling when a method is annotated with @main.command() generate_impl( - baseline_generation_config_path=config_path, - current_generation_config_path=config_path, + generation_config_path=config_path, library_names=None, repository_path=".", api_definitions_path=".", @@ -114,7 +106,7 @@ def test_generate_non_monorepo_without_changes_triggers_full_generation( ) @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_non_monorepo_without_changes_with_includes_triggers_selective_generation( + def test_generate_non_monorepo_with_library_names_full_generation( self, generate_from_yaml, ): @@ -129,41 +121,8 @@ def test_generate_non_monorepo_without_changes_with_includes_triggers_selective_ # we call the implementation method directly since click # does special handling when a method is annotated with @main.command() generate_impl( - baseline_generation_config_path=config_path, - current_generation_config_path=config_path, - library_names="cloudasset,non-existent-library", - repository_path=".", - api_definitions_path=".", - ) - generate_from_yaml.assert_called_with( - config=ANY, - repository_path=ANY, - api_definitions_path=ANY, - target_library_names=["cloudasset", "non-existent-library"], - ) - - @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_non_monorepo_with_changes_triggers_full_generation( - self, - generate_from_yaml, - ): - """ - this tests confirms the behavior of generation of non monorepos - (HW libraries). generate() should call generate_from_yaml() - with target_library_names=None in order to trigger the full generation - """ - baseline_config_path = f"{test_resource_dir}/generation_config.yaml" - current_config_path = ( - f"{test_resource_dir}/generation_config_library_modified.yaml" - ) - self.assertFalse(from_yaml(current_config_path).is_monorepo()) - self.assertFalse(from_yaml(baseline_config_path).is_monorepo()) - # we call the implementation method directly since click - # does special handling when a method is annotated with @main.command() - generate_impl( - baseline_generation_config_path=baseline_config_path, - current_generation_config_path=current_config_path, - library_names=None, + generation_config_path=config_path, + library_names="non-existent-library", repository_path=".", api_definitions_path=".", ) @@ -175,40 +134,7 @@ def test_generate_non_monorepo_with_changes_triggers_full_generation( ) @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_non_monorepo_with_changes_with_includes_triggers_selective_generation( - self, - generate_from_yaml, - ): - """ - this tests confirms the behavior of generation of non monorepos - (HW libraries). - generate() should call generate_from_yaml() with - target_library_names equals includes - """ - baseline_config_path = f"{test_resource_dir}/generation_config.yaml" - current_config_path = ( - f"{test_resource_dir}/generation_config_library_modified.yaml" - ) - self.assertFalse(from_yaml(current_config_path).is_monorepo()) - self.assertFalse(from_yaml(baseline_config_path).is_monorepo()) - # we call the implementation method directly since click - # does special handling when a method is annotated with @main.command() - generate_impl( - baseline_generation_config_path=baseline_config_path, - current_generation_config_path=current_config_path, - library_names="cloudasset,non-existent-library", - repository_path=".", - api_definitions_path=".", - ) - generate_from_yaml.assert_called_with( - config=ANY, - repository_path=ANY, - api_definitions_path=ANY, - target_library_names=["cloudasset", "non-existent-library"], - ) - - @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_monorepo_with_common_protos_triggers_full_generation( + def test_generate_monorepo_with_common_protos_without_library_names_triggers_full_generation( self, generate_from_yaml, ): @@ -223,8 +149,7 @@ def test_generate_monorepo_with_common_protos_triggers_full_generation( # we call the implementation method directly since click # does special handling when a method is annotated with @main.command() generate_impl( - baseline_generation_config_path=config_path, - current_generation_config_path=config_path, + generation_config_path=config_path, library_names=None, repository_path=".", api_definitions_path=".", @@ -237,7 +162,7 @@ def test_generate_monorepo_with_common_protos_triggers_full_generation( ) @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_monorepo_with_common_protos_with_includes_triggers_selective_generation( + def test_generate_monorepo_with_common_protos_with_library_names_triggers_full_generation( self, generate_from_yaml, ): @@ -251,8 +176,7 @@ def test_generate_monorepo_with_common_protos_with_includes_triggers_selective_g # we call the implementation method directly since click # does special handling when a method is annotated with @main.command() generate_impl( - baseline_generation_config_path=config_path, - current_generation_config_path=config_path, + generation_config_path=config_path, library_names="iam,non-existent-library", repository_path=".", api_definitions_path=".", @@ -261,11 +185,11 @@ def test_generate_monorepo_with_common_protos_with_includes_triggers_selective_g config=ANY, repository_path=ANY, api_definitions_path=ANY, - target_library_names=["iam", "non-existent-library"], + target_library_names=None, ) @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_monorepo_without_change_does_not_trigger_generation( + def test_generate_monorepo_without_library_names_trigger_full_generation( self, generate_from_yaml, ): @@ -281,8 +205,7 @@ def test_generate_monorepo_without_change_does_not_trigger_generation( # we call the implementation method directly since click # does special handling when a method is annotated with @main.command() generate_impl( - baseline_generation_config_path=config_path, - current_generation_config_path=config_path, + generation_config_path=config_path, library_names=None, repository_path=".", api_definitions_path=".", @@ -291,11 +214,11 @@ def test_generate_monorepo_without_change_does_not_trigger_generation( config=ANY, repository_path=ANY, api_definitions_path=ANY, - target_library_names=[], + target_library_names=None, ) @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_monorepo_without_change_with_includes_trigger_selective_generation( + def test_generate_monorepo_with_library_names_trigger_selective_generation( self, generate_from_yaml, ): @@ -311,8 +234,7 @@ def test_generate_monorepo_without_change_with_includes_trigger_selective_genera # we call the implementation method directly since click # does special handling when a method is annotated with @main.command() generate_impl( - baseline_generation_config_path=config_path, - current_generation_config_path=config_path, + generation_config_path=config_path, library_names="asset", repository_path=".", api_definitions_path=".", @@ -323,94 +245,3 @@ def test_generate_monorepo_without_change_with_includes_trigger_selective_genera api_definitions_path=ANY, target_library_names=["asset"], ) - - @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_monorepo_with_changed_config_without_includes_trigger_changed_generation( - self, - generate_from_yaml, - ): - """ - this tests confirms the behavior of generation of a monorepo without - common protos. - target_library_names should be the changed libraries if includes - is not specified. - """ - current_config_path = f"{test_resource_dir}/monorepo_current.yaml" - baseline_config_path = f"{test_resource_dir}/monorepo_baseline.yaml" - self.assertTrue(from_yaml(current_config_path).is_monorepo()) - self.assertTrue(from_yaml(baseline_config_path).is_monorepo()) - # we call the implementation method directly since click - # does special handling when a method is annotated with @main.command() - generate_impl( - baseline_generation_config_path=baseline_config_path, - current_generation_config_path=current_config_path, - library_names=None, - repository_path=".", - api_definitions_path=".", - ) - generate_from_yaml.assert_called_with( - config=ANY, - repository_path=ANY, - api_definitions_path=ANY, - target_library_names=["asset"], - ) - - @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_monorepo_with_changed_config_and_includes_trigger_selective_generation( - self, - generate_from_yaml, - ): - """ - this tests confirms the behavior of generation of a monorepo without - common protos. - target_library_names should be the same as include libraries, regardless - the library exists or not. - """ - current_config_path = f"{test_resource_dir}/monorepo_current.yaml" - baseline_config_path = f"{test_resource_dir}/monorepo_baseline.yaml" - self.assertTrue(from_yaml(current_config_path).is_monorepo()) - self.assertTrue(from_yaml(baseline_config_path).is_monorepo()) - # we call the implementation method directly since click - # does special handling when a method is annotated with @main.command() - generate_impl( - baseline_generation_config_path=baseline_config_path, - current_generation_config_path=current_config_path, - library_names="cloudbuild,non-existent-library", - repository_path=".", - api_definitions_path=".", - ) - generate_from_yaml.assert_called_with( - config=ANY, - repository_path=ANY, - api_definitions_path=ANY, - target_library_names=["cloudbuild", "non-existent-library"], - ) - - @patch("library_generation.cli.entry_point.generate_from_yaml") - def test_generate_monorepo_without_changed_config_without_includes_does_not_trigger_generation( - self, - generate_from_yaml, - ): - """ - this tests confirms the behavior of generation of a monorepo without - common protos. - target_library_names should be the changed libraries if includes - is not specified. - """ - config_path = f"{test_resource_dir}/monorepo_without_common_protos.yaml" - self.assertTrue(from_yaml(config_path).is_monorepo()) - # we call the implementation method directly since click - # does special handling when a method is annotated with @main.command() - generate_impl( - baseline_generation_config_path=config_path, - current_generation_config_path=config_path, - library_names=None, - repository_path=".", - api_definitions_path=".", - ) - generate_from_yaml.assert_called_with( - config=ANY, - repository_path=ANY, - api_definitions_path=ANY, - target_library_names=[], - ) diff --git a/hermetic_build/library_generation/tests/integration_tests.py b/hermetic_build/library_generation/tests/integration_tests.py index fd534ff207..5a1d5f7e53 100644 --- a/hermetic_build/library_generation/tests/integration_tests.py +++ b/hermetic_build/library_generation/tests/integration_tests.py @@ -43,7 +43,6 @@ "google-cloud-java": "chore/test-hermetic-build", "java-bigtable": "chore/test-hermetic-build", } -baseline_config_name = "baseline_generation_config.yaml" current_config_name = "current_generation_config.yaml" googleapis_commitish = "113a378d5aad5018876ec0a8cbfd4d6a4f746809" # This variable is used to override the jar created by building the image @@ -88,8 +87,7 @@ def test_entry_point_running_in_container(self): self.__run_entry_point_in_docker_container( repo_location=repo_location, config_location=config_location, - baseline_config=baseline_config_name, - current_config=current_config_name, + generation_config=current_config_name, api_definition=api_definitions_path, ) # 4. compare generation result with golden files @@ -222,6 +220,8 @@ def __download_generator_jar(cls, coordinates_file: str) -> None: [ "mvn", "dependency:copy", + "-B", + "-ntp", f"-Dartifact={coordinates}", f"-DoutputDirectory={config_dir}", ] @@ -287,8 +287,7 @@ def __run_entry_point_in_docker_container( cls, repo_location: str, config_location: str, - baseline_config: str, - current_config: str, + generation_config: str, api_definition: str, ): # we use the calling user to prevent the mapped volumes from changing @@ -314,8 +313,7 @@ def __run_entry_point_in_docker_container( "-w", "/workspace/repo", image_tag, - f"--baseline-generation-config-path=/workspace/config/{baseline_config}", - f"--current-generation-config-path=/workspace/config/{current_config}", + f"--generation-config-path=/workspace/config/{generation_config}", f"--api-definitions-path=/workspace/api", ], ) diff --git a/hermetic_build/library_generation/tests/utils/proto_path_utils_unit_tests.py b/hermetic_build/library_generation/tests/utils/proto_path_utils_unit_tests.py index 2e23c2b403..76b663c6cf 100644 --- a/hermetic_build/library_generation/tests/utils/proto_path_utils_unit_tests.py +++ b/hermetic_build/library_generation/tests/utils/proto_path_utils_unit_tests.py @@ -15,10 +15,7 @@ import os import unittest from pathlib import Path -from library_generation.utils.proto_path_utils import ( - find_versioned_proto_path, - remove_version_from, -) +from library_generation.utils.proto_path_utils import remove_version_from script_dir = os.path.dirname(os.path.realpath(__file__)) resources_dir = os.path.join(script_dir, "..", "resources") @@ -26,21 +23,6 @@ class ProtoPathsUtilsTest(unittest.TestCase): - def test_find_versioned_proto_path_nested_version_success(self): - proto_path = "google/cloud/aiplatform/v1/schema/predict/params/image_classification.proto" - expected = "google/cloud/aiplatform/v1" - self.assertEqual(expected, find_versioned_proto_path(proto_path)) - - def test_find_versioned_proto_path_success(self): - proto_path = "google/cloud/asset/v1p2beta1/assets.proto" - expected = "google/cloud/asset/v1p2beta1" - self.assertEqual(expected, find_versioned_proto_path(proto_path)) - - def test_find_versioned_proto_without_version_return_itself(self): - proto_path = "google/type/color.proto" - expected = "google/type/color.proto" - self.assertEqual(expected, find_versioned_proto_path(proto_path)) - def test_remove_version_from_returns_non_versioned_path(self): proto_path = "google/cloud/aiplatform/v1" self.assertEqual("google/cloud/aiplatform", remove_version_from(proto_path)) diff --git a/hermetic_build/library_generation/utils/proto_path_utils.py b/hermetic_build/library_generation/utils/proto_path_utils.py index d2ae25f602..27e3f8aa38 100644 --- a/hermetic_build/library_generation/utils/proto_path_utils.py +++ b/hermetic_build/library_generation/utils/proto_path_utils.py @@ -27,21 +27,3 @@ def remove_version_from(proto_path: str) -> str: if re.match(version_pattern, version): return proto_path[:index] return proto_path - - -def find_versioned_proto_path(proto_path: str) -> str: - """ - Returns a versioned proto_path from a given proto_path; or proto_path itself - if it doesn't contain a versioned proto_path. - :param proto_path: a proto file path - :return: the versioned proto_path - """ - version_regex = re.compile(r"^v[1-9].*") - directories = proto_path.split("/") - for directory in directories: - result = version_regex.search(directory) - if result: - version = result[0] - idx = proto_path.find(version) - return proto_path[:idx] + version - return proto_path diff --git a/hermetic_build/release_note_generation/cli/generate_release_note.py b/hermetic_build/release_note_generation/cli/generate_release_note.py index adc93d9ea7..294f4baf50 100644 --- a/hermetic_build/release_note_generation/cli/generate_release_note.py +++ b/hermetic_build/release_note_generation/cli/generate_release_note.py @@ -16,7 +16,7 @@ import click as click from release_note_generation.generate_pr_description import generate_pr_descriptions from common.model.generation_config import from_yaml -from library_generation.utils.generation_config_comparator import compare_config +from common.utils.generation_config_comparator import compare_config @click.group(invoke_without_command=False) diff --git a/hermetic_build/release_note_generation/commit_message_formatter.py b/hermetic_build/release_note_generation/commit_message_formatter.py index 48c060c571..c86c1df192 100644 --- a/hermetic_build/release_note_generation/commit_message_formatter.py +++ b/hermetic_build/release_note_generation/commit_message_formatter.py @@ -14,7 +14,7 @@ import re from git import Commit -from library_generation.model.config_change import ConfigChange, ChangeType +from common.model.config_change import ConfigChange, ChangeType from common.model.generation_config import ( GAPIC_GENERATOR_VERSION, LIBRARIES_BOM_VERSION, diff --git a/hermetic_build/release_note_generation/generate_pr_description.py b/hermetic_build/release_note_generation/generate_pr_description.py index b71facd7a4..d544268092 100755 --- a/hermetic_build/release_note_generation/generate_pr_description.py +++ b/hermetic_build/release_note_generation/generate_pr_description.py @@ -18,8 +18,8 @@ from typing import Dict from git import Commit, Repo -from library_generation.model.config_change import ConfigChange -from library_generation.utils.proto_path_utils import find_versioned_proto_path +from common.model.config_change import ConfigChange +from common.utils.proto_path_utils import find_versioned_proto_path from release_note_generation.commit_message_formatter import ( format_commit_message, format_repo_level_change, diff --git a/hermetic_build/release_note_generation/tests/commit_message_formatter_unit_tests.py b/hermetic_build/release_note_generation/tests/commit_message_formatter_unit_tests.py index ac888cea7b..b7891495ee 100644 --- a/hermetic_build/release_note_generation/tests/commit_message_formatter_unit_tests.py +++ b/hermetic_build/release_note_generation/tests/commit_message_formatter_unit_tests.py @@ -13,7 +13,7 @@ # limitations under the License. import unittest from unittest.mock import patch -from library_generation.model.config_change import ( +from common.model.config_change import ( ConfigChange, ChangeType, LibraryChange, diff --git a/hermetic_build/release_note_generation/tests/generate_pr_description_unit_tests.py b/hermetic_build/release_note_generation/tests/generate_pr_description_unit_tests.py index 7c6f0db9ac..9659af5b2d 100644 --- a/hermetic_build/release_note_generation/tests/generate_pr_description_unit_tests.py +++ b/hermetic_build/release_note_generation/tests/generate_pr_description_unit_tests.py @@ -18,7 +18,7 @@ get_repo_level_commit_messages, generate_pr_descriptions, ) -from library_generation.model.config_change import ( +from common.model.config_change import ( ConfigChange, ChangeType, LibraryChange,