add ci pipeline scripts
diff --git a/buildkite/pipelines/common.py b/buildkite/pipelines/common.py
new file mode 100644
index 0000000..805281e
--- /dev/null
+++ b/buildkite/pipelines/common.py
@@ -0,0 +1,19 @@
+import json
+import urllib.request
+
+OUTPUT_DIRECTORY = ".bazelci_outputs/"
+BEP_OUTPUT_FILENAME = OUTPUT_DIRECTORY + "test.json"
+
+def supported_platforms():
+ return {
+ "ubuntu1404" : "Ubuntu 14.04",
+ "ubuntu1604" : "Ubuntu 16.04",
+ "macos" : "macOS"
+ }
+
+def fetch_configs(http_config):
+ if http_config is None:
+ with open(".bazelci/config.json", "r") as fd:
+ return json.load(fd)
+ with urllib.request.urlopen(http_config) as resp:
+ return json.loads(resp.read())
\ No newline at end of file
diff --git a/buildkite/pipelines/generate_pipeline.py b/buildkite/pipelines/generate_pipeline.py
new file mode 100644
index 0000000..bfa91c8
--- /dev/null
+++ b/buildkite/pipelines/generate_pipeline.py
@@ -0,0 +1,46 @@
+import argparse
+from common import supported_platforms
+from common import fetch_configs
+
+def generate_pipeline(configs, http_config):
+ if not configs:
+ print("The CI config is empty.")
+ exit(1)
+ pipeline_steps = []
+ for platform, config in configs.items():
+ if platform not in supported_platforms():
+ print("'{0}' is not a supported platform on Bazel CI".format(platform))
+ exit(1)
+ pipeline_steps.append(command_step(supported_platforms()[platform], platform, http_config))
+ if not pipeline_steps:
+ print("The CI config is empty.")
+ exit(1)
+ write_pipeline_file(pipeline_steps)
+
+def write_pipeline_file(steps):
+ print("steps:")
+ for step in steps:
+ print(step)
+
+def command_step(label, platform, http_config):
+ return """
+ - label: \"{0}\"
+ command: \"{1}\"
+ agents:
+ - \"os={2}\"""".format(label, "{0} --platform={1} {2} ".format(runner_command(platform), platform, http_config_flag(http_config)), platform)
+
+def runner_command(platform):
+ return "python3 runner.py"
+
+def http_config_flag(http_config):
+ if http_config is not None:
+ return "--http_config=" + http_config
+ return ""
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='Bazel Continous Integration Pipeline Generator')
+ parser.add_argument("--http_config", type=str, help="The URL of the config file. Optional.")
+ args = parser.parse_args()
+
+ configs = fetch_configs(args.http_config)
+ generate_pipeline(configs.get("platforms", None), args.http_config)
diff --git a/buildkite/pipelines/runner.py b/buildkite/pipelines/runner.py
new file mode 100644
index 0000000..ca70166
--- /dev/null
+++ b/buildkite/pipelines/runner.py
@@ -0,0 +1,128 @@
+import argparse
+import json
+import os.path
+import shutil
+import subprocess
+import sys
+from common import fetch_configs
+from common import OUTPUT_DIRECTORY
+from common import BEP_OUTPUT_FILENAME
+from urllib.parse import urlparse
+
+def run(config, platform, bazel_binary, git_repository):
+ try:
+ if git_repository:
+ clone_repository(git_repository)
+ cleanup(bazel_binary)
+ else:
+ cleanup(bazel_binary)
+ os.mkdir(OUTPUT_DIRECTORY)
+ shell_commands(config.get("shell_commands", None))
+ bazel_run(bazel_binary, config.get("run_targets", None))
+ bazel_build(bazel_binary, config.get("build_flags", []), config.get("build_targets", None))
+ exit_code = bazel_test(bazel_binary, config.get("test_flags", []), config.get("test_targets", None))
+ upload_failed_test_logs(BEP_OUTPUT_FILENAME)
+ if git_repository:
+ delete_repository(git_repository)
+ exit(exit_code)
+ finally:
+ cleanup(bazel_binary)
+
+def clone_repository(git_repository):
+ run_command(["git", "clone", git_repository, "downstream-repo"])
+ os.chdir("downstream-repo")
+
+def delete_repository(git_repository):
+ os.chdir("..")
+ shutil.rmtree("downstream-repo")
+
+def shell_commands(commands):
+ if not commands:
+ return
+ print("--- Shell Commands")
+ shell_command = "\n".join(commands)
+ run_command(shell_command, shell=True)
+
+def bazel_run(bazel_binary, targets):
+ if not targets:
+ return
+ print("--- Run Targets")
+ for target in targets:
+ run_command([bazel_binary, "run", target])
+
+def bazel_build(bazel_binary, flags, targets):
+ if not targets:
+ return
+ print("+++ Build")
+ run_command([bazel_binary, "build", "--color=yes"] + flags + targets)
+
+def bazel_test(bazel_binary, flags, targets):
+ if not targets:
+ return
+ print("+++ Test")
+ res = subprocess.run([bazel_binary, "test", "--color=yes", "--build_event_json_file=" + BEP_OUTPUT_FILENAME] + flags + targets)
+ return res.returncode
+
+def upload_failed_test_logs(bep_path):
+ for logfile in failed_test_logs(bep_path):
+ run_command(["buildkite-agent", "artifact", "upload", logfile])
+
+def failed_test_logs(bep_path):
+ test_logs = []
+ raw_data = ""
+ with open(bep_path) as f:
+ raw_data = f.read()
+ decoder = json.JSONDecoder()
+
+ pos = 0
+ while pos < len(raw_data):
+ json_dict, size = decoder.raw_decode(raw_data[pos:])
+ if "testResult" in json_dict:
+ test_result = json_dict["testResult"]
+ if test_result["status"] != "PASSED":
+ outputs = test_result["testActionOutput"]
+ for output in outputs:
+ if output["name"] == "test.log":
+ new_path = label_to_path(json_dict["id"]["testResult"]["label"])
+ os.makedirs(os.path.dirname(new_path), exist_ok=True)
+ copyfile(urlparse(output["uri"]).path, new_path)
+ test_logs.append(new_path)
+ pos += size + 1
+ return test_logs
+
+def label_to_path(label):
+ # remove leading //
+ path = label[2:]
+ path = path.replace(":", "/")
+ return OUTPUT_DIRECTORY + path + ".log"
+
+def cleanup(bazel_binary):
+ print("--- Cleanup")
+ if os.path.exists("WORKSPACE"):
+ run_command([bazel_binary, "clean", "--expunge"])
+ if os.path.exists(OUTPUT_DIRECTORY):
+ shutil.rmtree(OUTPUT_DIRECTORY)
+ if os.path.exists("downstream-repo"):
+ shutil.rmtree("downstream-repo")
+
+def run_command(args, shell=False):
+ print(" ".join(args))
+ res = subprocess.run(args, shell)
+ if res.returncode != 0:
+ exit(res.returncode)
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='Bazel Continous Integration Runner')
+ parser.add_argument("--platform", type=str, required=True, help="The platform the script is running on. Required.")
+ parser.add_argument("--bazel_binary", type=str, help="The path to the Bazel binary. Optional.")
+ parser.add_argument("--http_config", type=str, help="The URL of the config file. Optional.")
+ args = parser.parse_args()
+
+ configs = fetch_configs(args.http_config)
+ bazel_binary = "bazel"
+ if args.bazel_binary is not None:
+ bazel_binary = args.bazel_binary
+ git_repository = configs.get("git_repository", None)
+ if args.platform not in configs["platforms"]:
+ print("No configuration exists for '{0}'".format(args.platform))
+ run(configs["platforms"][args.platform], args.platform, bazel_binary, git_repository)
diff --git a/buildkite/pipelines/subpar-config.json b/buildkite/pipelines/subpar-config.json
new file mode 100644
index 0000000..068000c
--- /dev/null
+++ b/buildkite/pipelines/subpar-config.json
@@ -0,0 +1,8 @@
+{
+ "git_repository": "https://github.com/google/subpar.git",
+ "platforms": {
+ "ubuntu1404": {"build_targets": ["..."], "test_targets": ["..."]},
+ "ubuntu1604": {"build_targets": ["..."], "test_targets": ["..."]}
+ }
+}
+