blob: 0859a45c1533c3b2addb315dac7e68a83a38a3d8 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2018 The Bazel Authors. All rights reserved.
#
# 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 argparse
import base64
import codecs
import datetime
import hashlib
import json
import multiprocessing
import os
import os.path
import random
import re
from shutil import copyfile
import shutil
import stat
import subprocess
import sys
import tempfile
import time
import urllib.request
import uuid
import yaml
from urllib.request import url2pathname
from urllib.parse import urlparse
# Initialize the random number generator.
random.seed()
DOWNSTREAM_PROJECTS = {
"Android Testing": {
"git_repository": "https://github.com/googlesamples/android-testing.git",
"http_config": "https://raw.githubusercontent.com/googlesamples/android-testing/master/bazelci/buildkite-pipeline.yml",
"pipeline_slug": "android-testing",
},
"Bazel": {
"git_repository": "https://github.com/bazelbuild/bazel.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/bazel/master/.bazelci/postsubmit.yml",
"pipeline_slug": "bazel-bazel",
},
"Bazel Remote Execution": {
"git_repository": "https://github.com/bazelbuild/bazel.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/pipelines/bazel-remote-execution-postsubmit.yml",
"pipeline_slug": "remote-execution",
},
"BUILD_file_generator": {
"git_repository": "https://github.com/bazelbuild/BUILD_file_generator.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/BUILD_file_generator/master/.bazelci/presubmit.yml",
"pipeline_slug": "build-file-generator",
},
"bazel-toolchains": {
"git_repository": "https://github.com/bazelbuild/bazel-toolchains.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/bazel-toolchains/master/.bazelci/presubmit.yml",
"pipeline_slug": "bazel-toolchains",
},
"bazel-skylib": {
"git_repository": "https://github.com/bazelbuild/bazel-skylib.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/bazel-skylib/master/.bazelci/presubmit.yml",
"pipeline_slug": "bazel-skylib",
},
"buildtools": {
"git_repository": "https://github.com/bazelbuild/buildtools.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/buildtools/master/.bazelci/presubmit.yml",
"pipeline_slug": "buildtools",
},
"CLion Plugin": {
"git_repository": "https://github.com/bazelbuild/intellij.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/pipelines/clion-postsubmit.yml",
"pipeline_slug": "clion-plugin",
},
"Gerrit": {
"git_repository": "https://gerrit.googlesource.com/gerrit.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/pipelines/gerrit-postsubmit.yml",
"pipeline_slug": "gerrit",
},
"Google Logging": {
"git_repository": "https://github.com/google/glog.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/pipelines/glog-postsubmit.yml",
"pipeline_slug": "google-logging",
},
"IntelliJ Plugin": {
"git_repository": "https://github.com/bazelbuild/intellij.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/pipelines/intellij-postsubmit.yml",
"pipeline_slug": "intellij-plugin",
},
"migration-tooling": {
"git_repository": "https://github.com/bazelbuild/migration-tooling.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/migration-tooling/master/.bazelci/presubmit.yml",
"pipeline_slug": "migration-tooling",
},
"protobuf": {
"git_repository": "https://github.com/google/protobuf.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/pipelines/protobuf-postsubmit.yml",
"pipeline_slug": "protobuf",
},
"re2": {
"git_repository": "https://github.com/google/re2.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/pipelines/re2-postsubmit.yml",
"pipeline_slug": "re2",
},
"rules_appengine": {
"git_repository": "https://github.com/bazelbuild/rules_appengine.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_appengine/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-appengine-appengine",
},
"rules_apple": {
"git_repository": "https://github.com/bazelbuild/rules_apple.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_apple/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-apple-darwin",
},
"rules_closure": {
"git_repository": "https://github.com/bazelbuild/rules_closure.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_closure/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-closure-closure-compiler",
},
"rules_d": {
"git_repository": "https://github.com/bazelbuild/rules_d.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_d/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-d",
},
"rules_docker": {
"git_repository": "https://github.com/bazelbuild/rules_docker.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_docker/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-docker-docker",
},
"rules_foreign_cc": {
"git_repository": "https://github.com/bazelbuild/rules_foreign_cc.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_foreign_cc/master/.bazelci/config.yaml",
"pipeline_slug": "rules-foreign-cc",
},
"rules_go": {
"git_repository": "https://github.com/bazelbuild/rules_go.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_go/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-go-golang",
},
"rules_groovy": {
"git_repository": "https://github.com/bazelbuild/rules_groovy.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_groovy/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-groovy",
},
"rules_gwt": {
"git_repository": "https://github.com/bazelbuild/rules_gwt.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_gwt/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-gwt",
},
"rules_jsonnet": {
"git_repository": "https://github.com/bazelbuild/rules_jsonnet.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_jsonnet/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-jsonnet",
},
"rules_kotlin": {
"git_repository": "https://github.com/bazelbuild/rules_kotlin.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_kotlin/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-kotlin-kotlin",
},
"rules_k8s": {
"git_repository": "https://github.com/bazelbuild/rules_k8s.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_k8s/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-k8s-k8s",
},
"rules_nodejs": {
"git_repository": "https://github.com/bazelbuild/rules_nodejs.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_nodejs/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-nodejs-nodejs",
},
"rules_perl": {
"git_repository": "https://github.com/bazelbuild/rules_perl.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_perl/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-perl",
},
"rules_python": {
"git_repository": "https://github.com/bazelbuild/rules_python.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_python/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-python-python",
},
"rules_rust": {
"git_repository": "https://github.com/bazelbuild/rules_rust.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_rust/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-rust-rustlang",
},
"rules_sass": {
"git_repository": "https://github.com/bazelbuild/rules_sass.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_sass/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-sass",
},
"rules_scala": {
"git_repository": "https://github.com/bazelbuild/rules_scala.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_scala/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-scala-scala",
},
"rules_typescript": {
"git_repository": "https://github.com/bazelbuild/rules_typescript.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_typescript/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-typescript-typescript",
},
"rules_webtesting": {
"git_repository": "https://github.com/bazelbuild/rules_webtesting.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/rules_webtesting/master/.bazelci/presubmit.yml",
"pipeline_slug": "rules-webtesting-saucelabs",
"disabled_reason": "Re-enable once fixed: https://github.com/bazelbuild/continuous-integration/issues/191",
},
"skydoc": {
"git_repository": "https://github.com/bazelbuild/skydoc.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/skydoc/master/.bazelci/presubmit.yml",
"pipeline_slug": "skydoc",
},
"subpar": {
"git_repository": "https://github.com/google/subpar.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/pipelines/subpar-postsubmit.yml",
"pipeline_slug": "subpar",
},
"TensorFlow": {
"git_repository": "https://github.com/tensorflow/tensorflow.git",
"http_config": "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/pipelines/tensorflow-postsubmit.yml",
"pipeline_slug": "tensorflow",
},
}
# A map containing all supported platform names as keys, with the values being
# the platform name in a human readable format, and a the buildkite-agent's
# working directory.
PLATFORMS = {
"ubuntu1404": {
"name": "Ubuntu 14.04, JDK 8",
"emoji-name": ":ubuntu: 14.04 (JDK 8)",
"agent-directory": "/var/lib/buildkite-agent/builds/${BUILDKITE_AGENT_NAME}",
"publish_binary": True,
"java": "8",
"docker-image": "gcr.io/bazel-untrusted/ubuntu1404:java8",
},
"ubuntu1604": {
"name": "Ubuntu 16.04, JDK 8",
"emoji-name": ":ubuntu: 16.04 (JDK 8)",
"agent-directory": "/var/lib/buildkite-agent/builds/${BUILDKITE_AGENT_NAME}",
"publish_binary": False,
"java": "8",
"docker-image": "gcr.io/bazel-untrusted/ubuntu1604:java8",
},
"ubuntu1804": {
"name": "Ubuntu 18.04, JDK 8",
"emoji-name": ":ubuntu: 18.04 (JDK 8)",
"agent-directory": "/var/lib/buildkite-agent/builds/${BUILDKITE_AGENT_NAME}",
"publish_binary": False,
"java": "8",
"docker-image": "gcr.io/bazel-untrusted/ubuntu1804:java8",
},
"ubuntu1804_nojava": {
"name": "Ubuntu 18.04, no JDK",
"emoji-name": ":ubuntu: 18.04 (no JDK)",
"agent-directory": "/var/lib/buildkite-agent/builds/${BUILDKITE_AGENT_NAME}",
"publish_binary": False,
"java": "no",
"docker-image": "gcr.io/bazel-untrusted/ubuntu1804:nojava",
},
"ubuntu1804_java9": {
"name": "Ubuntu 18.04, JDK 9",
"emoji-name": ":ubuntu: 18.04 (JDK 9)",
"agent-directory": "/var/lib/buildkite-agent/builds/${BUILDKITE_AGENT_NAME}",
"publish_binary": False,
"java": "9",
"docker-image": "gcr.io/bazel-untrusted/ubuntu1804:java9",
},
"ubuntu1804_java10": {
"name": "Ubuntu 18.04, JDK 10",
"emoji-name": ":ubuntu: 18.04 (JDK 10)",
"agent-directory": "/var/lib/buildkite-agent/builds/${BUILDKITE_AGENT_NAME}",
"publish_binary": False,
"java": "10",
"docker-image": "gcr.io/bazel-untrusted/ubuntu1804:java10",
},
"macos": {
"name": "macOS, JDK 8",
"emoji-name": ":darwin: (JDK 8)",
"agent-directory": "/Users/buildkite/builds/${BUILDKITE_AGENT_NAME}",
"publish_binary": True,
"java": "8",
},
"windows": {
"name": "Windows, JDK 8",
"emoji-name": ":windows: (JDK 8)",
"agent-directory": "d:/b/${BUILDKITE_AGENT_NAME}",
"publish_binary": True,
"java": "8",
},
"rbe_ubuntu1604": {
"name": "RBE (Ubuntu 16.04, JDK 8)",
"emoji-name": ":gcloud: (JDK 8)",
"agent-directory": "/var/lib/buildkite-agent/builds/${BUILDKITE_AGENT_NAME}",
"publish_binary": False,
"host-platform": "ubuntu1604",
"java": "8",
"docker-image": "gcr.io/bazel-untrusted/ubuntu1604:java8",
},
}
# The platform used for various steps (e.g. stuff that formerly ran on the "pipeline" workers).
DEFAULT_PLATFORM = "ubuntu1804"
ENCRYPTED_SAUCELABS_TOKEN = """
CiQAGuqy23f9LNPzp0AetddpO5CXjducZuBB/dfp6ccpX4LxM+8STQBj1BIUMJMXFAWd9BxYJmcM
W7hzbbFFEfpDuqwVwzD2xF3KugY3Otwv+lPLf6K+8ZI55SbpryFFbt7eSlvVTJIBlElfwIU6OpuK
OuI/
""".strip()
class BuildkiteException(Exception):
"""
Raised whenever something goes wrong and we should exit with an error.
"""
pass
class BinaryUploadRaceException(Exception):
"""
Raised when try_publish_binaries wasn't able to publish a set of binaries,
because the generation of the current file didn't match the expected value.
"""
pass
def eprint(*args, **kwargs):
"""
Print to stderr and flush (just in case).
"""
print(*args, flush=True, file=sys.stderr, **kwargs)
def rchop(string_, *endings):
for ending in endings:
if string_.endswith(ending):
return string_[: -len(ending)]
return string_
def python_binary(platform=None):
if platform == "windows":
return "python.exe"
if platform == "macos":
return "python3.7"
return "python3.6"
def is_windows():
return os.name == "nt"
def gsutil_command():
return "gsutil.cmd" if is_windows() else "gsutil"
def gcloud_command():
return "gcloud.cmd" if is_windows() else "gcloud"
def bazelcipy_url():
"""
URL to the latest version of this script.
"""
return "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/bazelci.py?{}".format(
int(time.time())
)
def incompatible_flag_verbose_failures_url():
"""
URL to the latest version of this script.
"""
return "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/incompatible_flag_verbose_failures.py?{}".format(
int(time.time())
)
def downstream_projects_root(platform):
downstream_projects_dir = os.path.expandvars(
"${BUILDKITE_ORGANIZATION_SLUG}-downstream-projects"
)
agent_directory = os.path.expandvars(PLATFORMS[platform]["agent-directory"])
path = os.path.join(agent_directory, downstream_projects_dir)
if not os.path.exists(path):
os.makedirs(path)
return path
def fetch_configs(http_url, file_config):
"""
If specified fetches the build configuration from file_config or http_url, else tries to
read it from .bazelci/presubmit.yml.
Returns the json configuration as a python data structure.
"""
if file_config is not None and http_url is not None:
raise BuildkiteException("file_config and http_url cannot be set at the same time")
if file_config is not None:
with open(file_config, "r") as fd:
return yaml.load(fd)
if http_url is not None:
with urllib.request.urlopen(http_url) as resp:
reader = codecs.getreader("utf-8")
return yaml.load(reader(resp))
with open(".bazelci/presubmit.yml", "r") as fd:
return yaml.load(fd)
def print_collapsed_group(name):
eprint("\n\n--- {0}\n\n".format(name))
def print_expanded_group(name):
eprint("\n\n+++ {0}\n\n".format(name))
def execute_commands(
config,
platform,
git_repository,
git_commit,
git_repo_location,
use_bazel_at_commit,
use_but,
save_but,
build_only,
test_only,
monitor_flaky_tests,
incompatible_flags,
):
build_only = build_only or "test_targets" not in config
test_only = test_only or "build_targets" not in config
if build_only and test_only:
raise BuildkiteException("build_only and test_only cannot be true at the same time")
if use_bazel_at_commit and use_but:
raise BuildkiteException("use_bazel_at_commit cannot be set when use_but is true")
tmpdir = tempfile.mkdtemp()
sc_process = None
try:
if git_repo_location:
os.chdir(git_repo_location)
elif git_repository:
clone_git_repository(git_repository, platform, git_commit)
else:
git_repository = os.getenv("BUILDKITE_REPO")
if use_bazel_at_commit:
print_collapsed_group(":gcloud: Downloading Bazel built at " + use_bazel_at_commit)
bazel_binary = download_bazel_binary_at_commit(tmpdir, platform, use_bazel_at_commit)
elif use_but:
print_collapsed_group(":gcloud: Downloading Bazel Under Test")
bazel_binary = download_bazel_binary(tmpdir, platform)
else:
bazel_binary = "bazel"
print_bazel_version_info(bazel_binary, platform)
print_environment_variables_info()
if incompatible_flags:
print_expanded_group("Build and test with the following incompatible flags:")
for flag in incompatible_flags:
eprint(flag + "\n")
if platform == "windows":
execute_batch_commands(config.get("batch_commands", None))
else:
execute_shell_commands(config.get("shell_commands", None))
execute_bazel_run(
bazel_binary, platform, config.get("run_targets", None), incompatible_flags
)
if config.get("sauce", None):
print_collapsed_group(":saucelabs: Starting Sauce Connect Proxy")
os.environ["SAUCE_USERNAME"] = "bazel_rules_webtesting"
os.environ["SAUCE_ACCESS_KEY"] = saucelabs_token()
os.environ["TUNNEL_IDENTIFIER"] = str(uuid.uuid4())
os.environ["BUILD_TAG"] = str(uuid.uuid4())
readyfile = os.path.join(tmpdir, "sc_is_ready")
if platform == "windows":
cmd = ["sauce-connect.exe", "/i", os.environ["TUNNEL_IDENTIFIER"], "/f", readyfile]
else:
cmd = ["sc", "-i", os.environ["TUNNEL_IDENTIFIER"], "-f", readyfile]
sc_process = execute_command_background(cmd)
wait_start = time.time()
while not os.path.exists(readyfile):
if time.time() - wait_start > 30:
raise BuildkiteException(
"Sauce Connect Proxy is still not ready after 30 seconds, aborting!"
)
time.sleep(1)
print("Sauce Connect Proxy is ready, continuing...")
if not test_only:
execute_bazel_build(
bazel_binary,
platform,
config.get("build_flags", []),
config.get("build_targets", None),
None,
incompatible_flags,
)
if save_but:
upload_bazel_binary(platform)
if not build_only:
test_bep_file = os.path.join(tmpdir, "test_bep.json")
try:
execute_bazel_test(
bazel_binary,
platform,
config.get("test_flags", []),
config.get("test_targets", None),
test_bep_file,
monitor_flaky_tests,
incompatible_flags,
)
if has_flaky_tests(test_bep_file):
if monitor_flaky_tests:
# Upload the BEP logs from Bazel builds for later analysis on flaky tests
build_id = os.getenv("BUILDKITE_BUILD_ID")
pipeline_slug = os.getenv("BUILDKITE_PIPELINE_SLUG")
execute_command(
[
gsutil_command(),
"cp",
test_bep_file,
"gs://bazel-buildkite-stats/flaky-tests-bep/"
+ pipeline_slug
+ "/"
+ build_id
+ ".json",
]
)
finally:
upload_test_logs(test_bep_file, tmpdir)
finally:
if sc_process:
sc_process.terminate()
try:
sc_process.wait(timeout=10)
except subprocess.TimeoutExpired:
sc_process.kill()
if tmpdir:
shutil.rmtree(tmpdir)
def tests_with_status(bep_file, status):
return set(label for label, _ in test_logs_for_status(bep_file, status=status))
def saucelabs_token():
return (
subprocess.check_output(
[
gcloud_command(),
"kms",
"decrypt",
"--location",
"global",
"--keyring",
"buildkite",
"--key",
"saucelabs-access-key",
"--ciphertext-file",
"-",
"--plaintext-file",
"-",
],
input=base64.b64decode(ENCRYPTED_SAUCELABS_TOKEN),
env=os.environ,
)
.decode("utf-8")
.strip()
)
def is_pull_request():
third_party_repo = os.getenv("BUILDKITE_PULL_REQUEST_REPO", "")
return len(third_party_repo) > 0
def has_flaky_tests(bep_file):
return len(test_logs_for_status(bep_file, status="FLAKY")) > 0
def print_bazel_version_info(bazel_binary, platform):
print_collapsed_group(":information_source: Bazel Info")
execute_command(
[bazel_binary]
+ common_startup_flags(platform)
+ ["--nomaster_bazelrc", "--bazelrc=/dev/null", "version"]
)
execute_command(
[bazel_binary]
+ common_startup_flags(platform)
+ ["--nomaster_bazelrc", "--bazelrc=/dev/null", "info"]
)
def print_environment_variables_info():
print_collapsed_group(":information_source: Environment Variables")
for key, value in os.environ.items():
eprint("%s=(%s)" % (key, value))
def upload_bazel_binary(platform):
print_collapsed_group(":gcloud: Uploading Bazel Under Test")
binary_path = "bazel-bin/src/bazel"
if platform == "windows":
binary_path = r"bazel-bin\src\bazel"
execute_command(["buildkite-agent", "artifact", "upload", binary_path])
def download_bazel_binary(dest_dir, platform):
host_platform = PLATFORMS[platform].get("host-platform", platform)
binary_path = "bazel-bin/src/bazel"
if platform == "windows":
binary_path = r"bazel-bin\src\bazel"
source_step = create_label(host_platform, "Bazel", build_only=True)
execute_command(
["buildkite-agent", "artifact", "download", binary_path, dest_dir, "--step", source_step]
)
bazel_binary_path = os.path.join(dest_dir, binary_path)
st = os.stat(bazel_binary_path)
os.chmod(bazel_binary_path, st.st_mode | stat.S_IEXEC)
return bazel_binary_path
def download_bazel_binary_at_commit(dest_dir, platform, bazel_git_commit):
# We only build bazel binary on ubuntu14.04 for every bazel commit.
# It should be OK to use it on other ubuntu platforms.
if "ubuntu" in PLATFORMS[platform].get("host-platform", platform):
platform = "ubuntu1404"
bazel_binary_path = os.path.join(dest_dir, "bazel.exe" if platform == "windows" else "bazel")
execute_command(
[
gsutil_command(),
"cp",
bazelci_builds_gs_url(platform, bazel_git_commit),
bazel_binary_path,
]
)
st = os.stat(bazel_binary_path)
os.chmod(bazel_binary_path, st.st_mode | stat.S_IEXEC)
return bazel_binary_path
def clone_git_repository(git_repository, platform, git_commit=None):
root = downstream_projects_root(platform)
project_name = re.search(r"/([^/]+)\.git$", git_repository).group(1)
clone_path = os.path.join(root, project_name)
print_collapsed_group(
"Fetching %s sources at %s" % (project_name, git_commit if git_commit else "HEAD")
)
if not os.path.exists(clone_path):
if platform in ["ubuntu1404", "ubuntu1604", "ubuntu1804", "rbe_ubuntu1604"]:
execute_command(
["git", "clone", "--reference", "/var/lib/bazelbuild", git_repository, clone_path]
)
elif platform in ["macos"]:
execute_command(
[
"git",
"clone",
"--reference",
"/usr/local/var/bazelbuild",
git_repository,
clone_path,
]
)
elif platform in ["windows"]:
execute_command(
[
"git",
"clone",
"--reference",
"c:\\buildkite\\bazelbuild",
git_repository,
clone_path,
]
)
else:
execute_command(["git", "clone", git_repository, clone_path])
os.chdir(clone_path)
execute_command(["git", "remote", "set-url", "origin", git_repository])
execute_command(["git", "clean", "-fdqx"])
execute_command(["git", "submodule", "foreach", "--recursive", "git", "clean", "-fdqx"])
execute_command(["git", "fetch", "origin"])
if git_commit:
# sync to a specific commit of this repository
execute_command(["git", "reset", git_commit, "--hard"])
else:
# sync to the latest commit of HEAD. Unlikely git pull this also works after a force push.
remote_head = (
subprocess.check_output(["git", "symbolic-ref", "refs/remotes/origin/HEAD"])
.decode("utf-8")
.rstrip()
)
execute_command(["git", "reset", remote_head, "--hard"])
execute_command(["git", "submodule", "sync", "--recursive"])
execute_command(["git", "submodule", "update", "--init", "--recursive", "--force"])
execute_command(["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"])
execute_command(["git", "clean", "-fdqx"])
execute_command(["git", "submodule", "foreach", "--recursive", "git", "clean", "-fdqx"])
return clone_path
def execute_batch_commands(commands):
if not commands:
return
print_collapsed_group(":batch: Setup (Batch Commands)")
batch_commands = "&".join(commands)
return subprocess.run(batch_commands, shell=True, check=True, env=os.environ).returncode
def execute_shell_commands(commands):
if not commands:
return
print_collapsed_group(":bash: Setup (Shell Commands)")
shell_command = "\n".join(commands)
execute_command([shell_command], shell=True)
def execute_bazel_run(bazel_binary, platform, targets, incompatible_flags):
if not targets:
return
print_collapsed_group("Setup (Run Targets)")
for target in targets:
execute_command(
[bazel_binary]
+ common_startup_flags(platform)
+ ["run"]
+ common_build_flags(None, platform)
+ (incompatible_flags or [])
+ [target]
)
def remote_caching_flags(platform):
if platform not in [
"ubuntu1404",
"ubuntu1604",
"ubuntu1804",
"ubuntu1804_nojava",
"ubuntu1804_java9",
"ubuntu1804_java10",
# "macos",
# "windows",
]:
return []
return [
"--google_default_credentials",
"--experimental_guard_against_concurrent_changes",
"--remote_timeout=10",
# TODO(ulfjack): figure out how to resolve
# https://github.com/bazelbuild/bazel/issues/5382 and as part of that keep
# or remove the `--disk_cache=` flag.
"--disk_cache=",
"--remote_max_connections=200",
'--experimental_remote_platform_override=properties:{name:"platform" value:"%s"}'
% platform,
"--remote_http_cache=https://storage.googleapis.com/bazel-untrusted-buildkite-cache",
]
def remote_enabled(flags):
# Detect if the project configuration enabled its own remote caching / execution.
remote_flags = ["--remote_executor", "--remote_cache", "--remote_http_cache"]
for flag in flags:
for remote_flag in remote_flags:
if flag.startswith(remote_flag):
return True
return False
def concurrent_jobs(platform):
return "75" if platform.startswith("rbe_") else str(multiprocessing.cpu_count())
def concurrent_test_jobs(platform):
if platform.startswith("rbe_"):
return "75"
elif platform == "windows":
return "8"
elif platform == "macos":
return "8"
return "12"
def common_startup_flags(platform):
return ["--output_user_root=D:/b"] if platform == "windows" else []
def common_build_flags(bep_file, platform):
flags = [
"--show_progress_rate_limit=5",
"--curses=yes",
"--color=yes",
"--verbose_failures",
"--keep_going",
"--jobs=" + concurrent_jobs(platform),
"--announce_rc",
"--experimental_multi_threaded_digest",
]
if platform != "windows":
flags += ["--sandbox_tmpfs_path=/tmp"]
if bep_file:
flags += [
"--experimental_build_event_json_file_path_conversion=false",
"--build_event_json_file=" + bep_file,
]
return flags
def rbe_flags(original_flags, accept_cached):
# Enable remote execution via RBE.
flags = [
"--remote_executor=remotebuildexecution.googleapis.com",
"--remote_instance_name=projects/bazel-untrusted/instances/default_instance",
"--remote_timeout=3600",
"--spawn_strategy=remote",
"--strategy=Javac=remote",
"--strategy=Closure=remote",
"--genrule_strategy=remote",
"--experimental_strict_action_env",
"--tls_enabled=true",
"--google_default_credentials",
]
# Enable BES / Build Results reporting.
flags += [
"--bes_backend=buildeventservice.googleapis.com",
"--bes_timeout=360s",
"--project_id=bazel-untrusted",
]
if not accept_cached:
flags += ["--noremote_accept_cached"]
# Copied from https://github.com/bazelbuild/bazel-toolchains/blob/master/configs/ubuntu16_04_clang/1.0/toolchain.bazelrc
flags += [
# These should NOT be modified before @bazel_toolchains repo pin is
# updated in projects' WORKSPACE files.
#
# Toolchain related flags to append at the end of your .bazelrc file.
"--host_javabase=@bazel_toolchains//configs/ubuntu16_04_clang/latest:javabase",
"--javabase=@bazel_toolchains//configs/ubuntu16_04_clang/latest:javabase",
"--host_java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8",
"--java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8",
"--crosstool_top=@bazel_toolchains//configs/ubuntu16_04_clang/latest:crosstool_top_default",
"--action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1",
]
# Platform flags:
# The toolchain container used for execution is defined in the target indicated
# by "extra_execution_platforms", "host_platform" and "platforms".
# If you are using your own toolchain container, you need to create a platform
# target with "constraint_values" that allow for the toolchain specified with
# "extra_toolchains" to be selected (given constraints defined in
# "exec_compatible_with").
# More about platforms: https://docs.bazel.build/versions/master/platforms.html
# Don't add platform flags if they are specified already.
platform_flags = {
"--extra_toolchains": "@bazel_toolchains//configs/ubuntu16_04_clang/latest:toolchain_default",
"--extra_execution_platforms": "@bazel_toolchains//configs/ubuntu16_04_clang/latest:platform",
"--host_platform": "@bazel_toolchains//configs/ubuntu16_04_clang/latest:platform",
"--platforms": "@bazel_toolchains//configs/ubuntu16_04_clang/latest:platform",
}
for platform_flag, value in list(platform_flags.items()):
found = False
for original_flag in original_flags:
if original_flag.startswith(platform_flag):
found = True
break
if not found:
flags += [platform_flag + "=" + value]
return flags
def compute_flags(platform, flags, incompatible_flags, bep_file, enable_remote_cache=False):
aggregated_flags = common_build_flags(bep_file, platform)
if not remote_enabled(flags):
if platform.startswith("rbe_"):
aggregated_flags += rbe_flags(flags, accept_cached=enable_remote_cache)
elif enable_remote_cache:
aggregated_flags += remote_caching_flags(platform)
aggregated_flags += flags
if incompatible_flags:
aggregated_flags += incompatible_flags
return aggregated_flags
def execute_bazel_build(bazel_binary, platform, flags, targets, bep_file, incompatible_flags):
print_expanded_group(":bazel: Build")
aggregated_flags = compute_flags(
platform, flags, incompatible_flags, bep_file, enable_remote_cache=True
)
try:
execute_command(
[bazel_binary] + common_startup_flags(platform) + ["build"] + aggregated_flags + targets
)
except subprocess.CalledProcessError as e:
raise BuildkiteException("bazel build failed with exit code {}".format(e.returncode))
def execute_bazel_test(
bazel_binary, platform, flags, targets, bep_file, monitor_flaky_tests, incompatible_flags
):
print_expanded_group(":bazel: Test")
aggregated_flags = [
"--flaky_test_attempts=3",
"--build_tests_only",
"--local_test_jobs=" + concurrent_test_jobs(platform),
]
# Don't enable remote caching if the user enabled remote execution / caching themselves
# or flaky test monitoring is enabled, as remote caching makes tests look less flaky than
# they are.
aggregated_flags += compute_flags(
platform, flags, incompatible_flags, bep_file, enable_remote_cache=not monitor_flaky_tests
)
try:
execute_command(
[bazel_binary] + common_startup_flags(platform) + ["test"] + aggregated_flags + targets
)
except subprocess.CalledProcessError as e:
raise BuildkiteException("bazel test failed with exit code {}".format(e.returncode))
def upload_test_logs(bep_file, tmpdir):
if not os.path.exists(bep_file):
return
test_logs = test_logs_to_upload(bep_file, tmpdir)
if test_logs:
cwd = os.getcwd()
try:
os.chdir(tmpdir)
test_logs = [os.path.relpath(test_log, tmpdir) for test_log in test_logs]
test_logs = sorted(test_logs)
print_collapsed_group(":gcloud: Uploading Test Logs")
execute_command(["buildkite-agent", "artifact", "upload", ";".join(test_logs)])
finally:
os.chdir(cwd)
def test_logs_to_upload(bep_file, tmpdir):
failed = test_logs_for_status(bep_file, status="FAILED")
timed_out = test_logs_for_status(bep_file, status="TIMEOUT")
flaky = test_logs_for_status(bep_file, status="FLAKY")
# Rename the test.log files to the target that created them
# so that it's easy to associate test.log and target.
new_paths = []
for label, test_logs in failed + timed_out + flaky:
attempt = 0
if len(test_logs) > 1:
attempt = 1
for test_log in test_logs:
try:
new_path = test_label_to_path(tmpdir, label, attempt)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
copyfile(test_log, new_path)
new_paths.append(new_path)
attempt += 1
except IOError as err:
# Log error and ignore.
eprint(err)
return new_paths
def test_label_to_path(tmpdir, label, attempt):
# remove leading //
path = label[2:]
path = path.replace("/", os.sep)
path = path.replace(":", os.sep)
if attempt == 0:
path = os.path.join(path, "test.log")
else:
path = os.path.join(path, "attempt_" + str(attempt) + ".log")
return os.path.join(tmpdir, path)
def test_logs_for_status(bep_file, status):
targets = []
raw_data = ""
with open(bep_file, encoding="utf-8") as f:
raw_data = f.read()
decoder = json.JSONDecoder()
pos = 0
while pos < len(raw_data):
bep_obj, size = decoder.raw_decode(raw_data[pos:])
if "testSummary" in bep_obj:
test_target = bep_obj["id"]["testSummary"]["label"]
test_status = bep_obj["testSummary"]["overallStatus"]
if test_status == status:
outputs = bep_obj["testSummary"]["failed"]
test_logs = []
for output in outputs:
test_logs.append(url2pathname(urlparse(output["uri"]).path))
targets.append((test_target, test_logs))
pos += size + 1
return targets
def execute_command(args, shell=False, fail_if_nonzero=True):
eprint(" ".join(args))
return subprocess.run(args, shell=shell, check=fail_if_nonzero, env=os.environ).returncode
def execute_command_background(args):
eprint(" ".join(args))
return subprocess.Popen(args, env=os.environ)
def create_step(label, commands, platform=DEFAULT_PLATFORM):
host_platform = PLATFORMS[platform].get("host-platform", platform)
if "docker-image" in PLATFORMS[platform]:
return {
"label": label,
"command": commands,
"agents": {"kind": "docker", "os": "linux"},
"plugins": {
"philwo/docker": {
"always-pull": True,
"debug": True,
"environment": ["BUILDKITE_ARTIFACT_UPLOAD_DESTINATION", "BUILDKITE_GS_ACL"],
"image": PLATFORMS[platform]["docker-image"],
"privileged": True,
"propagate-environment": True,
"tmpfs": ["/home/bazel/.cache:exec,uid=999,gid=999"],
}
},
}
else:
return {
"label": label,
"command": commands,
"agents": {
"kind": "worker",
"java": PLATFORMS[platform]["java"],
"os": rchop(host_platform, "_nojava", "_java8", "_java9", "_java10"),
},
}
def print_project_pipeline(
platform_configs,
project_name,
http_config,
file_config,
git_repository,
monitor_flaky_tests,
use_but,
incompatible_flags,
):
if not platform_configs:
raise BuildkiteException("{0} pipeline configuration is empty.".format(project_name))
pipeline_steps = []
# In Bazel Downstream Project pipelines, git_repository and project_name must be specified,
# and we should test the project at the last green commit.
git_commit = None
if (use_but or incompatible_flags) and git_repository and project_name:
git_commit = get_last_green_commit(
git_repository, DOWNSTREAM_PROJECTS[project_name]["pipeline_slug"]
)
for platform in platform_configs:
step = runner_step(
platform,
project_name,
http_config,
file_config,
git_repository,
git_commit,
monitor_flaky_tests,
use_but,
incompatible_flags,
)
pipeline_steps.append(step)
pipeline_slug = os.getenv("BUILDKITE_PIPELINE_SLUG")
all_downstream_pipeline_slugs = []
for _, config in DOWNSTREAM_PROJECTS.items():
all_downstream_pipeline_slugs.append(config["pipeline_slug"])
# We don't need to update last green commit in the following cases:
# 1. This job is a github pull request
# 2. This job uses a custom built Bazel binary (In Bazel Downstream Projects pipeline)
# 3. This job doesn't run on master branch (Could be a custom build launched manually)
# 4. We don't intend to run the same job in downstream with Bazel@HEAD (eg. google-bazel-presubmit)
# 5. We are testing incompatible flags
if not (
is_pull_request()
or use_but
or os.getenv("BUILDKITE_BRANCH") != "master"
or pipeline_slug not in all_downstream_pipeline_slugs
or incompatible_flags
):
pipeline_steps.append("wait")
# If all builds succeed, update the last green commit of this project
pipeline_steps.append(
create_step(
label="Try Update Last Green Commit",
commands=[
fetch_bazelcipy_command(),
python_binary() + " bazelci.py try_update_last_green_commit",
],
)
)
print(yaml.dump({"steps": pipeline_steps}))
def runner_step(
platform,
project_name=None,
http_config=None,
file_config=None,
git_repository=None,
git_commit=None,
monitor_flaky_tests=False,
use_but=False,
incompatible_flags=None,
):
host_platform = PLATFORMS[platform].get("host-platform", platform)
command = python_binary(host_platform) + " bazelci.py runner --platform=" + platform
if http_config:
command += " --http_config=" + http_config
if file_config:
command += " --file_config=" + file_config
if git_repository:
command += " --git_repository=" + git_repository
if git_commit:
command += " --git_commit=" + git_commit
if monitor_flaky_tests:
command += " --monitor_flaky_tests"
if use_but:
command += " --use_but"
for flag in incompatible_flags or []:
command += " --incompatible_flag=" + flag
label = create_label(platform, project_name)
return create_step(
label=label, commands=[fetch_bazelcipy_command(), command], platform=platform
)
def fetch_bazelcipy_command():
return "curl -s {0} -o bazelci.py".format(bazelcipy_url())
def fetch_incompatible_flag_verbose_failures_command():
return "curl -s {0} -o incompatible_flag_verbose_failures.py".format(
incompatible_flag_verbose_failures_url()
)
def upload_project_pipeline_step(
project_name, git_repository, http_config, file_config, incompatible_flags
):
pipeline_command = (
'{0} bazelci.py project_pipeline --project_name="{1}" ' + "--git_repository={2}"
).format(python_binary(), project_name, git_repository)
if incompatible_flags is None:
pipeline_command += " --use_but"
else:
for flag in incompatible_flags:
pipeline_command += " --incompatible_flag=" + flag
if http_config:
pipeline_command += " --http_config=" + http_config
if file_config:
pipeline_command += " --file_config=" + file_config
pipeline_command += " | buildkite-agent pipeline upload"
return create_step(
label="Setup {0}".format(project_name),
commands=[fetch_bazelcipy_command(), pipeline_command],
)
def create_label(platform, project_name, build_only=False, test_only=False):
if build_only and test_only:
raise BuildkiteException("build_only and test_only cannot be true at the same time")
platform_name = PLATFORMS[platform]["emoji-name"]
if build_only:
label = "Build "
elif test_only:
label = "Test "
else:
label = ""
if project_name:
label += "{0} ({1})".format(project_name, platform_name)
else:
label += platform_name
return label
def bazel_build_step(
platform, project_name, http_config=None, file_config=None, build_only=False, test_only=False
):
host_platform = PLATFORMS[platform].get("host-platform", platform)
pipeline_command = python_binary(host_platform) + " bazelci.py runner"
if build_only:
pipeline_command += " --build_only"
if "host-platform" not in PLATFORMS[platform]:
pipeline_command += " --save_but"
if test_only:
pipeline_command += " --test_only"
if http_config:
pipeline_command += " --http_config=" + http_config
if file_config:
pipeline_command += " --file_config=" + file_config
pipeline_command += " --platform=" + platform
return create_step(
label=create_label(platform, project_name, build_only, test_only),
commands=[fetch_bazelcipy_command(), pipeline_command],
platform=platform,
)
def print_bazel_publish_binaries_pipeline(configs, http_config, file_config):
if not configs:
raise BuildkiteException("Bazel publish binaries pipeline configuration is empty.")
for platform in configs.copy():
if platform not in PLATFORMS:
raise BuildkiteException("Unknown platform '{}'".format(platform))
if not PLATFORMS[platform]["publish_binary"]:
del configs[platform]
if set(configs) != set(
name for name, platform in PLATFORMS.items() if platform["publish_binary"]
):
raise BuildkiteException(
"Bazel publish binaries pipeline needs to build Bazel for every commit on all publish_binary-enabled platforms."
)
# Build Bazel
pipeline_steps = []
for platform in configs:
pipeline_steps.append(
bazel_build_step(platform, "Bazel", http_config, file_config, build_only=True)
)
pipeline_steps.append("wait")
# If all builds succeed, publish the Bazel binaries to GCS.
pipeline_steps.append(
create_step(
label="Publish Bazel Binaries",
commands=[fetch_bazelcipy_command(), python_binary() + " bazelci.py publish_binaries"],
)
)
print(yaml.dump({"steps": pipeline_steps}))
def print_disabled_projects_info_box_step():
info_text = ["Downstream testing is disabled for the following projects :sadpanda:"]
for project, config in DOWNSTREAM_PROJECTS.items():
disabled_reason = config.get("disabled_reason", None)
if disabled_reason:
info_text.append("* **%s**: %s" % (project, disabled_reason))
if len(info_text) == 1:
return None
return create_step(
label=":sadpanda:",
commands=[
'buildkite-agent annotate --append --style=info "\n' + "\n".join(info_text) + '\n"'
],
)
def print_incompatible_flags_info_box_step(incompatible_flags_map):
info_text = ["Build and test with the following incompatible flags:"]
for flag in incompatible_flags_map:
info_text.append("* **%s**: %s" % (flag, incompatible_flags_map[flag]))
if len(info_text) == 1:
return None
return create_step(
label="Incompatible flags info",
commands=[
'buildkite-agent annotate --append --style=info "\n' + "\n".join(info_text) + '\n"'
],
)
def fetch_incompatible_flags_from_github():
"""
Return a list of incompatible flags to be tested in downstream with the current release Bazel
"""
# Get bazel major version on CI, eg. 0.21 from "Build label: 0.21.0\n..."
output = subprocess.check_output(
["bazel", "--nomaster_bazelrc", "--bazelrc=/dev/null", "version"]
).decode("utf-8")
bazel_major_version = output.split()[2].rsplit(".", 1)[0]
output = subprocess.check_output(
[
"curl",
"https://api.github.com/search/issues?q=repo:bazelbuild/bazel+label:migration-%s+state:open"
% bazel_major_version,
]
).decode("utf-8")
issue_info = json.loads(output)
incompatible_flags = {}
for issue in issue_info["items"]:
# Every incompatible flags issue should start with "<incompatible flag name (without --)>:"
name = "--" + issue["title"].split(":")[0]
url = issue["html_url"]
if name.startswith("--incompatible_"):
incompatible_flags[name] = url
else:
eprint(
f"{name} is not recognized as an incompatible flag, please modify the issue title "
f'of {url} to "<incompatible flag name (without --)>:..."'
)
return incompatible_flags
def print_bazel_downstream_pipeline(
configs, http_config, file_config, test_incompatible_flags, test_disabled_projects
):
if not configs:
raise BuildkiteException("Bazel downstream pipeline configuration is empty.")
if set(configs) != set(PLATFORMS):
raise BuildkiteException(
"Bazel downstream pipeline needs to build Bazel on all supported platforms (has=%s vs. want=%s)."
% (sorted(set(configs)), sorted(set(PLATFORMS)))
)
pipeline_steps = []
info_box_step = print_disabled_projects_info_box_step()
if info_box_step is not None:
pipeline_steps.append(info_box_step)
for platform in configs:
pipeline_steps.append(
bazel_build_step(platform, "Bazel", http_config, file_config, build_only=True)
)
pipeline_steps.append("wait")
incompatible_flags = None
if test_incompatible_flags:
incompatible_flags_map = fetch_incompatible_flags_from_github()
info_box_step = print_incompatible_flags_info_box_step(incompatible_flags_map)
if info_box_step is not None:
pipeline_steps.append(info_box_step)
incompatible_flags = list(incompatible_flags_map.keys())
for project, config in DOWNSTREAM_PROJECTS.items():
disabled_reason = config.get("disabled_reason", None)
# If test_disabled_projects is true, we add configs for disabled projects.
# If test_disabled_projects is false, we add configs for not disbaled projects.
if (test_disabled_projects and disabled_reason) or (
not test_disabled_projects and not disabled_reason
):
pipeline_steps.append(
upload_project_pipeline_step(
project_name=project,
git_repository=config["git_repository"],
http_config=config.get("http_config", None),
file_config=config.get("file_config", None),
incompatible_flags=incompatible_flags,
)
)
if test_incompatible_flags:
pipeline_steps.append({"wait": "~", "continue_on_failure": "true"})
current_build_number = os.environ.get("BUILDKITE_BUILD_NUMBER", None)
if not current_build_number:
raise BuildkiteException("Not running inside Buildkite")
pipeline_steps.append(
create_step(
label="Test failing jobs with incompatible flag separately",
commands=[
fetch_bazelcipy_command(),
fetch_incompatible_flag_verbose_failures_command(),
python_binary()
+ " incompatible_flag_verbose_failures.py --build_number=%s | buildkite-agent pipeline upload"
% current_build_number,
],
)
)
print(yaml.dump({"steps": pipeline_steps}))
def bazelci_builds_download_url(platform, git_commit):
return "https://storage.googleapis.com/bazel-builds/artifacts/{0}/{1}/bazel".format(
platform, git_commit
)
def bazelci_builds_gs_url(platform, git_commit):
return "gs://bazel-builds/artifacts/{0}/{1}/bazel".format(platform, git_commit)
def bazelci_builds_metadata_url():
return "gs://bazel-builds/metadata/latest.json"
def bazelci_last_green_commit_url(git_repository, pipeline_slug):
return "gs://bazel-builds/last_green_commit/%s/%s" % (
git_repository[len("https://") :],
pipeline_slug,
)
def get_last_green_commit(git_repository, pipeline_slug):
last_green_commit_url = bazelci_last_green_commit_url(git_repository, pipeline_slug)
try:
return (
subprocess.check_output(
[gsutil_command(), "cat", last_green_commit_url], env=os.environ
)
.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError:
return None
def try_update_last_green_commit():
pipeline_slug = os.getenv("BUILDKITE_PIPELINE_SLUG")
git_repository = os.getenv("BUILDKITE_REPO")
last_green_commit = get_last_green_commit(git_repository, pipeline_slug)
current_commit = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("utf-8").strip()
if last_green_commit:
result = (
subprocess.check_output(
["git", "rev-list", "%s..%s" % (last_green_commit, current_commit)]
)
.decode("utf-8")
.strip()
)
# If current_commit is newer that last_green_commit, `git rev-list A..B` will output a bunch of
# commits, otherwise the output should be empty.
if not last_green_commit or result:
execute_command(
[
"echo %s | %s cp - %s"
% (
current_commit,
gsutil_command(),
bazelci_last_green_commit_url(git_repository, pipeline_slug),
)
],
shell=True,
)
else:
eprint(
"Updating abandoned: last green commit (%s) is not older than current commit (%s)."
% (last_green_commit, current_commit)
)
def latest_generation_and_build_number():
output = None
attempt = 0
while attempt < 5:
output = subprocess.check_output(
[gsutil_command(), "stat", bazelci_builds_metadata_url()], env=os.environ
)
match = re.search("Generation:[ ]*([0-9]+)", output.decode("utf-8"))
if not match:
raise BuildkiteException("Couldn't parse generation. gsutil output format changed?")
generation = match.group(1)
match = re.search(r"Hash \(md5\):[ ]*([^\s]+)", output.decode("utf-8"))
if not match:
raise BuildkiteException("Couldn't parse md5 hash. gsutil output format changed?")
expected_md5hash = base64.b64decode(match.group(1))
output = subprocess.check_output(
[gsutil_command(), "cat", bazelci_builds_metadata_url()], env=os.environ
)
hasher = hashlib.md5()
hasher.update(output)
actual_md5hash = hasher.digest()
if expected_md5hash == actual_md5hash:
break
attempt += 1
info = json.loads(output.decode("utf-8"))
return (generation, info["build_number"])
def sha256_hexdigest(filename):
sha256 = hashlib.sha256()
with open(filename, "rb") as f:
for block in iter(lambda: f.read(65536), b""):
sha256.update(block)
return sha256.hexdigest()
def try_publish_binaries(build_number, expected_generation):
now = datetime.datetime.now()
git_commit = os.environ["BUILDKITE_COMMIT"]
info = {
"build_number": build_number,
"build_time": now.strftime("%d-%m-%Y %H:%M"),
"git_commit": git_commit,
"platforms": {},
}
for platform in (name for name in PLATFORMS if PLATFORMS[name]["publish_binary"]):
tmpdir = tempfile.mkdtemp()
try:
bazel_binary_path = download_bazel_binary(tmpdir, platform)
execute_command(
[
gsutil_command(),
"cp",
"-a",
"public-read",
bazel_binary_path,
bazelci_builds_gs_url(platform, git_commit),
]
)
info["platforms"][platform] = {
"url": bazelci_builds_download_url(platform, git_commit),
"sha256": sha256_hexdigest(bazel_binary_path),
}
finally:
shutil.rmtree(tmpdir)
tmpdir = tempfile.mkdtemp()
try:
info_file = os.path.join(tmpdir, "info.json")
with open(info_file, mode="w", encoding="utf-8") as fp:
json.dump(info, fp, indent=2, sort_keys=True)
try:
execute_command(
[
gsutil_command(),
"-h",
"x-goog-if-generation-match:" + expected_generation,
"-h",
"Content-Type:application/json",
"cp",
"-a",
"public-read",
info_file,
bazelci_builds_metadata_url(),
]
)
except subprocess.CalledProcessError:
raise BinaryUploadRaceException()
finally:
shutil.rmtree(tmpdir)
def publish_binaries():
"""
Publish Bazel binaries to GCS.
"""
current_build_number = os.environ.get("BUILDKITE_BUILD_NUMBER", None)
if not current_build_number:
raise BuildkiteException("Not running inside Buildkite")
current_build_number = int(current_build_number)
for _ in range(5):
latest_generation, latest_build_number = latest_generation_and_build_number()
if current_build_number <= latest_build_number:
eprint(
(
"Current build '{0}' is not newer than latest published '{1}'. "
+ "Skipping publishing of binaries."
).format(current_build_number, latest_build_number)
)
break
try:
try_publish_binaries(current_build_number, latest_generation)
except BinaryUploadRaceException:
# Retry.
continue
eprint(
"Successfully updated '{0}' to binaries from build {1}.".format(
bazelci_builds_metadata_url(), current_build_number
)
)
break
else:
raise BuildkiteException("Could not publish binaries, ran out of attempts.")
# This is so that multiline python strings are represented as YAML
# block strings.
def str_presenter(dumper, data):
if len(data.splitlines()) > 1: # check for multiline string
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
yaml.add_representer(str, str_presenter)
parser = argparse.ArgumentParser(description="Bazel Continuous Integration Script")
subparsers = parser.add_subparsers(dest="subparsers_name")
bazel_publish_binaries_pipeline = subparsers.add_parser("bazel_publish_binaries_pipeline")
bazel_publish_binaries_pipeline.add_argument("--file_config", type=str)
bazel_publish_binaries_pipeline.add_argument("--http_config", type=str)
bazel_publish_binaries_pipeline.add_argument("--git_repository", type=str)
bazel_downstream_pipeline = subparsers.add_parser("bazel_downstream_pipeline")
bazel_downstream_pipeline.add_argument("--file_config", type=str)
bazel_downstream_pipeline.add_argument("--http_config", type=str)
bazel_downstream_pipeline.add_argument("--git_repository", type=str)
bazel_downstream_pipeline.add_argument(
"--test_incompatible_flags", type=bool, nargs="?", const=True
)
bazel_downstream_pipeline.add_argument(
"--test_disabled_projects", type=bool, nargs="?", const=True
)
project_pipeline = subparsers.add_parser("project_pipeline")
project_pipeline.add_argument("--project_name", type=str)
project_pipeline.add_argument("--file_config", type=str)
project_pipeline.add_argument("--http_config", type=str)
project_pipeline.add_argument("--git_repository", type=str)
project_pipeline.add_argument("--monitor_flaky_tests", type=bool, nargs="?", const=True)
project_pipeline.add_argument("--use_but", type=bool, nargs="?", const=True)
project_pipeline.add_argument("--incompatible_flag", type=str, action="append")
runner = subparsers.add_parser("runner")
runner.add_argument("--platform", action="store", choices=list(PLATFORMS))
runner.add_argument("--file_config", type=str)
runner.add_argument("--http_config", type=str)
runner.add_argument("--git_repository", type=str)
runner.add_argument(
"--git_commit", type=str, help="Reset the git repository to this commit after cloning it"
)
runner.add_argument(
"--git_repo_location",
type=str,
help="Use an existing repository instead of cloning from github",
)
runner.add_argument(
"--use_bazel_at_commit", type=str, help="Use Bazel binariy built at a specifc commit"
)
runner.add_argument("--use_but", type=bool, nargs="?", const=True)
runner.add_argument("--save_but", type=bool, nargs="?", const=True)
runner.add_argument("--build_only", type=bool, nargs="?", const=True)
runner.add_argument("--test_only", type=bool, nargs="?", const=True)
runner.add_argument("--monitor_flaky_tests", type=bool, nargs="?", const=True)
runner.add_argument("--incompatible_flag", type=str, action="append")
runner = subparsers.add_parser("publish_binaries")
runner = subparsers.add_parser("try_update_last_green_commit")
args = parser.parse_args(argv)
try:
if args.subparsers_name == "bazel_publish_binaries_pipeline":
configs = fetch_configs(args.http_config, args.file_config)
print_bazel_publish_binaries_pipeline(
configs=configs.get("platforms", None),
http_config=args.http_config,
file_config=args.file_config,
)
elif args.subparsers_name == "bazel_downstream_pipeline":
configs = fetch_configs(args.http_config, args.file_config)
print_bazel_downstream_pipeline(
configs=configs.get("platforms", None),
http_config=args.http_config,
file_config=args.file_config,
test_incompatible_flags=args.test_incompatible_flags,
test_disabled_projects=args.test_disabled_projects,
)
elif args.subparsers_name == "project_pipeline":
configs = fetch_configs(args.http_config, args.file_config)
print_project_pipeline(
platform_configs=configs.get("platforms", None),
project_name=args.project_name,
http_config=args.http_config,
file_config=args.file_config,
git_repository=args.git_repository,
monitor_flaky_tests=args.monitor_flaky_tests,
use_but=args.use_but,
incompatible_flags=args.incompatible_flag,
)
elif args.subparsers_name == "runner":
configs = fetch_configs(args.http_config, args.file_config)
execute_commands(
config=configs.get("platforms", None)[args.platform],
platform=args.platform,
git_repository=args.git_repository,
git_commit=args.git_commit,
git_repo_location=args.git_repo_location,
use_bazel_at_commit=args.use_bazel_at_commit,
use_but=args.use_but,
save_but=args.save_but,
build_only=args.build_only,
test_only=args.test_only,
monitor_flaky_tests=args.monitor_flaky_tests,
incompatible_flags=args.incompatible_flag,
)
elif args.subparsers_name == "publish_binaries":
publish_binaries()
elif args.subparsers_name == "try_update_last_green_commit":
try_update_last_green_commit()
else:
parser.print_help()
return 2
except BuildkiteException as e:
eprint(str(e))
return 1
return 0
if __name__ == "__main__":
sys.exit(main())