def main(args=None):
    if args is None:
        args = sys.argv[1:]

    parser = argparse.ArgumentParser(description="Bazel Bench CI Pipeline")
    parser.add_argument("--day", type=str)
    parser.add_argument("--bazel_bench_options", type=str, default="")
    parsed_args = parser.parse_args(args)

    bazel_bench_ci_steps = []
    day = (datetime.datetime.strptime(parsed_args.day, "%Y-%m-%d").date()
           if parsed_args.day else datetime.date.today())
    bazel_commits = None
    for project in PROJECTS:
        for platform in _get_platforms(project["name"]):
            # bazel-bench doesn't support Windows for now.
            if platform in ["windows"]:
                continue

            # When running on the first platform, get the bazel commits.
            # The bazel commits should be the same regardless of platform.
            if not bazel_commits:
                bazel_clone_path = bazelci.clone_git_repository(
                    BAZEL_REPOSITORY, platform)
                bazel_commits = _get_bazel_commits(day, bazel_clone_path)

            bazel_bench_ci_steps.append(
                _ci_step_for_platform_and_commits(
                    bazel_commits, platform, project,
                    parsed_args.bazel_bench_options))

    bazelci.eprint(yaml.dump({"steps": bazel_bench_ci_steps}))
    buildkite_pipeline_cmd = "cat <<EOF | buildkite-agent pipeline upload\n%s\nEOF" % yaml.dump(
        {"steps": bazel_bench_ci_steps})
    subprocess.call(buildkite_pipeline_cmd, shell=True)
def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(description="Bazel Bench Environment Setup")
    parser.add_argument("--platform", type=str)
    parser.add_argument("--bazel_commits", type=str)
    args = parser.parse_args(argv)

    bazel_commits = args.bazel_commits.split(",")
    # We use one binary for all Linux platforms.
    # Context: https://github.com/bazelbuild/continuous-integration/blob/master/buildkite/bazelci.py
    binary_platform = (
        args.platform if args.platform in ["macos", "windows"] else bazelci.LINUX_BINARY_PLATFORM
    )
    bazel_bin_dir = BAZEL_BINARY_BASE_PATH + "/" + args.platform

    for bazel_commit in bazel_commits:
        destination = bazel_bin_dir + "/" + bazel_commit
        if os.path.exists(destination):
            continue
        try:
            bazelci.download_bazel_binary_at_commit(destination, binary_platform, bazel_commit)
        except bazelci.BuildkiteException:
            # Carry on.
            bazelci.eprint("Binary for Bazel commit %s not found." % bazel_commit)
Esempio n. 3
0
def _create_and_upload_metadata(project_label, command, date, platforms):
    """Generate the METADATA file for each project & upload to Storage.

    METADATA provides information about the runs and where to get the
    measurements. It is later used by the script that generates the daily report
    to construct the graphs.

    Args:
        project_label: the label of the project on Storage.
        command: the bazel command executed during the runs e.g. bazel build ...
        date: the date of the runs.
        platform: the platform the runs were performed on.
    """
    metadata_file_path = "{}/{}-metadata".format(TMP, project_label)

    with open(metadata_file_path, "w") as f:
        data = _metadata_file_content(project_label, command, date, platforms)
        json.dump(data, f)

    destination = "gs://bazel-bench/{}/{}/METADATA".format(
        project_label, date.strftime("%Y/%m/%d"))
    args = ["gsutil", "cp", metadata_file_path, destination]

    try:
        subprocess.check_output(args)
        bazelci.eprint("Uploaded {}'s METADATA to {}.".format(
            project_label, destination))
    except subprocess.CalledProcessError as e:
        bazelci.eprint("Error uploading: {}".format(e))
