Last active
December 4, 2020 08:07
-
-
Save qrkourier/b33af7fc9825b2d694d8f2d9fb0a075c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# | |
# blame(@qrkourier) | |
# | |
from jira import JIRA | |
import os | |
import sys | |
from argparse import ArgumentParser | |
import webbrowser | |
def unblocked(project, issues): | |
""" | |
issues that need attention because they are new or were blocked | |
""" | |
interesting = list() | |
for i in issues: | |
# nested try/except here is used as a control structure to exclude uninteresting issues that | |
# are blocked by an issue that is not done because I didn't know of another way to break out | |
# of a particular level of a nested loop | |
try: | |
if i['fields']['issuetype']['name'] not in ['Epic', 'Sub-task']: | |
issuelinks = i['fields']['issuelinks'] | |
if len(issuelinks) > 0: | |
try: | |
for l in issuelinks: | |
for d in ['inward','outward']: | |
if l['type'][d] == "is blocked by" \ | |
and d+'Issue' in l \ | |
and l[d+'Issue']['fields']['status']['statusCategory']['name'] != "Done": | |
raise Exception("linked issue not done") | |
except Exception as e: | |
raise Exception("Not interesting: ", e) | |
# interesting if no links or previously blocked by a linked issue that is now done | |
interesting += [i['key']] | |
except Exception as e: | |
#print("SKIP "+i['key']) | |
pass | |
return(interesting) | |
def blocking(project, issues): | |
""" | |
issues that need attention because they are blocking another project | |
""" | |
interesting = list() | |
for i in issues: | |
try: | |
issuelinks = i['fields']['issuelinks'] | |
if len(issuelinks) > 0: | |
for l in issuelinks: | |
for d in ['inward','outward']: | |
if l['type'][d] == "blocks" \ | |
and d+'Issue' in l \ | |
and l[d+'Issue']['fields']['status']['statusCategory']['name'] != "Done" \ | |
and l[d+'Issue']['key'].split('-')[0] != project.upper(): | |
interesting += [i['key']] | |
else: | |
raise Exception("Not interesting: no issue links") | |
except Exception as e: | |
#print("SKIP "+i['key']) | |
pass | |
return(interesting) | |
def blocked(project, issues): | |
""" | |
issues that need attention because they are blocked | |
""" | |
interesting = list() | |
for i in issues: | |
issuelinks = i['fields']['issuelinks'] | |
if len(issuelinks) > 0: | |
for l in issuelinks: | |
for d in ['inward','outward']: | |
if l['type'][d] == "is blocked by" \ | |
and d+'Issue' in l \ | |
and l[d+'Issue']['fields']['status']['statusCategory']['name'] != "Done": | |
interesting += [i['key']] | |
return(interesting) | |
def makeFilterQuery(keys): | |
if len(keys) > 0: | |
query = "issuekey in ("+keys[0] | |
for i in keys[1:]: | |
query += ","+i | |
query += ")" | |
else: | |
query = "project is EMPTY" | |
return(query) | |
def excludeLabels(labels, issues): | |
uninteresting = list() | |
for i in issues: | |
for l in labels: | |
if l in i['fields']['labels']: | |
uninteresting += [i['key']] | |
interesting = list() | |
for i in issues: | |
if i['key'] not in uninteresting: | |
interesting += [i] | |
return(interesting) | |
PARSER = ArgumentParser() | |
PARSER.add_argument( | |
'jira_url', | |
help='JIRA URL e.g. "https://example.atlassian.net"' | |
) | |
PARSER.add_argument( | |
'project', | |
help='JIRA project e.g. "TOW"' | |
) | |
PARSER.add_argument( | |
'policy', | |
help='one policy that decides which issues are interesting', | |
choices=['unblocked','blocked','blocking'] | |
) | |
PARSER.add_argument( | |
'--exclude-labels', | |
dest='exclude_labels', | |
nargs='+', | |
help='Exclude issues with one or more labels' | |
) | |
ACTIONS.add_argument( | |
'--no-list', | |
dest='list_results', | |
action='store_false', | |
default=True, | |
help='suppress printing the URL for each issue' | |
) | |
ACTIONS = PARSER.add_argument_group(title='ACTIONS') | |
ACTIONS.add_argument( | |
'--filter-id', | |
dest='filter_id', | |
type=int, | |
help='Numeric ID of the JIRA issue filter to update with the list of issues' | |
) | |
ACTIONS.add_argument( | |
'--open', | |
dest='open_browser', | |
action='store_true', | |
default=False, | |
help='open JIRA in a web browser for each issue' | |
) | |
ARGS = PARSER.parse_args() | |
JIRA_URL = ARGS.jira_url | |
J = JIRA( | |
{'server': JIRA_URL}, | |
basic_auth=( | |
os.environ['ATLASSIAN_API_USER'], | |
os.environ['ATLASSIAN_API_TOKEN'] | |
) | |
) | |
JQL = 'project = "'+ARGS.project.upper()+'" AND resolution = Unresolved AND Sprint is EMPTY' | |
DATA = J.search_issues(JQL, json_result=True) | |
ISSUES = DATA['issues'] | |
if ARGS.exclude_labels: | |
ISSUES = excludeLabels(ARGS.exclude_labels, ISSUES) | |
# call the eponymous function for the given policy e.g. "unblocked", | |
# passing in the dict of potentially interesting ISSUES | |
INTERESTING = globals()[ARGS.policy](ARGS.project, ISSUES) | |
# we'll set False if we do anything interesting so we can complain if nothing was done | |
NOOP=True | |
if ARGS.list_results: | |
# print the policy as a heading with issue count | |
print('\nfound '+str(len(INTERESTING))+' for '+ARGS.policy.upper()) | |
# slice the list of issues into batches | |
for i in INTERESTING: | |
u = '\t'+JIRA_URL+'/browse/'+i | |
print('\t'+u) | |
NOOP=False | |
if ARGS.filter_id: | |
# build a query like "issuekey in (TOW-123, TOW-456)" from the INTERESTING results | |
filter_query = makeFilterQuery(INTERESTING) | |
try: | |
# update the filter with the new query which is a list of issue keys | |
filter_result = J.update_filter( | |
filter_id=ARGS.filter_id, | |
name=ARGS.project.upper()+' '+ARGS.policy, | |
description="updated by /systems-tools/scripts/backlog.py", | |
jql=filter_query | |
) | |
print('\nsent '+str(len(INTERESTING))+' to '+JIRA_URL+'/issues/?filter='+str(ARGS.filter_id)) | |
except: | |
raise | |
NOOP=False | |
if ARGS.open_browser: | |
# print the policy as a heading with issue count | |
print('\nopening '+str(len(INTERESTING))+' for '+ARGS.policy.upper()) | |
# slice the list of issues into batches | |
batches = [INTERESTING[i:i + 4] for i in range(0, len(INTERESTING), 4)] | |
for b in batches: | |
for i in b: | |
u = '\t'+JIRA_URL+'/browse/'+i | |
print('\t'+u) | |
webbrowser.open(u, new = 2) | |
input("Press Enter to continue...") | |
NOOP=False | |
if NOOP: | |
print("\nNothing was done.\n") | |
PARSER.print_help() |
Author
qrkourier
commented
Sep 4, 2019
•
./backlog.py --help
usage: backlog.py [-h] [--exclude-labels EXCLUDE_LABELS [EXCLUDE_LABELS ...]]
[--filter-id FILTER_ID] [--open] [--no-list]
jira_url project {unblocked,blocked,blocking}
positional arguments:
jira_url JIRA URL e.g. "https://example.atlassian.net"
project JIRA project e.g. "TOW"
{unblocked,blocked,blocking}
one policy that decides which issues are interesting
optional arguments:
-h, --help show this help message and exit
--exclude-labels EXCLUDE_LABELS [EXCLUDE_LABELS ...]
Exclude issues with one or more labels
--no-list suppress printing the URL for each issue
ACTIONS:
--filter-id FILTER_ID
Numeric ID of the JIRA issue filter to update with the
list of issues
--open open JIRA in a web browser for each issue
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment