diff options
Diffstat (limited to 'python/mozrelease/mozrelease/buglist_creator.py')
-rw-r--r-- | python/mozrelease/mozrelease/buglist_creator.py | 261 |
1 files changed, 261 insertions, 0 deletions
diff --git a/python/mozrelease/mozrelease/buglist_creator.py b/python/mozrelease/mozrelease/buglist_creator.py new file mode 100644 index 0000000000..8c7b8d0391 --- /dev/null +++ b/python/mozrelease/mozrelease/buglist_creator.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import logging +import os +import re +from operator import itemgetter + +import requests +from mozilla_version.gecko import GeckoVersion +from taskcluster import Notify, optionsFromEnvironment + +BUGLIST_TEMPLATE = "* [Bugs since previous changeset]({url})\n" +BACKOUT_REGEX = re.compile(r"back(\s?)out|backed out|backing out", re.IGNORECASE) +BACKOUT_TEMPLATE = "* [Backouts since previous changeset]({url})\n" +BUGZILLA_BUGLIST_TEMPLATE = "https://bugzilla.mozilla.org/buglist.cgi?bug_id={bugs}" +BUG_NUMBER_REGEX = re.compile(r"bug \d+", re.IGNORECASE) +CHANGELOG_TO_FROM_STRING = "{product}_{version}_RELEASE" +CHANGESET_URL_TEMPLATE = ( + "{repo}/{logtype}" "?rev={to_version}+%25+{from_version}&revcount=1000" +) +FULL_CHANGESET_TEMPLATE = "* [Full Mercurial changelog]({url})\n" +LIST_DESCRIPTION_TEMPLATE = "Comparing Mercurial tag {from_version} to {to_version}:\n" +MAX_BUGS_IN_BUGLIST = 250 +MERCURIAL_TAGS_URL_TEMPLATE = "{repo}/json-tags" +NO_BUGS = "" # Return this when bug list can't be created +URL_SHORTENER_TEMPLATE = "https://bugzilla.mozilla.org/rest/bitly/shorten?url={url}" + +log = logging.getLogger(__name__) + + +def create_bugs_url(product, current_version, current_revision, repo=None): + """ + Creates list of bugs and backout bugs for release-drivers email + + :param release: dict -> containing information about release, from Ship-It + :return: str -> description of compared releases, with Bugzilla links + containing all bugs in changeset + """ + try: + # Extract the important data, ignore if beta1 release + if current_version.beta_number == 1: + # If the version is beta 1, don't make any links + return NO_BUGS + + if repo is None: + repo = get_repo_by_version(current_version) + # Get the tag version, for display purposes + current_version_tag = tag_version(product, current_version) + + # Get all Hg tags for this branch, determine the previous version + tag_url = MERCURIAL_TAGS_URL_TEMPLATE.format(repo=repo) + mercurial_tags_json = requests.get(tag_url).json() + previous_version_tag = get_previous_tag_version( + product, current_version, current_version_tag, mercurial_tags_json + ) + + # Get the changeset between these versions, parse for all unique bugs and backout bugs + resp = requests.get( + CHANGESET_URL_TEMPLATE.format( + repo=repo, + from_version=previous_version_tag, + to_version=current_revision, + logtype="json-log", + ) + ) + changeset_data = resp.json() + unique_bugs, unique_backout_bugs = get_bugs_in_changeset(changeset_data) + + # Return a descriptive string with links if any relevant bugs are found + if unique_bugs or unique_backout_bugs: + description = LIST_DESCRIPTION_TEMPLATE.format( + from_version=previous_version_tag, to_version=current_version_tag + ) + + if unique_bugs: + description += BUGLIST_TEMPLATE.format( + url=create_buglist_url(unique_bugs) + ) + if unique_backout_bugs: + description += BACKOUT_TEMPLATE.format( + url=create_buglist_url(unique_backout_bugs) + ) + + changeset_html = CHANGESET_URL_TEMPLATE.format( + repo=repo, + from_version=previous_version_tag, + to_version=current_revision, + logtype="log", + ) + description += FULL_CHANGESET_TEMPLATE.format(url=changeset_html) + + return description + else: + return NO_BUGS + + except Exception as err: + log.info(err) + return NO_BUGS + + +def get_bugs_in_changeset(changeset_data): + unique_bugs, unique_backout_bugs = set(), set() + for changeset in changeset_data["entries"]: + if is_excluded_change(changeset): + continue + + changeset_desc = changeset["desc"] + bug_re = BUG_NUMBER_REGEX.search(changeset_desc) + + if bug_re: + bug_number = bug_re.group().split(" ")[1] + + if is_backout_bug(changeset_desc): + unique_backout_bugs.add(bug_number) + else: + unique_bugs.add(bug_number) + + return unique_bugs, unique_backout_bugs + + +def is_excluded_change(changeset): + excluded_change_keywords = [ + "a=test-only", + "a=release", + ] + return any(keyword in changeset["desc"] for keyword in excluded_change_keywords) + + +def is_backout_bug(changeset_description): + return bool(BACKOUT_REGEX.search(changeset_description)) + + +def create_buglist_url(buglist): + return BUGZILLA_BUGLIST_TEMPLATE.format(bugs="%2C".join(buglist)) + + +def tag_version(product, version): + underscore_version = str(version).replace(".", "_") + return CHANGELOG_TO_FROM_STRING.format( + product=product.upper(), version=underscore_version + ) + + +def parse_tag_version(tag): + dot_version = ".".join(tag.split("_")[1:-1]) + return GeckoVersion.parse(dot_version) + + +def get_previous_tag_version( + product, + current_version, + current_version_tag, + mercurial_tags_json, +): + """ + Gets the previous hg version tag for the product and branch, given the current version tag + """ + + def _invalid_tag_filter(tag): + """Filters by product and removes incorrect major version + base, end releases""" + prod_major_version_re = r"^{product}_{major_version}".format( + product=product.upper(), major_version=current_version.major_number + ) + + return ( + "BASE" not in tag + and "END" not in tag + and "RELEASE" in tag + and re.match(prod_major_version_re, tag) + ) + + # Get rid of irrelevant tags, sort by date and extract the tag string + tags = { + (parse_tag_version(item["tag"]), item["tag"]) + for item in mercurial_tags_json["tags"] + if _invalid_tag_filter(item["tag"]) + } + # Add the current version to the list + tags.add((current_version, current_version_tag)) + tags = sorted(tags, key=lambda tag: tag[0]) + + # Find where the current version is and go back one to get the previous version + next_version_index = list(map(itemgetter(0), tags)).index(current_version) - 1 + + return tags[next_version_index][1] + + +def get_repo_by_version(version): + """ + Get the repo a given version is found on. + """ + if version.is_beta: + return "https://hg.mozilla.org/releases/mozilla-beta" + elif version.is_release: + return "https://hg.mozilla.org/releases/mozilla-release" + elif version.is_esr: + return "https://hg.mozilla.org/releases/mozilla-esr{}".format( + version.major_number + ) + else: + raise Exception( + "Unsupported version type {}: {}".format(version.version_type.name, version) + ) + + +def email_release_drivers( + addresses, + product, + version, + build_number, + repo, + revision, + task_group_id, +): + # Send an email to the mailing after the build + email_buglist_string = create_bugs_url(product, version, revision, repo=repo) + + content = """\ +A new build has been started: + +Commit: [{revision}]({repo}/rev/{revision}) +Task group: [{task_group_id}]({root_url}/tasks/groups/{task_group_id}) + +{email_buglist_string} +""".format( + repo=repo, + revision=revision, + root_url=os.environ["TASKCLUSTER_ROOT_URL"], + task_group_id=task_group_id, + email_buglist_string=email_buglist_string, + ) + + # On r-d, we prefix the subject of the email in order to simplify filtering + subject_prefix = "" + if product in {"fennec"}: + subject_prefix = "[mobile] " + if product in {"firefox", "devedition"}: + subject_prefix = "[desktop] " + + subject = "{} Build of {} {} build {}".format( + subject_prefix, product, version, build_number + ) + + # use proxy if configured, otherwise local credentials from env vars + if "TASKCLUSTER_PROXY_URL" in os.environ: + notify_options = {"rootUrl": os.environ["TASKCLUSTER_PROXY_URL"]} + else: + notify_options = optionsFromEnvironment() + + notify = Notify(notify_options) + for address in addresses: + notify.email( + { + "address": address, + "subject": subject, + "content": content, + } + ) |