def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description=
        "Script to aggregate `bazelisk --migrate` test result for incompatible flags and generate pretty Buildkite info messages."
    )
    parser.add_argument("--build_number", type=str)

    args = parser.parse_args(argv)
    try:
        if args.build_number:
            client = bazelci.BuildkiteClient(org=BUILDKITE_ORG,
                                             pipeline=PIPELINE)
            migration_required = print_result_info(args.build_number, client)
            if migration_required and FAIL_IF_MIGRATION_REQUIRED:
                bazelci.eprint(
                    "Exiting with code 3 since a migration is required.")
                return 3
        else:
            parser.print_help()
            return 2

    except bazelci.BuildkiteException as e:
        bazelci.eprint(str(e))
        return 1
    return 0
def create_all_issues(details_per_flag, links_per_project_and_flag):
    errors = set()
    issue_client = get_github_client()
    for (project_label, flag), links in links_per_project_and_flag.items():
        try:
            details = details_per_flag.get(flag, (None, None))
            if details.bazel_version in (None, "unreleased binary"):
                raise bazelci.BuildkiteException(
                    "Notifications: Invalid Bazel version '{}' for flag {}".
                    format(details.bazel_version or "", flag))

            if not details.issue_url:
                raise bazelci.BuildkiteException(
                    "Notifications: Missing GitHub issue URL for flag {}".
                    format(flag))

            repo_owner, repo_name, do_not_notify = get_project_details(
                project_label)
            if do_not_notify:
                bazelci.eprint(
                    "{} has opted out of notifications.".format(project_label))
                continue

            temporary_title = get_temporary_issue_title(project_label, flag)
            final_title = get_final_issue_title(project_label,
                                                details.bazel_version, flag)
            has_target_release = details.bazel_version != "TBD"

            # Three possible scenarios:
            # 1. There is already an issue with the target release in the title -> do nothing
            # 2. There is an issue, but without the target release, and we now know the target release -> update title
            # 3. There is no issue -> create one
            if issue_client.get_issue(repo_owner, repo_name, final_title):
                bazelci.eprint(
                    "There is already an issue in {}/{} for project {}, flag {} and Bazel {}"
                    .format(repo_owner, repo_name, project_label, flag,
                            details.bazel_version))
            else:
                number = issue_client.get_issue(repo_owner, repo_name,
                                                temporary_title)
                if number:
                    if has_target_release:
                        issue_client.update_title(repo_owner, repo_name,
                                                  number, final_title)
                else:
                    body = create_issue_body(project_label, flag, details,
                                             links)
                    title = final_title if has_target_release else temporary_title
                    issue_client.create_issue(repo_owner, repo_name, title,
                                              body)
        except (bazelci.BuildkiteException, GitHubError) as ex:
            errors.add("Could not notify project '{}': {}".format(
                project_label, ex))

    if errors:
        print_info("notify_errors", "error", list(errors))
def get_failing_jobs(build_info):
    failing_jobs = []
    for job in build_info["jobs"]:
        if "state" in job and job["state"] == "failed":
            command = job["command"]
            if not command:
                bazelci.eprint("'command' field not found in the job: " +
                               str(job))
                continue
            # Skip if the job is not a runner job
            if command.find("bazelci.py runner") == -1:
                continue

            # Get rid of the incompatible flags in the command line because we are going to test them individually
            command_without_incompatible_flags = " ".join([
                i for i in command.split(" ")
                if not i.startswith("--incompatible_flag")
            ])

            # Recover the task name from job command
            flags = get_flags_from_command(command)
            task = flags.get("task")
            if not task:
                raise BuildkiteException(
                    "The following command has no --task argument: %s." %
                    command)

            # Fetch the original job config to retrieve the platform name.
            job_config = bazelci.load_config(
                http_url=flags.get("http_config"),
                file_config=flags.get("file_config"))

            # The config can either contain a "tasks" dict (new format) or a "platforms" dict (legacy format).
            all_tasks = job_config.get("tasks", job_config.get("platforms"))
            if not all_tasks:
                raise BuildkiteException(
                    "Malformed configuration: No 'tasks' or 'platforms' entry found."
                )

            task_config = all_tasks.get(task)
            if not task_config:
                raise BuildkiteException(
                    "Configuration does not contain an entry for task '%s'" %
                    task)

            # Shortcut: Users can skip the "platform" field if its value equals the task name.
            platform = task_config.get("platform") or task
            failing_jobs.append({
                "name":
                job["name"],
                "command":
                command_without_incompatible_flags.split("\n"),
                "platform":
                platform,
            })
    return failing_jobs
