def enumerate_pending(jira): since = datetime.datetime.now() - datetime.timedelta(days=7) jql = default_jql() jql += "AND status = 'In Progress' \ AND issuetype != Initiative \ AND issuetype != Epic" vprint(jql) my_issues = jira.search_issues(jql, expand="changelog", fields="summary,assignee,created") if my_issues.total > my_issues.maxResults: my_issues = jira.search_issues(jql, expand="changelog", fields="summary,assignee,created", maxResults=my_issues.total) for issue in my_issues: status = {} status['issue'] = str(issue) if issue.fields.assignee: status['assignee'] = issue.fields.assignee.displayName else: status['assignee'] = 'Unassigned' status['summary'] = issue.fields.summary created = datetime.datetime.strptime(issue.fields.created, '%Y-%m-%dT%H:%M:%S.%f%z') status['new'] = created.replace(tzinfo=None) > since yield (status)
def enumerate_pending(jira): user = cfg.args.user since = datetime.datetime.now() - datetime.timedelta(days=7) jql = "(project = QLT OR assignee in membersOf('linaro-landing-team-qualcomm')) \ AND status = 'In Progress' \ AND issuetype != Initiative \ AND issuetype != Epic" if user: jql += ' AND assignee = "%s"' % add_domain(user) vprint(jql) my_issues = jira.search_issues(jql, expand="changelog", fields="summary,assignee,created") if my_issues.total > my_issues.maxResults: my_issues = jira.search_issues(jql, expand="changelog", fields="summary,assignee,created", maxResults=my_issues.total) for issue in my_issues: status = {} status['issue'] = str(issue) if issue.fields.assignee: status['assignee'] = issue.fields.assignee.displayName else: status['assignee'] = 'Unassigned' status['summary'] = issue.fields.summary created = datetime.datetime.strptime(issue.fields.created, '%Y-%m-%dT%H:%M:%S.%f%z') status['new'] = created.replace(tzinfo=None) > since yield(status)
def enumerate_updates(jira): since = datetime.datetime.now() - datetime.timedelta( days=int(cfg.args.days)) jql = default_jql() jql += "AND updatedDate > -%sd" % cfg.args.days vprint(jql) my_issues = jira.search_issues(jql, expand="changelog", fields="summary,comment,assignee,created") if my_issues.total > my_issues.maxResults: my_issues = jira.search_issues( jql, expand="changelog", fields="summary,comment,assignee,created", maxResults=my_issues.total) for issue in my_issues: changelog = issue.changelog comments = issue.fields.comment.comments status = {} status['issue'] = str(issue) if issue.fields.assignee: status['assignee'] = issue.fields.assignee.displayName else: status['assignee'] = 'Unassigned' status['summary'] = issue.fields.summary status['comments'] = [] status['resolution'] = None created = datetime.datetime.strptime(issue.fields.created, '%Y-%m-%dT%H:%M:%S.%f%z') if created.replace(tzinfo=None) > since: status['resolution'] = 'Created' for comment in comments: when = datetime.datetime.strptime(comment.created, '%Y-%m-%dT%H:%M:%S.%f%z') if when.replace(tzinfo=None) < since: continue status['comments'].append(comment.body) for history in changelog.histories: when = datetime.datetime.strptime(history.created, '%Y-%m-%dT%H:%M:%S.%f%z') if when.replace(tzinfo=None) < since: continue for item in history.items: if item.field == 'resolution': status['resolution'] = item.toString if len(status['comments']) != 0 or status['resolution']: yield (status)
def open_file(filename): """ This will open the user provided file and if there has not been any file provided it will create and open a temporary file instead. """ vprint("filename: %s\n" % filename) if filename: return open(filename, "w") else: return tempfile.NamedTemporaryFile(mode='w+t', delete=False)
def initiate_config(): """ Reads the config file (yaml format) and returns the sets the global instance. """ cfg.config_file = get_config_file() if not os.path.isfile(cfg.config_file): create_default_config() vprint("Using config file: %s" % cfg.config_file) with open(cfg.config_file, 'r') as yml: cfg.yml_config = yaml.load(yml)
def build_epics_node(jira, epic_key, d_handled=None, initiative_node=None): ei = jira.issue(epic_key) if ei.fields.status.name in ["Closed", "Resolved"]: d_handled[str(ei.key)] = [None, ei] return None summary = str(ei.fields.summary.encode('ascii', 'ignore').decode()) epic = Node(str(ei.key), summary, str(ei.fields.issuetype)) try: assignee = str( ei.fields.assignee.displayName.encode('ascii', 'ignore').decode()) except AttributeError: assignee = str(ei.fields.assignee) epic.add_assignee(assignee) epic.set_state(str(ei.fields.status.name)) try: sponsors = ei.fields.customfield_10101 if sponsors is not None: for s in sponsors: epic.add_sponsor(str(s.value)) except AttributeError: epic.add_sponsor("No sponsor") epic.set_base_url(cfg.server) if initiative_node is not None: epic.add_parent(initiative_node.get_key()) initiative_node.add_child(epic) else: # This cateches when people are not using implements/implemented by, but # there is atleast an "Initiative" link that we can use. parent = get_parent_key(jira, ei) if parent is not None and parent in d_handled: parent_node = d_handled[parent][0] if parent_node is not None: epic.add_parent(parent_node) parent_node.add_child(epic) else: vprint("Didn't find any parent") d_handled[epic.get_key()] = [epic, ei] # Deal with stories for link in ei.fields.issuelinks: if "inwardIssue" in link.raw: story_key = str(link.inwardIssue.key) build_story_node(jira, story_key, d_handled, epic) print(epic) return epic
def write_last_jira_comment(f, jira, issue): """ Pulls the last comment from Jira from an issue and writes it to the file object. """ c = jira.comments(issue) if len(c) > 0: try: comment = "# Last comment:\n# ---8<---\n# %s\n# --->8---\n" % \ "\n# ".join(c[-1].body.splitlines()) f.write(comment) except UnicodeEncodeError: vprint("Can't encode character")
def update_jira(jira, i, c): """ This is the function that do the actual updates to Jira and in this case it is adding comments to a certain issue. """ vprint("Updating Jira issue: %s with comment:" % i) vprint( "-- 8< --------------------------------------------------------------------------" ) vprint("%s" % c) vprint( "-- >8 --------------------------------------------------------------------------\n\n" ) if not cfg.args.dry_run: jira.add_comment(i, c)
def build_story_node(jira, story_key, d_handled=None, epic_node=None): si = jira.issue(story_key) if si.fields.status.name in ["Closed", "Resolved"]: d_handled[str(si.key)] = [None, si] return None # To prevent UnicodeEncodeError ignore unicode summary = str(si.fields.summary.encode('ascii', 'ignore').decode()) story = Node(str(si.key), summary, str(si.fields.issuetype)) try: assignee = str( si.fields.assignee.displayName.encode('ascii', 'ignore').decode()) except AttributeError: assignee = str(si.fields.assignee) story.add_assignee(assignee) story.set_state(str(si.fields.status.name)) story.set_base_url(cfg.server) if epic_node is not None: story.add_parent(epic_node.get_key()) epic_node.add_child(story) else: # This cateches when people are not using implements/implemented by, but # there is atleast an "Epic" link that we can use. parent = get_parent_key(jira, si) if parent is not None and parent in d_handled: parent_node = d_handled[parent][0] if parent_node is not None: story.add_parent(parent_node) parent_node.add_child(story) else: vprint("Didn't find any parent") print(story) d_handled[story.get_key()] = [story, si] return story
def build_orphans_tree(jira, key, d_handled): jql = "project=%s" % (key) all_issues = jira.search_issues(jql) orphans_initiatives = [] orphans_epics = [] orphans_stories = [] for i in all_issues: if str(i.key) not in d_handled: if i.fields.status.name in ["Closed", "Resolved"]: continue else: if i.fields.issuetype.name == "Initiative": orphans_initiatives.append(i) elif i.fields.issuetype.name == "Epic": orphans_epics.append(i) elif i.fields.issuetype.name == "Story": orphans_stories.append(i) # Now we three list of Jira tickets not touched before, let's go over them # staring with Initiatives, then Epics and last Stories. By doing so we # should get them nicely layed out in the orphan part of the tree. nodes = [] vprint("Orphan Initiatives ...") for i in orphans_initiatives: node = build_initiatives_node(jira, i, d_handled) nodes.append(node) vprint("Orphan Epics ...") for i in orphans_epics: node = build_epics_node(jira, str(i.key), d_handled) nodes.append(node) vprint("Orphan Stories ...") for i in orphans_stories: node = build_story_node(jira, str(i.key), d_handled) nodes.append(node) return nodes
def parse_status_file(jira, filename): """ The main parsing function, which will decide what should go into the actual Jira call. This for example removes the beginning until it finds a standalone [ISSUE] tag. It will also remove all comments prefixed with '#'. """ # Regexp to match Jira issue on a single line, i.e: # [SWG-28] # [LITE-32] # ... regex = r"^\[([A-Z]+-[0-9]+).*\]\n$" # Regexp to match a tag that indicates we should stop processing, ex: # [STOP] # [JIPDATE-STOP] # [OTHER] # ... regex_stop = r"^\[.*\]\n$" # Regexp to mach a tag that indicates to stop processing completely: # [FIN] regex_fin = r"^\[FIN\]\n$" # Contains the status text, it could be a file or a status email status = "" with open(filename) as f: status = f.readlines() myissue = "" mycomment = "" # build list of {issue-key,comment} tuples found in status issue_comments = [] for line in status: # New issue? match = re.search(regex, line) if match: myissue = match.group(1) validissue = True try: issue = jira.issue(myissue) except Exception as e: if 'Issue Does Not Exist' in e.text: print('[{}] : {}'.format(myissue, e.text)) validissue = False if validissue: issue_comments.append((myissue, "")) # Stop parsing entirely. This needs to be placed before regex_stop # or the .* will match and [FIN] won't be processed elif re.search(regex_fin, line): break # If we have non-JIRA issue tags, stop parsing until we find a valid tag elif re.search(regex_stop, line): validissue = False else: # Don't add lines with comments if (line[0] != "#" and issue_comments and validissue): (i, c) = issue_comments[-1] issue_comments[-1] = (i, c + line) issue_upload = [] print("These JIRA cards will be updated as follows:\n") for (idx, t) in enumerate(issue_comments): (issue, comment) = issue_comments[idx] # Strip beginning and trailing blank lines comment = comment.strip('\n') if comment == "": vprint("Issue [%s] has no comment, not updating the issue" % (issue)) continue issue_upload.append((issue, comment)) print("[%s]\n %s" % (issue, "\n ".join(comment.splitlines()))) print("") issue_comments = issue_upload if issue_comments == [] or cfg.args.dry_run or should_update() == "n": if issue_comments == []: print("No change, Jira was not updated!\n") else: print("Comments will not be written to Jira!\n") if not cfg.args.s: print_status(status) sys.exit() # if we found something, let's update jira for (issue, comment) in issue_comments: update_jira(jira, issue, comment) print("Successfully updated your Jira tickets!\n") if not cfg.args.s: print_status(status)
def get_jira_issues(jira, username): """ Query Jira and then creates a status update file (either temporary or named) containing all information found from the JQL query. """ exclude_stories = cfg.args.x epics_only = cfg.args.e all_status = cfg.args.all filename = cfg.args.file user = cfg.args.user last_comment = cfg.args.l issue_types = ["Epic"] if not epics_only: issue_types.append("Initiative") if not exclude_stories: issue_types.append("Story") issue_type = "issuetype in (%s)" % ", ".join(issue_types) status = "status in (\"In Progress\")" if all_status: status = "status not in (Resolved, Closed)" if user is None: user = "******" else: user = "******"%s\"" % add_domain(user) jql = "%s AND assignee = %s AND %s" % (issue_type, user, status) vprint(jql) my_issues = jira.search_issues(jql) showdate = strftime("%Y-%m-%d", gmtime()) subject = "Subject: [Weekly] Week ending " + showdate + "\n\n" msg = get_header() if msg != "": msg += email_to_name(username) + "\n\n" f = open_file(filename) filename = f.name f.write(subject) f.write(msg) vprint("Found issue:") for issue in my_issues: vprint("%s : %s" % (issue, issue.fields.summary)) if (merge_issue_header()): f.write("[%s%s%s]\n" % (issue, get_header_separator(), issue.fields.summary)) else: f.write("[%s]\n" % issue) f.write("# Header: %s\n" % issue.fields.summary) f.write("# Type: %s\n" % issue.fields.issuetype) f.write("# Status: %s\n" % issue.fields.status) f.write(get_extra_comments()) if last_comment: write_last_jira_comment(f, jira, issue) f.write("\n") f.close() return filename
def parse_status_file(jira, filename, issues): """ The main parsing function, which will decide what should go into the actual Jira call. This for example removes the beginning until it finds a standalone [ISSUE] tag. It will also remove all comments prefixed with '#'. """ # Regexp to match Jira issue on a single line, i.e: # [SWG-28] # [LITE-32] # ... regex = r"^\[([A-Z]+-[0-9]+).*\]\n$" # Regexp to match a tag that indicates we should stop processing, ex: # [STOP] # [JIPDATE-STOP] # [OTHER] # ... regex_stop = r"^\[.*\]\n$" # Regexp to mach a tag that indicates to stop processing completely: # [FIN] regex_fin = r"^\[FIN\]\n$" # Regexp to match for a status update, this will remove 'Status' from the # match: regex_status = r'(?:^Status:) *(.+)\n$' # Contains the status text, it could be a file or a status email status = "" # List of resolutions (when doing a transition to Resolved). Query once globally. resolution_map = dict([(t.name.title(), t.id) for t in jira.resolutions()]) with open(filename) as f: status = f.readlines() myissue = "" mycomment = "" # build list of {issue,comment} tuples found in status issue_comments = [] for line in status: # New issue? match = re.search(regex, line) # Evaluate and save the transition regex for later. We have to do this # here, since we cannot assign and save the variable in the if # construction as you can do in C for example. transition = re.search(regex_status, line) if match: myissue = match.group(1) validissue = True # if we ran a query, we might already have fetched the issue # let's try to find the issue there first, otherwise ask Jira try: issue = [x for x in issues if str(x) == myissue][0] issue_comments.append((issue, "", "")) # IndexError: we had fetched already, but issue is not found # TypeError: issues is None, we haven't queried Jira yet, at all except (IndexError, TypeError) as e: try: issue = jira.issue(myissue) issue_comments.append((issue, "", "")) except Exception as e: if 'Issue Does Not Exist' in e.text: print('[{}] : {}'.format(myissue, e.text)) validissue = False # Stop parsing entirely. This needs to be placed before regex_stop # or the .* will match and [FIN] won't be processed elif re.search(regex_fin, line): break # If we have non-JIRA issue tags, stop parsing until we find a valid tag elif re.search(regex_stop, line): validissue = False elif transition and validissue: # If we have a match, then the new status should be first in the # group. Jira always expect the name of the state transitions to be # word capitalized, hence the call to the title() function. This # means that it doesn't matter if the user enter all lower case, # mixed or all upper case. All of them will work. new_status = transition.groups()[0].title() (i, c, _) = issue_comments[-1] issue_comments[-1] = (i, c, new_status) else: # Don't add lines with comments if (line[0] != "#" and issue_comments and validissue): (i, c, t) = issue_comments[-1] issue_comments[-1] = (i, c + line, t) issue_upload = [] print("These JIRA cards will be updated as follows:\n") for (idx, t) in enumerate(issue_comments): (issue, comment, transition) = issue_comments[idx] # Strip beginning and trailing blank lines comment = comment.strip('\n') # initialize here to avoid unassigned variables and useless code complexity resolution_id = transition_id = None resolution = transition_summary = "" if transition != "" and transition != str(issue.fields.status): # An optional 'resolution' attribute can be set when doing a transition # to Resolved, using the following pattern: Resolved / <resolution> if transition.startswith('Resolved') and '/' in transition: (transition, resolution) = map(str.strip, transition.split('/')) if not resolution in resolution_map: print("Invalid resolution \"{}\" for issue {}".format( resolution, issue)) print("Possible resolution: {}".format( [t for t in resolution_map])) sys.exit(1) resolution_id = resolution_map[resolution] transition_map = dict([(t['name'].title(), t['id']) for t in jira.transitions(issue)]) if not transition in transition_map: print("Invalid transition \"{}\" for issue {}".format( transition, issue)) print("Possible transitions: {}".format( [t for t in transition_map])) sys.exit(1) transition_id = transition_map[transition] if resolution: transition_summary = " %s => %s (%s)" % ( issue.fields.status, transition, resolution) else: transition_summary = " %s => %s" % (issue.fields.status, transition) if comment == "" and not transition_id: vprint( "Issue [%s] has no comment or transitions, not updating the issue" % (issue)) continue issue_upload.append((issue, comment, { 'transition': transition_id, 'resolution': resolution_id })) print("[%s]%s\n %s" % (issue, transition_summary, "\n ".join(comment.splitlines()))) print("") issue_comments = issue_upload if issue_comments == [] or cfg.args.dry_run or should_update() == "n": if issue_comments == []: print("No change, Jira was not updated!\n") else: print("Comments will not be written to Jira!\n") if not cfg.args.s: print_status(status) sys.exit() # if we found something, let's update jira for (issue, comment, transition) in issue_comments: update_jira(jira, issue, comment, transition) print("Successfully updated your Jira tickets!\n") if not cfg.args.s: print_status(status)
def update_jira(jira, i, c, t): """ This is the function that do the actual updates to Jira and in this case it is adding comments to a certain issue. """ if t['transition']: if t['resolution']: vprint("Updating Jira issue: %s with transition: %s (%s)" % (i, t['transition'], t['resolution'])) jira.transition_issue( i, t['transition'], fields={'resolution': { 'id': t['resolution'] }}) else: vprint("Updating Jira issue: %s with transition: %s" % (i, t['transition'])) jira.transition_issue(i, t['transition']) if c != "": vprint("Updating Jira issue: %s with comment:" % i) vprint( "-- 8< --------------------------------------------------------------------------" ) vprint("%s" % c) vprint( "-- >8 --------------------------------------------------------------------------\n\n" ) jira.add_comment(i, c)