Beispiel #1
0
def synthesize_library(
    library: typing.Dict,
    github_token: str,
    extra_args: typing.List[str],
    base_log_path: pathlib.Path,
    runner: Runner = _execute,
) -> typing.Dict:
    """Run autosynth on a single library.

    Arguments:
        library {dict} - Library configuration

    """
    logger.info(f"Synthesizing {library['name']}.")

    command = [sys.executable, "-m", "autosynth.synth"]

    env = os.environ
    env["GITHUB_TOKEN"] = github_token

    library_args = [
        "--repository",
        library["repository"],
        "--synth-path",
        library.get("synth-path", ""),
        "--branch-suffix",
        library.get("branch-suffix", ""),
        "--pr-title",
        library.get("pr-title", ""),
    ]

    if library.get("metadata-path"):
        library_args.extend(["--metadata-path", library.get("metadata-path")])

    if library.get("deprecated-execution", False):
        library_args.append("--deprecated-execution")

    log_file_path = base_log_path / library["repository"] / "sponge_log.log"
    # run autosynth in a separate process
    (returncode, output) = runner(
        command + library_args + library.get("args", []) + extra_args,
        env,
        log_file_path,
    )
    error = returncode not in (0, synth.EXIT_CODE_SKIPPED)
    skipped = returncode == synth.EXIT_CODE_SKIPPED
    if error:
        logger.error(f"Synthesis failed for {library['name']}")
    return {
        "name": library["name"],
        "output": output,
        "error": error,
        "skipped": skipped,
    }
    def check_if_pr_already_exists(self, branch) -> bool:
        repo = self._repository
        owner = repo.split("/")[0]
        prs = self._gh.list_pull_requests(repo,
                                          state="open",
                                          head=f"{owner}:{branch}")

        if prs:
            pr = prs[0]
            logger.info(f'PR already exists: {pr["html_url"]}')
        return bool(prs)
Beispiel #3
0
def _file_or_comment_on_issue(gh, name, repository, issue_title,
                              existing_issue, output):
    # GitHub rejects issues with bodies > 64k
    output_to_report = output[-10000:]
    sponge_log_url = _get_sponge_log_url(repository)
    message = f"""\
Please investigate and fix this issue within 5 business days.  While it remains broken,
this library cannot be updated with changes to the {name} API, and the library grows
stale.

See https://github.com/googleapis/synthtool/blob/master/autosynth/TroubleShooting.md
for trouble shooting tips.

Here's the output from running `synth.py`:

```
{output_to_report}
```

Google internal developers can see the full log [here]({sponge_log_url}).
"""

    if not existing_issue:
        issue_details = (
            f"Hello! Autosynth couldn't regenerate {name}. :broken_heart:\n\n{message}"
        )
        labels = ["autosynth failure", "priority: p1", "type: bug"]

        api_label = gh.get_api_label(repository, name)
        if api_label:
            labels.append(api_label)

        issue = gh.create_issue(
            repository,
            title=issue_title,
            body=issue_details,
            labels=labels,
        )
        logger.info(f"Opened issue: {issue['url']}")

    # otherwise leave a comment on the existing issue.
    else:
        comment_body = (
            f"Autosynth is still having trouble generating {name}. :sob:\n\n{message}"
        )

        gh.create_issue_comment(
            repository,
            issue_number=existing_issue["number"],
            comment=comment_body,
        )
        logger.info(f"Updated issue: {existing_issue['url']}")
Beispiel #4
0
def _close_issue(gh, repository: str, existing_issue: dict):
    if existing_issue is None:
        return

    logger.info(f"Closing issue: {existing_issue['url']}")
    gh.create_issue_comment(
        repository,
        issue_number=existing_issue["number"],
        comment="Autosynth passed, closing! :green_heart:",
    )
    gh.patch_issue(
        repository, issue_number=existing_issue["number"], state="closed",
    )
Beispiel #5
0
    def check_if_pr_already_exists(self, branch) -> bool:
        repo = self._repository
        owner = repo.split("/")[0]
        prs = self._gh.list_pull_requests(repo, state="open", head=f"{owner}:{branch}")

        if prs:
            pr = prs[0]
            self._existing_pull_requests[branch] = pr
            logger.info(f'PR already exists: {pr["html_url"]}')
            body: str = pr["body"]
            if REGENERATE_CHECKBOX_TEXT in body:
                logger.info("Someone requested the PR to be regenerated.")
                return False

        return bool(prs)
