owlbot.py (108 lines of code) (raw):
# Copyright 2023 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 glob
import os
import re
import subprocess
from logging import Logger
from pathlib import Path
from synthtool import shell
from synthtool.log import logger
logger: Logger = logger
_TOOLS_DIRECTORY = "/synthtool"
_EXCLUDED_DIRS = [r"node_modules", r"^\."]
_TRIM_EXPRESSIONS = [
"s/export {};$/\\n/",
]
_TYPELESS_EXPRESSION = r"Generated (.*)"
_NPM_CONFIG_CACHE = "/var/tmp/.npm"
def walk_through_owlbot_dirs(dir: Path, search_for_changed_files: bool) -> list[str]:
"""
Walks through all sample directories
Returns:
A list of directories
"""
owlbot_dirs: list[str] = []
packages_to_exclude = _EXCLUDED_DIRS
if search_for_changed_files:
try:
# Need to run this step first in the post processor since we only clone
# the branch the PR is on in the Docker container
output = subprocess.run(
["git", "fetch", "origin", "main:main", "--deepen=200"], check=False
)
output.check_returncode()
except subprocess.CalledProcessError as error:
if error.returncode == 128:
logger.info(f"Error: ${error.output}; skipping fetching main")
else:
raise error
for path_object in dir.glob("**/package.json"):
object_dir = str(Path(path_object).parents[0])
if (
path_object.is_file()
and object_dir != str(dir)
and not re.search(
"(?:% s)" % "|".join(packages_to_exclude), str(Path(path_object))
)
):
if search_for_changed_files:
if (
subprocess.run(
["git", "diff", "--quiet", "main...", object_dir], check=False
).returncode
== 1
):
owlbot_dirs.append(object_dir)
else:
owlbot_dirs.append(object_dir)
for path_object in dir.glob("owl-bot-staging/*"):
owlbot_dirs.append(
f"{Path(path_object).parents[1]}/packages/{Path(path_object).name}"
)
return owlbot_dirs
def typeless_samples_hermetic(targets: str, hide_output: bool = False) -> list[str]:
"""
Converts TypeScript samples to JavaScript samples.
Run this step before fix() and friends.
Assumes that typeless-sample-bot is already installed in a well
known location on disk (node_modules/.bin).
This is currently an optional, opt-in part of an individual repo's
OwlBot.py, and must be called from there before calling owlbot_main.
Args:
targets: A list of directories to convert
hide_output: Flag specifying whether stdout should be hidden
Returns:
A list of JavaScript files that were generated
"""
logger.debug("Run typeless sample bot")
## Do not run bot when there are no TypeScript files
if not any(glob.glob(f"{targets}/**/*.ts", recursive=True)):
logger.debug("No TypeScript files in path. Skipping typeless bot.")
return []
proc: subprocess.CompletedProcess[str] = shell.run(
[
f"{_TOOLS_DIRECTORY}/node_modules/.bin/typeless-sample-bot",
"--targets",
targets,
"--recursive",
],
check=True,
hide_output=True, # Capture stdout
)
if not proc.stdout:
return []
if not hide_output:
logger.debug(proc.stdout)
return re.findall(_TYPELESS_EXPRESSION, proc.stdout)
def trim(files: list[str], hide_output: bool = False) -> None:
"""
Fixes the formatting of generated JS files
"""
logger.debug("Trim generated files")
for file in files:
logger.debug(f"Updating {file}")
for expr in _TRIM_EXPRESSIONS:
shell.run(
["sed", "-i", "-e", expr, f"{file}"],
check=True,
hide_output=hide_output,
)
def fix_hermetic(targets: str = ".", hide_output: bool = False) -> None:
"""
Fixes the formatting in the current Node.js library. It assumes that gts
is already installed in a well known location on disk (node_modules/.bin).
"""
logger.debug("Copy eslint config")
shell.run(
["cp", "-r", f"{_TOOLS_DIRECTORY}/node_modules", "."],
check=True,
hide_output=hide_output,
)
logger.debug("Running fix...")
shell.run(
[f"{_TOOLS_DIRECTORY}/node_modules/.bin/gts", "fix", f"{targets}"],
check=True,
hide_output=hide_output,
)
# Update typeless-sample-bot
old_path = os.getcwd()
os.chdir("/synthtool")
logger.debug("Update typeless sample bot [1.3.2]")
shell.run(["npm", "i", "@google-cloud/typeless-sample-bot@1.3.2"])
os.chdir(old_path)
# Avoid "Your cache folder contains root-owned files" error
os.environ["npm_config_cache"] = _NPM_CONFIG_CACHE
# Retrieve list of directories
dirs: list[str] = walk_through_owlbot_dirs(Path.cwd(), search_for_changed_files=True)
for d in dirs:
logger.debug(f"Directory: {d}")
# Run typeless bot to convert from TS -> JS
f = typeless_samples_hermetic(targets=d)
# Remove extra characters
trim(files=f)