Esempio n. 7
0
def main(args=None):
    if args is None:
        args = sys.argv[1:]

    parser = argparse.ArgumentParser(description="Bazel Bench CI Pipeline")
    parser.add_argument("--date", type=str)
    parser.add_argument("--bazel_bench_options", type=str, default="")
    parser.add_argument("--bucket", type=str, default="")
    parser.add_argument("--max_commits", type=int, default="")
    parser.add_argument("--report_name", type=str, default="report")
    parser.add_argument("--update_latest", action="store_true", default=False)
    parsed_args = parser.parse_args(args)

    bazel_bench_ci_steps = []
    date = (datetime.datetime.strptime(parsed_args.date, "%Y-%m-%d").date()
            if parsed_args.date else datetime.date.today())

    bazel_clone_path = bazelci.clone_git_repository(BAZEL_REPOSITORY,
                                                    STARTER_JOB_PLATFORM)
    bazel_commits_full_list, bazel_commits_to_benchmark = _get_bazel_commits(
        date, bazel_clone_path, parsed_args.max_commits)

    for project in PROJECTS:
        if not project["active"]:
            continue
        platforms = _get_platforms(project["name"],
                                   whitelist=PLATFORMS_WHITELIST)
        for platform in platforms:
            bazel_bench_ci_steps.append(
                _ci_step_for_platform_and_commits(
                    bazel_commits_to_benchmark, platform, project,
                    parsed_args.bazel_bench_options, date, parsed_args.bucket))
        _create_and_upload_metadata(
            project_label=project["storage_subdir"],
            project_source=project["git_repository"],
            command=project["bazel_command"],
            date=date,
            platforms=platforms,
            bucket=parsed_args.bucket,
            all_commits=bazel_commits_full_list,
            benchmarked_commits=bazel_commits_to_benchmark)

        bazel_bench_ci_steps.append("wait")
        # If all the above steps succeed, generate the report.
        bazel_bench_ci_steps.append(
            _report_generation_step(date, project["storage_subdir"],
                                    parsed_args.bucket,
                                    REPORT_GENERATION_PLATFORM,
                                    parsed_args.report_name,
                                    parsed_args.update_latest))

        bazelci.eprint(yaml.dump({"steps": bazel_bench_ci_steps}))
        subprocess.run(["buildkite-agent", "pipeline", "upload"],
                       input=yaml.dump({"steps": bazel_bench_ci_steps},
                                       encoding="utf-8"))
def run_test(repo_location, module_name, module_version, task):
    try:
        return bazelci.main([
            "runner",
            "--task=" + task,
            "--file_config=%s" %
            get_presubmit_yml(module_name, module_version),
            "--repo_location=%s" % repo_location,
        ])
    except subprocess.CalledProcessError as e:
        bazelci.eprint(str(e))
        return 1
def start_bisecting(project_name, platform_name, git_repo_location, commits_list, needs_clean, repeat_times):
    left = 0
    right = len(commits_list)
    while left < right:
        mid = (left + right) // 2
        mid_commit = commits_list[mid]
        bazelci.print_expanded_group(":bazel: Test with Bazel built at " + mid_commit)
        bazelci.eprint("Remaining suspected commits are:\n")
        for i in range(left, right):
            bazelci.eprint(commits_list[i] + "\n")
        if test_with_bazel_at_commit(
            project_name, platform_name, git_repo_location, mid_commit, needs_clean, repeat_times
        ):
            bazelci.print_collapsed_group(":bazel: Succeeded at " + mid_commit)
            left = mid + 1
        else:
            bazelci.print_collapsed_group(":bazel: Failed at " + mid_commit)
            right = mid

    bazelci.print_expanded_group(":bazel: Bisect Result")
    if right == len(commits_list):
        bazelci.eprint("first bad commit not found, every commit succeeded.")
    else:
        first_bad_commit = commits_list[right]
        bazelci.eprint("first bad commit is " + first_bad_commit)
        os.chdir(BAZEL_REPO_DIR)
        bazelci.execute_command(["git", "--no-pager", "log", "-n", "1", first_bad_commit])
