blob: 9dd88050bf3cb468f0e5082bdf8e23fb8790216a [file] [log] [blame]
# Copyright 2013 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module containing the report stages."""
import datetime
import logging
import os
import sys
from chromite.cbuildbot import cbuildbot_alerts
from chromite.cbuildbot import cbuildbot_run
from chromite.cbuildbot import commands
from chromite.cbuildbot.stages import completion_stages
from chromite.cbuildbot.stages import generic_stages
from chromite.lib import build_target_lib
from chromite.lib import cidb
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import failures_lib
from chromite.lib import goma_lib
from chromite.lib import metadata_lib
from chromite.lib import metrics
from chromite.lib import osutils
from chromite.lib import portage_util
from chromite.lib import results_lib
from chromite.lib import retry_stats
from chromite.lib import toolchain
from chromite.lib import uri_lib
from chromite.utils import gs_urls_util
from chromite.utils import hostname_util
from chromite.utils import key_value_store
def WriteBasicMetadata(builder_run) -> None:
"""Writes basic metadata that should be known at start of execution.
This method writes to |build_run|'s metadata instance the basic metadata
values that should be known at the beginning of the first cbuildbot
execution, prior to any reexecutions.
In particular, this method does not write any metadata values that depend
on the builder config, as the config may be modified by patches that are
applied before the final reexectuion. (exception: the config's name itself)
This method is safe to run more than once (for instance, once per cbuildbot
execution) because it will write the same data each time.
Args:
builder_run: The BuilderRun instance for this build.
"""
start_time = results_lib.Results.start_time
start_time_stamp = cros_build_lib.UserDateTimeFormat(timeval=start_time)
metadata = {
# Data for this build.
"bot-hostname": hostname_util.get_host_name(fully_qualified=True),
"build-number": builder_run.buildnumber,
"builder-name": builder_run.GetBuilderName(),
"bot-config": builder_run.config["name"],
"time": {
"start": start_time_stamp,
},
"master_build_id": builder_run.options.master_build_id,
"suite_scheduling": builder_run.config["suite_scheduling"],
}
builder_run.attrs.metadata.UpdateWithDict(metadata)
def WriteTagMetadata(builder_run) -> None:
"""Add a 'tags' sub-dict to metadata.
This is a proof of concept for using tags to help find commonality
in failures.
"""
build_identifier, _ = builder_run.GetCIDBHandle()
build_id = build_identifier.cidb_id
# Yes, these values match general metadata values, but they are just
# proof of concept, so far.
tags = {
"bot_config": builder_run.config["name"],
"bot_hostname": hostname_util.get_host_name(fully_qualified=True),
"build_id": build_id,
"build_number": builder_run.buildnumber,
"builder_name": builder_run.GetBuilderName(),
"buildbot_master_name": os.environ.get("BUILDBOT_MASTERNAME", ""),
"id": ("Build", build_id),
"master_build_id": builder_run.options.master_build_id,
"important": builder_run.config["important"],
}
# Guess type of bot.
tags["bot_type"] = "unknown"
if ".golo." in tags["bot_hostname"]:
tags["bot_type"] = "golo"
else:
gce_types = ["beefy", "standard", "wimpy"]
for t in gce_types:
host_string = "cros-%s" % t
if host_string in tags["bot_hostname"]:
tags["bot_type"] = "gce-%s" % t
break
# Look up the git version.
try:
cmd_result = cros_build_lib.run(
["git", "--version"], capture_output=True, encoding="utf-8"
)
tags["git_version"] = cmd_result.stdout.strip()
except cros_build_lib.RunCommandError:
pass # If we fail, just don't include the tag.
# Look up the repo version.
try:
cmd_result = cros_build_lib.run(
["repo", "--version"], capture_output=True, encoding="utf-8"
)
# Convert the following output into 'v1.12.17-cr3':
#
# repo version v1.12.17-cr3
# (from https://chromium.googlesource.com/external/repo.git)
# repo launcher version 1.21
# (from /usr/local/google/home/dgarrett/sand/depot_tools/repo)
# git version 2.8.0.rc3.226.g39d4020
# Python 2.7.6 (default, Jun 22 2015, 17:58:13)
# [GCC 4.8.2]
tags["repo_version"] = cmd_result.stdout.splitlines()[0].split(" ")[-1]
except (cros_build_lib.RunCommandError, IndexError):
pass # If we fail, just don't include the tag.
builder_run.attrs.metadata.UpdateKeyDictWithDict(
constants.METADATA_TAGS, tags
)
def _UploadAndLinkGomaLogIfNecessary(
stage_name, cbb_config_name, goma_dir, goma_tmp_dir
) -> None:
"""Uploads the logs for goma, if needed.
Also create a link to the visualizer.
If |goma_tmp_dir| is given, |goma_dir| must not be None.
Args:
stage_name: Name of the stage where goma is used.
cbb_config_name: Name of cbb_config used for the build.
goma_dir: Path to goma installed directory.
goma_tmp_dir: Goma's working directory.
"""
if not goma_tmp_dir:
return
goma = goma_lib.Goma(goma_dir, goma_tmp_dir=goma_tmp_dir)
# Just in case, stop the goma. E.g. In case of timeout, we do not want to
# keep goma compiler_proxy running.
goma.Stop()
goma_urls = goma.UploadLogs(cbb_config_name)
if goma_urls:
for label, url in goma_urls:
cbuildbot_alerts.PrintBuildbotLink(
"%s %s" % (stage_name, label), url
)
class BuildStartStage(generic_stages.BuilderStage):
"""The first stage to run.
This stage writes a few basic metadata values that are known at the start of
build, and inserts the build into the database, if appropriate.
"""
category = constants.CI_INFRA_STAGE
def _GetBuildTimeoutSeconds(self):
"""Get the overall build timeout to be published to cidb.
Returns:
Timeout in seconds. None if no sensible timeout can be inferred.
"""
timeout_seconds = self._run.options.timeout
if self._run.config.master:
master_timeout = self._run.config.build_timeout
if timeout_seconds > 0:
master_timeout = min(master_timeout, timeout_seconds)
return master_timeout
return timeout_seconds if timeout_seconds > 0 else None
@failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
def PerformStage(self) -> None:
if self._run.config["doc"]:
cbuildbot_alerts.PrintBuildbotLink(
"Builder documentation", self._run.config["doc"]
)
WriteBasicMetadata(self._run)
# This is a heuristic value for |important|, since patches that get
# applied later in the build might change the config. We write it now
# anyway, because in case the build fails before Sync, it is better to
# have this heuristic value than None. In BuildReexecutionFinishedStage,
# we re-write the definitive value.
self._run.attrs.metadata.UpdateWithDict(
{"important": self._run.config["important"]}
)
d = self._run.attrs.metadata.GetDict()
# BuildStartStage should only run once per build. But just in case it
# is somehow running a second time, we do not want to insert an
# additional database entry. Detect if a database entry has been
# inserted already and if so quit the stage.
if "build_id" in d:
logging.info(
"Already have build_id %s, not inserting an entry.",
d["build_id"],
)
return
buildbucket_id = self._run.options.buildbucket_id
# Note: In other build stages we use self._run.GetCIDBHandle to fetch
# a cidb handle. However, since we don't yet have a build_id, we can't
# do that here.
if self.buildstore.AreClientsReady():
db_type = cidb.CIDBConnectionFactory.GetCIDBConnectionType()
try:
build_id = self.buildstore.InsertBuild(
builder_name=d["builder-name"],
build_number=d["build-number"],
build_config=d["bot-config"],
bot_hostname=d["bot-hostname"],
master_build_id=d["master_build_id"],
timeout_seconds=self._GetBuildTimeoutSeconds(),
important=d["important"],
buildbucket_id=buildbucket_id,
branch=self._run.manifest_branch,
)
except Exception as e:
logging.error(
"Error: %s\n If the buildbucket_id to insert is "
"duplicated to the buildbucket_id of an old build and "
"the old build was canceled because of a waterfall "
"master restart, please ignore this error. Else, "
"the error needs more investigation. More context: "
"crbug.com/679974 and crbug.com/685889",
e,
)
raise e
master_bb_id = self._run.options.master_buildbucket_id
self._run.attrs.metadata.UpdateWithDict(
{
"build_id": build_id,
"buildbucket_id": buildbucket_id,
"master_buildbucket_id": master_bb_id,
"db_type": db_type,
}
)
logging.info(
"Inserted build_id %s into cidb database type %s.",
build_id,
db_type,
)
cbuildbot_alerts.PrintBuildbotStepText(
"database: %s, build_id: %s" % (db_type, build_id)
)
master_build_id = d["master_build_id"]
if master_build_id is not None:
master_build_status = self.buildstore.GetBuildStatuses(
build_ids=[master_build_id]
)[0]
if master_build_status["buildbucket_id"]:
master_url = uri_lib.ConstructMiloBuildUri(
master_build_status["buildbucket_id"]
)
else:
master_url = uri_lib.ConstructDashboardUri(
master_build_status["waterfall"],
master_build_status["builder_name"],
master_build_status["build_number"],
)
cbuildbot_alerts.PrintBuildbotLink(
"Link to master build", master_url
)
# Set annealing snapshot revision build property for Findit integration.
if self._run.options.cbb_snapshot_revision:
cbuildbot_alerts.PrintKitchenSetBuildProperty(
"GOT_REVISION", self._run.options.cbb_snapshot_revision
)
# Write the tag metadata last so that a build_id is available.
WriteTagMetadata(self._run)
def HandleSkip(self) -> None:
"""Ensure that re-executions use the same db instance as initial db."""
metadata_dict = self._run.attrs.metadata.GetDict()
if "build_id" in metadata_dict:
db_type = cidb.CIDBConnectionFactory.GetCIDBConnectionType()
if not "db_type" in metadata_dict:
# This will only execute while this CL is in the commit queue.
# After this CL lands, this block can be removed.
self._run.attrs.metadata.UpdateWithDict({"db_type": db_type})
return
if db_type != metadata_dict["db_type"]:
cidb.CIDBConnectionFactory.InvalidateCIDBSetup()
raise AssertionError(
"Invalid attempt to switch from database %s to %s."
% (metadata_dict["db_type"], db_type)
)
class SlaveFailureSummaryStage(generic_stages.BuilderStage):
"""Stage which summarizes and links to the failures of slave builds."""
category = constants.CI_INFRA_STAGE
@failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
def PerformStage(self) -> None:
if not self._run.config.master:
logging.info(
"This stage is only meaningful for master builds. "
"Doing nothing."
)
return
if not self.buildstore.AreClientsReady():
logging.info(
"No buildstore connection for this build. Doing nothing."
)
return
child_failures = self.buildstore.GetBuildsFailures(
self.GetScheduledSlaveBuildbucketIds()
)
for failure in child_failures:
if (
failure.stage_status != constants.BUILDER_STATUS_FAILED
or failure.build_status == constants.BUILDER_STATUS_INFLIGHT
):
continue
slave_stage_url = uri_lib.ConstructMiloBuildUri(
failure.buildbucket_id
)
cbuildbot_alerts.PrintBuildbotLink(
"%s %s" % (failure.build_config, failure.stage_name),
slave_stage_url,
)
class BuildReexecutionFinishedStage(
generic_stages.BuilderStage, generic_stages.ArchivingStageMixin
):
"""The first stage to run after the final cbuildbot reexecution.
This stage is the first stage run after the final cbuildbot
bootstrap/reexecution. By the time this stage is run, the sync stages
are complete and version numbers of chromeos are known (though chrome
version may not be known until SyncChrome).
This stage writes metadata values that are first known after the final
reexecution (such as those that come from the config). This stage also
updates the build's cidb entry if appropriate.
Where possible, metadata that is already known at this time should be
written at this time rather than in ReportStage.
"""
category = constants.CI_INFRA_STAGE
@failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
def PerformStage(self) -> None:
config = self._run.config
build_root = self._build_root
# Workspace builders use a different buildroot for overlays.
if config.workspace_branch and self._run.options.workspace:
build_root = self._run.options.workspace
logging.info(
"Build re-executions have finished. Chromite source "
"will not be modified for remainder of run."
)
logging.info("config['important']=%s", config["important"])
cbuildbot_alerts.PrintBuildbotStepText(
"config['important']=%s" % config["important"]
)
sdk_verinfo = key_value_store.LoadFile(
os.path.join(build_root, constants.SDK_VERSION_FILE),
ignore_missing=True,
)
verinfo = self._run.GetVersionInfo()
platform_tag = getattr(self._run.attrs, "release_tag")
if not platform_tag:
platform_tag = verinfo.VersionString()
version = {
"full": self._run.GetVersion(),
"milestone": verinfo.chrome_branch,
"platform": platform_tag,
}
metadata = {
# Version of the metadata format.
"metadata-version": "2",
"boards": config["boards"],
"child-configs": [],
"build_type": config["build_type"],
"important": config["important"],
# Data for the toolchain used.
"sdk-version": sdk_verinfo.get("SDK_LATEST_VERSION", "<unknown>"),
"toolchain-url": sdk_verinfo.get("TC_PATH", "<unknown>"),
}
if len(config["boards"]) == 1:
metadata["toolchain-tuple"] = toolchain.GetToolchainTupleForBoard(
config["boards"][0], buildroot=build_root
)
logging.info("Metadata being written: %s", metadata)
self._run.attrs.metadata.UpdateWithDict(metadata)
toolchains = set()
toolchain_tuples = []
primary_toolchains = []
for board in config["boards"]:
toolchain_tuple = toolchain.GetToolchainTupleForBoard(
board, buildroot=build_root
)
toolchains |= set(toolchain_tuple)
toolchain_tuples.append(",".join(toolchain_tuple))
if toolchain_tuple:
primary_toolchains.append(toolchain_tuple[0])
# Update 'version' separately to avoid overwriting the existing
# entries in it (e.g. PFQ builders may have written the Chrome
# version to uprev).
logging.info("Metadata 'version' being written: %s", version)
self._run.attrs.metadata.UpdateKeyDictWithDict("version", version)
tags = {
"boards": config["boards"],
"child_config_names": [],
"build_type": config["build_type"],
"important": config["important"],
# Data for the toolchain used.
"sdk_version": sdk_verinfo.get("SDK_LATEST_VERSION", "<unknown>"),
"toolchain_url": sdk_verinfo.get("TC_PATH", "<unknown>"),
"toolchains": list(toolchains),
"toolchain_tuples": toolchain_tuples,
"primary_toolchains": primary_toolchains,
}
full_version = self._run.attrs.metadata.GetValue("version")
tags.update({"version_%s" % v: full_version[v] for v in full_version})
self._run.attrs.metadata.UpdateKeyDictWithDict(
constants.METADATA_TAGS, tags
)
# Ensure that all boards and child config boards have a per-board
# metadata subdict.
for b in config["boards"]:
self._run.attrs.metadata.UpdateBoardDictWithDict(b, {})
# Upload build metadata (and write it to database if necessary)
self.UploadMetadata(filename=constants.PARTIAL_METADATA_JSON)
# Write child-per-build and board-per-build rows to database
build_identifier, db = self._run.GetCIDBHandle()
build_id = build_identifier.cidb_id
if db:
# TODO(akeshet): replace this with a GetValue call once
# crbug.com/406522 is resolved
per_board_dict = self._run.attrs.metadata.GetDict()[
"board-metadata"
]
for board, board_metadata in per_board_dict.items():
self.buildstore.InsertBoardPerBuild(build_id, board)
if board_metadata:
self.buildstore.InsertBoardPerBuild(
build_id, board, board_metadata
)
class ConfigDumpStage(generic_stages.BuilderStage):
"""Stage that dumps the current build config to the build log.
This stage runs immediately after BuildReexecutionFinishedStage, at which
point the build is finalized.
"""
category = constants.CI_INFRA_STAGE
@failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
def PerformStage(self) -> None:
"""Dump the running config to info logs."""
config = self._run.config
logging.info(
"The current build config is dumped below:\n%s",
config_lib.PrettyJsonDict(config),
)
class ReportStage(
generic_stages.BuilderStage, generic_stages.ArchivingStageMixin
):
"""Summarize all the builds."""
_STATS_HISTORY_DAYS = 7
category = constants.CI_INFRA_STAGE
def __init__(
self, builder_run, buildstore, completion_instance, **kwargs
) -> None:
super().__init__(builder_run, buildstore, **kwargs)
# TODO(mtennant): All these should be retrieved from builder_run
# instead. Or, more correctly, the info currently retrieved from these
# stages should be stored and retrieved from builder_run instead.
self._completion_instance = completion_instance
self._post_completion = False
def _LinkArtifacts(self, builder_run) -> None:
"""Upload an HTML index and uploaded.json for artifacts.
If there are no artifacts in the archive then do nothing.
Args:
builder_run: BuilderRun object for this run.
"""
archive = builder_run.GetArchive()
archive_path = archive.archive_path
boards = builder_run.config.boards
if boards:
board_names = " ".join(boards)
else:
boards = [None]
board_names = "<no board>"
# See if there are any artifacts found for this run.
uploaded = os.path.join(archive_path, commands.UPLOADED_LIST_FILENAME)
if not os.path.exists(uploaded):
# UPLOADED doesn't exist. Normal if Archive stage never ran, which
# is possibly normal. Regardless, no archive index is needed.
logging.info(
"No archived artifacts found for %s run (%s)",
builder_run.config.name,
board_names,
)
return
cbuildbot_alerts.PrintKitchenSetBuildProperty(
"artifact_link", archive.upload_url
)
uploaded_json = "uploaded.json"
commands.GenerateUploadJSON(
os.path.join(archive_path, uploaded_json), archive_path, uploaded
)
commands.UploadArchivedFile(
archive_path,
[archive.upload_url],
uploaded_json,
debug=self._run.options.debug_forced,
update_list=True,
acl=self.acl,
)
if builder_run.config.internal:
# Internal builds simply link to pantheon directories, which require
# authenticated access that most Googlers should have.
artifacts_url = archive.download_url
else:
# External builds must allow unauthenticated access to build
# artifacts. GS doesn't let unauthenticated users browse selected
# locations without being able to browse everything (which would
# expose secret stuff). So, we upload an index.html file and link to
# it instead of the directory.
title = (
"Artifacts Index: %(board)s / %(version)s (%(config)s config)"
% {
"board": board_names,
"config": builder_run.config.name,
"version": builder_run.GetVersion(),
}
)
files = osutils.ReadFile(uploaded).splitlines() + [
".|Google Storage Index",
"..|",
]
index = os.path.join(archive_path, "index.html")
# TODO (sbasi) crbug.com/362776: Rework the way we do uploading to
# multiple buckets. Currently this can only be done in the Archive
# Stage therefore index.html will only end up in the normal Chrome
# OS bucket.
commands.GenerateHtmlIndex(
index,
files,
title=title,
url_base=gs_urls_util.GsUrlToHttp(archive.upload_url),
)
commands.UploadArchivedFile(
archive_path,
[archive.upload_url],
os.path.basename(index),
debug=self._run.options.debug_forced,
acl=self.acl,
)
artifacts_url = os.path.join(
archive.download_url_file, "index.html"
)
links_build_description = "%s/%s" % (
builder_run.config.name,
archive.version,
)
cbuildbot_alerts.PrintBuildbotLink(
"Artifacts[%s]" % links_build_description, artifacts_url
)
def GetReportMetadata(
self,
config=None,
stage=None,
final_status=None,
completion_instance=None,
):
"""Generate ReportStage metadata.
Args:
config: The build config for this run. Defaults to
self._run.config.
stage: The stage name that this metadata file is being uploaded for.
final_status: Whether the build passed or failed. If None, the build
will be treated as still running.
completion_instance: The stage instance that was used to wait for
slave completion. Used to add slave build information to master
builder's metadata. If None, no such status information will be
included. It not None, this should be a derivative of
MasterSlaveSyncCompletionStage.
Returns:
A JSON-able dictionary representation of the metadata object.
"""
builder_run = self._run
config = config or builder_run.config
get_statuses_from_slaves = (
config["master"]
and completion_instance
and isinstance(
completion_instance,
completion_stages.MasterSlaveSyncCompletionStage,
)
)
return metadata_lib.CBuildbotMetadata.GetReportMetadataDict(
builder_run,
get_statuses_from_slaves,
config,
stage,
final_status,
completion_instance,
)
def ArchiveResults(self, final_status) -> None:
"""Archive our build results.
Args:
final_status: constants.BUILDER_STATUS_PASSED or
constants.BUILDER_STATUS_FAILED
"""
# Make sure local archive directory is prepared, if it was not already.
if not os.path.exists(self.archive_path):
self.archive.SetupArchivePath()
# Upload metadata, and update the pass/fail streak counter for the main
# run only. These aren't needed for the child builder runs.
self.UploadMetadata()
build_identifier, _ = self._run.GetCIDBHandle()
build_id = build_identifier.cidb_id
# Iterate through each builder run.
for builder_run in [self._run]:
if build_id is not None:
details_link = uri_lib.ConstructViceroyBuildDetailsUri(build_id)
cbuildbot_alerts.PrintBuildbotLink(
"Build details", details_link
)
# Generate links to archived artifacts if there are any. All the
# archived artifacts for one run/config are in one location, so the
# link is only specific to each run/config. In theory multiple
# boards could share that archive, but in practice it is usually one
# board. A run/config without a board will also usually not have
# artifacts to archive, but that restriction is not assumed here.
self._LinkArtifacts(builder_run)
# Check if the builder_run is tied to any boards and if so get all
# upload urls.
if final_status == constants.BUILDER_STATUS_PASSED:
# Update the LATEST files if the build passed.
try:
upload_urls = self._GetUploadUrls(
"LATEST-*", builder_run=builder_run
)
except portage_util.MissingOverlayError as e:
# If the build failed prematurely, some overlays might be
# missing. Ignore them in this stage.
logging.warning(e)
else:
if upload_urls:
archive = builder_run.GetArchive()
archive.UpdateLatestMarkers(
builder_run.manifest_branch,
builder_run.options.debug_forced,
upload_urls=upload_urls,
)
def PerformStage(self) -> None:
"""Perform the actual work for this stage.
This includes final metadata archival, and update CIDB with our final
status as well as producing a logged build result summary.
"""
build_identifier, _ = self._run.GetCIDBHandle()
build_id = build_identifier.cidb_id
buildbucket_id = build_identifier.buildbucket_id
if results_lib.Results.BuildSucceededSoFar(
self.buildstore, buildbucket_id, self.name
):
final_status = constants.BUILDER_STATUS_PASSED
else:
final_status = constants.BUILDER_STATUS_FAILED
if not hasattr(self._run.attrs, "release_tag"):
# If, for some reason, sync stage was not completed and
# release_tag was not set. Set it to None here because
# ArchiveResults() depends the existence of this attr.
self._run.attrs.release_tag = None
# Set up our report metadata.
self._run.attrs.metadata.UpdateWithDict(
self.GetReportMetadata(
final_status=final_status,
completion_instance=self._completion_instance,
)
)
src_root = self._build_root
# Workspace builders use a different buildroot for overlays.
if self._run.config.workspace_branch and self._run.options.workspace:
src_root = self._run.options.workspace
# Add tags for the arches and statuses of the build.
# arches requires crossdev which isn't available at the early part of
# the build.
arches = []
for board in self._run.config["boards"]:
build_target = build_target_lib.BuildTarget(board)
toolchains = toolchain.get_toolchains_for_build_target(
build_target, source_root=src_root
)
default = list(
toolchain.FilterToolchains(toolchains, "default", True)
)
if default:
try:
arches.append(toolchain.GetArchForTarget(default[0]))
except cros_build_lib.RunCommandError as e:
logging.warning(
"Unable to retrieve arch for board %s default toolchain"
" %s: %s",
board,
default,
e,
)
tags = {
"arches": arches,
"status": final_status,
}
results = self._run.attrs.metadata.GetValue("results")
for stage in results:
tags["stage_status:%s" % stage["name"]] = stage["status"]
tags["stage_summary:%s" % stage["name"]] = stage["summary"]
self._run.attrs.metadata.UpdateKeyDictWithDict(
constants.METADATA_TAGS, tags
)
# Some operations can only be performed if a valid version is available.
try:
self._run.GetVersionInfo()
self.ArchiveResults(final_status)
metadata_url = os.path.join(
self.upload_url, constants.METADATA_JSON
)
except cbuildbot_run.VersionNotSetError:
logging.error(
"A valid version was never set for this run. "
"Can not archive results."
)
metadata_url = ""
results_lib.Results.Report(
sys.stdout, current_version=(self._run.attrs.release_tag or "")
)
# Upload goma log if used for BuildPackage and TestSimpleChrome.
_UploadAndLinkGomaLogIfNecessary(
"BuildPackages",
self._run.config.name,
self._run.options.goma_dir,
self._run.attrs.metadata.GetValueWithDefault("goma_tmp_dir"),
)
_UploadAndLinkGomaLogIfNecessary(
"TestSimpleChromeWorkflow",
self._run.config.name,
self._run.options.goma_dir,
self._run.attrs.metadata.GetValueWithDefault(
"goma_tmp_dir_for_simple_chrome"
),
)
if self.buildstore.AreClientsReady():
status_for_db = final_status
# TODO(pprabhu): After BuildData and CBuildbotMetadata are merged,
# remove this extra temporary object creation.
# XXX:HACK We're creating a BuildData with an empty URL. Don't try
# to MarkGathered this object.
build_data = metadata_lib.BuildData(
"", self._run.attrs.metadata.GetDict()
)
# TODO(akeshet): Find a clearer way to get the "primary upload url"
# for the metadata.json file. One alternative is
# _GetUploadUrls(...)[0]. Today it seems that element 0 of its
# return list is the primary upload url, but there is no guarantee
# or unit test coverage of that.
self.buildstore.FinishBuild(
build_id,
status=status_for_db,
summary=build_data.failure_message,
metadata_url=metadata_url,
)
duration = self._GetBuildDuration()
mon_fields = {
"status": status_for_db,
"build_config": self._run.config.name,
"important": self._run.config.important,
}
metrics.Counter(constants.MON_BUILD_COMP_COUNT).increment(
fields=mon_fields
)
metrics.CumulativeSecondsDistribution(
constants.MON_BUILD_DURATION
).add(duration, fields=mon_fields)
# From this point forward, treat all exceptions as warnings.
self._post_completion = True
# Dump report about things we retry.
retry_stats.ReportStats(sys.stdout)
def _GetBuildDuration(self):
"""Fetches the duration of this build in seconds, from cidb.
This method should be called only after the build has been Finished in
cidb.
"""
build_identifier, _ = self._run.GetCIDBHandle()
if self.buildstore.AreClientsReady():
buildbucket_id = build_identifier.buildbucket_id
build_info = self.buildstore.GetBuildStatuses(
buildbucket_ids=[buildbucket_id]
)[0]
# If we query from Buildbucket, the build isn't finished yet.
# So, finish_time is None. Use current time instead.
if build_info["finish_time"] is None:
build_info["finish_time"] = datetime.datetime.now()
duration = (
build_info["finish_time"] - build_info["start_time"]
).total_seconds()
return duration
return 0
def _HandleStageException(self, exc_info):
"""Override and don't set status to FAIL but FORGIVEN instead."""
if self._post_completion:
# If we've already reported the stage completion, treat exceptions
# as warnings so we keep reported success in-line with waterfall
# displayed results.
return self._HandleExceptionAsWarning(exc_info)
return super()._HandleStageException(exc_info)