def updateIssues(issuelist, NEXTorDOTX, description): numExistingIssues = len(issuelist) if not issuelist == None else 0 if numExistingIssues > 0 : if debug: print "[DEBUG] Move " + str(numExistingIssues) + " " + description jira = JIRA(options={'server':jiraserver}, basic_auth=(jirauser, jirapwd)) cnt = 0 for s in issuelist : key = components.getText(components.findChildNodeByName(s, 'key').childNodes) issue = jira.issue(key) cnt += 1 doThisJIRA = True whichLabelSkipped = "" for label in issue.fields.labels: for skipLabel in skipLabels: if label == skipLabel.strip(): whichLabelSkipped = label doThisJIRA = False linkURL = components.getText(components.findChildNodeByName(s, 'link').childNodes) summary = components.getText(components.findChildNodeByName(s, 'summary').childNodes).strip() operation = " + [" + str(cnt) + "/" + str(len(issuelist)) + "] Update " + linkURL + " : " + summary if debug: operation = operation + " :: " + str(issue.fields.labels) if doThisJIRA == False: operation = " - [" + str(cnt) + "/" + str(len(issuelist)) + "] -Skip- " + linkURL + " (" + whichLabelSkipped + ") : " + summary print operation else: if options.autoApplyChanges or options.dryrun: print operation yesno = "" else: yesno = raw_input(operation + " ? [y/N] ") if options.autoApplyChanges or yesno.capitalize() in ["Y"]: # move issue to next fixversion if components.findChildNodeByName(s, 'project').attributes["key"].value == "JBIDE": # JBIDE or JBDS fixversion = version_jbt fixversion_NEXT = version_jbt_NEXT if NEXTorDOTX else version_jbt_DOTX else: fixversion = version_ds fixversion_NEXT = version_ds_NEXT if NEXTorDOTX else version_ds_DOTX fixVersions = [] # NOTE: if there is more than one fixversion, the others will not be changed for version in issue.fields.fixVersions: if version.name != fixversion: fixVersions.append({'name': version.name}) fixVersions.append({'name': fixversion_NEXT}) issue.update(fields={'fixVersions': fixVersions}) # only for NEXT, not for .x if NEXTorDOTX: # move issue to new sprint jira.add_issues_to_sprint(sprintId_NEXT, [key]) jira.add_comment(key, "[checkUnresolvedIssues.py] Slip to fixversion = *" + fixversion_NEXT + "* and sprint = *" + sprint_NEXT + "*") else: jira.add_comment(key, "[checkUnresolvedIssues.py] Slip to fixversion = *" + fixversion_NEXT + "*")
def updateIssues(issuelist, NEXTorDOTX, description): numExistingIssues = len(issuelist) if not issuelist == None else 0 if numExistingIssues > 0 : if debug: print "[DEBUG] Move " + str(numExistingIssues) + " " + description jira = JIRA(options={'server':jiraserver}, basic_auth=(jirauser, jirapwd)) cnt = 0 for s in issuelist : key = components.getText(components.findChildNodeByName(s, 'key').childNodes) issue = jira.issue(key) cnt += 1 doThisJIRA = True whichLabelSkipped = "" for label in issue.fields.labels: for skipLabel in skipLabels: if label == skipLabel.strip(): whichLabelSkipped = label doThisJIRA = False linkURL = components.getText(components.findChildNodeByName(s, 'link').childNodes) summary = components.getText(components.findChildNodeByName(s, 'summary').childNodes).strip() operation = " + [" + str(cnt) + "/" + str(len(issuelist)) + "] Update " + linkURL + " : " + summary if debug: operation = operation + " :: " + str(issue.fields.labels) if doThisJIRA == False: operation = " - [" + str(cnt) + "/" + str(len(issuelist)) + "] -Skip- " + linkURL + " (" + whichLabelSkipped + ") : " + summary print operation else: if options.autoApplyChanges or options.dryrun: print operation yesno = "" else: yesno = raw_input(operation + " ? [y/N] ") if options.autoApplyChanges or yesno.capitalize() in ["Y"]: # move issue to next fixversion if components.findChildNodeByName(s, 'project').attributes["key"].value == "JBIDE": # JBIDE or JBDS fixversion = version_jbt fixversion_NEXT = version_jbt_NEXT if NEXTorDOTX else version_jbt_DOTX else: fixversion = version_ds fixversion_NEXT = version_ds_NEXT if NEXTorDOTX else version_ds_DOTX fixVersions = [] # NOTE: if there is more than one fixversion, the others will not be changed for version in issue.fields.fixVersions: if version.name != fixversion: fixVersions.append({'name': version.name}) fixVersions.append({'name': fixversion_NEXT}) issue.update(fields={'fixVersions': fixVersions}) # only for NEXT, not for .x if NEXTorDOTX: # move issue to new sprint jira.add_issues_to_sprint(sprintId_NEXT, [key]) jira.add_comment(key, "[checkUnresolvedIssues.py] Slip to fixversion = " + fixversion_NEXT + " and sprint " + sprintId_NEXT) else: jira.add_comment(key, "[checkUnresolvedIssues.py] Slip to fixversion = " + fixversion_NEXT)
def set_jira_rtbf(jira_rtbf_issue, jira_usr, jira_pwd, result): from jira import JIRA from jira.resources import Issue jira = JIRA(basic_auth=(jira_usr, jira_pwd), options={'server':'https://jira.smc.com'}) issue = jira.issue(jira_rtbf_issue) jira.add_comment(jira_rtbf_issue, result) issue.update(assignee={'name': (issue.fields.reporter.name)}) issues_lst=[] issues_lst.append(issue.key) #spint_id 3844 is for 'COMPLETED GDPR Requests' jira.add_issues_to_sprint(3844,issues_lst) print('success')
class JIRAUtilities: # Translate state changes to JIRA transition values transition_ids = {'ToDo': '11', 'Prog': '21', 'Imped': '41', 'Done': '31'} user_story_dict_cloud = { #'project': 'SP', #'project': 'TTP', # Tsafi, 15 Jan 2020 'project': DEFAULT_JIRA_PARAMS[JIRA_INST]['PROJECT'], # Tsafi 3 Feb 2020 'issuetype': 'Story', 'summary': "Story 302", 'customfield_10322': [{ 'value': 'Dev Team 1' }] # Team field ********cloud } user_story_dict_vm = { #'project': 'SP', #'project': 'TTP', #Tsafi 14 Jan 2020 - temp change to work with TTP project already exists on local Jira 'project': DEFAULT_JIRA_PARAMS[JIRA_INST]['PROJECT'], # Tsafi 3 Feb 2020 'issuetype': 'Story', 'summary': "Story 302", #'customfield_10201': [{'value': 'Dev Team 1'}] # Team field vm 'customfield_10200': [{ 'value': 'Dev Team 1' }] # Tsafi 21 Jan 2020 } # 10120 is the 'Epic Name' and it's a must have epic_dict_cloud = { #'project': 'SP', #'project': 'TTP', # Tsafi, 15 Jan 2020 'project': DEFAULT_JIRA_PARAMS[JIRA_INST]['PROJECT'], # Tsafi 3 Feb 2020 #'issuetype': 'Epic', 'issuetype': { 'name': 'Epic' }, # Tsafi 15 Jan 2020 'summary': "Epic 1", 'customfield_10120': "Epic 1", # Epic name is the same as Epic summary ********cloud 'customfield_10322': [{ 'value': 'Dev Team 1' }] # Team field ********cloud } epic_dict_vm = { 'project': DEFAULT_JIRA_PARAMS[JIRA_INST]['PROJECT'], # Tsafi 3 Feb 2020 'issuetype': 'Epic', 'summary': "Epic 1", 'customfield_10103': "Epic 1", # Epic name is the same as Epic summary vm #'customfield_10102': "Epic 1", # Tsafi 14 Jan 2020 'customfield_10200': [{ 'value': 'Dev Team 1' }] # Tsafi 21 Jan 2020 - Team field vm } def __init__(self, instance_type): self.instance_type = instance_type if instance_type == 'cloud': # Using Nela's Jira cloud account #self.jira_inst = JIRA(basic_auth=('*****@*****.**', 'FiJBeI3H81sceRofBcY4E84E'), # options={'server': 'https://dr-agile.atlassian.net'}) # Using Tsafi's Jira cloud account #self.jira_inst = JIRA(basic_auth=('*****@*****.**', 'tDshA7M9zEhMkaiC13RA146E'), # options={'server': 'https://dr-agile.atlassian.net'}) self.jira_inst = JIRA( basic_auth=(DEFAULT_JIRA_PARAMS['CLOUD']['USER'], DEFAULT_JIRA_PARAMS['CLOUD']['PASS']), options={'server': DEFAULT_JIRA_PARAMS['CLOUD']['URL']}) else: # Using Nela's Jira local VM #self.jira_inst = JIRA(basic_auth=('nela.g', 'q1w2e3r4'), options={'server': 'http://192.168.56.101:8080'}) # Using Tsafi's Jira local VM #self.jira_inst = JIRA(basic_auth=('tsafrir.m', 'Sim1965'), options={'server': 'http://192.168.43.55:8080'}) #self.jira_inst = JIRA(basic_auth=('tsafrir.m', 'Sim1965'), options={'server': 'http://192.168.43.41:8080'}) #self.jira_inst = JIRA(basic_auth=('tsafrir.m', 'Sim1965'), options={'server': 'http://10.0.0.61:8080'}) self.jira_inst = JIRA( basic_auth=(DEFAULT_JIRA_PARAMS['LOCAL']['USER'], DEFAULT_JIRA_PARAMS['LOCAL']['PASS']), options={'server': DEFAULT_JIRA_PARAMS['LOCAL']['URL']}) def __del__(self): del self.jira_inst # Creation def create_epic(self, name, team): if self.instance_type == 'cloud': dict = self.epic_dict_cloud dict['customfield_10120'] = name # ********cloud dict['customfield_10322'] = [{'value': team.name}] # ********cloud else: dict = self.epic_dict_vm dict['customfield_10103'] = name # tsafi 21 Jan 2020 #dict['customfield_10102'] = name # Tsafi 14 Jan 2020 #dict['customfield_10201'] = {'value': team.name} dict['customfield_10200'] = { 'value': team.name } # Tsafi 21 Jan 2020 dict['summary'] = name epic = self.jira_inst.create_issue(fields=dict) return epic def create_user_story_with_epic(self, user_story_name, team, epic_key=None): if self.instance_type == 'cloud': dict = self.user_story_dict_cloud dict['customfield_10322'] = [{'value': team.name}] # ********cloud else: dict = self.user_story_dict_vm #dict['customfield_10201'] = {'value': team.name} dict['customfield_10200'] = { 'value': team.name } # Tsafi 21 Jan 2020 dict['summary'] = user_story_name user_story = self.jira_inst.create_issue(fields=dict) if epic_key: self.jira_inst.add_issues_to_epic(epic_key, [user_story.key]) # Note the performance impact, this is another fetch after # adding a story to epic user_story = self.jira_inst.issue(user_story.id) return user_story def create_list_of_epics(self, list_of_epics, team): print('\nCreating list of epics for team %s in JIRA' % team.name) for epic in list_of_epics: jira_epic = self.create_epic(epic.name, team) epic.key = jira_epic.key def create_list_of_user_stories(self, list_of_user_stories, team): print('\nCreating list of user stories for team %s in JIRA' % team.name) for user_story in list_of_user_stories: jira_user_story = self.create_user_story_with_epic( user_story.name, team, user_story.epic.key) user_story.key = jira_user_story.key # Create Sprint # Best would be to provide a board_id of a board that contains all issues def create_sprint(self, board_id, sprint_name, start_date, sprint_size): end_date = start_date + timedelta(days=sprint_size) start_date_str = start_date.strftime("%d/%b/%y %#I:%M %p") end_date_str = end_date.strftime("%d/%b/%y %#I:%M %p") print(start_date_str) print(end_date_str) new_sprint = self.jira_inst.create_sprint(sprint_name, board_id, startDate=start_date_str, endDate=end_date_str) print("Created new Sprint: name = %s, id = %s" % (new_sprint.name, new_sprint.id)) print("Start date %s, end date %s" % (new_sprint.startDate, new_sprint.endDate)) return new_sprint # Set up Sprints def start_sprint(self, sprint_id): print("Please start the sprint manually on a global board") key = input() def end_sprint(self, sprint_id): print("Please end the sprint manually on a global board") key = input() def add_issues_to_sprint(self, sprint_id, user_stories_keys): print("Adding %d issues to Sprint %d" % (len(user_stories_keys), sprint_id)) self.jira_inst.add_issues_to_sprint(sprint_id, user_stories_keys) def update_one_issue(self, issue_key, transition): print("Updating issue %s, moving to %s" % (issue_key, transition)) self.jira_inst.transition_issue(issue_key, self.transition_ids[transition]) def advance_time_by_one_day(self): if self.instance_type != 'cloud': #Tsafi 20 Jan 2020, change path for Tsafi's local VM #path = "C:\\Windows\\WinSxS\\amd64_openssh-client-components-onecore_31bf3856ad364e35_10.0.17763.1_none_f0c3262e74c7e35c\\ssh.exe [email protected] \"cd /home/nelkag/Simulator/misc; python /home/nelkag/Simulator/misc/advanceoneday.py\"" #path = "C:\\Windows\\System32\\OpenSSH\\ssh.exe [email protected] \"cd /home/tsafi/SAFESimulator; python3 ./advanceoneday.py\"" #path = "C:\\Windows\\System32\\OpenSSH\\ssh.exe [email protected] python3 /home/tsafi/SAFESimulator/advanceoneday.py" #path = "C:\\Windows\\System32\\OpenSSH\\ssh.exe [email protected] date" #path = "C:\\Windows\\Sysnative\\OpenSSH\\ssh.exe [email protected] python3 ./SAFESimulator/advanceoneday.py" #path = "C:\\Windows\\Sysnative\\OpenSSH\\ssh.exe [email protected] python3 ./SAFESimulator/advanceoneday.py" path = 'C:\\Windows\\Sysnative\\OpenSSH\\ssh.exe tsafi@' + LOCAL_JIRA_IP + ' python3 ./SAFESimulator/advanceoneday.py' print(path) os.system(path)
def main(): parser = argparse.ArgumentParser(description='Creates TestRail Test Runs and Jira tasks for test execution') parser.add_argument('--filter', dest='filter', action='store_false', help='will also add test cases which have been automated') args = parser.parse_args() user_details = { 'id': 'ATLASSIAN ID HERE', 'email': 'EMAIL HERE', 'jiraKey': 'ATLASSIAN KEY HERE', 'testrailKey': 'TESTRAIL KEY HERE' } ssl._create_default_https_context = ssl._create_unverified_context # Provide authentication details for accessing TestRail's API client = APIClient('https://snapsheet.testrail.io/') client.user = user_details['email'] client.password = user_details['testrailKey'] # Dictionary to hold relevant IDs from QA Regression project id_dict = {1: {'suite_id': 64, 'project': 'S2' }, 2: {'suite_id': 112, 'project': 'VICE' }, 3: {'suite_id': 92, 'project': 'SnapTx' }, 4: {'suite_id': 67, 'project': 'Shops' }, 5: {'suite_id': 66, 'project': 'Hertz' }, 6: {'suite_id': 68, 'project': 'S1' }, 7: {'suite_id': 78, 'project': 'Turo' }, 8: {'suite_id': 69, 'project': 'Policy App' } } print( '\n[1] Standard 2\n' '[2] VICE\n' '[3] SnapTx\n' '[4] Shops\n' '[5] Hertz\n' '[6] Standard 1\n' '[7] Turo\n' '[8] Policy', ) userInput = int(input('\nEnter the key (1 - 8) corresponding to the product for which you\'d like to create a new run: ')) while userInput not in range(len(id_dict) + 1) or userInput == 0: try: userInput = int(input()) except: print('That\'s not a number...') sys.exit(1) caseIdArray = [] testCases = client.send_get("get_cases/10&suite_id=%s" % (str(getSuiteId(userInput, id_dict)))) for index in range(len(testCases)): if testCases[index]['custom_test_case_automated'] is not True: caseIdArray.append(testCases[index]['id']) buildName = input('\nPlease enter a name for the new build: ') if args.filter is not False: newRun = client.send_post('add_run/10', { 'suite_id': getSuiteId(userInput, id_dict), 'name': buildName, 'include_all': False, 'case_ids': caseIdArray }) else: newRun = client.send_post('add_run/10', { 'suite_id': getSuiteId(userInput, id_dict), 'name': buildName, 'include_all': True }) # Provide authentication details for Jira options = {'server': 'https://snapsheettech.atlassian.net/'} jira = JIRA(options, basic_auth=(user_details['email'], user_details['jiraKey'])) createTask = input('\nDo you want to create a Jira task for this test run? (Yes/No): ') while createTask.lower() not in ['y','n','yes','no']: try: createTask = input('Try again...') except: pass if createTask.lower() in ['y', 'yes']: task = jira.create_issue(project='QA', summary='%s - Regression Run' % buildName, issuetype={'name': 'Task'}, description=newRun['url'], assignee={'id': user_details['id']}) addToSprint = input('\nDo you want to add the task to the current sprint? (Yes/No): ') while addToSprint.lower() not in ['y', 'n', 'yes', 'no']: try: addToSprint = input('Try again...') except: pass if addToSprint.lower() in ['y', 'yes']: sprints = jira.sprints(30, maxResults=None) jira.add_issues_to_sprint(sprints[len(sprints)-1].id, [task.key]) else: pass print("\nHere\'s the URL for the new %s build: %s \n" % (id_dict[userInput]['project'], newRun['url']))
def create_issues_in_jira(*, issue_dicts, server_url, basic_auth, project_name, board_name, assignee_key, components, epic_link, max_results): jira = JIRA(server=server_url, basic_auth=basic_auth) def get_board(board_name): for board in jira.boards(): if board.name == board_name: return board return None def get_project(project_name): for project in jira.projects(): if project.name == project_name: return project return None project = get_project(project_name) if not project: raise Exception('project not found: \'{}\''.format(board_name)) board = get_board(board_name) if not board: raise Exception('board not found: \'{}\''.format(board_name)) component_ids = None if components is not None: component_objs = jira.project_components(project) def get_component(component_name): for component_obj in component_objs: if component_obj.name == component_name: return component_obj return None component_ids = [] for component in components: component_obj = get_component(component) if component_obj is None: raise Exception( 'component not found: \'{}\''.format(component)) component_ids.append(component_obj.id) if epic_link: search_result = jira.search_issues( 'summary ~ "{}"'.format(f'\\"{epic_link}\\"')) if len(search_result) == 0: raise Exception('epic not found: \'{}\''.format(epic_link)) elif len(search_result) > 1: raise Exception( 'more than one epic found for name \'{}\''.format(epic_link)) epic = search_result[0] else: epic = None create_issues_results = [] def _create_issue(issue_dict, parent=None): fields_list = [{ 'project': { 'key': project.key }, 'summary': issue_dict['summary'], 'description': issue_dict['description'], 'issuetype': { 'name': 'Task' if parent is None else 'Sub Task' }, }] if parent is None: fields_list[0]['assignee'] = { 'name': issue_dict['assignee'] if issue_dict['assignee'] else assignee_key } if component_ids is not None: fields_list[0]['components'] = [{ 'id': component_id } for component_id in component_ids] if parent is not None: fields_list[0]['parent'] = {'key': parent.key} results = jira.create_issues(fields_list) assert len(results) == 1 result = results[0] issue_obj = result['issue'] for sub_issue_dict in issue_dict['sub_issues']: _create_issue(sub_issue_dict, issue_obj) create_issues_results.append( dict(issue_dict=issue_dict, issue_obj=issue_obj)) for issue_dict in issue_dicts: _create_issue(issue_dict) if epic is not None: jira.add_issues_to_epic(epic.id, [ create_issues_result['issue_obj'].key for create_issues_result in create_issues_results if create_issues_result['issue_obj'].fields.issuetype.name == 'Task' ]) issues_to_add_to_sprint = [ create_issues_result['issue_obj'].key for create_issues_result in create_issues_results if create_issues_result['issue_dict'].get('add_to_sprint', False) ] if issues_to_add_to_sprint: sprints = jira.sprints(board.id, extended=['startDate', 'endDate'], maxResults=max_results) if len(sprints) == 0: raise Exception('There\'s no open sprint') last_sprint = sprints[-1] jira.add_issues_to_sprint(last_sprint.id, issues_to_add_to_sprint)
class JiraTool: def __init__(self): self.server = jira_url self.basic_auth = (usrer, password) self.jiraClinet = None def login(self): self.jiraClinet = JIRA(server=self.server, basic_auth=self.basic_auth) if self.jiraClinet != None: return True else: return False def findIssueById(self, issueId): if issueId: if self.jiraClinet == None: self.login() return self.jiraClinet.issue(issueId) else: return 'Please input your issueId' def deleteAllIssue(self, project): project_issues = self.jiraClinet.search_issues('project=' + project.name) for issue in project_issues: logging.info('delete issue %s' % (issue)) issue.delete() def deleteAllSprint(self, board): sprints = self.jiraClinet.sprints(board.id) for sprint in sprints: logging.info('delete sprint %s' % (sprint.name)) sprint.delete() def getProject(self, name): projects = self.jiraClinet.projects() #logging.info("get project %s" %(name)) for project in projects: #logging.info("project %s" %(project.name)) if (name == project.name): return project return None #return self.jiraClinet.create_project(key='SCRUM', name=name, assignee='yujiawang', type="Software", template_name='Scrum') def getBoard(self, project, name): boards = self.jiraClinet.boards() for board in boards: if (name == board.name): logging.info("board:%s id:%d" % (board.name, board.id)) return board return self.jiraClinet.create_board(name, [project.id]) def createSprint(self, board, name, startDate=None, endDate=None): logging.info("==== create sprint[%s] in board[%s] ====>" % (name, board.name)) sprint = self.jiraClinet.create_sprint(name, board.id, startDate, endDate) return sprint def getSprint(self, board_id, sprint_name): sprints = self.jiraClinet.sprints(board_id) for sprint in sprints: if sprint.name == sprint_name: return sprint return None def createEpicTask(self, project, sprint, summary, description, assignee, participant, duedate): issue_dict = { 'project': { 'key': project.key }, 'issuetype': { 'id': issuetypes['Epic'] }, 'customfield_10002': summary, # epic 名称 'summary': summary, 'description': description, "customfield_10004": sprint.id, # sprint 'assignee': { 'name': assignee }, 'customfield_10303': participant, 'customfield_10302': '2018-08-24T05:41:00.000+0800' } logging.info(duedate) logging.info(issue_dict) #juse for debug issue = self.jiraClinet.create_issue(issue_dict) self.jiraClinet.add_issues_to_sprint(sprint.id, [issue.raw['key']]) logging.info( "===> add epic task[%s key:%s] to sprint [%s]" % (issue.raw['fields']['summary'], issue.raw['key'], sprint.name)) #dumpIssue(issue) #juse for debug return issue def createTask(self, project, sprint, epic, summary, description, assignee, participant): issue_dict = { 'project': { 'key': project.key }, 'issuetype': { 'id': issuetypes['Task'] }, 'summary': summary, 'description': description, 'assignee': { 'name': assignee }, 'customfield_10303': participant } issue = self.jiraClinet.create_issue(issue_dict) logging.info("==> add task[%s key:%s] link epic [%s key: %s]" % (issue.raw['fields']['summary'], issue.raw['key'], epic.raw['fields']['summary'], epic.raw['key'])) self.jiraClinet.add_issues_to_epic(epic.id, [issue.raw['key']]) self.jiraClinet.add_issues_to_sprint(sprint.id, [issue.raw['key']]) return issue def createSubTask(self, project, parent, summary, description, assignee, participant): issue_dict = { 'project': { 'key': project.key }, 'parent': { 'key': parent.raw['key'] }, 'issuetype': { 'id': issuetypes['Sub-Task'] }, 'summary': summary, 'description': description, 'assignee': { 'name': assignee }, 'customfield_10303': participant } issue = self.jiraClinet.create_issue(issue_dict) logging.info("=> add sub task[%s key:%s] to task [%s key: %s]" % (issue.raw['fields']['summary'], issue.raw['key'], parent.raw['fields']['summary'], parent.raw['key'])) return issue
class JiraAPI(object): """Access Jira api using python-jira project.""" def __init__(self, user, passwd, logger): """Init JiraApi object and logger.""" self.logger = logger self.jira = JIRA(server=config.JIRA_HOST, basic_auth=(user, passwd)) def get_boards(self): """Get Jira boards list as json.""" self.logger.info("Getting Jira boards") json = {'boards': []} boards = self.jira.boards() for board in boards: json.get('boards').append({'id': board.id, 'name': board.name}) return json # ############### # ### Sprints ### # ############### def _get_board_sprints(self, board_id): """Get sprints of a board as json.""" self.logger.info("Getting board {} sprints".format(board_id)) json = {'sprints': []} sprints = self.jira.sprints(board_id, extended=True) for sprint in sprints: # self.logger.debug("Sprint content: {}".format(sprint.__dict__)) json.get('sprints').append({ 'id': sprint.id, 'key': sprint.id, 'name': sprint.name, 'start': sprint.startDate, 'end': sprint.endDate }) return json def search_sprint(self, board_key, sprint_name): """Search a sprint in a board and returns its info.""" self.logger.info("Searching board {} sprint '{}'".format( board_key, sprint_name)) found_sprint = None sprints = self._get_board_sprints(board_key) for sprint in sprints.get('sprints'): if (sprint.get('name') == sprint_name): found_sprint = sprint break return found_sprint def create_sprint(self, board_id, sprint_name, start, end): """Create a sprint in a board with given start and end dates. Dates must be in Jira format. """ self.logger.info("Creating sprint {}".format(sprint_name)) sprint = self.jira.create_sprint(name=sprint_name, board_id=board_id, startDate=start, endDate=end) # self.logger.debug("Created sprint content {}".format(sprint.__dict__)) return sprint.id def start_sprint(self, sprint_id, sprint_name, start, end): """Start a Jira sprint with given dates. *** Does not work because Jira API call fails with 'state' param. *** Dates must be in Jira format. """ self.logger.info("Starting sprint {} [Not working]".format(sprint_id)) # self.jira.update_sprint(sprint_id, name=sprint_name, startDate=start, endDate=end, state="active") def close_sprint(self, sprint_id, sprint_name, start, end): """Closes a Jira sprint with given dates. *** Does not work because Jira API call fails with 'state' param. *** Dates must be in Jira format. """ self.logger.info("Closing sprint {} [Not working]".format(sprint_id)) # self.jira.update_sprint(sprint_id, name=sprint_name, startDate=start, endDate=end, state=None) def delete_board_sprints(self, board_id): """Delete all sprints of a board.""" self.logger.info("Deleting board {} sprints".format(board_id)) sprints = self.jira.sprints(board_id, extended=False) for sprint in tqdm(sprints): self.logger.info("Deleting sprint {}".format(sprint.name)) sprint.delete() def _get_board_project_id(self, board_id): """Get the project id of a board.""" self.logger.debug("Getting project id of board {}".format(board_id)) project_id = None boards = self.jira.boards() for board in boards: # self.logger.debug("Comparing boards {}-{}".format(board.id, board_id)) if str(board.id) == str(board_id): # self.logger.debug("Found matching board {}".format(board.raw)) project_id = board.raw.get("filter").get("queryProjects").get( "projects")[0].get("id") break return project_id # #################### # ### User stories ### # #################### def _get_project_user_stories(self, project_key): """Get all user stories of a project.""" self.logger.info("Getting project {} user stories".format(project_key)) issues = self.jira.search_issues('project=' + project_key + ' and issuetype=' + config.JIRA_USER_STORY_TYPE, maxResults=200) return issues def delete_all_project_user_stories(self, project_key): """Delete all user stories of a project.""" self.logger.info( "Deleting project {} user stories".format(project_key)) issues = self._get_project_user_stories(project_key) for issue in tqdm(issues): self.logger.info("Deleting user story {}".format(issue)) issue.delete() def _create_user_story(self, project_id, subject, description, tags, points): """Create a user story with provided information.""" story_fields = { "project": project_id, "issuetype": config.JIRA_USER_STORY_TYPE, "summary": subject, "description": description, "labels": tags, config.JIRA_ESTIMATION_FIELD: points } created_story = self.jira.create_issue(fields=story_fields) self.logger.info("Created user story {} # {}".format( created_story.id, created_story.key)) # self.logger.debug("Created user story details: {}".format(created_story.__dict__)) return created_story def update_user_story_status(self, user_story_key, is_closed, status): """Update the status of a user story to Done or Not Done only if it's closed. A translation is made between the status given, which is the status in Taiga and the Jira status as defined in JIRA_USER_STORY_STATUS_DONE, JIRA_USER_STORY_STATUS_NOT_DONE and TAIGA_USER_STORY_STATUS_NOT_DONE constants of config.py file. """ self.logger.info( "Updating user story {} with Taiga status={} ({})".format( user_story_key, status, is_closed)) if is_closed: task_status = config.JIRA_USER_STORY_STATUS_DONE if status == config.TAIGA_USER_STORY_STATUS_NOT_DONE: task_status = config.JIRA_USER_STORY_STATUS_NOT_DONE self.jira.transition_issue(user_story_key, task_status) self.logger.info("Updated user story {} status to {}".format( user_story_key, task_status)) else: self.logger.warn( "Not updated user story {} beacuse is not closed: is_closed={}" .format(user_story_key, is_closed)) # ######################## # ### User story tasks ### # ######################## def _create_user_story_task(self, project_id, user_story_id, subject, description, finished_date): """Create a task inside a user story. The story task type is defined in config.py in JIRA_USER_STORY_TASK_TYPE constant. default is 'Sub-type'. """ self.logger.info("Creating user story task {}".format(subject)) created_task = self.jira.create_issue( project=project_id, parent={"id": user_story_id}, issuetype=config.JIRA_USER_STORY_TASK_TYPE, summary=subject) self.logger.info("Created user story task {} # {}".format( created_task.id, created_task.key)) # self.logger.debug("Created story task details: {}".format(created_task.__dict__)) return created_task def _update_task_status(self, task_key, status): """Update task status with Done or not Done status depending on incoming Taiga status. Taiga done status is defined in TAIGA_TASK_STATUS_DONE constant inside config.py file. Jira done and not done statuses are defined in JIRA_TASK_STATUS_DONE and JIRA_TASK_STATUS_NOT_DONE inside config.py file. """ self.logger.info( "Updating user story task {} with Taiga status={}".format( task_key, status)) task_status = config.JIRA_TASK_STATUS_DONE if status != config.TAIGA_TASK_STATUS_DONE: task_status = config.JIRA_TASK_STATUS_NOT_DONE self.jira.transition_issue(task_key, task_status) self.logger.info("Updated user story task {} status to {}".format( task_key, task_status)) # ####################### # ### Sprint creation ### # ####################### def _add_comment(self, issue_id, comment): """Add a comment to an issue.""" self.logger.debug("Adding comment to issue {}".format(issue_id)) self.jira.add_comment(issue_id, comment, visibility={'group': 'jira-users'}) def create_sprint_stories(self, board_id, sprint_id, user_stories): """Create user stories in a board and link them to a sprint. A json array with user story key, is_closed and status info is returned. User stories subtasks are also added to the story. User story tasks finished date and current taiga status are added as comments. User story finished date and current taiga status are added as comments. """ project_id = self._get_board_project_id(board_id) self.logger.info( "Creating user stories in project {} - sprint {}".format( project_id, sprint_id)) created_stories = [] for user_story in user_stories: self.logger.info("Creating user story {}".format( user_story.get("subject"))) created_story = self._create_user_story( project_id, user_story.get("subject"), user_story.get("description"), user_story.get("tags"), user_story.get("total_points")) self.logger.info("Adding user story {} to sprint {}".format( created_story.key, sprint_id)) self.jira.add_issues_to_sprint(sprint_id, [created_story.key]) for task in user_story.get("tasks"): created_task = self._create_user_story_task( project_id, created_story.id, task.get("subject"), task.get("description"), task.get("finished_date")) # Add as comment user story finished date self._add_comment( created_task.id, "Finished date: '{}'".format(task.get('finished_date'))) self._add_comment( created_task.id, "Taiga status: '{}'".format(task.get("status"))) # Update task status self._update_task_status(created_task.key, task.get("status")) created_stories.append({ "key": created_story.key, "is_closed": user_story.get("is_closed"), "status": user_story.get("status") }) # Add as comment user story finished date self._add_comment( created_story.id, "Finished date: '{}'".format(user_story.get('finish_date'))) self._add_comment( created_story.id, "Taiga status: '{}'".format(user_story.get("status"))) return created_stories def add_backlogs_stories(self, board_id, user_stories): """Add user stories to the backlog of the board. Taiga original status and backlog order are added as comments. """ project_id = self._get_board_project_id(board_id) self.logger.info( "Creating user stories in project {} backlog".format(project_id)) created_stories = [] for user_story in user_stories: self.logger.info("Creating user story {}".format( user_story.get("subject"))) created_story = self._create_user_story( project_id, user_story.get("subject"), user_story.get("description"), user_story.get("tags"), user_story.get("total_points")) created_stories.append({"key": created_story.key}) # Add as comment user story finished date self._add_comment( created_story.id, "Taiga status: '{}'".format(user_story.get("status"))) self._add_comment( created_story.id, "Taiga backlog order: '{}'".format( user_story.get("backlog_order"))) return created_stories
class JSprint(cmd.Cmd): jira = None __current_sprint = None def __init__(self): super().__init__() username = settings.get("jira_username") password = settings.get("jira_password") or getpass.getpass() options = { "server": settings.get("jira_url"), "agile_rest_path": "agile" } self.jira = JIRA(options, auth=(username, password)) @property def prompt(self): sprint = Style.BRIGHT + self.current_sprint.name + Style.RESET_ALL return f"JSprint [{sprint}] >>> " @property def current_sprint(self): if self.__current_sprint is None: self.__current_sprint = self.get_active_sprint() return self.__current_sprint @current_sprint.setter def current_sprint(self, value): self.__current_sprint = value def get_sprints(self, state: str = "active") -> List[Sprint]: return self.jira.sprints(settings.get("jira_board_id"), state=state) def get_active_sprint(self) -> Sprint: sprints = self.get_sprints(state="active") if len(sprints): # Use fisrt active sprint as default sprint = sorted(sprints, key=attrgetter("name"))[0] else: sprint = None return sprint # ------------------ # Set current sprint # ------------------ def help_use(self): print_help(""" use [sprint_id] Set the current sprint. If no `sprint_id` is passed the current active sprint will be used. """) @do_exception def do_use(self, line): # Parse arguments args = shlex.split(line) # Select sprint if len(args) == 0: sprint = self.get_active_sprint() if sprint is None: print("No active sprint") return else: sprint_id = get_sprint_id_from_number(args[0]) sprint = self.jira.sprint(sprint_id) print( f"Using sprint {Style.BRIGHT + sprint.name + Style.RESET_ALL} ({sprint.id})" ) self.current_sprint = sprint # ------------ # Show sprints # ------------ def help_sprints(self): print_help("List all the active and future sprints") def do_sps(self, line): return self.do_sprints(line) @do_exception def do_sprints(self, line): actives = self.get_sprints(state="active") futures = self.get_sprints(state="future") sprints = futures + actives sprints = sorted(sprints, key=attrgetter("name")) for sprint in sprints: name = sprint.name id_ = Style.BRIGHT + str(sprint.id) + Style.RESET_ALL state = ("*" if sprint.state == "active" else " ") + colored_sprint_state(sprint) print(f"{state} {name} ({id_})") # ----------- # Show sprint # ----------- def help_sprint(self): print_help(""" sprint [sprint_id] Show the issues in the sprint grouped by assignee. If no `sprint_id` is given use the current selected sprint. """) def do_sp(self, line): return self.do_sprint(line) @do_exception def do_sprint(self, line): def group_by_assignee(acc, issue: Issue) -> Dict[str, Iterable[Issue]]: assignee = get_assignee_from_issue(issue) acc.setdefault(assignee, []).append(issue) return acc # Parse arguments args = shlex.split(line) # Show sprint if len(args) == 0: sprint = self.current_sprint else: sprint_id = get_sprint_id_from_number(args[0]) sprint = self.jira.sprint(sprint_id) print(f"Displaying sprint {sprint.name}") # Show issue by assignee jira_project = settings.get("jira_project") team_members = settings.get("team_members") team_labels = settings.get("team_labels") jql = f"project = '{jira_project}' AND sprint = {sprint.id}" if team_members: jql += f" AND (assignee IS NULL or assignee IN {tuple(team_members)})" if team_labels: jql += f" AND (labels IS NULL or labels IN {tuple(team_labels)})" issues = self.jira.search_issues(jql) if len(issues): permalink_padding = max(len(i.permalink()) for i in issues) status_padding = max(len(i.fields.status.name) for i in issues) issue_key_padding = max(len(i.key) for i in issues) else: permalink_padding = status_padding = issue_key_padding = 0 issues_by_user = functools.reduce(group_by_assignee, issues, {}) assignees = sorted(issues_by_user.keys()) for i, assignee in enumerate(assignees): user_issues = issues_by_user[assignee] user_issues = sorted(user_issues, key=attrgetter("id")) print(Style.BRIGHT + f"{assignee}:" + Style.RESET_ALL) for issue in user_issues: issue_key = Style.BRIGHT + issue.key.ljust( issue_key_padding) + Style.RESET_ALL url = issue.permalink().ljust(permalink_padding) status = colored_issue_status(issue, status_padding) summary = Style.BRIGHT + issue.fields.summary + Style.RESET_ALL print(f"{status} - {issue_key} ({url}) {summary}") if i != (len(assignees) - 1): print() # --------------------- # Show sprint as report # --------------------- def help_report(self): print_help(""" sprint [sprint_id] Show the issues in the sprint grouped by user for reporting. If no `sprint_id` is given use the current selected sprint. The difference with the `sprint` command is that the grouping by user is done by whom actually worked on the issue instead of just the assignee. """) def do_rp(self, line): return self.do_report(line) """ @do_exception def do_report(self, line): def group_by_developers(acc, issue: Issue) -> Dict[str, Iterable[Issue]]: developers = get_developers_from_issue(issue) for developer in developers: acc.setdefault(developer.displayName, []).append(issue) return acc def is_developer_in_team(team_members: List[str], issue: Issue) -> bool: developers = get_developers_from_issue(issue) for developer in developers: if developer.name in team_members: return True return False # Parse arguments args = shlex.split(line) # Show sprint if len(args) == 0: sprint = self.current_sprint else: sprint_id = get_sprint_id_from_number(args[0]) sprint = self.jira.sprint(sprint_id) print(f"Displaying sprint {sprint.name}") # Show issue by developers jira_project = settings.get("jira_project") team_members = settings.get("team_members") jql = f"project = '{jira_project}' AND sprint = {sprint.id}" issues = self.jira.search_issues(jql) issues = (i for i in issues if is_developer_in_team(team_members, i)) issues_by_developer = functools.reduce(group_by_developers, issues, {}) developers = sorted(set(issues_by_developer.keys())) for i, developer in enumerate(developers): developer_issues = issues_by_developer[developer] developer_issues = sorted(developer_issues, key=attrgetter("id")) print(Style.BRIGHT + f"{developer}:" + Style.RESET_ALL) for issue in developer_issues: url = issue.permalink() summary = Style.BRIGHT + issue.fields.summary + Style.RESET_ALL print(f"- {url} {summary}") if i != (len(developers) - 1): print() """ @do_exception def do_report(self, line): def group_by_assignee(acc, issue: Issue) -> Dict[str, Iterable[Issue]]: assignee = get_assignee_from_issue(issue) acc.setdefault(assignee, []).append(issue) return acc # Parse arguments args = shlex.split(line) # Show sprint if len(args) == 0: sprint = self.current_sprint else: sprint_id = get_sprint_id_from_number(args[0]) sprint = self.jira.sprint(sprint_id) print(f"Displaying sprint {sprint.name}") # Show issue by assignee jira_project = settings.get("jira_project") team_members = settings.get("team_members") team_labels = settings.get("team_labels") jql = f"project = '{jira_project}' AND sprint = {sprint.id} AND resolution = 'Unresolved'" if team_members: jql += f" AND (assignee IS NULL or assignee IN {tuple(team_members)})" if team_labels: jql += f" AND (labels IS NULL or labels IN {tuple(team_labels)})" issues = self.jira.search_issues(jql) issues_by_user = functools.reduce(group_by_assignee, issues, {}) assignees = sorted(issues_by_user.keys()) status_padding = max(len(i.fields.status.name) for i in issues) if len(issues) else 0 for i, assignee in enumerate(assignees): user_issues = issues_by_user[assignee] user_issues = sorted(user_issues, key=attrgetter("fields.customfield_14560")) print(Style.BRIGHT + f"{assignee}:" + Style.RESET_ALL) for issue in user_issues: url = issue.permalink() status = colored_issue_status(issue, status_padding) summary = Style.BRIGHT + issue.fields.summary + Style.RESET_ALL print(f"- {url} ({status}) {summary}") if i != (len(assignees) - 1): print() # -------------------- # Assign user to issue # -------------------- def help_assign(self): print_help(""" assign <issue_number> <assignee> Assign the issue to an user. """) def complete_a(self, *args): return self.complete_assign(*args) def do_a(self, *args): return self.do_assign(*args) @do_exception def complete_assign(self, text, line, begin_index, end_index): s = text.lower() matches = filter(lambda x: x.startswith(s), settings.get("team_members")) return list(matches) @do_exception def do_assign(self, line): # Parse args args = shlex.split(line) if len(args) < 2: print("Need two arguments: issue number and assignee name") return # Assign issue issue_key = get_issue_key_from_number(args[0]) assignee = args[1] self.jira.assign_issue(issue_key, assignee) # ---------------------- # Unassign user to issue # ---------------------- def help_unassign(self): print_help(""" unassign <issue_number> Remove assignee from the given issue. """) def do_u(self, line): return self.do_unassign(line) @do_exception def do_unassign(self, line): # Parse args args = shlex.split(line) if len(args) == 0: print("Needs at least an issue number") return # Unassign issue issue_key = get_issue_key_from_number(args[0]) self.jira.assign_issue(issue_key, None) # --------------------------- # Add issue to current sprint # --------------------------- def help_add(self): print_help(""" add <issue_number> Add issue to the current sprint. """) @do_exception def do_add(self, line): # Parse args args = shlex.split(line) if len(args) == 0: print("Needs at least an issue number") return # Add issue to sprint issue_keys = [get_issue_key_from_number(arg) for arg in args] sprint = self.current_sprint self.jira.add_issues_to_sprint(sprint.id, issue_keys) # ----------------------------- # Move or add issue to a sprint # ----------------------------- def help_move(self): print_help(""" move <sprint_id> <issue_number> Move an issue into a sprint. """) def do_mv(self, line): return self.do_move(line) @do_exception def do_move(self, line): # Parse args args = shlex.split(line) if len(args) < 2: print("Needs a sprint ID and at least one issue number") return # Move issues sprint_id = get_sprint_id_from_number(args[0]) issue_keys = [get_issue_key_from_number(arg) for arg in args] self.jira.add_issues_to_sprint(sprint_id, issue_keys) # --------------- # Show issue info # --------------- def help_show(self): print_help(""" show <issue_number> Show informations about the given issue number """) def do_sh(self, line): return self.do_show(line) @do_exception def do_show(self, line): # Parse args args = shlex.split(line) if len(args) < 1: print("Needs at least one issue number") return # Show issue stats issue_key = get_issue_key_from_number(args[0]) issue = self.jira.issue(issue_key, "assignee,status,summary,customfield_11360") status = colored_issue_status(issue) up_in_build = get_up_in_build_form_issue(issue) print( f"Issue : {Style.BRIGHT + issue.key + Style.RESET_ALL} ({issue.permalink()})" ) print(f"Status : {Style.BRIGHT + status + Style.RESET_ALL}") print( f"Assignee : {Style.BRIGHT + get_assignee_from_issue(issue) + Style.RESET_ALL}" ) print(f"Up in build : {Style.BRIGHT + up_in_build + Style.RESET_ALL}") print( f"Summary : {Style.BRIGHT + issue.fields.summary + Style.RESET_ALL}" ) # ----------------------- # Move issue into backlog # ----------------------- def help_backlog(self): print_help(""" backlog <issue_number> Move an issue out of the sprint and back into backlog. """) def do_bk(self, line): return self.do_backlog(line) @do_exception def do_backlog(self, line): # Parse args args = shlex.split(line) if len(args) < 1: print("Needs at least one issue number") return # Get active sprint issue_keys = [get_issue_key_from_number(arg) for arg in args] # Move issue into backlog self.jira.move_to_backlog(issue_keys) # ---------------- # Leave the REPL # ---------------- def do_quit(self, line): return True def do_q(self, args): return True def do_EOF(self, line): return True