def test_with_bazel_at_commit(project_name, platform_name, git_repo_location,
                              bazel_commit, needs_clean):
    http_config = DOWNSTREAM_PROJECTS[project_name]["http_config"]
    try:
        return_code = bazelci.main([
            "runner",
            "--platform=" + platform_name,
            "--http_config=" + http_config,
            "--git_repo_location=" + git_repo_location,
            "--use_bazel_at_commit=" + bazel_commit,
        ] + (["--needs_clean"] if needs_clean else []))
    except subprocess.CalledProcessError as e:
        bazelci.eprint(str(e))
        return False
    return return_code == 0
Esempio n. 11
0
def _get_clone_path(repository, platform):
    """Returns the path to a local clone of the project.

    If there's a mirror available, use that. bazel-bench will take care of
    pulling/checking out commits. Else, clone the repo.

    Args:
      repository: the URL to the git repository.
      platform: the platform on which to build the project.

    Returns:
      A path to the local clone.
    """
    mirror_path = bazelci.get_mirror_path(repository, platform)
    if os.path.exists(mirror_path):
        bazelci.eprint("Found mirror for %s on %s." % repository, platform)
        return mirror_path

    return repository
def test_with_bazel_at_commit(project_name, task_name, git_repo_location,
                              bazel_commit, needs_clean, repeat_times):
    http_config = bazelci.DOWNSTREAM_PROJECTS[project_name]["http_config"]
    for i in range(1, repeat_times + 1):
        if repeat_times > 1:
            bazelci.print_collapsed_group(":bazel: Try %s time" % i)
        try:
            return_code = bazelci.main([
                "runner",
                "--task=" + task_name,
                "--http_config=" + http_config,
                "--git_repo_location=" + git_repo_location,
                "--use_bazel_at_commit=" + bazel_commit,
            ] + (["--needs_clean"] if needs_clean else []))
        except subprocess.CalledProcessError as e:
            bazelci.eprint(str(e))
            return False
        if return_code != 0:
            return False
    return True
def handle_already_flipped_flags(failed_jobs_per_flag, details_per_flag):
    # Process and remove all flags that have already been flipped.
    # Bazelisk may return already flipped flags if a project uses an old Bazel version
    # via its .bazelversion file.
    current_major_version = bazelci.get_bazel_major_version()
    failed_jobs_for_new_flags = {}
    details_for_new_flags = {}

    for flag, details in details_per_flag.items():
        if details.bazel_version < current_major_version:
            # TOOD(fweikert): maybe display a Buildkite annotation
            bazelci.eprint(
                "Ignoring {} since it has already been flipped in Bazel {} (latest is {})."
                .format(flag, details.bazel_version, current_major_version))
            continue

        details_for_new_flags[flag] = details
        if flag in failed_jobs_per_flag:
            failed_jobs_for_new_flags[flag] = failed_jobs_per_flag[flag]

    return failed_jobs_for_new_flags, details_for_new_flags
def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description=
        "Script for testing failing jobs with individual incompatible flag")
    parser.add_argument("--build_number", type=str)

    args = parser.parse_args(argv)
    try:
        if args.build_number:
            print_steps_for_failing_jobs(args.build_number)
        else:
            parser.print_help()
            return 2

    except BuildkiteException as e:
        bazelci.eprint(str(e))
        return 1
    return 0
