aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/jenkins-gerrit/comment_generate.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/jenkins-gerrit/comment_generate.py')
-rwxr-xr-xscripts/jenkins-gerrit/comment_generate.py260
1 files changed, 260 insertions, 0 deletions
diff --git a/scripts/jenkins-gerrit/comment_generate.py b/scripts/jenkins-gerrit/comment_generate.py
new file mode 100755
index 0000000..e127c4c
--- /dev/null
+++ b/scripts/jenkins-gerrit/comment_generate.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright 2022 sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+import argparse
+import io
+import json
+import re
+import urllib.request
+
+jenkins_url = "https://jenkins.osmocom.org"
+re_start_build = re.compile("Starting building: gerrit-[a-zA-Z-_0-9]* #[0-9]*")
+re_result = re.compile("^pipeline_([a-zA-Z-_0-9:]*): (SUCCESS|FAILED)$")
+re_job_type = re.compile("JOB_TYPE=([a-zA-Z-_0-9]*),")
+re_distro = re.compile("Building binary packages for distro: '([a-zA-Z0-9:].*)'")
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ description="Prepare a comment to be submitted to gerrit. Depending on"
+ " the comment type, (start) either a link to the pipeline,"
+ " or (result) a summary of failed / successful builds from"
+ " the pipeline we run for patches submitted to gerrit.")
+ parser.add_argument("build_url",
+ help="$BUILD_URL of the pipeline job, e.g."
+ " https://jenkins.osmocom.org/jenkins/job/gerrit-osmo-bsc-nat/17/")
+ parser.add_argument("-o", "--output", help="output json file")
+ parser.add_argument("-t", "--type", help="comment type",
+ choices=["start", "result"], required=True)
+ parser.add_argument("-n", "--notify-on-success", action="store_true",
+ help="always indicate in json that the owner should be"
+ " notified via mail, not only on failure")
+ return parser.parse_args()
+
+
+def stage_binpkgs_from_url(job_url):
+ """ Multiple gerrit-binpkgs jobs may be started to build binary packages
+ for multiple distributions. It is not clear from the job name / URL of
+ a job for which distro it is building, so read it from the log output.
+ :returns: a distro like "debian:12" """
+ global re_distro
+
+ url = f"{job_url}/consoleText"
+ with urllib.request.urlopen(url) as response:
+ content = response.read().decode("utf-8")
+ match = re_distro.search(content)
+ assert match, f"couldn't find distro name in log: {url}"
+ return match.group(1)
+
+
+def stage_from_job_name(job_name, job_url):
+ if job_name == "gerrit-verifications-comment":
+ # The job that runs this script. Don't include it in the summary.
+ return None
+ if job_name == "gerrit-lint":
+ return "lint"
+ if job_name == "gerrit-binpkgs":
+ return stage_binpkgs_from_url(job_url)
+ if job_name == "gerrit-pipeline-endianness":
+ return "endianness"
+ if job_name.endswith("-build"):
+ return "build"
+ assert False, f"couldn't figure out stage from job_name: {job_name}"
+
+
+def parse_pipeline(build_url):
+ """ Parse started jobs and result from the pipeline log.
+ :returns: a dict that looks like:
+ {"build": {"name": "gerrit-osmo-bsc-nat-build", id=7,
+ "passed": True, "url": "https://..."},
+ "lint": {...},
+ "deb": {...},
+ "rpm: {...}} """
+ global re_start_build
+ global re_result
+ global jenkins_url
+ ret = {}
+
+ url = f"{build_url}/consoleText"
+ with urllib.request.urlopen(url) as response:
+ for line in io.TextIOWrapper(response, encoding='utf-8'):
+ # Parse start build lines
+ for match in re_start_build.findall(line):
+ job_name = match.split(" ")[2]
+ job_id = int(match.split(" ")[3].replace("#", ""))
+ job_url = f"{jenkins_url}/jenkins/job/{job_name}/{job_id}"
+ stage = stage_from_job_name(job_name, job_url)
+ if stage:
+ ret[stage] = {"url": job_url, "name": job_name, "id": job_id}
+
+ # Parse result lines
+ match = re_result.match(line)
+ if match:
+ stage = match.group(1)
+ if stage.startswith("comment_"):
+ # Jobs that run this script, not relevant for summary
+ continue
+ assert stage in ret, f"found result for stage {stage}, but" \
+ " didn't find where it was started. The" \
+ " re_start_build regex probably needs to be adjusted" \
+ " to match the related gerrit-*-build job.\n\n" \
+ f"ret: {ret}"
+ ret[stage]["passed"] = (match.group(2) == "SUCCESS")
+
+ return ret
+
+
+def parse_build_matrix(job):
+ """ Parse started jobs and result from the matrix of the build job. Usually
+ it is only one job, but for some projects we build for multiple arches
+ (x86_64, arm) or build multiple times with different configure flags.
+ :param job: "build" dict from parse_pipeline()
+ :returns: a list of jobs in the matrix, looks like:
+ [{"passed": True, "url": "https://..."}, ...]
+ """
+ global jenkins_url
+
+ ret = []
+ url = f"{job['url']}/consoleFull"
+ with urllib.request.urlopen(url) as response:
+ for line in io.TextIOWrapper(response, encoding='utf-8'):
+ if " completed with result " in line:
+ url = line.split("<a href='", 1)[1].split("'", 1)[0]
+ url = f"{jenkins_url}{url}{job['id']}"
+ result = line.split(" completed with result ")[1].rstrip()
+ passed = result == "SUCCESS"
+ ret += [{"passed": passed, "url": url}]
+ return ret
+
+
+def jobs_for_summary(pipeline, build_matrix):
+ """ Sort the jobs from pipeline and build matrix into passed/failed lists.
+ :returns: a dict that looks like:
+ {"passed": [{"stage": "build", "url": "https://..."}, ...],
+ "failed": [...]} """
+ ret = {"passed": [], "failed": []}
+
+ # Build errors are most interesting, display them first
+ for job in build_matrix:
+ category = "passed" if job["passed"] else "failed"
+ ret[category] += [{"stage": "build", "url": job["url"]}]
+
+ # Hide the build matrix job (we show the jobs started by it instead), as
+ # long as there is at least one failed started job when the matrix failed
+ matrix_failed = "build" in pipeline and not pipeline["build"]["passed"]
+ show_build_matrix_job = matrix_failed and not ret["failed"]
+
+ # Add jobs from the pipeline
+ for stage, job in pipeline.items():
+ if stage == "build" and not show_build_matrix_job:
+ continue
+ category = "passed" if job["passed"] else "failed"
+ ret[category] += [{"stage": stage, "url": job["url"]}]
+
+ return ret
+
+
+def get_job_short_name(job):
+ """ :returns: a short job name, usually the stage (lint, deb, rpm, build).
+ Or in case of build a more useful name like the JOB_TYPE part
+ of the URL if it is found. For osmo-e1-hardware it could be
+ one of: manuals, gateware, firmware, software """
+ global re_job_type
+ stage = job["stage"]
+
+ if stage == "build":
+ match = re_job_type.search(job["url"])
+ if match:
+ return match.group(1)
+
+ return stage
+
+
+def get_jobs_list_str(jobs):
+ lines = []
+ for job in jobs:
+ lines += [f"* [{get_job_short_name(job)}] {job['url']}/consoleFull\n"]
+ return "".join(sorted(lines))
+
+
+def get_comment_result(build_url, notify_on_success):
+ """ Generate a summary of failed and successful builds for gerrit.
+ :returns: a dict that is expected by gerrit's set-review api, e.g.
+ {"tag": "jenkins",
+ "message": "...",
+ "labels": {"Code-Review": -1},
+ "notify": "OWNER"} """
+ summary = ""
+ pipeline = parse_pipeline(build_url)
+
+ build_matrix = []
+ if "build" in pipeline:
+ build_matrix = parse_build_matrix(pipeline["build"])
+
+ jobs = jobs_for_summary(pipeline, build_matrix)
+
+ if jobs["failed"]:
+ summary += f"{len(jobs['failed'])} failed:\n"
+ summary += get_jobs_list_str(jobs["failed"])
+ summary += "\n"
+
+ summary += f"{len(jobs['passed'])} passed:\n"
+ summary += get_jobs_list_str(jobs["passed"])
+
+ if "build" in pipeline and "deb" in pipeline and "rpm" in pipeline and \
+ not pipeline["build"]["passed"] and pipeline["deb"]["passed"] \
+ and pipeline["rpm"]["passed"]:
+ summary += "\n"
+ summary += "The build job(s) failed, but deb/rpm jobs passed.\n"
+ summary += "We don't enable external/vty tests when building\n"
+ summary += "packages, so maybe those failed. Check the logs.\n"
+
+ if "lint" in pipeline and not pipeline["lint"]["passed"]:
+ summary += "\n"
+ summary += "Please fix the linting errors. More information:\n"
+ summary += "https://osmocom.org/projects/cellular-infrastructure/wiki/Linting\n"
+
+ summary += "\n"
+ if jobs["failed"]:
+ summary += "Build Failed\n"
+ summary += "\n"
+ summary += f"Find the Retrigger button here:\n{build_url}\n"
+ vote = -1
+ notify = "OWNER"
+ else:
+ summary += "Build Successful\n"
+ vote = 1
+ notify = "OWNER" if notify_on_success else "NONE"
+
+ # Reference:
+ # https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#set-review
+ # https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-input
+ return {"tag": "jenkins",
+ "message": summary,
+ "labels": {"Verified": vote},
+ "notify": notify}
+
+
+def get_comment_start(build_url):
+ return {"tag": "jenkins",
+ "message": f"Build Started\n{build_url}consoleFull",
+ "notify": "NONE"}
+
+
+def main():
+ args = parse_args()
+ if args.type == "result":
+ comment = get_comment_result(args.build_url, args.notify_on_success)
+ else:
+ comment = get_comment_start(args.build_url)
+
+ print()
+ print(comment["message"])
+ print(f"notify: {comment['notify']}")
+
+ if args.output:
+ with open(args.output, "w") as handle:
+ json.dump(comment, handle, indent=4)
+
+if __name__ == "__main__":
+ main()