xref: /aosp_15_r20/external/google-cloud-java/owl-bot-postprocessor/synthtool/gcp/common.py (revision 55e87721aa1bc457b326496a7ca40f3ea1a63287)
1*55e87721SMatt Gilbride# Copyright 2018 Google LLC
2*55e87721SMatt Gilbride#
3*55e87721SMatt Gilbride# Licensed under the Apache License, Version 2.0 (the "License");
4*55e87721SMatt Gilbride# you may not use this file except in compliance with the License.
5*55e87721SMatt Gilbride# You may obtain a copy of the License at
6*55e87721SMatt Gilbride#
7*55e87721SMatt Gilbride#     https://www.apache.org/licenses/LICENSE-2.0
8*55e87721SMatt Gilbride#
9*55e87721SMatt Gilbride# Unless required by applicable law or agreed to in writing, software
10*55e87721SMatt Gilbride# distributed under the License is distributed on an "AS IS" BASIS,
11*55e87721SMatt Gilbride# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*55e87721SMatt Gilbride# See the License for the specific language governing permissions and
13*55e87721SMatt Gilbride# limitations under the License.
14*55e87721SMatt Gilbride
15*55e87721SMatt Gilbrideimport json
16*55e87721SMatt Gilbrideimport os
17*55e87721SMatt Gilbrideimport re
18*55e87721SMatt Gilbrideimport shutil
19*55e87721SMatt Gilbrideimport fnmatch
20*55e87721SMatt Gilbridefrom copy import deepcopy
21*55e87721SMatt Gilbridefrom pathlib import Path
22*55e87721SMatt Gilbridefrom typing import Dict, List, Optional
23*55e87721SMatt Gilbrideimport jinja2
24*55e87721SMatt Gilbridefrom datetime import date
25*55e87721SMatt Gilbride
26*55e87721SMatt Gilbridefrom synthtool import shell, _tracked_paths
27*55e87721SMatt Gilbridefrom synthtool.gcp import partials
28*55e87721SMatt Gilbridefrom synthtool.languages import node, node_mono_repo
29*55e87721SMatt Gilbridefrom synthtool.log import logger
30*55e87721SMatt Gilbridefrom synthtool.sources import git, templates
31*55e87721SMatt Gilbride
32*55e87721SMatt GilbridePathOrStr = templates.PathOrStr
33*55e87721SMatt GilbrideTEMPLATES_URL: str = git.make_repo_clone_url("googleapis/synthtool")
34*55e87721SMatt GilbrideDEFAULT_TEMPLATES_PATH = "synthtool/gcp/templates"
35*55e87721SMatt GilbrideLOCAL_TEMPLATES: Optional[str] = os.environ.get("SYNTHTOOL_TEMPLATES")
36*55e87721SMatt Gilbride
37*55e87721SMatt Gilbride
38*55e87721SMatt Gilbrideclass CommonTemplates:
39*55e87721SMatt Gilbride    def __init__(self, template_path: Optional[Path] = None):
40*55e87721SMatt Gilbride        if template_path:
41*55e87721SMatt Gilbride            self._template_root = template_path
42*55e87721SMatt Gilbride        elif LOCAL_TEMPLATES:
43*55e87721SMatt Gilbride            logger.debug(f"Using local templates at {LOCAL_TEMPLATES}")
44*55e87721SMatt Gilbride            self._template_root = Path(LOCAL_TEMPLATES)
45*55e87721SMatt Gilbride        else:
46*55e87721SMatt Gilbride            templates_git = git.clone(TEMPLATES_URL)
47*55e87721SMatt Gilbride            self._template_root = templates_git / DEFAULT_TEMPLATES_PATH
48*55e87721SMatt Gilbride
49*55e87721SMatt Gilbride        self._templates = templates.Templates(self._template_root)
50*55e87721SMatt Gilbride        self.excludes = []  # type: List[str]
51*55e87721SMatt Gilbride
52*55e87721SMatt Gilbride    def _generic_library(self, directory: str, relative_dir=None, **kwargs) -> Path:
53*55e87721SMatt Gilbride        # load common repo meta information (metadata that's not language specific).
54*55e87721SMatt Gilbride        if "metadata" in kwargs:
55*55e87721SMatt Gilbride            self._load_generic_metadata(kwargs["metadata"], relative_dir=relative_dir)
56*55e87721SMatt Gilbride            # if no samples were found, don't attempt to render a
57*55e87721SMatt Gilbride            # samples/README.md.
58*55e87721SMatt Gilbride            if "samples" not in kwargs["metadata"] or not kwargs["metadata"]["samples"]:
59*55e87721SMatt Gilbride                self.excludes.append("samples/README.md")
60*55e87721SMatt Gilbride
61*55e87721SMatt Gilbride        t = templates.TemplateGroup(self._template_root / directory, self.excludes)
62*55e87721SMatt Gilbride
63*55e87721SMatt Gilbride        if "repository" in kwargs["metadata"] and "repo" in kwargs["metadata"]:
64*55e87721SMatt Gilbride            kwargs["metadata"]["repo"]["default_branch"] = _get_default_branch_name(
65*55e87721SMatt Gilbride                kwargs["metadata"]["repository"]
66*55e87721SMatt Gilbride            )
67*55e87721SMatt Gilbride
68*55e87721SMatt Gilbride        # TODO: migrate to python.py once old sample gen is deprecated
69*55e87721SMatt Gilbride        if directory == "python_samples":
70*55e87721SMatt Gilbride            t.env.globals["get_help"] = lambda filename: shell.run(
71*55e87721SMatt Gilbride                ["python", filename, "--help"]
72*55e87721SMatt Gilbride            ).stdout
73*55e87721SMatt Gilbride
74*55e87721SMatt Gilbride        result = t.render(**kwargs)
75*55e87721SMatt Gilbride        _tracked_paths.add(result)
76*55e87721SMatt Gilbride
77*55e87721SMatt Gilbride        return result
78*55e87721SMatt Gilbride
79*55e87721SMatt Gilbride    def py_samples(self, **kwargs) -> List[Path]:
80*55e87721SMatt Gilbride        """
81*55e87721SMatt Gilbride        Handles generation of README.md templates for Python samples
82*55e87721SMatt Gilbride        - Determines whether generation is being done in a client library or in a samples
83*55e87721SMatt Gilbride        folder automatically
84*55e87721SMatt Gilbride        - Otherwise accepts manually set sample_project_dir through kwargs metadata
85*55e87721SMatt Gilbride        - Delegates generation of additional sample documents alternate/overridden folders
86*55e87721SMatt Gilbride        through py_samples_override()
87*55e87721SMatt Gilbride        """
88*55e87721SMatt Gilbride        # kwargs["metadata"] is required to load values from .repo-metadata.json
89*55e87721SMatt Gilbride        if "metadata" not in kwargs:
90*55e87721SMatt Gilbride            kwargs["metadata"] = {}
91*55e87721SMatt Gilbride
92*55e87721SMatt Gilbride        # load common repo meta information (metadata that's not language specific).
93*55e87721SMatt Gilbride        self._load_generic_metadata(kwargs["metadata"])
94*55e87721SMatt Gilbride
95*55e87721SMatt Gilbride        # temporary exclusion prior to old templates being migrated out
96*55e87721SMatt Gilbride        self.excludes.extend(
97*55e87721SMatt Gilbride            [
98*55e87721SMatt Gilbride                "README.rst",
99*55e87721SMatt Gilbride                "auth_api_key.tmpl.rst",
100*55e87721SMatt Gilbride                "auth.tmpl.rst",
101*55e87721SMatt Gilbride                "install_deps.tmpl.rst",
102*55e87721SMatt Gilbride                "install_portaudio.tmpl.rst",
103*55e87721SMatt Gilbride                "noxfile.py.j2",
104*55e87721SMatt Gilbride            ]
105*55e87721SMatt Gilbride        )
106*55e87721SMatt Gilbride
107*55e87721SMatt Gilbride        # ensure samples will generate
108*55e87721SMatt Gilbride        kwargs["metadata"]["samples"] = True
109*55e87721SMatt Gilbride
110*55e87721SMatt Gilbride        # determine if in client lib and set custom root sample dir if specified, else None
111*55e87721SMatt Gilbride        in_client_library = Path("samples").exists()
112*55e87721SMatt Gilbride        sample_project_dir = kwargs["metadata"]["repo"].get("sample_project_dir")
113*55e87721SMatt Gilbride
114*55e87721SMatt Gilbride        if sample_project_dir is None:  # Not found in metadata
115*55e87721SMatt Gilbride            if in_client_library:
116*55e87721SMatt Gilbride                sample_project_dir = "samples"
117*55e87721SMatt Gilbride            else:
118*55e87721SMatt Gilbride                sample_project_dir = "."
119*55e87721SMatt Gilbride        elif not Path(sample_project_dir).exists():
120*55e87721SMatt Gilbride            raise Exception(f"'{sample_project_dir}' does not exist")
121*55e87721SMatt Gilbride
122*55e87721SMatt Gilbride        override_paths_to_samples: Dict[
123*55e87721SMatt Gilbride            str, List[str]
124*55e87721SMatt Gilbride        ] = {}  # Dict of format { override_path : sample(s) }
125*55e87721SMatt Gilbride        samples_dict = deepcopy(kwargs["metadata"]["repo"].get("samples"))
126*55e87721SMatt Gilbride        default_samples_dict = []  # Dict which will generate in sample_project_dir
127*55e87721SMatt Gilbride
128*55e87721SMatt Gilbride        # Iterate through samples to store override_paths_to_samples for all existing
129*55e87721SMatt Gilbride        # override paths
130*55e87721SMatt Gilbride        for sample_idx, sample in enumerate(samples_dict):
131*55e87721SMatt Gilbride            override_path = samples_dict[sample_idx].get("override_path")
132*55e87721SMatt Gilbride
133*55e87721SMatt Gilbride            if override_path is not None:
134*55e87721SMatt Gilbride                # add absolute path to metadata so `python foo.py --help` succeeds
135*55e87721SMatt Gilbride                if sample.get("file") is not None:
136*55e87721SMatt Gilbride                    path = os.path.join(
137*55e87721SMatt Gilbride                        sample_project_dir, override_path, sample.get("file")
138*55e87721SMatt Gilbride                    )
139*55e87721SMatt Gilbride                    sample["abs_path"] = Path(path).resolve()
140*55e87721SMatt Gilbride
141*55e87721SMatt Gilbride                cur_override_sample = override_paths_to_samples.get(override_path)
142*55e87721SMatt Gilbride                # Base case: No samples are yet planned to gen in this override dir
143*55e87721SMatt Gilbride                if cur_override_sample is None:
144*55e87721SMatt Gilbride                    override_paths_to_samples[override_path] = [sample]
145*55e87721SMatt Gilbride                # Else: Sample docs will be generated in README merged with other
146*55e87721SMatt Gilbride                # sample doc(s) already planned to generate in this dir
147*55e87721SMatt Gilbride                else:
148*55e87721SMatt Gilbride                    cur_override_sample.append(sample)
149*55e87721SMatt Gilbride                    override_paths_to_samples[override_path] = cur_override_sample
150*55e87721SMatt Gilbride            # If override path none, will be generated in the default
151*55e87721SMatt Gilbride            # folder: sample_project_dir
152*55e87721SMatt Gilbride            else:
153*55e87721SMatt Gilbride                if sample.get("file") is not None:
154*55e87721SMatt Gilbride                    path = os.path.join(sample_project_dir, sample.get("file"))
155*55e87721SMatt Gilbride                    sample["abs_path"] = Path(path).resolve()
156*55e87721SMatt Gilbride                default_samples_dict.append(sample)
157*55e87721SMatt Gilbride
158*55e87721SMatt Gilbride        # List of paths to tempdirs which will be copied into sample folders
159*55e87721SMatt Gilbride        result = []
160*55e87721SMatt Gilbride
161*55e87721SMatt Gilbride        # deep copy is req. here to avoid kwargs being affected
162*55e87721SMatt Gilbride        overridden_samples_kwargs = deepcopy(kwargs)
163*55e87721SMatt Gilbride        for override_path in override_paths_to_samples:
164*55e87721SMatt Gilbride            # Generate override sample docs
165*55e87721SMatt Gilbride            result.append(
166*55e87721SMatt Gilbride                self.py_samples_override(
167*55e87721SMatt Gilbride                    root=sample_project_dir,
168*55e87721SMatt Gilbride                    override_path=override_path,
169*55e87721SMatt Gilbride                    override_samples=override_paths_to_samples[override_path],
170*55e87721SMatt Gilbride                    **overridden_samples_kwargs,
171*55e87721SMatt Gilbride                )
172*55e87721SMatt Gilbride            )
173*55e87721SMatt Gilbride        kwargs["metadata"]["repo"]["samples"] = default_samples_dict
174*55e87721SMatt Gilbride
175*55e87721SMatt Gilbride        logger.debug(
176*55e87721SMatt Gilbride            f"Generating templates for samples directory '{sample_project_dir}'"
177*55e87721SMatt Gilbride        )
178*55e87721SMatt Gilbride        kwargs["subdir"] = sample_project_dir
179*55e87721SMatt Gilbride        # Generate default sample docs
180*55e87721SMatt Gilbride        result.append(self._generic_library("python_samples", **kwargs))
181*55e87721SMatt Gilbride
182*55e87721SMatt Gilbride        for path in result:
183*55e87721SMatt Gilbride            # .add() records the root of the paths and needs to be applied to each
184*55e87721SMatt Gilbride            _tracked_paths.add(path)
185*55e87721SMatt Gilbride
186*55e87721SMatt Gilbride        return result
187*55e87721SMatt Gilbride
188*55e87721SMatt Gilbride    def py_samples_override(
189*55e87721SMatt Gilbride        self, root, override_path, override_samples, **overridden_samples_kwargs
190*55e87721SMatt Gilbride    ) -> Path:
191*55e87721SMatt Gilbride        """
192*55e87721SMatt Gilbride        Handles additional generation of READMEs where "override_path"s
193*55e87721SMatt Gilbride        are set in one or more samples' metadata
194*55e87721SMatt Gilbride        """
195*55e87721SMatt Gilbride        overridden_samples_kwargs["metadata"]["repo"][
196*55e87721SMatt Gilbride            "sample_project_dir"
197*55e87721SMatt Gilbride        ] = override_path
198*55e87721SMatt Gilbride        # Set samples metadata to ONLY samples intended to generate
199*55e87721SMatt Gilbride        # under this directory (override_path)
200*55e87721SMatt Gilbride        overridden_samples_kwargs["metadata"]["repo"]["samples"] = override_samples
201*55e87721SMatt Gilbride        if root != ".":
202*55e87721SMatt Gilbride            override_path = Path(root) / override_path
203*55e87721SMatt Gilbride
204*55e87721SMatt Gilbride        logger.debug(f"Generating templates for override path '{override_path}'")
205*55e87721SMatt Gilbride
206*55e87721SMatt Gilbride        overridden_samples_kwargs["subdir"] = override_path
207*55e87721SMatt Gilbride        return self._generic_library("python_samples", **overridden_samples_kwargs)
208*55e87721SMatt Gilbride
209*55e87721SMatt Gilbride    def python_notebooks(self, **kwargs) -> Path:
210*55e87721SMatt Gilbride        # kwargs["metadata"] is required to load values from .repo-metadata.json
211*55e87721SMatt Gilbride        if "metadata" not in kwargs:
212*55e87721SMatt Gilbride            kwargs["metadata"] = {}
213*55e87721SMatt Gilbride        return self._generic_library("python_notebooks", **kwargs)
214*55e87721SMatt Gilbride
215*55e87721SMatt Gilbride    def py_mono_repo_library(self, relative_dir, **kwargs) -> Path:
216*55e87721SMatt Gilbride        # kwargs["metadata"] is required to load values from .repo-metadata.json
217*55e87721SMatt Gilbride        if "metadata" not in kwargs:
218*55e87721SMatt Gilbride            kwargs["metadata"] = {}
219*55e87721SMatt Gilbride
220*55e87721SMatt Gilbride        # load common repo meta information (metadata that's not language specific).
221*55e87721SMatt Gilbride        self._load_generic_metadata(kwargs["metadata"], relative_dir)
222*55e87721SMatt Gilbride
223*55e87721SMatt Gilbride        # initialize default_version if it doesn't exist in kwargs["metadata"]['repo']
224*55e87721SMatt Gilbride        if "default_version" not in kwargs["metadata"]["repo"]:
225*55e87721SMatt Gilbride            kwargs["metadata"]["repo"]["default_version"] = ""
226*55e87721SMatt Gilbride
227*55e87721SMatt Gilbride        # Don't add `docs/index.rst` if `versions` is not provided or `default_version` is empty
228*55e87721SMatt Gilbride        if (
229*55e87721SMatt Gilbride            "versions" not in kwargs
230*55e87721SMatt Gilbride            or not kwargs["metadata"]["repo"]["default_version"]
231*55e87721SMatt Gilbride            or kwargs["metadata"]["repo"]["default_version"] == "apiVersion"
232*55e87721SMatt Gilbride        ):
233*55e87721SMatt Gilbride            self.excludes += ["docs/index.rst"]
234*55e87721SMatt Gilbride
235*55e87721SMatt Gilbride        # If the directory `google/cloud` exists, add kwargs to signal that the client library is for a Cloud API
236*55e87721SMatt Gilbride        if Path("google/cloud").exists():
237*55e87721SMatt Gilbride            kwargs["is_google_cloud_api"] = True
238*55e87721SMatt Gilbride
239*55e87721SMatt Gilbride        return self._generic_library("python_mono_repo_library", relative_dir, **kwargs)
240*55e87721SMatt Gilbride
241*55e87721SMatt Gilbride    def py_library(self, **kwargs) -> Path:
242*55e87721SMatt Gilbride        # kwargs["metadata"] is required to load values from .repo-metadata.json
243*55e87721SMatt Gilbride        if "metadata" not in kwargs:
244*55e87721SMatt Gilbride            kwargs["metadata"] = {}
245*55e87721SMatt Gilbride
246*55e87721SMatt Gilbride        # load common repo meta information (metadata that's not language specific).
247*55e87721SMatt Gilbride        self._load_generic_metadata(kwargs["metadata"])
248*55e87721SMatt Gilbride
249*55e87721SMatt Gilbride        # initialize default_version if it doesn't exist in kwargs["metadata"]['repo']
250*55e87721SMatt Gilbride        if "default_version" not in kwargs["metadata"]["repo"]:
251*55e87721SMatt Gilbride            kwargs["metadata"]["repo"]["default_version"] = ""
252*55e87721SMatt Gilbride
253*55e87721SMatt Gilbride        # rename variable to accommodate existing owlbot.py files
254*55e87721SMatt Gilbride        if "system_test_dependencies" in kwargs:
255*55e87721SMatt Gilbride            kwargs["system_test_local_dependencies"] = kwargs[
256*55e87721SMatt Gilbride                "system_test_dependencies"
257*55e87721SMatt Gilbride            ]
258*55e87721SMatt Gilbride            logger.warning(
259*55e87721SMatt Gilbride                "Template argument 'system_test_dependencies' is deprecated."
260*55e87721SMatt Gilbride                "Use 'system_test_local_dependencies' or 'system_test_external_dependencies'"
261*55e87721SMatt Gilbride                "instead."
262*55e87721SMatt Gilbride            )
263*55e87721SMatt Gilbride
264*55e87721SMatt Gilbride        # Set default Python versions for noxfile.py
265*55e87721SMatt Gilbride        if "default_python_version" not in kwargs:
266*55e87721SMatt Gilbride            kwargs["default_python_version"] = "3.8"
267*55e87721SMatt Gilbride        if "unit_test_python_versions" not in kwargs:
268*55e87721SMatt Gilbride            kwargs["unit_test_python_versions"] = ["3.7", "3.8", "3.9", "3.10"]
269*55e87721SMatt Gilbride
270*55e87721SMatt Gilbride        if "system_test_python_versions" not in kwargs:
271*55e87721SMatt Gilbride            kwargs["system_test_python_versions"] = ["3.8"]
272*55e87721SMatt Gilbride
273*55e87721SMatt Gilbride        # If cov_level is not given, set it to None.
274*55e87721SMatt Gilbride        if "cov_level" not in kwargs:
275*55e87721SMatt Gilbride            kwargs["cov_level"] = None
276*55e87721SMatt Gilbride
277*55e87721SMatt Gilbride        # Don't add samples templates if there are no samples
278*55e87721SMatt Gilbride        if "samples" not in kwargs:
279*55e87721SMatt Gilbride            self.excludes += ["samples/AUTHORING_GUIDE.md", "samples/CONTRIBUTING.md"]
280*55e87721SMatt Gilbride
281*55e87721SMatt Gilbride        # Don't add `docs/index.rst` if `versions` is not provided or `default_version` is empty
282*55e87721SMatt Gilbride        if (
283*55e87721SMatt Gilbride            "versions" not in kwargs
284*55e87721SMatt Gilbride            or not kwargs["metadata"]["repo"]["default_version"]
285*55e87721SMatt Gilbride        ):
286*55e87721SMatt Gilbride            self.excludes += ["docs/index.rst"]
287*55e87721SMatt Gilbride
288*55e87721SMatt Gilbride        # Add kwargs to signal that UPGRADING.md should be included in docs/index.rst if it exists
289*55e87721SMatt Gilbride        if Path("docs/UPGRADING.md").exists() or Path("docs/UPGRADING.rst").exists():
290*55e87721SMatt Gilbride            kwargs["include_uprading_doc"] = True
291*55e87721SMatt Gilbride
292*55e87721SMatt Gilbride        # If the directory `google/cloud` exists, add kwargs to signal that the client library is for a Cloud API
293*55e87721SMatt Gilbride        if Path("google/cloud").exists():
294*55e87721SMatt Gilbride            kwargs["is_google_cloud_api"] = True
295*55e87721SMatt Gilbride
296*55e87721SMatt Gilbride        # If Dockerfile exists in .kokoro/docker/samples, add kwargs to
297*55e87721SMatt Gilbride        # signal that a custom docker image should be used when testing samples.
298*55e87721SMatt Gilbride        kwargs["custom_samples_dockerfile"] = Path(
299*55e87721SMatt Gilbride            ".kokoro/docker/samples/Dockerfile"
300*55e87721SMatt Gilbride        ).exists()
301*55e87721SMatt Gilbride
302*55e87721SMatt Gilbride        ret = self._generic_library("python_library", **kwargs)
303*55e87721SMatt Gilbride
304*55e87721SMatt Gilbride        # If split_system_tests is set to True, we disable the system
305*55e87721SMatt Gilbride        # test in the main presubmit build and create individual build
306*55e87721SMatt Gilbride        # configs for each python versions.
307*55e87721SMatt Gilbride        if kwargs.get("split_system_tests", False):
308*55e87721SMatt Gilbride            template_root = self._template_root / "py_library_split_systests"
309*55e87721SMatt Gilbride            # copy the main presubmit config
310*55e87721SMatt Gilbride            shutil.copy2(
311*55e87721SMatt Gilbride                template_root / ".kokoro/presubmit/presubmit.cfg",
312*55e87721SMatt Gilbride                ret / ".kokoro/presubmit/presubmit.cfg",
313*55e87721SMatt Gilbride            )
314*55e87721SMatt Gilbride            env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(template_root)))
315*55e87721SMatt Gilbride            tmpl = env.get_template(".kokoro/presubmit/system.cfg")
316*55e87721SMatt Gilbride            for v in kwargs["system_test_python_versions"]:
317*55e87721SMatt Gilbride                nox_session = f"system-{v}"
318*55e87721SMatt Gilbride                dest = ret / f".kokoro/presubmit/system-{v}.cfg"
319*55e87721SMatt Gilbride                content = tmpl.render(nox_session=nox_session)
320*55e87721SMatt Gilbride                with open(dest, "w") as f:
321*55e87721SMatt Gilbride                    f.write(content)
322*55e87721SMatt Gilbride        return ret
323*55e87721SMatt Gilbride
324*55e87721SMatt Gilbride    def java_library(self, **kwargs) -> Path:
325*55e87721SMatt Gilbride        # kwargs["metadata"] is required to load values from .repo-metadata.json
326*55e87721SMatt Gilbride        if "metadata" not in kwargs:
327*55e87721SMatt Gilbride            kwargs["metadata"] = {}
328*55e87721SMatt Gilbride        return self._generic_library("java_library", **kwargs)
329*55e87721SMatt Gilbride
330*55e87721SMatt Gilbride    def node_library(self, **kwargs) -> Path:
331*55e87721SMatt Gilbride        # TODO: once we've migrated all Node.js repos to either having
332*55e87721SMatt Gilbride        #  .repo-metadata.json, or excluding README.md, we can remove this.
333*55e87721SMatt Gilbride        if not os.path.exists("./.repo-metadata.json"):
334*55e87721SMatt Gilbride            self.excludes.append("README.md")
335*55e87721SMatt Gilbride            if "samples/README.md" not in self.excludes:
336*55e87721SMatt Gilbride                self.excludes.append("samples/README.md")
337*55e87721SMatt Gilbride
338*55e87721SMatt Gilbride        kwargs["metadata"] = node.template_metadata()
339*55e87721SMatt Gilbride        kwargs["publish_token"] = node.get_publish_token(kwargs["metadata"]["name"])
340*55e87721SMatt Gilbride
341*55e87721SMatt Gilbride        ignore_src_index = [
342*55e87721SMatt Gilbride            "yes" for f in self.excludes if fnmatch.fnmatch("src/index.ts", f)
343*55e87721SMatt Gilbride        ]
344*55e87721SMatt Gilbride        # generate root-level `src/index.ts` to export multiple versions and its default clients
345*55e87721SMatt Gilbride        if (
346*55e87721SMatt Gilbride            "versions" in kwargs
347*55e87721SMatt Gilbride            and "default_version" in kwargs
348*55e87721SMatt Gilbride            and not ignore_src_index
349*55e87721SMatt Gilbride        ):
350*55e87721SMatt Gilbride            node.generate_index_ts(
351*55e87721SMatt Gilbride                versions=kwargs["versions"], default_version=kwargs["default_version"]
352*55e87721SMatt Gilbride            )
353*55e87721SMatt Gilbride
354*55e87721SMatt Gilbride        return self._generic_library("node_library", **kwargs)
355*55e87721SMatt Gilbride
356*55e87721SMatt Gilbride    def node_mono_repo_library(self, relative_dir, **kwargs) -> Path:
357*55e87721SMatt Gilbride        # TODO: once we've migrated all Node.js repos to either having
358*55e87721SMatt Gilbride        #  .repo-metadata.json, or excluding README.md, we can remove this.
359*55e87721SMatt Gilbride        if not os.path.exists(Path(relative_dir, ".repo-metadata.json").resolve()):
360*55e87721SMatt Gilbride            self.excludes.append("README.md")
361*55e87721SMatt Gilbride            if "samples/README.md" not in self.excludes:
362*55e87721SMatt Gilbride                self.excludes.append("samples/README.md")
363*55e87721SMatt Gilbride
364*55e87721SMatt Gilbride        kwargs["metadata"] = node_mono_repo.template_metadata(relative_dir)
365*55e87721SMatt Gilbride
366*55e87721SMatt Gilbride        ignore_src_index = [
367*55e87721SMatt Gilbride            "yes" for f in self.excludes if fnmatch.fnmatch("src/index.ts", f)
368*55e87721SMatt Gilbride        ]
369*55e87721SMatt Gilbride        # generate root-level `src/index.ts` to export multiple versions and its default clients
370*55e87721SMatt Gilbride        if (
371*55e87721SMatt Gilbride            "versions" in kwargs
372*55e87721SMatt Gilbride            and "default_version" in kwargs
373*55e87721SMatt Gilbride            and not ignore_src_index
374*55e87721SMatt Gilbride        ):
375*55e87721SMatt Gilbride            node_mono_repo.generate_index_ts(
376*55e87721SMatt Gilbride                versions=kwargs["versions"],
377*55e87721SMatt Gilbride                default_version=kwargs["default_version"],
378*55e87721SMatt Gilbride                relative_dir=relative_dir,
379*55e87721SMatt Gilbride                year=str(date.today().year),
380*55e87721SMatt Gilbride            )
381*55e87721SMatt Gilbride
382*55e87721SMatt Gilbride        return self._generic_library(
383*55e87721SMatt Gilbride            "node_mono_repo_library", relative_dir=relative_dir, **kwargs
384*55e87721SMatt Gilbride        )
385*55e87721SMatt Gilbride
386*55e87721SMatt Gilbride    def php_library(self, **kwargs) -> Path:
387*55e87721SMatt Gilbride        return self._generic_library("php_library", **kwargs)
388*55e87721SMatt Gilbride
389*55e87721SMatt Gilbride    def ruby_library(self, **kwargs) -> Path:
390*55e87721SMatt Gilbride        # kwargs["metadata"] is required to load values from .repo-metadata.json
391*55e87721SMatt Gilbride        if "metadata" not in kwargs:
392*55e87721SMatt Gilbride            kwargs["metadata"] = {}
393*55e87721SMatt Gilbride        return self._generic_library("ruby_library", **kwargs)
394*55e87721SMatt Gilbride
395*55e87721SMatt Gilbride    def render(self, template_name: str, **kwargs) -> Path:
396*55e87721SMatt Gilbride        template = self._templates.render(template_name, **kwargs)
397*55e87721SMatt Gilbride        _tracked_paths.add(template)
398*55e87721SMatt Gilbride        return template
399*55e87721SMatt Gilbride
400*55e87721SMatt Gilbride    def _load_generic_metadata(self, metadata: Dict, relative_dir=None):
401*55e87721SMatt Gilbride        """
402*55e87721SMatt Gilbride        loads additional meta information from .repo-metadata.json.
403*55e87721SMatt Gilbride        """
404*55e87721SMatt Gilbride        metadata["partials"] = partials.load_partials()
405*55e87721SMatt Gilbride
406*55e87721SMatt Gilbride        # Loads repo metadata information from the default location if it
407*55e87721SMatt Gilbride        # hasn't already been set. Some callers may have already loaded repo
408*55e87721SMatt Gilbride        # metadata, so we don't need to do it again or overwrite it. Also, only
409*55e87721SMatt Gilbride        # set the "repo" key.
410*55e87721SMatt Gilbride        if "repo" not in metadata:
411*55e87721SMatt Gilbride            metadata["repo"] = _load_repo_metadata(relative_dir=relative_dir)
412*55e87721SMatt Gilbride
413*55e87721SMatt Gilbride
414*55e87721SMatt Gilbridedef detect_versions(
415*55e87721SMatt Gilbride    path: str = "./src",
416*55e87721SMatt Gilbride    default_version: Optional[str] = None,
417*55e87721SMatt Gilbride    default_first: Optional[bool] = None,
418*55e87721SMatt Gilbride) -> List[str]:
419*55e87721SMatt Gilbride    """
420*55e87721SMatt Gilbride    Detects the versions a library has, based on distinct folders
421*55e87721SMatt Gilbride    within path. This is based on the fact that our GAPIC libraries are
422*55e87721SMatt Gilbride    structured as follows:
423*55e87721SMatt Gilbride
424*55e87721SMatt Gilbride    src/v1
425*55e87721SMatt Gilbride    src/v1beta
426*55e87721SMatt Gilbride    src/v1alpha
427*55e87721SMatt Gilbride
428*55e87721SMatt Gilbride    With folder names mapping directly to versions.
429*55e87721SMatt Gilbride
430*55e87721SMatt Gilbride    Returns: a list of the sorted subdirectories; for the example above:
431*55e87721SMatt Gilbride      ['v1', 'v1alpha', 'v1beta']
432*55e87721SMatt Gilbride      If the `default_version` argument is not provided, the `default_version`
433*55e87721SMatt Gilbride      will be read from `.repo-metadata.json`, if it exists.
434*55e87721SMatt Gilbride      If `default_version` is available, the `default_version` is moved to
435*55e87721SMatt Gilbride      at the front or the end of the sorted list depending on the value of `default_first`.
436*55e87721SMatt Gilbride      The `default_version` will be first in the list when `default_first` is `True`.
437*55e87721SMatt Gilbride    """
438*55e87721SMatt Gilbride
439*55e87721SMatt Gilbride    versions = []
440*55e87721SMatt Gilbride
441*55e87721SMatt Gilbride    if not default_version:
442*55e87721SMatt Gilbride        try:
443*55e87721SMatt Gilbride            # Get the `default_version` from ``.repo-metadata.json`.
444*55e87721SMatt Gilbride            default_version = json.load(open(".repo-metadata.json", "rt")).get(
445*55e87721SMatt Gilbride                "default_version"
446*55e87721SMatt Gilbride            )
447*55e87721SMatt Gilbride        except FileNotFoundError:
448*55e87721SMatt Gilbride            pass
449*55e87721SMatt Gilbride
450*55e87721SMatt Gilbride    # Detect versions up to a depth of 4 in directory hierarchy
451*55e87721SMatt Gilbride    for level in ("*v[1-9]*", "*/*v[1-9]*", "*/*/*v[1-9]*", "*/*/*/*v[1-9]*"):
452*55e87721SMatt Gilbride        # Sort the sub directories alphabetically.
453*55e87721SMatt Gilbride        sub_dirs = sorted([p.name for p in Path(path).glob(level) if p.is_dir()])
454*55e87721SMatt Gilbride        # Don't proceed to the next level if we've detected versions in this depth level
455*55e87721SMatt Gilbride        if sub_dirs:
456*55e87721SMatt Gilbride            break
457*55e87721SMatt Gilbride
458*55e87721SMatt Gilbride    if sub_dirs:
459*55e87721SMatt Gilbride        # if `default_version` is not specified, return the sorted directories.
460*55e87721SMatt Gilbride        if not default_version:
461*55e87721SMatt Gilbride            versions = sub_dirs
462*55e87721SMatt Gilbride        else:
463*55e87721SMatt Gilbride            # The subdirectory with the same suffix as the default_version
464*55e87721SMatt Gilbride            # will be the default client.
465*55e87721SMatt Gilbride            default_client = next(
466*55e87721SMatt Gilbride                iter([d for d in sub_dirs if d.endswith(default_version)]), None
467*55e87721SMatt Gilbride            )
468*55e87721SMatt Gilbride
469*55e87721SMatt Gilbride            # start with all the versions except for the default client
470*55e87721SMatt Gilbride            versions = [d for d in sub_dirs if not d.endswith(default_version)]
471*55e87721SMatt Gilbride
472*55e87721SMatt Gilbride            if default_client:
473*55e87721SMatt Gilbride                # If `default_first` is true, the default_client will be first
474*55e87721SMatt Gilbride                # in the list.
475*55e87721SMatt Gilbride                if default_first:
476*55e87721SMatt Gilbride                    versions = [default_client] + versions
477*55e87721SMatt Gilbride                else:
478*55e87721SMatt Gilbride                    versions += [default_client]
479*55e87721SMatt Gilbride    return versions
480*55e87721SMatt Gilbride
481*55e87721SMatt Gilbride
482*55e87721SMatt Gilbridedef decamelize(value: str):
483*55e87721SMatt Gilbride    """Parser to convert fooBar.js to Foo Bar."""
484*55e87721SMatt Gilbride    if not value:
485*55e87721SMatt Gilbride        return ""
486*55e87721SMatt Gilbride    str_decamelize = re.sub("^.", value[0].upper(), value)  # apple -> Apple.
487*55e87721SMatt Gilbride    str_decamelize = re.sub(
488*55e87721SMatt Gilbride        "([A-Z]+)([A-Z])([a-z0-9])", r"\1 \2\3", str_decamelize
489*55e87721SMatt Gilbride    )  # ACLBatman -> ACL Batman.
490*55e87721SMatt Gilbride    return re.sub("([a-z0-9])([A-Z])", r"\1 \2", str_decamelize)  # FooBar -> Foo Bar.
491*55e87721SMatt Gilbride
492*55e87721SMatt Gilbride
493*55e87721SMatt Gilbridedef _load_repo_metadata(
494*55e87721SMatt Gilbride    relative_dir=None, metadata_file: str = "./.repo-metadata.json"
495*55e87721SMatt Gilbride) -> Dict:
496*55e87721SMatt Gilbride    """Parse a metadata JSON file into a Dict.
497*55e87721SMatt Gilbride
498*55e87721SMatt Gilbride    Currently, the defined fields are:
499*55e87721SMatt Gilbride    * `name` - The service's API name
500*55e87721SMatt Gilbride    * `name_pretty` - The service's API title. This will be used for generating titles on READMEs
501*55e87721SMatt Gilbride    * `product_documentation` - The product documentation on cloud.google.com
502*55e87721SMatt Gilbride    * `client_documentation` - The client library reference documentation
503*55e87721SMatt Gilbride    * `issue_tracker` - The public issue tracker for the product
504*55e87721SMatt Gilbride    * `release_level` - The release level of the client library. One of: alpha, beta,
505*55e87721SMatt Gilbride      ga, deprecated, preview, stable
506*55e87721SMatt Gilbride    * `language` - The repo language. One of dotnet, go, java, nodejs, php, python, ruby
507*55e87721SMatt Gilbride    * `repo` - The GitHub repo in the format {owner}/{repo}
508*55e87721SMatt Gilbride    * `distribution_name` - The language-idiomatic package/distribution name
509*55e87721SMatt Gilbride    * `api_id` - The API ID associated with the service. Fully qualified identifier use to
510*55e87721SMatt Gilbride      enable a service in the cloud platform (e.g. monitoring.googleapis.com)
511*55e87721SMatt Gilbride    * `requires_billing` - Whether or not the API requires billing to be configured on the
512*55e87721SMatt Gilbride      customer's acocunt
513*55e87721SMatt Gilbride
514*55e87721SMatt Gilbride    Args:
515*55e87721SMatt Gilbride        metadata_file (str, optional): Path to the metadata json file
516*55e87721SMatt Gilbride
517*55e87721SMatt Gilbride    Returns:
518*55e87721SMatt Gilbride        A dictionary of metadata. This may not necessarily include all the defined fields above.
519*55e87721SMatt Gilbride    """
520*55e87721SMatt Gilbride    if relative_dir is not None:
521*55e87721SMatt Gilbride        if os.path.exists(Path(relative_dir, metadata_file).resolve()):
522*55e87721SMatt Gilbride            with open(Path(relative_dir, metadata_file).resolve()) as f:
523*55e87721SMatt Gilbride                return json.load(f)
524*55e87721SMatt Gilbride    elif os.path.exists(metadata_file):
525*55e87721SMatt Gilbride        with open(metadata_file) as f:
526*55e87721SMatt Gilbride            return json.load(f)
527*55e87721SMatt Gilbride    return {}
528*55e87721SMatt Gilbride
529*55e87721SMatt Gilbride
530*55e87721SMatt Gilbridedef _get_default_branch_name(repository_name: str) -> str:
531*55e87721SMatt Gilbride    """Read the default branch name from the environment.
532*55e87721SMatt Gilbride
533*55e87721SMatt Gilbride    First checks environment variable DEFAULT_BRANCH_PATH.  If found, it
534*55e87721SMatt Gilbride    reads the contents of the file at DEFAULT_BRANCH_PATH and returns it.
535*55e87721SMatt Gilbride
536*55e87721SMatt Gilbride    Then checks environment varabile DEFAULT_BRANCH, and returns it if found.
537*55e87721SMatt Gilbride    """
538*55e87721SMatt Gilbride    default_branch_path = os.getenv("DEFAULT_BRANCH_PATH")
539*55e87721SMatt Gilbride    if default_branch_path:
540*55e87721SMatt Gilbride        return Path(default_branch_path).read_text().strip()
541*55e87721SMatt Gilbride
542*55e87721SMatt Gilbride    # This default should be switched to "main" once we've migrated
543*55e87721SMatt Gilbride    # the majority of our repositories:
544*55e87721SMatt Gilbride    return os.getenv("DEFAULT_BRANCH", "master")
545