def print_steps_for_failing_jobs(build_info):
    failing_jobs = get_failing_jobs(build_info)
    incompatible_flags = list(bazelci.fetch_incompatible_flags().keys())
    pipeline_steps = []
    counter = 0
    for incompatible_flag in incompatible_flags:
        for job in failing_jobs:
            counter += 1
            if counter > BUILDKITE_MAX_JOBS_LIMIT:
                continue
            label = "%s: %s" % (incompatible_flag, job["name"])
            command = list(job["command"])
            command[
                1] = command[1] + " --incompatible_flag=" + incompatible_flag
            pipeline_steps.append(
                bazelci.create_step(label, command, job["platform"]))
    if counter > BUILDKITE_MAX_JOBS_LIMIT:
        bazelci.eprint("We only allow " + str(BUILDKITE_MAX_JOBS_LIMIT) +
                       " jobs to be registered at once, skipping " +
                       str(counter - BUILDKITE_MAX_JOBS_LIMIT) + " jobs.")
    print(yaml.dump({"steps": pipeline_steps}))
def get_failing_jobs(build_info):
    failing_jobs = []
    for job in build_info["jobs"]:
        if "state" in job and job["state"] == "failed":
            command = job["command"]
            if not command:
                bazelci.eprint("'command' field not found in the job: " +
                               str(job))
                continue
            # Skip if the job is not a runner job
            if command.find("bazelci.py runner") == -1:
                continue

            # Get rid of the incompatible flags in the command line because we are going to test them individually
            command_without_incompatible_flags = " ".join([
                i for i in command.split(" ")
                if not i.startswith("--incompatible_flag")
            ])

            # Recover the platform name from job command
            platform = None
            for s in command.split(" "):
                # TODO(fweikert): Fix this once we use arbitrary task names.
                if s.startswith("--task="):
                    platform = s[len("--task="):]

            if not platform:
                raise BuildkiteException(
                    "Cannot recognize platform from job command: %s" % command)

            failing_jobs.append({
                "name":
                job["name"],
                "command":
                command_without_incompatible_flags.split("\n"),
                "platform":
                platform,
            })
    return failing_jobs
def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description=
        "Script to aggregate `bazelisk --migrate` test result for incompatible flags and generate pretty Buildkite info messages."
    )
    parser.add_argument("--build_number", type=str)
    parser.add_argument("--notify", type=bool, nargs="?", const=True)

    args = parser.parse_args(argv)
    try:
        if args.build_number:
            client = bazelci.BuildkiteClient(org=BUILDKITE_ORG,
                                             pipeline=PIPELINE)
            already_failing_jobs, failed_jobs_per_flag, details_per_flag = analyze_logs(
                args.build_number, client)
            failed_jobs_per_flag, details_per_flag = handle_already_flipped_flags(
                failed_jobs_per_flag, details_per_flag)
            migration_required = print_result_info(already_failing_jobs,
                                                   failed_jobs_per_flag,
                                                   details_per_flag)

            if args.notify:
                notify_projects(failed_jobs_per_flag, details_per_flag)

            if migration_required and FAIL_IF_MIGRATION_REQUIRED:
                bazelci.eprint(
                    "Exiting with code 3 since a migration is required.")
                return 3
        else:
            parser.print_help()
            return 2

    except bazelci.BuildkiteException as e:
        bazelci.eprint(str(e))
        return 1
    return 0
def create_all_issues(details_per_flag, links_per_project_and_flag):
    errors = []
    issue_client = get_github_client()
    for (project_label, flag), links in links_per_project_and_flag.items():
        try:
            details = details_per_flag.get(flag, (None, None))
            if details.bazel_version in (None, "unreleased binary"):
                raise bazelci.BuildkiteException(
                    "Notifications: Invalid Bazel version '{}' for flag {}".
                    format(details.bazel_version or "", flag))

            if not details.issue_url:
                raise bazelci.BuildkiteException(
                    "Notifications: Missing GitHub issue URL for flag {}".
                    format(flag))

            repo_owner, repo_name, do_not_notify = get_project_details(
                project_label)
            if do_not_notify:
                bazelci.eprint(
                    "{} has opted out of notifications.".format(project_label))
                continue

            title = get_issue_title(project_label, details.bazel_version, flag)
            if issue_client.get_issue(repo_owner, repo_name, title):
                bazelci.eprint(
                    "There is already an issue in {}/{} for project {}, flag {} and Bazel {}"
                    .format(repo_owner, repo_name, project_label, flag,
                            details.bazel_version))
            else:
                body = create_issue_body(project_label, flag, details, links)
                issue_client.create_issue(repo_owner, repo_name, title, body)
        except (bazelci.BuildkiteException, GitHubError) as ex:
            errors.append("Could not notify project '{}': {}".format(
                project_label, ex))

    if errors:
        print_info("notify_errors", "error", errors)
