blob: a55276f4bc8c29934cd0e6f16e9ee8ac8d6a70b7 [file] [log] [blame]
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Update the CHROMEOS_LKGM file in a chromium repository.
This script will first query Gerrit for an already-open CL updating the
CHROMEOS_LKGM file to the given version. If one exists, it will submit that
CL to the CQ. Else it will upload a new CL and quit _without_ submitting
it to the CQ.
"""
import distutils.version # pylint: disable=import-error,no-name-in-module
import logging
import urllib.parse
from chromite.cbuildbot import manifest_version
from chromite.lib import commandline
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import gerrit
from chromite.lib import gob_util
class LKGMNotValid(Exception):
"""Raised if the LKGM version is unset or not newer than the current value."""
class LKGMFileNotFound(Exception):
"""Raised if the LKGM file is not found."""
class ChromeLKGMCommitter(object):
"""Committer object responsible for obtaining a new LKGM and committing it."""
# The list of trybots we require LKGM updates to run and pass on before
# landing. Since they're internal trybots, the CQ won't automatically trigger
# them, so we have to explicitly tell it to.
_PRESUBMIT_BOTS = [
'chromeos-betty-pi-arc-chrome',
'chromeos-eve-chrome',
'chromeos-kevin-chrome',
'chromeos-octopus-chrome',
'lacros-amd64-generic-chrome',
]
# Files needed in a local checkout to successfully update the LKGM. The OWNERS
# file allows the --tbr-owners mechanism to select an appropriate OWNER to
# TBR. TRANSLATION_OWNERS is necesssary to parse CHROMEOS_OWNERS file since
# it has the reference.
_NEEDED_FILES = [
constants.PATH_TO_CHROME_CHROMEOS_OWNERS,
constants.PATH_TO_CHROME_LKGM,
'tools/translation/TRANSLATION_OWNERS',
]
# First line of the commit message for all LKGM CLs.
_COMMIT_MSG_HEADER = 'Automated Commit: LKGM %(lkgm)s for chromeos.'
def __init__(self, lkgm, dryrun=False, buildbucket_id=None):
self._dryrun = dryrun
self._buildbucket_id = buildbucket_id
self._gerrit_helper = gerrit.GetCrosExternal()
# We need to use the account used by the builder to upload git CLs when
# generating CLs.
self._user_email = None
if cros_build_lib.HostIsCIBuilder(golo_only=True):
self._user_email = 'chromeos-commit-bot@chromium.org'
elif cros_build_lib.HostIsCIBuilder(gce_only=True):
self._user_email = '3su6n15k.default@developer.gserviceaccount.com'
# Strip any chrome branch from the lkgm version.
self._lkgm = manifest_version.VersionInfo(lkgm).VersionString()
self._commit_msg_header = self._COMMIT_MSG_HEADER % {'lkgm': self._lkgm}
self._old_lkgm = None
if not self._lkgm:
raise LKGMNotValid('LKGM not provided.')
logging.info('lkgm=%s', lkgm)
def Run(self):
self.CloseOldLKGMRolls()
already_open_lkgm_cl = self.FindAlreadyOpenLKGMRoll()
if already_open_lkgm_cl:
self.SubmitToCQ(already_open_lkgm_cl)
else:
self.UpdateLKGM()
@property
def lkgm_file(self):
return self._committer.FullPath(constants.PATH_TO_CHROME_LKGM)
def CloseOldLKGMRolls(self):
"""Closes all open LKGM roll CLs that were last modified >24 hours ago.
Any roll that hasn't passed the CQ in 24 hours is likely broken and can be
discarded.
"""
query_params = {
'project': constants.CHROMIUM_SRC_PROJECT,
'branch': 'main',
'file': constants.PATH_TO_CHROME_LKGM,
'age': '2d',
'status': 'open',
# Use 'owner' rather than 'uploader' or 'author' since those last two
# can be overwritten when the gardener resolves a merge-conflict and
# uploads a new patchset.
'owner': self._user_email,
}
open_issues = self._gerrit_helper.Query(**query_params)
if not open_issues:
logging.info('No old LKGM rolls detected.')
return
for open_issue in open_issues:
if self._dryrun:
logging.info(
'Would have closed old LKGM roll crrev.com/c/%s',
open_issue.gerrit_number)
else:
logging.info(
'Closing old LKGM roll crrev.com/c/%s', open_issue.gerrit_number)
self._gerrit_helper.AbandonChange(
open_issue, msg='Superceded by LKGM %s' % self._lkgm)
def FindAlreadyOpenLKGMRoll(self):
"""Queries Gerrit for a CL that already rolls the LKGM to our version.
For a given LKGM, both the master-full and master-release builders provide
SDKs needed by Chrome-infra. These two builders aren't synchronized, so
one can finish hours before the other. To avoid updating the LKGM in Chrome
before they're both finished, we take a two-stage approach:
1. Whichever builder finishes first uploads the LKGM CL.
2. Whichever builder finishes last submits that CL to the CQ.
This method queries Gerrit to find the CL for step #2.
Returns:
Returns a patch.GerritPatch for the CL if it exists, None otherwise.
"""
query_params = {
'project': constants.CHROMIUM_SRC_PROJECT,
'branch': 'main',
'file': constants.PATH_TO_CHROME_LKGM,
'status': 'open',
# Use 'owner' rather than 'uploader' or 'author' since those last two
# can be overwritten when the gardener resolves a merge-conflict and
# uploads a new patchset.
'owner': self._user_email,
# The value of the LKGM is included in the first line of the commit
# message. So including that in our query should only return CLs that
# roll to our LKGM.
'message': urllib.parse.quote('"' + self._commit_msg_header + '"'),
}
issues = self._gerrit_helper.Query(**query_params)
if len(issues) > 1:
# This shouldn't happen?
raise LKGMNotValid('More than one roll CL open for LKGM %s'% self._lkgm)
return issues[0] if issues else None
def SubmitToCQ(self, already_open_lkgm_cl):
"""Sends the already_open_lkgm_cl to the CQ."""
labels = {'Commit-Queue': 2}
if self._dryrun:
logging.info('Would have applied CQ+2 to %s', already_open_lkgm_cl)
else:
logging.info('Applying CQ+2 to %s', already_open_lkgm_cl)
msg = None
if self._buildbucket_id:
msg = 'Applying CQ+2 from build %s' % self._buildbucket_id
self._gerrit_helper.SetReview(
already_open_lkgm_cl, labels=labels,
reviewers=[constants.CHROME_GARDENER_REVIEW_EMAIL],
msg=msg, ready=True)
def UpdateLKGM(self):
"""Updates the LKGM file with the new version."""
self._old_lkgm = gob_util.GetFileContentsOnHead(
constants.CHROMIUM_GOB_URL, 'chromeos/CHROMEOS_LKGM')
self._old_lkgm = self._old_lkgm.strip()
lv = distutils.version.LooseVersion
if self._old_lkgm is not None and lv(self._lkgm) <= lv(self._old_lkgm):
raise LKGMNotValid(
'LKGM version (%s) is not newer than current version (%s).' %
(self._lkgm, self._old_lkgm))
logging.info('Updating LKGM version: %s (was %s),',
self._lkgm, self._old_lkgm)
change = self._gerrit_helper.CreateChange(
'chromium/src', 'main', self.ComposeCommitMsg(), False)
self._gerrit_helper.ChangeEdit(
change.gerrit_number, 'chromeos/CHROMEOS_LKGM', self._lkgm)
# Apply Bot-Commit here to minimise the gap between uploading the
# CL and approving it.
labels = {'Bot-Commit': 1}
logging.info('Applying Bot-Commit+1')
self._gerrit_helper.SetReview(change.gerrit_number, labels=labels,
notify='NONE')
self._gerrit_helper.SetHashtags(change.gerrit_number, ['chrome-lkgm'], [])
def ComposeCommitMsg(self):
"""Constructs and returns the commit message for the LKGM update."""
commit_msg_template = (
'%(header)s\n'
'%(build_link)s'
'\nThis CL will remain in WIP until both master-full and '
'master-release\nbuilds for this version are finished. This CL '
'should not be submitted\nto the CQ until that happens.\n'
'\n%(cq_includes)s')
cq_includes = ''
for bot in self._PRESUBMIT_BOTS:
cq_includes += 'CQ_INCLUDE_TRYBOTS=luci.chrome.try:%s\n' % bot
build_link = ''
if self._buildbucket_id:
build_link = '\nUploaded by https://ci.chromium.org/b/%s\n' % (
self._buildbucket_id)
return commit_msg_template % dict(
header=self._commit_msg_header, cq_includes=cq_includes,
build_link=build_link)
def GetOpts(argv):
"""Returns a dictionary of parsed options.
Args:
argv: raw command line.
Returns:
Dictionary of parsed options.
"""
parser = commandline.ArgumentParser(description=__doc__, add_help=False)
parser.add_argument('--dryrun', action='store_true', default=False,
help="Don't commit changes or send out emails.")
parser.add_argument('--lkgm', required=True,
help='LKGM version to update to.')
parser.add_argument('--buildbucket-id',
help='Buildbucket ID of the build that ran this script. '
'Will be linked in the commit message if specified.')
return parser.parse_args(argv)
def main(argv):
opts = GetOpts(argv)
committer = ChromeLKGMCommitter(opts.lkgm, opts.dryrun, opts.buildbucket_id)
committer.Run()
return 0