blob: 8193532c96896ddc793681853f07009b54601708 [file] [log] [blame]
# Copyright 2017 The ChromiumOS Authors
# 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 upload an LKGM CL and potentially submit it to the CQ.
"""
import logging
from typing import Optional, Tuple
from chromite.lib import chromeos_version
from chromite.lib import commandline
from chromite.lib import constants
from chromite.lib import gerrit
from chromite.lib import gob_util
from chromite.utils import hostname_util
# Gerrit hashtag for the LKGM Uprev CLs.
HASHTAG = "chrome-lkgm"
# Keys for git footers
GIT_FOOTER_EXTERNAL_MANIFEST_POS = "CrOS-External-Manifest-Position"
GIT_FOOTER_INTERNAL_MANIFEST_POS = "CrOS-Internal-Manifest-Position"
GIT_FOOTER_LKGM = "CrOS-LKGM"
class LKGMNotValid(Exception):
"""The LKGM version is unset or not newer than the current value."""
class LKGMFileNotFound(Exception):
"""Raised if the LKGM file is not found."""
class ChromeLKGMCleaner:
"""Responsible for cleaning up the existing LKGM CLs if necessary.
In Particular, this class does:
- abandoning the obsolete CLs
- rebasing the merge-conflicted CLs
"""
def __init__(
self,
branch: str,
current_lkgm: chromeos_version.VersionInfo,
user_email: str,
dryrun: bool = False,
buildbucket_id: Optional[str] = None,
) -> None:
self._dryrun = dryrun
self._branch = branch
self._gerrit_helper = gerrit.GetGerritHelper(
gob=constants.EXTERNAL_GOB_INSTANCE
)
self._buildbucket_id = buildbucket_id
self._user_email = user_email
# Strip any chrome branch from the lkgm version.
self._current_lkgm = current_lkgm
def ProcessObsoleteLKGMRolls(self) -> None:
"""Clean up all obsolete LKGM roll CLs by abandoning or rebasing.
This method finds the LKGM roll CLs that were trying changing to an
older version than the current LKGM version, and abandons them.
"""
query_params = {
"project": constants.CHROMIUM_SRC_PROJECT,
"branch": self._branch,
"file": constants.PATH_TO_CHROME_LKGM,
"status": "open",
"hashtag": HASHTAG,
# 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_changes = self._gerrit_helper.Query(**query_params)
if not open_changes:
logging.info("No old LKGM rolls detected.")
return
logging.info(
"Retrieved the current LKGM version: %s",
self._current_lkgm.VersionString(),
)
build_link = ""
if self._buildbucket_id:
build_link = (
"\nUpdated by"
f" https://ci.chromium.org/b/{self._buildbucket_id}\n"
)
for change in open_changes:
logging.info(
"Found a open LKGM roll CL: %s (crrev.com/c/%s).",
change.subject,
change.gerrit_number,
)
# Retrieve the version that this CL tries to roll to.
roll_to_string = change.GetFileContents(
constants.PATH_TO_CHROME_LKGM
)
if roll_to_string is None:
logging.info("=> No LKGM change found in this CL.")
continue
roll_to = chromeos_version.VersionInfo(roll_to_string)
if roll_to <= self._current_lkgm:
# The target version that the CL is changing to is older than
# the current. The roll CL is useless so that it'd be abandoned.
logging.info(
"=> This CL is an older LKGM roll than current: Abandoning"
)
if not self._dryrun:
abandon_message = (
"The newer LKGM"
f" ({self._current_lkgm.VersionString()}) roll than"
f" this CL has been landed.{build_link}"
)
self._gerrit_helper.AbandonChange(
change,
msg=abandon_message,
)
continue
mergeable = change.IsMergeable()
if mergeable is None:
logging.info("=> Failed to get the mergeable state of the CL.")
continue
# This CL may be in "merge conflict" state. Resolve.
if not mergeable:
# Retrieve the version that this CL tries to roll from.
roll_from_string = change.GetOriginalFileContents(
constants.PATH_TO_CHROME_LKGM
)
roll_from = chromeos_version.VersionInfo(
roll_from_string.strip()
)
if roll_from == self._current_lkgm:
# The CL should not be in the merge-conflict state.
# mergeable=False might come from other reason.
logging.info(
"=> This CL tries to roll from the same LKGM. "
"Doing nothing."
)
continue
elif roll_from >= self._current_lkgm:
# This should not happen.
logging.info(
"=> This CL tries to roll from a newer LKGM. Maybe"
"LKGM in Chromium code has been rolled back. Anyway, "
"rebasing forcibly."
)
else:
logging.info(
"=> This CL tries to roll from the older LKGM. "
"Rebasing."
)
# Resolve the conflict by rebasing.
if not self._dryrun:
change.Rebase(allow_conflicts=True)
self._gerrit_helper.ChangeEdit(
change.gerrit_number,
"chromeos/CHROMEOS_LKGM",
roll_to_string,
)
continue
logging.info("=> This CL is not in the merge-conflict state.")
def Run(self) -> None:
self.ProcessObsoleteLKGMRolls()
class ChromeLKGMCommitter:
"""Committer object responsible for obtaining and committing a new LKGM."""
# 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. If you add a new
# internal builder here, make sure it's also listed in
# https://source.chromium.org/chromium/chromium/src/+/main:infra/config/subprojects/chrome/try.star.
_PRESUBMIT_BOTS = {
"luci.chrome.try": (
"chromeos-betty-chrome",
"chromeos-brya-chrome",
"chromeos-jacuzzi-chrome",
"chromeos-reven-chrome",
"chromeos-volteer-chrome",
),
"luci.chromium.try": (
"chromeos-octopus-rel",
"chromeos-jacuzzi-rel",
),
}
# 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 necessary 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: str,
branch: str,
current_lkgm: chromeos_version.VersionInfo,
dryrun: bool = False,
buildbucket_id: Optional[str] = None,
message: Optional[str] = None,
external_manifest_position: Optional[int] = None,
internal_manifest_position: Optional[int] = None,
allow_uprev_to_older_release: bool = False,
) -> None:
self._dryrun = dryrun
self._branch = branch
self._buildbucket_id = buildbucket_id
self._gerrit_helper = gerrit.GetGerritHelper(
gob=constants.EXTERNAL_GOB_INSTANCE
)
# Next LKGM, which is going to be updated to by an uprev CL.
# Strip any chrome branch from the lkgm version.
self._lkgm = chromeos_version.VersionInfo(lkgm).VersionString()
# Current LKGM, which is going to be updated from.
self._current_lkgm = current_lkgm
self._commit_msg_header = self._COMMIT_MSG_HEADER % {"lkgm": self._lkgm}
self._message = message
self._external_manifest_position = external_manifest_position
self._internal_manifest_position = internal_manifest_position
self._footers = {}
self._allow_uprev_to_older_release = allow_uprev_to_older_release
# Storing metadata in the git footer for automated processing.
self._footers[GIT_FOOTER_LKGM] = self._lkgm
# Manual bypass for go/cl-merge-blocker
self._footers["Merge-Approval-Bypass"] = "Automated LKGM update"
if buildbucket_id:
self._footers["Cr-Build-Id"] = str(buildbucket_id)
if external_manifest_position:
self._footers[GIT_FOOTER_EXTERNAL_MANIFEST_POS] = (
external_manifest_position
)
if internal_manifest_position:
self._footers[GIT_FOOTER_INTERNAL_MANIFEST_POS] = (
internal_manifest_position
)
if not self._lkgm:
if self._dryrun:
self._lkgm = "9999999.99.99"
logging.info("dry run, using version %s", self._lkgm)
else:
raise LKGMNotValid("LKGM not provided.")
logging.info("lkgm=%s", lkgm)
def Run(self) -> None:
self.UpdateLKGM()
@property
def lkgm_file(self):
return self._committer.FullPath(constants.PATH_TO_CHROME_LKGM)
def UpdateLKGM(self) -> None:
"""Updates the LKGM file with the new version."""
new_lkgm = chromeos_version.VersionInfo(self._lkgm)
if new_lkgm <= self._current_lkgm:
message = (
f"LKGM version ({self._lkgm}) is not newer than current version"
f" ({self._current_lkgm.VersionString()})."
)
if (
self._allow_uprev_to_older_release
and new_lkgm.snapshot_suffix is None
and self._current_lkgm.snapshot_suffix is not None
):
# Allows the downgrade update, in case of a promotion from a
# snapshot (version with a snapshot suffix) to a release
# (without any suffix). This occasionally happens on a release
# branch just after the branching from the main to a release
# branch.
logging.warning(message)
else:
# Generally we don't allow the downgrade upgrade.
raise LKGMNotValid(message)
logging.info(
"Updating LKGM version: %s (was %s),",
self._lkgm,
self._current_lkgm.VersionString(),
)
change = self._gerrit_helper.CreateChange(
"chromium/src", self._branch, self.ComposeCommitMsg(), False
)
self._gerrit_helper.ChangeEdit(
change.gerrit_number, "chromeos/CHROMEOS_LKGM", self._lkgm
)
if self._dryrun:
logging.info(
"Would have applied CQ+2 to crrev.com/c/%s",
change.gerrit_number,
)
self._gerrit_helper.AbandonChange(
change,
msg="Dry run",
)
return
labels = {
"Bot-Commit": 1,
"Commit-Queue": 2,
}
logging.info(
"Applying %s to crrev.com/c/%s", labels, change.gerrit_number
)
self._gerrit_helper.SetReview(
change.gerrit_number,
labels=labels,
notify="NONE",
ready=True,
reviewers=[constants.CHROME_GARDENER_REVIEW_EMAIL],
)
self._gerrit_helper.SetHashtags(change.gerrit_number, [HASHTAG], [])
def ComposeCommitMsg(self):
"""Constructs and returns the commit message for the LKGM update."""
message = ""
if self._message:
message += self._message
message += "\n\n"
changelog = ""
if self._external_manifest_position or self._internal_manifest_position:
(external_pos, internal_pos) = self.GetCurrentManifestPosition()
if self._external_manifest_position and external_pos:
changelog += (
"- External: http://go/cros-changes/"
+ f"{external_pos}..{self._external_manifest_position}"
+ "?ext=true\n"
)
if self._internal_manifest_position and internal_pos:
changelog += (
"- Internal: http://go/cros-changes/"
+ f"{internal_pos}..{self._internal_manifest_position}\n"
)
if changelog:
changelog = (
"CrOS Changes "
+ f"({self._current_lkgm.VersionString()} -> {self._lkgm}):\n"
+ changelog
+ "\n"
)
build_link = ""
if self._buildbucket_id:
build_link = "Uploaded by https://ci.chromium.org/b/%s\n\n" % (
self._buildbucket_id
)
cq_includes = ""
if self._branch == "main":
for group, bots in self._PRESUBMIT_BOTS.items():
for bot in bots:
cq_includes += "CQ_INCLUDE_TRYBOTS=%s:%s\n" % (group, bot)
cq_includes += "\n"
dry_run_message = ""
if self._dryrun:
dry_run_message = (
"This CL was created during a dry run and is not "
"intended to be committed.\n\n"
)
footers = ""
for key, value in self._footers.items():
footers += f"{key}: {value}\n"
commit_msg_template = (
"%(header)s\n\n"
"%(message)s"
"%(changelog)s"
"%(dry_run_message)s"
"%(build_link)s"
"%(cq_includes)s"
"%(footers)s"
)
return commit_msg_template % {
"header": self._commit_msg_header,
"message": message,
"changelog": changelog,
"cq_includes": cq_includes,
"build_link": build_link,
"dry_run_message": dry_run_message,
"footers": footers,
}
def GetCurrentManifestPosition(self) -> Tuple[Optional[str], Optional[str]]:
"""Retrieves the positions of the current manifests.
This retrieves the pair of positions of external and internal manifests
by reading the previous uprev CL.
Returns:
Tuple of following two values:
- Git position of external manifest of the current LKGM. None if
number does not exists.
- Git position of internal manifest of the current LKGM. None if
number does not exists.
"""
current_lkgm = self._current_lkgm.VersionString()
cls = self._gerrit_helper.Query(
hashtag="chrome-lkgm",
branch="main",
status="merged",
footer=f"{GIT_FOOTER_LKGM}={current_lkgm}",
)
logging.info("found %d CLs", len(cls))
# Making the ebavior deterministic.
cls.sort(key=lambda patch: patch.gerrit_number, reverse=True)
# There should be only 1 CL, unless someone has manually created a
# duplicated uprev CL. But supporting multiple CLs just in case.
for cl in cls:
external_manifest_pos = None
internal_manifest_pos = None
for key, value in cl.footers:
if key == GIT_FOOTER_EXTERNAL_MANIFEST_POS:
external_manifest_pos = value
elif key == GIT_FOOTER_INTERNAL_MANIFEST_POS:
internal_manifest_pos = value
if external_manifest_pos or internal_manifest_pos:
return (
external_manifest_pos,
internal_manifest_pos,
)
return (None, None)
def GetCurrentLKGM(branch: str) -> chromeos_version.VersionInfo:
"""Returns the current LKGM version on the branch.
On the first call, this method retrieves the LKGM version from Gitiles
server and returns it. On subsequent calls, this method returns the
cached LKGM version.
Raises:
LKGMNotValid: if the retrieved LKGM version from the repository is
invalid.
"""
current_lkgm = gob_util.GetFileContents(
constants.CHROMIUM_GOB_URL,
constants.PATH_TO_CHROME_LKGM,
ref=branch,
)
if current_lkgm is None:
raise LKGMNotValid(
"The retrieved LKGM version from the repository is invalid:"
f" {current_lkgm}."
)
return chromeos_version.VersionInfo(current_lkgm.strip())
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(
"--message",
action="store",
help="Extra message to add to the description of the generated CL.",
)
parser.add_argument(
"--force-overriding-user",
help="[For debugging] Forcibly overrides the user to manipulate "
"Gerrit, instead of determining it from the hostname.",
)
parser.add_argument("--lkgm", 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.",
)
parser.add_argument(
"--branch",
default="main",
help="Branch to upload change to, e.g. "
"refs/branch-heads/5112. Defaults to main.",
)
parser.add_argument(
"--internal-manifest-position",
type=int,
help="Annealing commit position of the internal manifest.",
)
parser.add_argument(
"--external-manifest-position",
type=int,
help="Annealing commit position of the external manifest.",
)
return parser.parse_args(argv)
def main(argv):
opts = GetOpts(argv)
current_lkgm = GetCurrentLKGM(opts.branch)
if opts.lkgm is not None:
committer = ChromeLKGMCommitter(
opts.lkgm,
opts.branch,
current_lkgm,
opts.dryrun,
opts.buildbucket_id,
message=opts.message,
internal_manifest_position=opts.internal_manifest_position,
external_manifest_position=opts.external_manifest_position,
allow_uprev_to_older_release=(opts.branch != "main"),
)
committer.Run()
# We need to know the account used by the builder to upload git CLs when
# listing up CLs.
user_email = ""
if opts.force_overriding_user:
user_email = opts.force_overriding_user
elif hostname_util.host_is_ci_builder(golo_only=True):
user_email = "chromeos-commit-bot@chromium.org"
elif hostname_util.host_is_ci_builder(gce_only=True):
user_email = "3su6n15k.default@developer.gserviceaccount.com"
else:
raise LKGMFileNotFound("Failed to determine an appropriate user email.")
cleaner = ChromeLKGMCleaner(
opts.branch,
current_lkgm,
user_email,
opts.dryrun,
opts.buildbucket_id,
)
cleaner.Run()
return 0