def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description="Bazel Bench Environment Setup")
    parser.add_argument("--platform", type=str)
    parser.add_argument("--bazel_commits", type=str)
    args = parser.parse_args(argv)

    bazel_commits = args.bazel_commits.split(",")

    for bazel_commit in bazel_commits:
        destination = BAZEL_BINARY_BASE_PATH + "/" + bazel_commit
        if os.path.exists(destination):
            continue
        try:
            bazelci.download_bazel_binary_at_commit(destination, args.platform,
                                                    bazel_commit)
        except bazelci.BuildkiteException:
            # Carry on.
            bazelci.eprint("Binary for Bazel commit %s not found." %
                           bazel_commit)
def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description="Bazel Central Regsitry Presubmit Test Generator")

    subparsers = parser.add_subparsers(dest="subparsers_name")

    subparsers.add_parser("bcr_presubmit")

    runner = subparsers.add_parser("runner")
    runner.add_argument("--module_name", type=str)
    runner.add_argument("--module_version", type=str)
    runner.add_argument("--task", type=str)

    args = parser.parse_args(argv)
    if args.subparsers_name == "bcr_presubmit":
        modules = get_target_modules()
        if not modules:
            bazelci.eprint("No target modules detected in this branch!")
        pipeline_steps = []
        for module_name, module_version in modules:
            configs = get_task_config(module_name, module_version)
            add_presubmit_jobs(module_name, module_version,
                               configs.get("tasks", None), pipeline_steps)
        print(yaml.dump({"steps": pipeline_steps}))
    elif args.subparsers_name == "runner":
        repo_location = create_test_repo(args.module_name, args.module_version,
                                         args.task)
        return run_test(repo_location, args.module_name, args.module_version,
                        args.task)
    else:
        parser.print_help()
        return 2
    return 0
def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description=
        "Script for testing failing jobs with individual incompatible flag")
    parser.add_argument("--build_number", type=str)

    args = parser.parse_args(argv)
    try:
        if args.build_number:
            client = bazelci.BuildkiteClient(org=BUILDKITE_ORG,
                                             pipeline=PIPELINE)
            build_info = client.get_build_info(args.build_number)
            print_steps_for_failing_jobs(build_info)
        else:
            parser.print_help()
            return 2

    except BuildkiteException as e:
        bazelci.eprint(str(e))
        return 1
    return 0
Esempio n. 22
0
def main(argv=None):
    org = os.getenv("BUILDKITE_ORGANIZATION_SLUG")
    repo = os.getenv("BUILDKITE_REPO")
    settings = DOCGEN_SETTINGS.get(org, {}).get(repo)
    if not settings:
        bazelci.eprint("docgen is not enabled for '%s' org and repository %s",
                       org, repo)
        return 1

    bazelci.print_expanded_group(
        ":bazel: Building documentation from {}".format(repo))
    try:
        bazelci.execute_command(["bazel", "build"] + DEFAULT_FLAGS +
                                settings.build_flags + [settings.target])
    except subprocess.CalledProcessError as e:
        bazelci.eprint("Bazel failed with exit code {}".format(e.returncode))
        return e.returncode

    src_root = os.path.join(os.getcwd(), settings.output_dir)
    if settings.rewrite:
        bazelci.print_expanded_group(
            ":bazel: Rewriting links in documentation files")
        dest_root = os.path.join(tempfile.mkdtemp(), "site")
        rewrite_and_copy(src_root, dest_root, settings.rewrite)
        src_root = dest_root

    bucket = "gs://{}".format(settings.gcs_bucket)
    dest = get_destination(bucket, settings.gcs_subdir)
    bazelci.print_expanded_group(
        ":bazel: Uploading documentation to {}".format(dest))
    try:
        bazelci.execute_command(
            ["gsutil", "-m", "rsync", "-r", "-c", "-d", src_root, dest])
        bazelci.execute_command([
            "gsutil", "web", "set", "-m", "index.html", "-e", "404.html",
            bucket
        ])
        # TODO: does not work with 404 pages in sub directories
    except subprocess.CalledProcessError as e:
        bazelci.eprint("Upload to GCS failed with exit code {}".format(
            e.returncode))
        return e.returncode

    bazelci.print_collapsed_group(":bazel: Publishing documentation URL")
    message = "You can find the documentation at {}".format(get_url(settings))
    bazelci.execute_command([
        "buildkite-agent", "annotate", "--style=info", message, "--context",
        "doc_url"
    ])
    bazelci.execute_command(
        ["buildkite-agent", "meta-data", "set", "message", message])

    return 0
def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(description="Bazel Culprit Finder Script")

    subparsers = parser.add_subparsers(dest="subparsers_name")

    subparsers.add_parser("culprit_finder")

    runner = subparsers.add_parser("runner")
    runner.add_argument("--project_name", type=str)
    runner.add_argument("--platform_name", type=str)
    runner.add_argument("--good_bazel_commit", type=str)
    runner.add_argument("--bad_bazel_commit", type=str)
    runner.add_argument("--needs_clean", type=bool, nargs="?", const=True)

    args = parser.parse_args(argv)
    try:
        if args.subparsers_name == "culprit_finder":
            try:
                project_name = os.environ["PROJECT_NAME"]
                platform_name = os.environ["PLATFORM_NAME"]
                good_bazel_commit = os.environ["GOOD_BAZEL_COMMIT"]
                bad_bazel_commit = os.environ["BAD_BAZEL_COMMIT"]
            except KeyError as e:
                raise BuildkiteException("Environment variable %s must be set" % str(e))

            needs_clean = False
            if "NEEDS_CLEAN" in os.environ:
                needs_clean = True

            if project_name not in DOWNSTREAM_PROJECTS:
                raise BuildkiteException(
                    "Project name '%s' not recognized, available projects are %s"
                    % (project_name, str((DOWNSTREAM_PROJECTS.keys())))
                )

            if platform_name not in PLATFORMS:
                raise BuildkiteException(
                    "Platform name '%s' not recognized, available platforms are %s"
                    % (platform_name, str((PLATFORMS.keys())))
                )
            print_culprit_finder_pipeline(
                project_name=project_name,
                platform_name=platform_name,
                good_bazel_commit=good_bazel_commit,
                bad_bazel_commit=bad_bazel_commit,
                needs_clean=needs_clean,
            )
        elif args.subparsers_name == "runner":
            git_repo_location = clone_git_repository(args.project_name, args.platform_name)
            bazelci.print_collapsed_group("Check good bazel commit " + args.good_bazel_commit)
            if not test_with_bazel_at_commit(
                project_name=args.project_name,
                platform_name=args.platform_name,
                git_repo_location=git_repo_location,
                bazel_commit=args.good_bazel_commit,
                needs_clean=args.needs_clean,
            ):
                raise BuildkiteException(
                    "Given good commit (%s) is not actually good, abort bisecting."
                    % args.good_bazel_commit
                )
            start_bisecting(
                project_name=args.project_name,
                platform_name=args.platform_name,
                git_repo_location=git_repo_location,
                commits_list=get_bazel_commits_between(
                    args.good_bazel_commit, args.bad_bazel_commit
                ),
                needs_clean=args.needs_clean,
            )
        else:
            parser.print_help()
            return 2

    except BuildkiteException as e:
        bazelci.eprint(str(e))
        return 1
    return 0