Repository: googleapis/google-cloud-python

Issue:
Automatically generate the release-please-config.json file
We should generate the [release-please-config.json file](https://github.com/googleapis/google-cloud-python/blob/main/release-please-config.json) automatically instead of maintaining it manually. We could do this using GitHub actions. 

Maintaining it manually is toilsome. In https://github.com/googleapis/google-cloud-python/pull/11051, I noticed errors in the release-please-config.json which could have been avoided if it was automatically generated. See [this commit](https://github.com/googleapis/google-cloud-python/pull/11051/commits/ec374bee70b71757f3f6a2f85cc6db438e5cb7f5) which fixed the release-please-config.json file.


Discussion:

Resolving patch:
diff --git a/scripts/configure_release_please/configure_release_please.py b/scripts/configure_release_please/configure_release_please.py
new file mode 100644
--- /dev/null
+++ b/scripts/configure_release_please/configure_release_please.py
@@ -0,0 +1,137 @@
+import json
+from pathlib import Path
+from typing import Union, Dict, List, Tuple
+import re
+
+SCRIPT_DIR = Path(__file__).resolve().parent
+ROOT_DIR = Path(SCRIPT_DIR / ".." / "..").resolve()
+PACKAGES_DIR = ROOT_DIR / "packages"
+
+
+def get_version_for_package(version_path: Path) -> Tuple[int]:
+    """
+    Given a `version_path` to a `gapic_version.py` file,
+    return Tuple<int> which contains the version.
+
+    Args:
+        version_path(pathlib.Path): Path to the gapic_version.py file
+
+    Returns:
+        Tuple[int] in the format (<major>, <minor>, <patch>)
+    """
+    VERSION_REGEX = r"__version__\s=\s\"(?P<major_version>\d+)\.(?P<minor_version>\d+)\.(?P<patch_version>\d+)\""
+    match = re.search(VERSION_REGEX, version_path.read_text())
+
+    if match is None:
+        raise Exception("Could not detect version")
+
+    major_version = int(match.group("major_version"))
+    minor_version = int(match.group("minor_version"))
+    patch_version = int(match.group("patch_version"))
+
+    if any(elem is None for elem in [major_version, minor_version, patch_version]):
+        raise Exception("could not detect version")
+
+    return (major_version, minor_version, patch_version)
+
+
+def get_packages_with_owlbot_yaml(packages_dir: Path = PACKAGES_DIR) -> List[Path]:
+    """
+    Walks through all API packages in the specified `packages_dir` path. 
+
+    Args:
+        packages_dir(pathlib.Path): Path to the directory which contains packages.
+
+    Returns:
+        List[pathlib.Path] where each entry corresponds to a package within the
+            specified `packages_dir`, which has a corresponding .OwlBot.yaml file.
+    """
+    if not Path(packages_dir).exists():
+        raise FileNotFoundError(f"Directory {packages_dir} not found")
+    return [obj.parents[0].resolve() for obj in packages_dir.rglob("**/.OwlBot.yaml")]
+
+
+def configure_release_please_manifest(
+    package_dirs: List[Path], root_dir: Path = ROOT_DIR
+) -> None:
+    """
+    This method updates the `.release-please-manifest.json` file in the directory
+    `root_dir`.
+
+    Args:
+        package_dirs(List[pathlib.Path]): A list of Paths, one for each package in the
+            `packages/` folder whose entry will be updated in the release-please manifest.
+        root_dir(pathlib.Path): The directory to update the `.release-please-manifest.json`
+
+    Returns:
+        None
+    """
+    release_please_manifest = root_dir / ".release-please-manifest.json"
+    with open(release_please_manifest, "r") as f:
+        manifest_json = json.load(f)
+        for package_dir in package_dirs:
+            if f"packages/{package_dir.name}" not in manifest_json:
+                manifest_json[f"packages/{package_dir.name}"] = "0.0.0"
+
+            gapic_version_file = next(package_dir.rglob("**/gapic_version.py"), None)
+            if gapic_version_file is None:
+                raise Exception("Failed to find gapic_version.py")
+            version = get_version_for_package(gapic_version_file)
+            # check the version in gapic_version.py and update if newer than the default which is
+            # 0.0.0 or 0.1.0.
+            if version != (0, 0, 0) and version != (0, 1, 0):
+                manifest_json[
+                    f"packages/{package_dir.name}"
+                ] = f"{version[0]}.{version[1]}.{version[2]}"
+
+    with open(release_please_manifest, "w") as f:
+        json.dump(manifest_json, f, indent=4, sort_keys=True)
+        f.write("\n")
+
+
+def configure_release_please_config(
+    package_dirs: List[Path], root_dir: Path = ROOT_DIR
+) -> None:
+    """
+        This method updates the `release-please-config.json` file in the directory
+        `root_dir`. If `root_dir` is not provided, `google-cloud-python` will be used as the root.
+
+        Args:
+            package_dirs(List[pathlib.Path]): A list of Paths, one for each package in
+                the `packages/` folder whose entry will be updated in the release-please config.
+            root_dir(pathlib.Path): The directory to update the `release-please-config.json`
+
+        Returns:
+            None
+    """
+    release_please_config = root_dir / "release-please-config.json"
+    config_json = {"packages": {}}
+    for package_dir in package_dirs:
+        extra_files: List[Union[str, Dict[str, str]]] = [
+            str(file.relative_to(package_dir))
+            for file in sorted(package_dir.rglob("**/gapic_version.py"))
+        ]
+        if len(extra_files) < 1:
+            raise Exception("Failed to find gapic_version.py")
+        for json_file in sorted(package_dir.glob("samples/**/*.json")):
+            sample_json = {}
+            sample_json["jsonpath"] = "$.clientLibrary.version"
+            sample_json["path"] = str(json_file.relative_to(package_dir))
+            sample_json["type"] = "json"
+            extra_files.append(sample_json)
+
+        config_json["packages"][f"packages/{package_dir.name}"] = {
+            "component": f"{package_dir.name}",
+            "release-type": "python",
+            "extra-files": extra_files,
+        }
+
+    with open(release_please_config, "w") as f:
+        json.dump(config_json, f, indent=4, sort_keys=True)
+        f.write("\n")
+
+
+if __name__ == "__main__":
+    owlbot_dirs = get_packages_with_owlbot_yaml()
+    configure_release_please_manifest(owlbot_dirs)
+    configure_release_please_config(owlbot_dirs)


Test patch:
diff --git a/scripts/configure_release_please/configure_release_please_test.py b/scripts/configure_release_please/configure_release_please_test.py
new file mode 100644
--- /dev/null
+++ b/scripts/configure_release_please/configure_release_please_test.py
@@ -0,0 +1,68 @@
+from pathlib import Path
+import shutil
+import unittest
+
+import configure_release_please
+
+SCRIPT_DIR = Path(__file__).parent.resolve()
+TEST_RESOURCES_DIR = SCRIPT_DIR / "test_resources"
+PACKAGES_DIR = TEST_RESOURCES_DIR / "packages"
+
+
+class TestChangeSummary(unittest.TestCase):
+    def test_get_version_for_package(self):
+        path_to_gapic_version_py = (
+            PACKAGES_DIR / "google-cloud-ids/google/cloud/ids/gapic_version.py"
+        )
+        version = configure_release_please.get_version_for_package(
+            path_to_gapic_version_py
+        )
+        self.assertEqual(version, (1, 5, 1))
+
+    def test_get_version_for_package_raises_when_file_not_found(self):
+        with self.assertRaises(FileNotFoundError):
+            configure_release_please.get_version_for_package(Path("does/not/exist"))
+
+    def test_get_packages_with_owlbot_yaml(self):
+        expected_result = [PACKAGES_DIR / "google-cloud-ids"]
+        self.assertEqual(
+            configure_release_please.get_packages_with_owlbot_yaml(PACKAGES_DIR),
+            expected_result,
+        )
+
+    def test_get_packages_with_owlbot_yaml_bad_path(self):
+        with self.assertRaises(FileNotFoundError):
+            configure_release_please.get_packages_with_owlbot_yaml(Path("does/not/exist"))
+
+    def test_configure_release_please_manifest(self):
+        expected_result = """{
+    "packages/google-cloud-ids": "1.5.1"
+}\n"""
+        package_dirs = configure_release_please.get_packages_with_owlbot_yaml(PACKAGES_DIR)
+        configure_release_please.configure_release_please_manifest(
+            package_dirs=package_dirs, root_dir=TEST_RESOURCES_DIR
+        )
+        self.assertEqual(
+            (TEST_RESOURCES_DIR / ".release-please-manifest.json").read_text(), expected_result
+        )
+
+    def test_configure_release_please_config(self):
+        expected_result = """{
+    "packages": {
+        "packages/google-cloud-ids": {
+            "component": "google-cloud-ids",
+            "extra-files": [
+                "google/cloud/ids/gapic_version.py",
+                "google/cloud/ids_v1/gapic_version.py"
+            ],
+            "release-type": "python"
+        }
+    }
+}\n"""
+        package_dirs = configure_release_please.get_packages_with_owlbot_yaml(PACKAGES_DIR)
+        configure_release_please.configure_release_please_config(
+            package_dirs=package_dirs, root_dir=TEST_RESOURCES_DIR
+        )
+        self.assertEqual(
+            (TEST_RESOURCES_DIR / "release-please-config.json").read_text(), expected_result
+        )
diff --git a/scripts/configure_release_please/test_resources/.release-please-manifest.json b/scripts/configure_release_please/test_resources/.release-please-manifest.json
new file mode 100644
--- /dev/null
+++ b/scripts/configure_release_please/test_resources/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+    "packages/google-cloud-ids": "1.5.1"
+}
diff --git a/scripts/configure_release_please/test_resources/packages/google-cloud-ids/.OwlBot.yaml b/scripts/configure_release_please/test_resources/packages/google-cloud-ids/.OwlBot.yaml
new file mode 100644
diff --git a/scripts/configure_release_please/test_resources/packages/google-cloud-ids/google/cloud/ids/gapic_version.py b/scripts/configure_release_please/test_resources/packages/google-cloud-ids/google/cloud/ids/gapic_version.py
new file mode 100644
--- /dev/null
+++ b/scripts/configure_release_please/test_resources/packages/google-cloud-ids/google/cloud/ids/gapic_version.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 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.
+#
+__version__ = "1.5.1"  # {x-release-please-version}
diff --git a/scripts/configure_release_please/test_resources/packages/google-cloud-ids/google/cloud/ids_v1/gapic_version.py b/scripts/configure_release_please/test_resources/packages/google-cloud-ids/google/cloud/ids_v1/gapic_version.py
new file mode 100644
--- /dev/null
+++ b/scripts/configure_release_please/test_resources/packages/google-cloud-ids/google/cloud/ids_v1/gapic_version.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 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.
+#
+__version__ = "1.5.1"  # {x-release-please-version}
diff --git a/scripts/configure_release_please/test_resources/release-please-config.json b/scripts/configure_release_please/test_resources/release-please-config.json
new file mode 100644
--- /dev/null
+++ b/scripts/configure_release_please/test_resources/release-please-config.json
@@ -0,0 +1,12 @@
+{
+    "packages": {
+        "packages/google-cloud-ids": {
+            "component": "google-cloud-ids",
+            "extra-files": [
+                "google/cloud/ids/gapic_version.py",
+                "google/cloud/ids_v1/gapic_version.py"
+            ],
+            "release-type": "python"
+        }
+    }
+}