Beispiel #6
0
def _file_or_comment_on_issue(gh, name, repository, issue_title,
                              existing_issue, output):
    # GitHub rejects issues with bodies > 64k
    output_to_report = output[-10000:]
    sponge_log_url = _get_sponge_log_url(repository)
    message = f"""\
Here's the output from running `synth.py`:

```
{output_to_report}
```

Google internal developers can see the full log [here]({sponge_log_url}).
"""

    if not existing_issue:
        issue_details = (
            f"Hello! Autosynth couldn't regenerate {name}. :broken_heart:\n\n{message}"
        )
        labels = ["autosynth failure", "priority: p1", "type: bug"]

        api_label = gh.get_api_label(repository, name)
        if api_label:
            labels.append(api_label)

        issue = gh.create_issue(
            repository,
            title=issue_title,
            body=issue_details,
            labels=labels,
        )
        logger.info(f"Opened issue: {issue['url']}")

    # otherwise leave a comment on the existing issue.
    else:
        comment_body = (
            f"Autosynth is still having trouble generating {name}. :sob:\n\n{message}"
        )

        gh.create_issue_comment(
            repository,
            issue_number=existing_issue["number"],
            comment=comment_body,
        )
        logger.info(f"Updated issue: {existing_issue['url']}")
Beispiel #7
0
    def synthesize(self,
                   log_file_path: pathlib.Path,
                   environ: typing.Mapping[str, str] = None) -> str:
        """
        Returns:
            The log of the call to synthtool.
        """
        logger.info("Running synthtool")
        if not self.deprecated_execution:
            command = [
                sys.executable,
                "-m",
                "synthtool",
                "--metadata",
                self.metadata_path,
                self.synth_py_path,
                "--",
            ]
        else:
            # Execute the synthesis script directly (deprecated)
            command = [sys.executable, self.synth_py_path]

        logger.info(command)
        logger.debug(f"log_file_path: {log_file_path}")

        # Ensure the logfile directory exists
        log_file_path.parent.mkdir(parents=True, exist_ok=True)
        # Tee the output into a provided location so we can see the return the final output
        tee_proc = subprocess.Popen(["tee", log_file_path],
                                    stdin=subprocess.PIPE)
        # Invoke synth.py.
        synth_proc = executor.run(
            command + self.extra_args,
            stderr=subprocess.STDOUT,
            stdout=tee_proc.stdin,
            env=(environ or os.environ),
            universal_newlines=True,
        )
        if synth_proc.returncode:
            logger.error("Synthesis failed")
            synth_proc.check_returncode()  # Raise an exception.

        with open(log_file_path, "rt") as fp:
            return fp.read()
Beispiel #8
0
def has_changes():
    output = subprocess.check_output(["git", "status", "--porcelain"])
    output = output.decode("utf-8").strip()
    logger.info("Changed files:")
    logger.info(output)

    # Parse the git status output. Ignore any blank lines.
    changed_files = [line.strip() for line in output.split("\n") if line]

    # Ignore any files that are in our IGNORED_FILES set and only report
    # that there are changes if these ignored files are not the *only* changed
    # files.
    filtered_changes = []
    for file in changed_files:
        for expr in IGNORED_FILE_PATTERNS:
            if expr.match(file):
                break
        else:
            filtered_changes.append(file)

    return True if filtered_changes else False
Beispiel #9
0
def synthesize_library(
    library: typing.Dict,
    github_token: str,
    extra_args: typing.List[str],
    base_log_path: pathlib.Path,
    runner: Runner = _execute,
) -> typing.Dict:
    """Run autosynth on a single library.

    Arguments:
        library {dict} - Library configuration

    """
    logger.info(f"Synthesizing {library['name']}.")

    command = [sys.executable, "-m", "autosynth.synth"]

    env = os.environ
    env["GITHUB_TOKEN"] = github_token

    library_args = [
        "--repository",
        library["repository"],
        "--synth-path",
        library.get("synth-path", ""),
        "--branch-suffix",
        library.get("branch-suffix", ""),
        "--pr-title",
        library.get("pr-title", ""),
        "--base-log-dir",
        str(base_log_path),
    ]

    if library.get("metadata-path"):
        library_args.extend(["--metadata-path", library.get("metadata-path")])

    if library.get("deprecated-execution", False):
        library_args.append("--deprecated-execution")

    log_file_dir = (pathlib.Path(base_log_path) / pathlib.Path(
        library.get("synth-path", "") or library["repository"]).name)
    log_file_path = log_file_dir / "sponge_log.log"
    # run autosynth in a separate process
    (returncode, output) = runner(
        command + library_args + library.get("args", []) + extra_args,
        env,
        log_file_path,
    )
    error = returncode not in (0, synth.EXIT_CODE_SKIPPED)
    skipped = returncode == synth.EXIT_CODE_SKIPPED
    # Leave a sponge_log.xml side-by-side with sponge_log.log, and sponge
    # will understand they're for the same task and render them accordingly.
    results = [{
        "name": library["name"],
        "error": error,
        "output": "See the test log.",
        "skipped": skipped,
    }]
    make_report(library["name"], results, log_file_dir)
    if error:
        logger.error(f"Synthesis failed for {library['name']}")
    return {
        "name": library["name"],
        "output": output.decode("utf-8"),
        "error": error,
        "skipped": skipped,
    }
