diff --git a/release-tools/jiraReleaseChecker.py b/release-tools/jiraReleaseChecker.py new file mode 100755 index 0000000000..47e1e82fb3 --- /dev/null +++ b/release-tools/jiraReleaseChecker.py @@ -0,0 +1,246 @@ +#!/usr/bin/python + +#------------------------------------------------------------------------------- +# +# Usage +# ======= +# +# ./jiraReleaseChecker.py [-m mode] +# ./jiraReleaseChecker.py release-V3.1 "Corda 3.3" some.user@r3.com [-m not-in-jira] +# +# is the point prior to the current branches head in history from +# which to inspect commits. Normally this will be the tag of the previous +# release. e.g. +# +# master ---------------------------------------------- +# \ +# release/4 -----------+--------------+------------ +# / / +# release/4.0 release/4.1 +# +# The current release in the above example will be 4.2 and those commits +# extend from 4.1 having been backported from master. Thus is +# release/4.1 +# +# should refer to the version string used within +# the R3 Corda Jira to track the release. For example, for 3.3 this would be +# "Corda 3.3" +# +# should be a registered user able to authenticate with the +# R3 Jira system. Authentication and password management is handled through +# the native OS keyring implementation. +# +# The script should be run on the relevant release branch within the git +# repository. +# +# Modes +# ------- +# +# The tool can operate in 3 modes +# +# * rst - The default when omitted. Will take the combined lists +# of issues fixed from both Jira and commit summaries and +# format that list in such a way it can be included within +# the release notes for the next release. Will include hyper +# links to the R3 Jira for each ticket. +# * not-in-jira - Print a list of tickets that are included in commit +# summaries but are not tagged in Jira as fixed in the release +# * not-in-commit - Print a list of tickets that are tagged in Jira but that +# are not mentioned in any commit summary, +# +# Pre Requisites +# ================ +# +# pip +# pyjira +# gitpython +# keyring (optional) +# +# Installation +# -------------- +# Should be a simple matter of ``pip install `` +# +# Issues +# ======== +# +# Doesn't really handle many errors all that well, also gives no mechanism +# to enter a correct password into the keyring if a wrong one is added which +# isn't great but for now this should do +# +#------------------------------------------------------------------------------- + +import re +import sys +import getpass +import argparse + +try : + import keyring +except ImportError : + disableKeyring = True +else : + disableKeyring = False + +from jira import JIRA +from git import Repo + +#------------------------------------------------------------------------------- + +R3_JIRA_ADDR = "https://r3-cev.atlassian.net" +JIRA_MAX_RESULTS = 50 + +#------------------------------------------------------------------------------- + +# +# For a given user (provide via the command line) authenticate with Jira and +# return an interface object instance +# +def jiraLogin(user) : + password = keyring.get_password ('jira', user) if not disableKeyring else None + + if not password: + password = getpass.getpass("Please enter your JIRA password, " + + "it will be stored in your OS Keyring: ") + if not disableKeyring : + keyring.set_password ('jira', user, password) + + return JIRA(R3_JIRA_ADDR, auth=(user, password)) + +#------------------------------------------------------------------------------- + +# +# Cope with Jira REST API paginating query results +# +def jiraQuery (jira, query) : + offset = 0 + results = JIRA_MAX_RESULTS + rtn = [] + while (results == JIRA_MAX_RESULTS) : + issues = jira.search_issues(query, maxResults=JIRA_MAX_RESULTS, startAt=offset) + results = len(issues) + if results > 0 : + offset += JIRA_MAX_RESULTS + rtn += issues + + return rtn + +#------------------------------------------------------------------------------- + +# +# Take a Jira issue and format it in such a way we can include it as a line +# item in the release notes formatted with a hyperlink to the issue in Jira +# +def issueToRST(issue) : + return "* %s [`%s <%s/browse/%s>`_]" % ( + issue.fields.summary, + issue.key, + R3_JIRA_ADDR, + issue.key) + +#------------------------------------------------------------------------------- + +# +# Get a list of jiras from Jira where those jiras are marked as fixed +# in some specific version (set on the command line). +# +# Optionally, an already authenticated Jira connection instance can be +# provided to avoid re-authenticating. The authenticated object +# is returned for reuse. +# +def getJirasFromJira(args_, jira_ = None) : + jira = jiraLogin(args_.jiraUser) if jira_ == None else jira_ + + return jiraQuery(jira, \ + 'project in (Corda, Ent) And fixVersion in ("%s") and status in (Done)' % (args_.jiraTag)) \ + , jira + +#------------------------------------------------------------------------------- + +def getJiraIdsFromJira(args_, jira_ = None) : + jira = jiraLogin(args_.jiraUser) if jira_ == None else jira_ + + jirasFromJira, _ = jiraQuery(jira, \ + 'project in (Corda, Ent) And fixVersion in ("%s") and status in (Done)' % (args_.jiraTag)) \ + , jira + + return [ j.key for j in jirasFromJira ], jira + +#------------------------------------------------------------------------------- + +def getJiraIdsFromCommits(args_) : + jiraMatch = re.compile("(CORDA-\d+|ENT-\d+)") + repo = Repo(".", search_parent_directories = True) + + jirasFromCommits = [] + for commit in list (repo.iter_commits ("%s.." % (args_.oldTag))) : + jirasFromCommits += jiraMatch.findall(commit.summary) + + return jirasFromCommits + +#------------------------------------------------------------------------------- + +# +# Take the set of all tickets completed in a release (the union of those +# tagged in Jira and those marked in commit summaries) and format them +# for inclusion in the release notes (rst format). +# +def rst (args_) : + jiraIdsFromCommits = getJiraIdsFromCommits(args_) + jirasFromJira, jiraObj = getJirasFromJira(args_) + + jiraIdsFromJira = [ jira.key for jira in jirasFromJira ] + + # + # Grab the set of JIRA's that aren't tagged as fixed in the release but are + # mentioned in a commit and pull down the JIRA information for those so as + # to get access to their summary + # + extraJiras = set(jiraIdsFromCommits).difference(jiraIdsFromJira) + jirasFromJira += jiraQuery(jiraObj, "key in (%s)" % (", ".join(extraJiras))) + + for jira in jirasFromJira : + print issueToRST(jira) + +#------------------------------------------------------------------------------- + +def notInJira(args_) : + jiraIdsFromCommits = getJiraIdsFromCommits(args_) + jiraIdsFromJira, _ = getJiraIdsFromJira(args_) + + print 'Issues mentioned in commits but not set as "fixed in" in Jira' + + for jiraId in set(jiraIdsFromJira).difference(jiraIdsFromCommits) : + print jiraId + +#------------------------------------------------------------------------------- + +def notInCommit(args_) : + jiraIdsFromCommits = getJiraIdsFromCommits(args_) + jiraIdsFromJira, _ = getJiraIdsFromJira(args_) + + print 'Issues tagged in Jira as fixed but not mentioned in any commit summary' + + for jiraId in set(jiraIdsFromCommits).difference(jiraIdsFromJira) : + print jiraId + +#------------------------------------------------------------------------------- + +if __name__ == "__main__" : + parser = argparse.ArgumentParser() + parser.add_argument("-m", "--mode", help="display a square of a given number", + choices = [ "rst", "not-in-jira", "not-in-commit"]) + parser.add_argument("oldTag", help="The previous release tag") + parser.add_argument("jiraTag", help="The current Jira release") + parser.add_argument("jiraUser", help="Who to authenticate with Jira as") + + args = parser.parse_args() + + if not args.mode : args.mode = "rst" + + if args.mode == "rst" : rst(args) + elif args.mode == "not-in-jira" : notInJira(args) + elif args.mode == "not-in-commit" : notInCommit(args) + +#------------------------------------------------------------------------------- + +