Beispiel #10
0
def _inner_main(temp_dir: str) -> int:
    """
    Returns:
        int -- Number of commits committed to the repo.
    """
    parser = argparse.ArgumentParser()
    parser.add_argument("--github-user", default=os.environ.get("GITHUB_USER"))
    parser.add_argument("--github-email",
                        default=os.environ.get("GITHUB_EMAIL"))
    parser.add_argument("--github-token",
                        default=os.environ.get("GITHUB_TOKEN"))
    parser.add_argument("--repository",
                        default=os.environ.get("REPOSITORY"),
                        required=True)
    parser.add_argument("--synth-path", default=os.environ.get("SYNTH_PATH"))
    parser.add_argument("--metadata-path",
                        default=os.environ.get("METADATA_PATH"))
    parser.add_argument(
        "--deprecated-execution",
        default=False,
        action="store_true",
        help=
        "If specified, execute synth.py directly instead of synthtool. This behavior is deprecated.",
    )
    parser.add_argument("--branch-suffix",
                        default=os.environ.get("BRANCH_SUFFIX", None))
    parser.add_argument("--pr-title", default="")
    parser.add_argument("extra_args", nargs=argparse.REMAINDER)

    args = parser.parse_args()

    gh = github.GitHub(args.github_token)

    branch = "-".join(filter(None, ["autosynth", args.branch_suffix]))

    pr_title = args.pr_title or (
        f"[CHANGE ME] Re-generated {args.synth_path or ''} to pick up changes in "
        f"the API or client library generator.")
    change_pusher: AbstractChangePusher = ChangePusher(args.repository, gh,
                                                       branch)

    # capture logs for later
    base_synth_log_path = pathlib.Path(
        os.path.realpath("./logs")) / args.repository
    if args.synth_path:
        base_synth_log_path /= args.synth_path
    logger.info(f"logs will be written to: {base_synth_log_path}")

    working_repo_path = synthtool_git.clone(
        f"https://github.com/{args.repository}.git")

    try:
        os.chdir(working_repo_path)

        git.configure_git(args.github_user, args.github_email)

        git.setup_branch(branch)

        if args.synth_path:
            os.chdir(args.synth_path)

        metadata_path = os.path.join(args.metadata_path or "",
                                     "synth.metadata")

        flags = autosynth.flags.parse_flags()
        # Override flags specified in synth.py with flags specified in environment vars.
        for key in flags.keys():
            env_value = os.environ.get(key, "")
            if env_value:
                flags[key] = False if env_value.lower(
                ) == "false" else env_value

        metadata = load_metadata(metadata_path)
        multiple_commits = flags[autosynth.flags.AUTOSYNTH_MULTIPLE_COMMITS]
        multiple_prs = flags[autosynth.flags.AUTOSYNTH_MULTIPLE_PRS]
        if (not multiple_commits and not multiple_prs) or not metadata:
            if change_pusher.check_if_pr_already_exists(branch):
                return 0

            synth_log = Synthesizer(
                metadata_path,
                args.extra_args,
                deprecated_execution=args.deprecated_execution,
            ).synthesize(base_synth_log_path)

            if not has_changes():
                logger.info("No changes. :)")
                sys.exit(EXIT_CODE_SKIPPED)

            git.commit_all_changes(pr_title)
            change_pusher.push_changes(1, branch, pr_title, synth_log)
            return 1

        else:
            if not multiple_prs and change_pusher.check_if_pr_already_exists(
                    branch):
                return 0  # There's already an existing PR

            # Enumerate the versions to loop over.
            sources = metadata.get("sources", [])
            source_versions = [
                git_source.enumerate_versions_for_working_repo(
                    metadata_path, sources)
            ]
            # Add supported source version types below:
            source_versions.extend(
                git_source.enumerate_versions(sources, pathlib.Path(temp_dir)))

            # Prepare to call synthesize loop.
            synthesizer = Synthesizer(
                metadata_path,
                args.extra_args,
                args.deprecated_execution,
                "synth.py",
            )
            x = SynthesizeLoopToolbox(
                source_versions,
                branch,
                temp_dir,
                metadata_path,
                args.synth_path,
                base_synth_log_path,
            )
            if not multiple_commits:
                change_pusher = SquashingChangePusher(change_pusher)

            # Call the loop.
            commit_count = synthesize_loop(x, multiple_prs, change_pusher,
                                           synthesizer)

            if commit_count == 0:
                logger.info("No changes. :)")
                sys.exit(EXIT_CODE_SKIPPED)

            return commit_count
    finally:
        if args.synth_path:
            # We're generating code in a mono repo.  The state left behind will
            # probably be useful for generating the next API.
            pass
        else:
            # We're generating a single API in a single repo, and using a different
            # repo to generate the next API.  So the next synth will not be able to
            # use any of this state.  Clean it up to avoid running out of disk space.
            executor.run(["git", "clean", "-fdx"], cwd=working_repo_path)
Beispiel #11
0
def select_shard(config, shard: str):
    shard_number, shard_count = map(int, shard.split("/"))
    logger.info(f"Selecting shard {shard_number} of {shard_count}.")
    config.sort(key=lambda repo: repo["name"])
    return shard_list(config, shard_count)[shard_number]