class JiraInstance: """ Class which is response for creataing FixVersion, marking it as RELEASED and for annote tasks which where done within lastday. """ def __init__(self, config): self.SERVER_ADDRESS = config["SERVER_ADDRESS"] self.USER = config["USER"] self.PASSWORD = config["PASSWORD"] self.PROJECT = config["PROJECT_TAG"] self.CHANGELOG_TAG = config["CHANGELOG_TAG"] self.tasks = [] self.fixedVersion = None options = { 'server': self.SERVER_ADDRESS } self.jira = JIRA(options, basic_auth=(self.USER, self.PASSWORD)) def is_any_new_changeset_in_changelog(self) -> bool: """ Check if there is any new changeset in changelog in Jira task """ changelog_issue = self.jira.issue(self.CHANGELOG_TAG, fields='comment') changelog = self.jira.comments(changelog_issue)[-1] regexp = "{}-([1-9]+[0-9]*)".format(self.PROJECT) for line in changelog.body.splitlines(): issueTag = re.search(regexp, line) if issueTag is None or issueTag is "": continue self.tasks.append(issueTag.group()) return True if len(self.tasks) > 0 else False def add_fixed_version_to_tasks(self, fixed_version): """ Create and add fixed version to tasks from chagnelog task pointed in conf.json """ if fixed_version is None or fixed_version is "": print("Wrong fixed_version") return today = datetime.date.today() today_str = today.strftime('%Y-%m-%d') self.jira.create_version(name=(fixed_version), project=self.PROJECT, released=True, startDate=today_str, releaseDate=today_str) for task in self.tasks: print("Add fixed version to {}".format(task)) issue = self.jira.issue(task, fields='fixVersions') issue.add_field_value('fixVersions', {'name': fixed_version})
def changejira(project,buildtime,judgement,newversion,releasejira): jira = JIRA('http://jira.ccdomain.com',basic_auth=('jira', 'jira@2017~!@')) if releasejira == "": jiralist = judgelist(project,buildtime,judgement) else: jiralist = comparelist(project,buildtime,releasejira) jirareq_list= searchjirareq(project,buildtime) #创建version: curTime = datetime.date.today() curTime = curTime.strftime("%Y-%m-%d") try: jira.create_version(newversion,project,description="Pollux %s 版本转测试" %newversion,startDate=curTime) except BaseException: print "create version %s error,maybe it's existed" %(newversion) #将所有组织测试的单转成回归测试 for a in jiralist: print "start to change bug %s to 回归测试" %(str(a)) try: issue = jira.issue(a) #修改 jira 单中的修复版本: issue_dict = { 'fixVersions':[{'name':newversion}] } issue.update(fields=issue_dict) #修改 jira 单状态: jira.transition_issue(issue,'191',comment="自动回归测试") #修改指向人 newassign = str(issue.fields.reporter.name) jira.assign_issue(issue,newassign) except BaseException: print "change fail in %s" %(str(a)) for b in jirareq_list: print "start to change req %s to 回归测试" %(str(b)) try: issue = jira.issue(b) #修改 jira 单中的修复版本: issue_dict = { 'fixVersions':[{'name':newversion}] } issue.update(fields=issue_dict) #修改 jira 单状态: jira.transition_issue(issue,'21',comment="需求自动回归测试") except BaseException: print "change fail in %s" %(str(b))
def create_versions(jira: JIRA, project: str, versions: t.List[Version]): if len(versions) == 0: click.echo("\nThere is no versions for creation") else: click.echo("\nVersions for creation:") for version in versions: click.echo("- {}".format(version.name)) if click.confirm( "Do you want to create this versions in {project} project?".format( project=project ) ): with click.progressbar(versions) as versions_bar: for version in versions_bar: jira.create_version( name=version.name, project=project, description=description_template.format( original_version_id=version.id ), )
# Creacion de versiones for month in range(12): month = month + 1 days = 30 if month in [1, 3, 5, 7, 8, 10, 12]: days = 31 if month == 2: days = 29 prefix = "" if month < 10: prefix = "0" monthStr = "{0}{1}".format(prefix, month) version = "{0}.{1}".format(year,monthStr) endDate = "{0}{1}-{2}-{3}".format("20", year, monthStr, days) createVersions and jira.create_version(version, projectKey, releaseDate=endDate) # Creacion de componentes for component in componentsToCreate(projectKey, componentsList): createComponents and jira.create_component(component, projectKey) print("------------>> " + component) # Eliminacion de componentes for myComponent in gettingComponentes(projectKey): deleteComponents and jira.delete_component(myComponent.id) # Obtener descripcion aleatoria def getIssueDescription(): return " ".join([random.choice(i) for i in l])
class JiraClient(object): """ Helper class for the JIRA """ def __init__(self, url, username, password): """ :param url: :param username: :param password: :return: """ self.url = url self.webhook_url = self.url.strip('/') + '/rest/webhooks/1.0/webhook' self.basic_auth = (username, password) self.client = JIRA(url, basic_auth=self.basic_auth) def __str__(self): return '{} {}'.format(self.__class__.__name__, self.url) ################ # Accounts # ################ def groups(self): """ :return: """ return self.client.groups() def users(self, json_result=True): """ :param json_result: :return: """ return self._search_users('_', json_result=json_result) def users_by_email(self, email, json_result=True): """ :param email: :param json_result: :return: """ return self._search_users(email, json_result=json_result) def users_by_group(self, name): """ :param name: :return: """ try: return self.client.group_members(name) except JIRAError as exc: logger.warning(exc) return dict() def _search_users(self, qstr, json_result=True): """ :param qstr: :param json_result: :return: """ def _user_dict_format(user): # Note: Keep the consistent return format with the users_by_group method return {'key': user.key, 'active': user.active, 'fullname': user.displayName, 'email': user.emailAddress } users = self.client.search_users(qstr) if json_result: # Note: Keep the consistent return format with the users_by_group method return [_user_dict_format(user) for user in users] else: return users ############# # Project # ############# def create_project(self, key): """ :param key: :return: """ return self.client.create_project(key) def get_project(self, key, json_result=True): """ :param key: :param json_result: :return: """ if json_result: return self.client.project(key).raw else: return self.client.project(key) def get_projects(self, json_result=True): """ :param json_result: :return: """ project_objs = self.client.projects() if json_result: return [_each.raw for _each in project_objs] else: return project_objs def delete_project(self, key): """ :param key: :return: """ return self.client.delete_project(key) ############# # Version # ############# def get_project_versions(self, name, json_result=True): """ :param name: project name :param json_result: :return: """ try: version_objs = self.client.project_versions(name) if json_result: return [_each.name for _each in version_objs] else: return version_objs except Exception as exc: logger.warn(exc) return [] def create_project_version(self, name, project_name, **kwargs): """ :param name: version name :param project_name: project name :param kwargs: :return: """ return self.client.create_version(name, project_name, **kwargs) ############# # fields # ########### # def get_fields(self): """ :return: """ return self.client.fields() def get_non_custom_fields(self): """ :return: """ return [each for each in self.client.fields() if not each.get('custom', True)] def get_custom_fields(self): """ :return: """ return [each for each in self.client.fields() if each.get('custom', True)] def get_field_id_by_name(self, name): """ :param name: :return: """ ids = [each['id'] for each in self.client.fields() if each.get('name', '') == name] if ids: return ids[0] else: return None def get_field_id_for_hours_left(self): """ :return: """ # For Argo customized field name = 'Hrs Left' return self.get_field_id_by_name(name) ############ # issues # ############ def get_issue(self, name, json_result=True): """ :param name: :param json_result: :return: """ try: issue_obj = self.client.issue(name) except JIRAError as exc: logger.warn('Not found: %s', exc) return None if json_result: issue_dict = copy.deepcopy(issue_obj.raw['fields']) issue_dict['url'] = issue_obj.self issue_dict['id'] = issue_obj.id issue_dict['key'] = issue_obj.key # Replace custom field name return issue_dict else: return issue_obj def add_fix_version_to_issue(self, issue_name, version_name, issuetype=None): """ :param issue_name: :param version_name: :param issuetype: :return: """ return self._add_versions_from_issue(issue_name, version_name, issuetype=issuetype, _version_type='fixVersions') def add_affected_version_to_issue(self, issue_name, version_name, issuetype=None): """ :param issue_name: :param version_name: :param issuetype: :return: """ return self._add_versions_from_issue(issue_name, version_name, issuetype=issuetype, _version_type='versions') def _add_versions_from_issue(self, issue_name, version_name, issuetype=None, _version_type='fixVersions'): """ :param issue_name: :param version_name: :param issuetype: :param _version_type: :return: """ assert _version_type in ['fixVersions', 'versions'], 'Unsupported version type' issue_obj = self.get_issue(issue_name, json_result=False) if not issue_obj: return None if issuetype is not None and issue_obj.fields.issuetype.name.lower() != issuetype.lower(): logger.info('SKIP. The issue type is %s, expected issue type is %s', issue_obj.fields.issuetype.name.lower(), issuetype.lower()) return None logger.info('Update issue %s, with %s %s', issue_obj.key, _version_type, version_name) ret = issue_obj.add_field_value(_version_type, {'name': version_name}) issue_obj.update() return ret def remove_affected_versions_from_issue(self, issue_name, versions_to_remove): """ :param issue_name: :param versions_to_remove: :return: """ return self._remove_versions_from_issue(issue_name, versions_to_remove, _version_type='versions') def remove_fix_versions_from_issue(self, issue_name, versions_to_remove): """ :param issue_name: :param versions_to_remove: :return: """ return self._remove_versions_from_issue(issue_name, versions_to_remove, _version_type='fixVersions') def _remove_versions_from_issue(self, issue_name, versions_to_remove, _version_type='fixVersions'): """ :param issue_name: :param versions_to_remove: :param _version_type: :return: """ assert _version_type in ['fixVersions', 'versions'], 'Unsupported version type' if type(versions_to_remove) not in [list, tuple, set]: versions_to_remove = [versions_to_remove] versions = [] issue_obj = self.get_issue(issue_name, json_result=False) for ver in getattr(issue_obj.fields, _version_type): if ver.name not in versions_to_remove: versions.append({'name': ver.name}) issue_obj.update(fields={_version_type: versions}) def create_issue(self, project, summary, description=None, issuetype='Bug', reporter=None, **kwargs): """ :param project: :param summary: :param description: :param issuetype: :param reporter: :param kwargs: :return: """ # { # "fields": { # "project": # { # "key": "TEST" # }, # "summary": "Always do right. This will gratify some people and astonish the REST.", # "description": "Creating an issue while setting custom field values", # "issuetype": { # "name": "Bug" # }, # "customfield_11050" : {"Value that we're putting into a Free Text Field."} # } # } fields_dict = dict() for k, v in kwargs.items(): fields_dict[k] = v fields_dict['project'] = {'key': project} fields_dict['description'] = description or '' fields_dict['summary'] = summary fields_dict['issuetype'] = {'name': issuetype} if reporter: users = self.users_by_email(reporter, json_result=False) if users: fields_dict['reporter'] = {'name': users[0].name} return self.client.create_issue(fields=fields_dict) def update_issue(self, name, **kwargs): """ :param name: :param kwargs: :return: """ issue_obj = self.get_issue(name, json_result=False) issue_obj.update(**kwargs) ###################### # issue comments # ###################### def get_issue_comments(self, issue_name, latest_num=5, json_result=True): """ :param issue_name: :param latest_num: :param json_result: :return: """ try: comments = self.client.comments(issue_name) except JIRAError as exc: logger.warn(exc) return [] comments = comments[::-1][:latest_num] if json_result: return [each.raw for each in comments] else: return comments def add_issue_comment(self, issue_name, msg, commenter=None): """ :param issue_name: :param msg: :param commenter: :return: """ if commenter: users = self.users_by_email(commenter, json_result=False) if users: msg_header = 'The comment is created by {}({}) from AX system. \n\n'.\ format(users[0].displayName, users[0].emailAddress) msg = msg_header + msg return self.client.add_comment(issue_name, msg) ############### # issue type # ############### def get_issue_types(self, json_result=True): """ :param json_result: :return: """ objs = self.client.issue_types() if json_result: return [obj.raw for obj in objs] else: return objs def get_issue_type_by_name(self, name, json_result=True): """ :param name: :param json_result: :return: """ try: obj = self.client.issue_type_by_name(name) except KeyError as exc: logger.warn(exc) return None else: if json_result: return obj.raw else: return obj ############# # Query # ############# def query_issues(self, **kwargs): """ :param kwargs: :return: max_results: maximum number of issues to return. Total number of results If max_results evaluates as False, it will try to get all issues in batches. json_result: JSON response will be returned when this parameter is set to True. Otherwise, ResultList will be returned """ SUPPORTED_KEYS = ('project', 'status', 'component', 'labels', 'issuetype', 'priority', 'creator', 'assignee', 'reporter', 'fixversion', 'affectedversion') max_results = kwargs.pop('max_results', 100) _json_result = kwargs.pop('json_result', False) jql_str_list = [] for k, v in kwargs.items(): if k not in SUPPORTED_KEYS: continue jql_str_list.append('{} = "{}"'.format(k.strip(), v.strip())) if jql_str_list: jql_str = ' AND '.join(jql_str_list) else: jql_str = '' # Fetch ALL issues try: ret = self.client.search_issues(jql_str, maxResults=max_results, json_result=_json_result) except Exception as exc: logger.warn(exc) ret = {"issues": []} return ret ################ # Query Issues # ################ def get_issues_by_project(self, project_name, **kwargs): """ :param project_name: :param kwargs: :return: """ return self.query_issues(project=project_name, **kwargs) def get_issues_by_component(self, component, **kwargs): """ :param component: :param kwargs: :return: """ return self.query_issues(component=component, **kwargs) def get_issues_by_assignee(self, assignee, **kwargs): """ :param assignee: :param kwargs: :return: """ return self.query_issues(assignee=assignee, **kwargs) def get_issues_by_status(self, status, **kwargs): """ :param status: :param kwargs: :return: """ return self.query_issues(status=status, **kwargs) def get_issues_by_label(self, labels, **kwargs): """ :param labels: :param kwargs: :return: """ return self.query_issues(labels=labels, **kwargs) def get_issues_by_fixversion(self, fix_version, **kwargs): """ :param fix_version: :param kwargs: :return: """ return self.query_issues(fixverion=fix_version, **kwargs) def get_issues_by_affectedversion(self, affected_version, **kwargs): """ :param affected_version: :param kwargs: :return: """ return self.query_issues(affectedversion=affected_version, **kwargs) ################ # Query Hours # ################ def get_total_hours(self, **kwargs): """ :param kwargs: :return: """ all_issues = self.query_issues(**kwargs) field_id = self.get_field_id_for_hours_left() hours = [getattr(iss_obj.fields, field_id) for iss_obj in all_issues] return sum([float(each) for each in hours if each]) def get_total_hours_by_project(self, project_name): """ :param project_name: :return: """ return self.get_total_hours(project=project_name) def get_total_hours_by_component(self, component): """ :param component: :return: """ return self.get_total_hours(component=component) def get_total_hours_by_assignee(self, assignee): """ :param assignee: :return: """ return self.get_total_hours(assignee=assignee) def get_total_hours_by_label(self, label): """ :param label: :return: """ return self.get_total_hours(labels=label) ############## # webhook # ############## def create_ax_webhook(self, url, projects=None): """Create AX Jira webhook :param url: :param projects: :return: """ payload = copy.deepcopy(AX_JIRA_WEBHOOK_PAYLOAD) payload['name'] = payload['name'] + self._get_cluster_name() payload['url'] = url filter_dict = self._generate_project_filter(projects) logger.info('Webhook project filter is: %s', filter_dict) payload.update(filter_dict) return self._requests(self.webhook_url, 'post', data=payload) def get_ax_webhook(self): """Get AX Jira webhook :return: """ response = self._requests(self.webhook_url, 'get') wh_name = AX_JIRA_WEBHOOK_PAYLOAD['name'] + self._get_cluster_name() ax_whs = [wh for wh in response.json() if wh['name'] == wh_name] if not ax_whs: logger.error('Could not get Jira webhook for this cluster: %s, ignore it', wh_name) else: return ax_whs[0] def update_ax_webhook(self, projects=None): """Update AX Jira webhook :param projects: :return: """ ax_wh = self.get_ax_webhook() if ax_wh: filter_dict = self._generate_project_filter(projects) logger.info('Webhook project filter is: %s', filter_dict) logger.info('Update the webhook %s', ax_wh['self']) return self._requests(ax_wh['self'], 'put', data=filter_dict) else: logger.warn('Skip the webhook update') def delete_ax_webhook(self): """Delete AX Jira webhook :return: """ response = self._requests(self.webhook_url, 'get') wh_name = AX_JIRA_WEBHOOK_PAYLOAD['name'] + self._get_cluster_name() ax_whs = [wh for wh in response.json() if wh['name'] == wh_name] for wh in ax_whs: logger.info('Delete webhook %s', wh['self']) self._delete_webhook(url=wh['self']) def get_ax_webhooks(self): """Get all AX webhooks :return: """ response = self._requests(self.webhook_url, 'get') webhooks = response.json() # filter out non-ax webhooks return [wh for wh in webhooks if wh['name'].startswith(AX_JIRA_WEBHOOK_PAYLOAD['name'])] def delete_ax_webhooks(self): """Delete all AX Jira webhooks :return: """ ax_whs = self.get_ax_webhooks() for wh in ax_whs: logger.info('Delete webhook %s', wh['self']) self._delete_webhook(url=wh['self']) def _generate_project_filter(self, projects): """ :param projects: :return: """ if not projects: filter_str = '' else: project_filter_list = [] project_objs = self.get_projects(json_result=False) for pkey in projects: ps = [p for p in project_objs if p.key == pkey] if not ps: logger.error('Could not get project %s, ignore it', pkey) else: project_filter_list.append('Project = {}'.format(ps[0].name)) filter_str = ' AND '.join(project_filter_list) return {'filters': {'issue-related-events-section': filter_str } } def _delete_webhook(self, url=None, id=None): """Delete webhook :param url: :param id: :return: """ if url is None: url = self.webhook_url + '/' + str(id) return self._requests(url, 'delete') def _get_cluster_name(self): """ :return: """ return os.environ.get('AX_CLUSTER', UNKNOWN_CLUSTER) def _requests(self, url, method, data=None, headers=None, auth=None, raise_exception=True, timeout=30): """ :param url: :param method: :param data: :param headers: :param auth: :param raise_exception: :param timeout: :return: """ headers = {'Content-Type': 'application/json'} if headers is None else headers auth = self.basic_auth if auth is None else auth try: response = requests.request(method, url, data=json.dumps(data), headers=headers, auth=auth, timeout=timeout) except requests.exceptions.RequestException as exc: logger.error('Unexpected exception occurred during request: %s', exc) raise logger.debug('Response status: %s (%s %s)', response.status_code, response.request.method, response.url) # Raise exception if status code indicates a failure if response.status_code >= 400: logger.error('Request failed (status: %s, reason: %s)', response.status_code, response.text) if raise_exception: response.raise_for_status() return response
class Jira(object): """Python-Jira API class""" def __init__(self, args): """Init""" self.jira_server = 'https://company.jira.net' self.jira = JIRA('%s' % self.jira_server, basic_auth=(args.jira_user, args.jira_pswd)) # Arguments self.release_name = args.release_name self.release_date = args.release_date self.project = args.project self.status = args.status self.fix_version = args.fix_version self.tkts_resolved = args.tkts_resolved self.verbose = args.verbose self.issues = [] def search_issues(self): """Return issues searched with JQL""" search_str = 'project = %s AND status = %s AND fixVersion = %s order by lastViewed DESC' % \ (self.project, self.status, self.fix_version) return self.jira.search_issues(search_str) def is_version_exist(self): """Check if version exist""" # New version is in the last element of the list version = self.jira.project_versions(self.project)[-1] if str(version) == self.release_name: return True else: return False def create_version(self): """Create Jira release version""" descrp = [] # Adding tickets summary to release description for tkt in self.issues: descrp.append((tkt.fields.summary).encode('UTF8')) descrp = '. '.join(descrp) self.jira.create_version(self.release_name,\ self.project,\ description=descrp,\ releaseDate=self.release_date,\ startDate=None,\ archived=False,\ released=True) def tickets_resolved_log(self): """Create a file with resolved tickets summary""" version = self.jira.project_versions(self.project)[-1] release_url = '%s/projects/%s/versions/%s/tab/release-report-all-issues/' % ( self.jira_server, self.project, str(version.id)) hdr_str = "======================================================\n" \ "The new Fixes or Features added in this release are:\n" \ "\n" \ "%s\n\n" % release_url tail_str = "======================================================" with open(self.tkts_resolved, "a") as tkts_file: tkts_file.write(hdr_str) for tkt in self.issues: tkts_file.write("%s: %s \n" % (tkt.key, tkt.fields.summary)) tkts_file.write(tail_str) def update_issue_fix_version(self): """ Update fixVersion of tickets""" fixVersion = [] fixVersion.append({'name': self.release_name}) for tkt in self.issues: tkt.update(fields={'fixVersions': fixVersion}) def create_release(self): """ Workflow to create release: 1. Search tickets for release 2. Create Jira release version 3. Update fixVersion of tickets 4. Parse tickets resolved to file for release announcment """ self.issues = self.search_issues() version_exist = self.is_version_exist() if version_exist == False: self.create_version() self.update_issue_fix_version() self.tickets_resolved_log() def command(self, args): """Commands for Jira release""" if 'create_release' in args.command: self.create_release() elif 'close_release' in args.command: self.close_release()
props = load_properties('gradle.properties') release_version = props['versionMajor'] + '.' + props['versionMinor'] + '.' + props['versionPatch'] for project in projects: version_exists = False versions = jira.project_versions(project) for version in versions: if version.name == release_version: version_exists = True break if(version_exists): print('version ' + release_version + ' in project ' + project + ' exists - dont create one\n') else: print('version ' + release_version + ' in project ' + project + ' not found - creating it!\n') version = jira.create_version(release_version, project_version) issues = [] added = [] bugs = [] issues = scan_for_tickets() for issueCode in issues: try: issue = jira.issue(issueCode) except JIRAError as e: print(issueCode + "not found") set_fixVersions(issue, version) if issue.fields.issuetype.name in bugTypes: bugs.append(issue) elif issue.fields.issuetype.name in ignoredTypes:
def main(): statusDict = { 'integration': { 'transitionId': 131, 'status': 'MERGED' }, 'uat': { 'transitionId': 151, 'status': 'APPROVED' } } # Determine which product is to be deployed product = input( 'Is this a \"VICE\", \"Workflow\", or \"Dispatch\" deployment?\n') while product.lower() not in [ 'vice', 'dispatch', 'dis', 'vc', 'workflow', 'wrk' ]: try: product = input('Invalid selection. Try again...\n') except: print('Ya blew it...') sys.exit(1) # Determine to which environment changes are to be deployed environment = input('Is this an \"integration\" or \"UAT\" deployment?\n') while environment.lower() not in ['integration', 'uat']: try: environment = input('Invalid selection. Try again...\n') except: print('Ya blew it...') sys.exit(1) # Assign appropriate values to variables based on selections if product.lower() in ['vice', 'vc']: project = 'VC' elif product.lower() in ['dispatch', 'dis']: project = 'DIS' elif product.lower() in ['workflow', 'wrk']: project = 'WRK' # project = 'SBX' if environment.lower() == 'integration': statusInfo = statusDict['integration'] else: statusInfo = statusDict['uat'] # Authenticate options = {'server': 'https://snapsheettech.atlassian.net/'} jira = JIRA(options, basic_auth=('EMAIL HERE', 'API KEY HERE')) # Create new version in the appropriate project. Use existing version if matching version already exists. version = input('Please provide a name for the Fix Version: ') try: newVersion = jira.create_version(version, project) except: projectVersions = jira.project_versions(project) for i in range(len(projectVersions)): if projectVersions[i].name == version: newVersion = projectVersions[i] pass # Transition issues and add new Fix Version print('\nWorking... This may take some time.\n') # Issue queries are broken up into 100 issue chunks due to limitations of the Jira API block_num = 0 block_size = 100 while True: start_idx = block_num * block_size issues = jira.search_issues( 'project = %s AND status = %s order by created DESC' % (project, statusInfo['status']), startAt=start_idx, maxResults=block_size) if len(issues) == 0: break block_num += 1 for issue in issues: fixVersions = [] for version in issue.fields.fixVersions: fixVersions.append({'name': version.name}) fixVersions.append({'name': newVersion.name}) jira.transition_issue(issue.id, statusInfo['transitionId'], fields={'fixVersions': fixVersions}, comment='Issue transitioned via automation.') print('All done.\n')
class ViraAPI(): ''' This class gets imported by __init__.py ''' def __init__(self): ''' Initialize vira ''' # Load user-defined config files file_servers = vim.eval('g:vira_config_file_servers') file_projects = vim.eval('g:vira_config_file_projects') try: self.vira_servers = load_config(file_servers) self.vira_projects = load_config(file_projects) except: print(f'Could not load {file_servers} or {file_projects}') self.userconfig_filter_default = { 'assignee': '', 'component': '', 'fixVersion': '', 'issuetype': '', 'priority': '', 'project': '', 'reporter': '', 'status': '', "'Epic Link'": '', 'statusCategory': ['To Do', 'In Progress'], 'text': '' } self.reset_filters() self.userconfig_newissue = { 'assignee': '', 'component': '', 'fixVersion': '', 'issuetype': 'Bug', 'priority': '', 'epics': '', 'status': '', } self.users = set() self.versions = set() self.servers = set() self.users_type = '' self.async_count = 0 self.versions_hide(True) def _async(self, func): try: func() except: pass def _async_vim(self): # TODO: VIRA-247 [210223] - Clean-up vim variables in python _async try: if len(vim.eval('s:versions')) == 0: vim.command('let s:projects = s:projects[1:]') if len(vim.eval('s:projects')) == 0: # TODO: VIRA-247 [210223] - Check for new projects and versions and start PRIORITY ranking for updates vim.command('let s:vira_async_timer = g:vira_async_timer') self.get_projects() self.get_versions() else: # self.version_percent(str(vim.eval('s:projects[0]')), str(vim.eval('s:versions[0]'))) vim.command('let s:versions = s:versions[1:]') if self.async_count == 0 and vim.eval( 's:vira_async_timer') == 10000: self.users = self.get_users() self.async_count = 1000 self.async_count -= 1 except: pass def create_issue(self, input_stripped): ''' Create new issue in jira ''' section = { 'summary': parse_prompt_text(input_stripped, '*Summary*', 'Description'), 'description': parse_prompt_text(input_stripped, 'Description', '*Project*'), 'project': parse_prompt_text(input_stripped, '*Project*', '*IssueType*'), 'issuetype': parse_prompt_text(input_stripped, '*IssueType*', 'Status'), 'status': parse_prompt_text(input_stripped, 'Status', 'Priority'), 'priority': parse_prompt_text(input_stripped, 'Priority', 'Component'), 'components': parse_prompt_text(input_stripped, 'Component', 'Version'), 'fixVersions': parse_prompt_text(input_stripped, 'Version', 'Assignee'), 'assignee': parse_prompt_text(input_stripped, 'Assignee'), } # Check if required fields was entered by user if section['summary'] == '' or section['project'] == '' or section[ 'issuetype'] == '': return issue_kwargs = { 'project': section['project'], 'summary': section['summary'], 'description': section['description'], 'issuetype': { 'name': section['issuetype'] }, 'priority': { 'name': section['priority'] }, 'components': [{ 'name': section['components'] }], 'fixVersions': [{ 'name': section['fixVersions'] }], 'assignee': { 'name': section['assignee'] }, } # Jira API doesn't accept empty fields for certain keys for key in issue_kwargs.copy().keys(): if section[key] == '': issue_kwargs.pop(key) # Create issue and transition issue_key = self.jira.create_issue(**issue_kwargs) if section['status'] != '': self.jira.transition_issue(issue_key, section['status']) jira_server = vim.eval('g:vira_serv') print(f'Added {jira_server}/browse/{issue_key}') def add_worklog(self, issue, timeSpentSeconds, comment): ''' Calculate the offset for the start time of the time tracking ''' earlier = datetime.now() - datetime.timedelta(seconds=timeSpentSeconds) self.jira.add_worklog(issue=issue, timeSpentSeconds=timeSpentSeconds, comment=comment, started=earlier) def connect(self, server): ''' Connect to Jira server with supplied auth details ''' self.users = set() self.versions = set() self.users_type = '' try: # Specify whether the server's TLS certificate needs to be verified if self.vira_servers[server].get('skip_cert_verify'): urllib3.disable_warnings( urllib3.exceptions.InsecureRequestWarning) cert_verify = False else: cert_verify = True # Get auth for current server username = self.vira_servers[server].get('username') password_cmd = self.vira_servers[server].get('password_cmd') if password_cmd: password = run_command(password_cmd)['stdout'].strip().split( '\n')[0] else: password = self.vira_servers[server]['password'] except: cert_verify = True server = vim.eval('input("server: ")') vim.command('let g:vira_serv = "' + server + '"') username = vim.eval('input("username: "******"password: "******"' + server + '"') # Authorize self.jira = JIRA(options={ 'server': server, 'verify': cert_verify, }, basic_auth=(username, password), timeout=2, async_=True, max_retries=2) # Initial list updates self.users = self.get_users() self.get_projects() self.get_versions() vim.command('echo "Connection to jira server was successful"') except JIRAError as e: if 'CAPTCHA' in str(e): vim.command( 'echo "Could not log into jira! Check authentication details and log in from web browser to enter mandatory CAPTCHA."' ) else: # vim.command('echo "' + str(e) + '"') vim.command('let g:vira_serv = ""') # raise e except: vim.command('let g:vira_serv = ""') vim.command( 'echo "Could not log into jira! See the README for vira_server.json information"' ) def filter_str(self, filterType): ''' Build a filter string to add to a JQL query The string will look similar to one of these: AND status in ('In Progress') AND status in ('In Progress', 'To Do') ''' if self.userconfig_filter.get(filterType, '') == '': return selection = str( self.userconfig_filter[filterType]).strip('[]') if type( self.userconfig_filter[filterType] ) == list else self.userconfig_filter[filterType] if type( self.userconfig_filter[filterType] ) == tuple else "'" + self.userconfig_filter[filterType] + "'" return str(f"{filterType} in ({selection})").replace( "'None'", "Null").replace("'Unassigned'", "Null").replace( "'currentUser'", "currentUser()").replace( "'currentUser()'", "currentUser()").replace( "'currentuser'", "currentUser()").replace( "'currentuser()'", "currentUser()").replace("'null'", "Null").replace( f"text in ({selection})", f"text ~ {selection}") def get_assign_issue(self): ''' Menu to select users ''' self.print_users() def get_assignees(self): ''' Get my issues with JQL ''' self.print_users() def get_comments(self, issue): ''' Get all the comments for an issue ''' # Get the issue requested issues = self.jira.search_issues('issue = "' + issue.key + '"', fields='summary,comment', json_result='True') # Loop through all of the comments comments = '' for comment in issues["issues"][0]["fields"]["comment"]["comments"]: comments += (f"{comment['author']['displayName']}" + ' | ', f"{comment['updated'][0:10]}" + ' @ ', f"{comment['updated'][11:16]}" + ' | ', f"{comment['body']} + '\n'") return comments def get_components(self): ''' Build a vim pop-up menu for a list of components ''' for component in self.jira.project_components( self.userconfig_filter['project']): print(component.name) print('None') def get_component(self): ''' Build a vim pop-up menu for a list of components ''' self.get_components() def get_epic(self): self.get_epics() def get_epics(self): ''' Get my issues with JQL ''' hold = dict(self.userconfig_filter) project = self.userconfig_filter['project'] self.reset_filters() self.userconfig_filter["issuetype"] = "Epic" self.userconfig_filter["project"] = project self.get_issues() print('None') self.userconfig_filter = hold def get_issue(self, issue): ''' Get single issue by issue id ''' return self.jira.issue(issue) def get_issues(self): ''' Get my issues with JQL ''' issues = [] key_length = 0 summary_length = 0 issuetype_length = 0 status_length = 4 user_length = 0 for issue in self.query_issues(): fields = issue['fields'] user = str(fields['assignee']['displayName']) if type( fields['assignee']) == dict else 'Unassigned' user_length = len(user) if len(user) > user_length else user_length key_length = len( issue['key']) if len(issue['key']) > key_length else key_length summary = fields['summary'] summary_length = len( summary) if len(summary) > summary_length else summary_length issuetype = fields['issuetype']['name'] issuetype_length = len(issuetype) if len( issuetype) > issuetype_length else issuetype_length status = fields['status']['name'] status_length = len( status) if len(status) > status_length else status_length issues.append([ issue['key'], fields['summary'], fields['issuetype']['name'], fields['status']['name'], user ]) # Add min/max limits on summary length columns = vim.eval("&columns") min_summary_length = 25 max_summary_length = int( columns) - key_length - issuetype_length - status_length - 28 summary_length = min_summary_length if max_summary_length < min_summary_length else max_summary_length if summary_length > max_summary_length else summary_length for issue in issues: print(('{: <' + str(key_length) + '}').format(issue[0]) + " │ " + ('{: <' + str(summary_length) + '}').format(issue[1][:summary_length]) + " │ " + ('{: <' + str(issuetype_length) + '}').format(issue[2]) + " │ " + ('{: <' + str(status_length) + '}').format(issue[3]) + ' │ ' + issue[4]) def get_issuetypes(self): ''' Get my issues with JQL ''' for issuetype in self.jira.issue_types(): print(issuetype) def get_issuetype(self): ''' Get my issues with JQL ''' for issuetype in self.jira.issue_types(): print(issuetype) def get_priorities(self): ''' Get my issues with JQL ''' for priority in self.jira.priorities(): print(priority) def print_projects(self): ''' Build a vim pop-up menu for a list of projects ''' all_projects = self.get_projects() batch_size = 10 project_batches = [ all_projects[i:i + batch_size] for i in range(0, len(all_projects), batch_size) ] for batch in project_batches: projects = self.jira.createmeta(projectKeys=','.join(batch), expand='projects')['projects'] [print(p['key'] + ' ~ ' + p['name']) for p in projects] def get_projects(self): ''' Build a vim pop-up menu for a list of projects ''' # Project filter for version list self.projects = [] for project in self.jira.projects(): self.projects.append(str(project)) vim.command('let s:projects = ' + str(self.projects)) return self.projects def get_priority(self): ''' Build a vim pop-up menu for a list of projects ''' self.get_priorities() def get_prompt_text(self, prompt_type, comment_id=None): ''' Get prompt text used for inputting text into jira ''' self.prompt_type = prompt_type # Edit filters if prompt_type == 'edit_filter': self.prompt_text_commented = '\n# Edit all filters in JSON format' self.prompt_text = json.dumps( self.userconfig_filter, indent=True) + self.prompt_text_commented return self.prompt_text # Edit summary active_issue = vim.eval("g:vira_active_issue") if prompt_type == 'summary': self.prompt_text_commented = '\n# Edit issue summary' summary = self.jira.search_issues( 'issue = "' + active_issue + '"', fields=','.join(['summary']), json_result='True')['issues'][0]['fields']['summary'] self.prompt_text = summary + self.prompt_text_commented return self.prompt_text # Edit description if prompt_type == 'description': self.prompt_text_commented = '\n# Edit issue description' description = self.jira.search_issues( 'issue = "' + active_issue + '"', fields=','.join(['description']), json_result='True')['issues'][0]['fields'].get('description') if description: description = description.replace('\r\n', '\n') else: description = '' self.prompt_text = description + self.prompt_text_commented return self.prompt_text self.prompt_text_commented = ''' # --------------------------------- # Please enter text above this line # An empty message will abort the operation. # # Below is a list of acceptable values for each input field. # # Users:''' for user in self.users: user = user.split(' ~ ') name = user[0] id = user[1] if self.users_type == 'accountId': self.prompt_text_commented += f''' # [{name}|~accountid:{id}]''' else: self.prompt_text_commented += f''' # [~{id}]''' # Add comment if self.prompt_type == 'add_comment': self.prompt_text = self.prompt_text_commented return self.prompt_text # Edit comment if self.prompt_type == 'edit_comment': self.active_comment = self.jira.comment(active_issue, comment_id) self.prompt_text = self.active_comment.body + self.prompt_text_commented return self.prompt_text statuses = [x.name for x in self.jira.statuses()] issuetypes = [x.name for x in self.jira.issue_types()] priorities = [x.name for x in self.jira.priorities()] components = [ x.name for x in self.jira.project_components( self.userconfig_filter['project']) ] if self.userconfig_filter['project'] != '' else '' versions = [ x.name for x in self.jira.project_versions( self.userconfig_filter['project']) ] if self.userconfig_filter['project'] != '' else '' projects = [x.key for x in self.jira.projects()] # Extra info for prompt_type == 'issue' self.prompt_text_commented += f''' # # Projects: {projects} # IssueTypes: {issuetypes} # Statuses: {statuses} # Priorities: {priorities} # Components in {self.userconfig_filter["project"]} Project: {components} # Versions in {self.userconfig_filter["project"]} Project: {versions}''' self.prompt_text = f'''[*Summary*] [Description] [*Project*] {self.userconfig_filter["project"]} [*IssueType*] {self.userconfig_newissue["issuetype"]} [Status] {self.userconfig_newissue["status"]} [Priority] {self.userconfig_newissue["priority"]} [Component] {self.userconfig_newissue["component"]} [Version] {self.userconfig_newissue["fixVersion"]} [Assignee] {self.userconfig_newissue["assignee"]} {self.prompt_text_commented}''' return self.prompt_text def format_date(self, date): time = datetime.now().strptime(date, '%Y-%m-%dT%H:%M:%S.%f%z').astimezone() return str(time)[0:10] + ' ' + str(time)[11:16] def get_report(self): ''' Print a report for the given issue ''' for customfield in self.jira.fields(): if customfield['name'] == 'Epic Link': epicID = customfield['id'] # Get passed issue content active_issue = vim.eval("g:vira_active_issue") issues = self.jira.search_issues( 'issue = "' + active_issue + '"', fields=','.join([ 'project', 'summary', 'comment', 'component', 'description', 'issuetype', 'priority', 'status', 'created', 'updated', 'assignee', 'reporter', 'fixVersion', 'customfield_10106', 'labels', epicID ]), json_result='True') issue = issues['issues'][0]['fields'] # Prepare report data open_fold = '{{{' close_fold = '}}}' summary = issue['summary'] story_points = str(issue.get('customfield_10106', '')) created = self.format_date(issue['created']) updated = self.format_date(issue['updated']) issuetype = issue['issuetype']['name'] status = issue['status']['name'] priority = issue['priority']['name'] assignee = issue['assignee']['displayName'] if type( issue['assignee']) == dict else 'Unassigned' reporter = issue['reporter']['displayName'] component = ', '.join([c['name'] for c in issue['components']]) version = ', '.join([v['name'] for v in issue['fixVersions']]) epics = str(issue.get(epicID)) vim.command(f'let s:vira_epic_field = "{epicID}"') description = str(issue.get('description')) # Version percent for single version attacted # if len(issue['fixVersions']) == 1 and version != '': # version += ' | ' + self.version_percent( # str(issue['project']['key']), version) + '%' comments = '' idx = 0 for idx, comment in enumerate((issue['comment']['comments'])): comments += ''.join([ comment['author']['displayName'] + ' @ ' + self.format_date(comment['updated']) + ' {{' + '{2\n' + comment['body'] + '\n}}}\n' ]) old_count = idx - 3 old_comment = 'Comment' if old_count == 1 else 'Comments' comments = ''.join( [str(old_count) + ' Older ' + old_comment + ' {{{1\n']) + comments if old_count >= 1 else comments comments = comments.replace('}}}', '}}}}}}', idx - 3) comments = comments.replace('}}}}}}', '}}}', idx - 4) # Find the length of the longest word [-1] words = [ created, updated, issuetype, status, story_points, priority, component, version, assignee, reporter, epics ] wordslength = sorted(words, key=len)[-1] s = '─' dashlength = s.join([char * len(wordslength) for char in s]) active_issue_spacing = int((16 + len(dashlength)) / 2 - len(active_issue) / 2) active_issue_spaces = ' '.join( [char * (active_issue_spacing) for char in ' ']) active_issue_space = ' '.join([ char * ((len(active_issue) + len(dashlength)) % 2) for char in ' ' ]) created_spaces = ' '.join( [char * (len(dashlength) - len(created)) for char in ' ']) updated_spaces = ' '.join( [char * (len(dashlength) - len(updated)) for char in ' ']) task_type_spaces = ' '.join( [char * (len(dashlength) - len(issuetype)) for char in ' ']) status_spaces = ' '.join( [char * (len(dashlength) - len(status)) for char in ' ']) story_points_spaces = ''.join( [char * (len(dashlength) - len(story_points)) for char in ' ']) priority_spaces = ''.join( [char * (len(dashlength) - len(priority)) for char in ' ']) component_spaces = ''.join( [char * (len(dashlength) - len(component)) for char in ' ']) version_spaces = ''.join( [char * (len(dashlength) - len(version)) for char in ' ']) assignee_spaces = ''.join( [char * (len(dashlength) - len(assignee)) for char in ' ']) reporter_spaces = ''.join( [char * (len(dashlength) - len(reporter)) for char in ' ']) epics_spaces = ''.join( [char * (len(dashlength) - len(epics)) for char in ' ']) # Create report template and fill with data report = '''┌────────────────{dashlength}─┐ │{active_issue_spaces}{active_issue}{active_issue_spaces}{active_issue_space} │ ├──────────────┬─{dashlength}─┤ │ Created │ {created}{created_spaces} │ │ Updated │ {updated}{updated_spaces} │ │ Type │ {issuetype}{task_type_spaces} │ │ Status │ {status}{status_spaces} │ │ Story Points │ {story_points}{story_points_spaces} │ │ Priority │ {priority}{priority_spaces} │ │ Epic Link │ {epics}{epics_spaces} │ │ Component(s) │ {component}{component_spaces} │ │ Version(s) │ {version}{version_spaces} │ │ Assignee │ {assignee}{assignee_spaces} │ │ Reporter │ {reporter}{reporter_spaces} │ └──────────────┴─{dashlength}─┘ ┌──────────────┐ │ Summary │ └──────────────┘ {summary} ┌──────────────┐ │ Description │ └──────────────┘ {description} ┌──────────────┐ │ Comments │ └──────────────┘ {comments}''' self.set_report_lines(report, description, issue) self.prompt_text = self.report_users(report.format(**locals())) return self.prompt_text def report_users(self, report): ''' Replace report accountid with names ''' for user in self.users: user = user.split(' ~ ') if user[0] != "Unassigned": report = report.replace('accountid:', '').replace('[~' + user[1] + ']', '[~' + user[0] + ']') return report def get_reporters(self): ''' Get my issues with JQL ''' self.print_users() def get_servers(self): ''' Get list of servers ''' try: for server in self.vira_servers.keys(): print(server) print('Null') except: self.connect('') def get_statuses(self): ''' Get my issues with JQL ''' statuses = [] for status in self.jira.statuses(): if str(status) not in statuses: statuses.append(str(status)) print(str(status)) def get_set_status(self): ''' Get my issues with JQL ''' self.get_statuses() def get_version(self): ''' Get my issues with JQL ''' self.print_versions() def new_component(self, name, project): ''' New component added to project ''' self.jira.create_component(name=name, project=project, description=name) def new_version(self, name, project, description): ''' Get my issues with JQL ''' self.jira.create_version(name=name, project=project, description=description) def print_users(self): ''' Print users ''' print(self.get_current_user() + ' ~ currentUser') for user in self.users: print(user) print('Unassigned') def get_users(self): ''' Get my issues with JQL ''' query = 'ORDER BY updated DESC' issues = self.jira.search_issues(query, fields='assignee, reporter', json_result='True', maxResults=-1) # Determine cloud/server jira self.users_type = 'accountId' if issues['issues'][0]['fields'][ 'reporter'].get('accountId') else 'name' for issue in issues['issues']: user = str(issue['fields']['reporter']['displayName'] ) + ' ~ ' + issue['fields']['reporter'][self.users_type] self.users.add(user) if type(issue['fields']['assignee']) == dict: user = str( issue['fields']['assignee']['displayName'] ) + ' ~ ' + issue['fields']['assignee'][self.users_type] self.users.add(user) return sorted(self.users) def get_current_user(self): query = 'reporter = currentUser() or assignee = currentUser()' issues = self.jira.search_issues(query, fields='assignee, reporter', json_result='True', maxResults=-1) issue = issues['issues'][0]['fields'] return str(issue['assignee'][self.users_type] if type( issue['assignee']) == dict else issue['reporter'][self.users_type] if type(issue['reporter']) == dict else 'Unassigned') def print_versions(self): ''' Print version list with project filters ''' try: versions = sorted(self.versions) wordslength = sorted(versions, key=len)[-1] s = ' ' dashlength = s.join([char * len(wordslength) for char in s]) for version in versions: print( version.split('|')[0] + ''.join([ char * (len(dashlength) - len(version)) for char in ' ' ]) + ' ' + version.split('|')[1] + ' ' + version.split('|')[2]) except: pass print('None') def version_percent(self, project, fixVersion): project = str(project) fixVersion = str(fixVersion) if str(project) != '[]' and str(project) != '' and str( fixVersion) != '[]' and str(fixVersion) != '': query = 'fixVersion = ' + fixVersion + ' AND project = "' + project + '"' issues = self.jira.search_issues(query, fields='fixVersion', json_result='True', maxResults=1) try: issue = issues['issues'][0]['fields']['fixVersions'][0] idx = issue['id'] total = self.jira.version_count_related_issues( idx)['issuesFixedCount'] pending = self.jira.version_count_unresolved_issues(idx) fixed = total - pending percent = str(round(fixed / total * 100, 1)) if total != 0 else 1 space = ''.join([char * (5 - len(percent)) for char in ' ']) name = fixVersion try: description = issue['description'] except: description = 'None' pass except: total = 0 pending = 0 fixed = total - pending percent = "0" space = ''.join([char * (5 - len(percent)) for char in ' ']) name = fixVersion description = '' pass version = str( str(name) + ' ~ ' + str(description) + '|' + str(fixed) + '/' + str(total) + space + '|' + str(percent) + '%') self.versions_hide = vim.eval('g:vira_version_hide') if fixed != total or total == 0 or not int( self.versions_hide) == 1: self.versions.add( str(project) + ' ~ ' + str(version.replace('\'', ''))) else: percent = 0 return percent def get_versions(self): ''' Build a vim pop-up menu for a list of versions with project filters ''' # Loop through each project and all versions within try: for v in reversed( self.jira.project_versions(vim.eval('s:projects[0]'))): vim.command('let s:versions = add(s:versions,\"' + str(v) + '\")') except: vim.command('let s:versions = []') def load_project_config(self, repo): ''' Load project configuration for the current git repo The current repo can either be determined by current files path or by the user setting g:vira_repo (part of :ViraLoadProject) For example, an entry in projects.yaml may be: vira: server: https://jira.tgall.ca project_name: VIRA ''' # Only proceed if projects file parsed successfully if not getattr(self, 'vira_projects', None): return # If current repo/folder doesn't exist, use __default__ project config if it exists if repo == '': repo = run_command( 'git rev-parse --show-toplevel')['stdout'].strip() if not self.vira_projects.get(repo): repo = repo.split('/')[-1] if not self.vira_projects.get(repo): repo = run_command('pwd')['stdout'].strip() if not self.vira_projects.get(repo): repo = repo.split('/')[-1] if not self.vira_projects.get(repo): repo = '__default__' if not self.vira_projects.get('__default__'): return # Set server server = self.vira_projects.get(repo, {}).get('server') if server: vim.command(f'let g:vira_serv = "{server}"') # Set user-defined filters for current project for key in self.userconfig_filter.keys(): value = self.vira_projects.get(repo, {}).get('filter', {}).get(key) if value: self.userconfig_filter[key] = value # Set user-defined new-issue defaults for current project for key in self.userconfig_newissue.keys(): value = self.vira_projects.get(repo, {}).get('newissue', {}).get(key) if value: self.userconfig_newissue[key] = value # Set user-defined issue sort options sort_order = self.vira_projects.get(repo, {}).get('issuesort', 'updated DESC') self.userconfig_issuesort = ', '.join(sort_order) if type( sort_order) == list else sort_order def query_issues(self): ''' Query issues based on current filters ''' q = [] for filterType in self.userconfig_filter.keys(): filter_str = self.filter_str(filterType) if filter_str: q.append(filter_str) query = ' AND '.join(q) + ' ORDER BY ' + self.userconfig_issuesort issues = self.jira.search_issues( query, fields='summary,comment,status,statusCategory,issuetype,assignee', json_result='True', maxResults=vim.eval('g:vira_issue_limit')) return issues['issues'] def reset_filters(self): ''' Reset filters to their default values ''' self.userconfig_filter = dict(self.userconfig_filter_default) def set_report_lines(self, report, description, issue): ''' Create dictionary for vira report that shows relationship between line numbers and fields to be edited ''' writable_fields = { 'Assignee': 'ViraSetAssignee', 'Component': 'ViraSetComponent', 'Priority': 'ViraSetPriority', 'Epic Link': 'ViraSetEpic', 'Status': 'ViraSetStatus', 'Type': 'ViraSetType', 'Version': 'ViraSetVersion', } self.report_lines = {} for idx, line in enumerate(report.split('\n')): for field, command in writable_fields.items(): if field in line: self.report_lines[idx + 1] = command continue for x in range(16, 21): self.report_lines[x] = 'ViraEditSummary' description_len = description.count('\n') + 3 for x in range(21, 23 + description_len): self.report_lines[x] = 'ViraEditDescription' offset = 2 if len(issue['comment']['comments']) > 4 else 1 comment_line = 25 + description_len + offset for comment in issue['comment']['comments']: comment_len = comment['body'].count('\n') + 3 for x in range(comment_line, comment_line + comment_len): self.report_lines[x] = 'ViraEditComment ' + comment['id'] comment_line = comment_line + comment_len def set_prompt_text(self): ''' Take the user prompt text and perform an action Usually, this involves writing to the jira server ''' # User input issue = vim.eval('g:vira_active_issue') userinput = vim.eval('g:vira_input_text') input_stripped = userinput.replace(self.prompt_text_commented.strip(), '').strip() # Check if anything was actually entered by user if input_stripped == '' or userinput.strip() == self.prompt_text.strip( ): print("No vira actions performed") return if self.prompt_type == 'edit_filter': self.userconfig_filter = json.loads(input_stripped) elif self.prompt_type == 'add_comment': self.jira.add_comment(issue, input_stripped) elif self.prompt_type == 'edit_comment': self.active_comment.update(body=input_stripped) elif self.prompt_type == 'summary': self.jira.issue(issue).update(summary=input_stripped) elif self.prompt_type == 'description': self.jira.issue(issue).update(description=input_stripped) elif self.prompt_type == 'issue': self.create_issue(input_stripped) def versions_hide(self, state): ''' Display and hide complete versions ''' if state is True or 1 or 'ture' or 'True': self.version_hide = True elif state is False or 0 or 'false' or 'False': self.version_hide = False else: self.version_hide = not self.version_hide
if dba == "dba": dba_needed = True devgroup = app.upper() + "-developers" datetime = day + "T" + deploytime project = jira.project(app.upper()) # Make sure Release Version exists in JIRA versions = jira.project_versions(project) print "Checking for the Version..." for v in versions: if v.name == version: version_exists = True if not version_exists: print "The version %s does NOT exist. Adding..." % version jira.create_version(version, app.upper()) # Make sure Components exists in JIRA Project components = jira.project_components(project) print "Checking for Components..." for c in components: if c.name == "webapp": webapp = True if c.name == "database": database = True if not webapp: print "webapp component does not exist. Adding..." jira.create_component("webapp", app.upper(), None, None, "UNASSIGNED", False) if not database:
class JiraAPI: """ Jira client has no documentation, so if you need one, use one for REST API: https://developer.atlassian.com/cloud/jira/platform/rest/v3/ """ def __init__(self, settings: Settings): self._settings = settings self.transition = settings.jira.transition self.release_task = settings.jira.release_task self.release_task_name = self.release_task.name.format( version=settings.version, component=self.release_task.component) self._api = JIRA( {"server": settings.jira.connection.server}, basic_auth=(settings.jira.connection.user, settings.jira.connection.token), ) def _create_version(self, project: Project): proposed_name = "Hotfix" if self._settings.version.minor > 0 else "Release" user_input = input(f"Input new Jira version name: [{proposed_name}]: ") name = user_input if user_input else proposed_name return self._api.create_version(name, project, startDate=_get_formatted_date()) def _select_version(self, project: Project, unreleased_versions) -> Optional[Version]: print("Jira versions:") print("1) Skip") print("2) Create new") print("or select existing one:") unreleased_versions = { idx: version for idx, version in enumerate(unreleased_versions, 3) } for idx, version in unreleased_versions.items(): print(f"{idx}) {version.name}") user_input = 0 valid_choices = list(range(1, len(unreleased_versions) + 3)) while user_input not in valid_choices: try: user_input = int( input( "\nChoose which Jira version use for this release: ")) except Exception: continue if user_input == 1: return None elif user_input == 2: return self._create_version(project) else: return unreleased_versions[user_input] def get_version(self) -> Optional[Version]: print_title(f"Searching for Jira release version") project = self._api.project(self.release_task.project) unreleased_versions = [ v for v in self._api.project_versions(project) if not v.released ] return self._select_version(project, unreleased_versions) def _get_jira_release_unfinished_tasks(self, version: Version): """ To check that all tasks in Jira release is finished select them using jql """ final_statuses = '", "'.join(self.transition.child_final_statuses) types_to_skip = '", "'.join(self.transition.child_task_types_to_skip) return self._api.search_issues( f'project = "{self.release_task.project}"' f' AND fixVersion = "{version.name}"' f' AND fixVersion in unreleasedVersions("{self.release_task.project}")' f' AND status NOT IN ("{final_statuses}")' f' AND type NOT IN ("{types_to_skip}")') def _get_transition(self, issue, transition_name): transitions = [ t for t in self._api.transitions(issue) if t["name"].lower() == transition_name.lower() ] if not transitions: return None return transitions[0] def release_version(self, release_task_key: str): print_title( f"Releasing Jira version of release task {release_task_key}") release_task = self._api.issue(release_task_key) for version in release_task.fields.fixVersions: version: Version print(f'Checking Jira release version: "{version.name}"...') if version.released: print_error("Version is already released") continue unfinished_tasks = self._get_jira_release_unfinished_tasks(version) if unfinished_tasks: tasks_str = ", ".join([i.key for i in unfinished_tasks]) print_error( f'Can\'t release Jira version: "{version.name}", it has unfinished tasks: {tasks_str}' ) continue print("Jira version is safe to release, releasing...", end=" ") version.update(released=True, releaseDate=_get_formatted_date()) print("Ok!") def _add_to_release_version(self, version: Version, release_task_key: str): issue = self._api.issue(release_task_key) issue.add_field_value("fixVersions", {"name": version.name}) def make_links(self, version: Optional[Version], release_task_key, related_keys): version_name = version.name if version else "-" print_title(f"Linking tasks found in release branch" f" to release task ({release_task_key})" f' and to Jira version "{version_name}"') if version: self._add_to_release_version(version, release_task_key) print(f"Linking {len(related_keys)} tasks:") partial_make_links = partial(self._make_links, version, release_task_key) with ThreadPool(5) as pool: pool.map(partial_make_links, related_keys) def _make_links(self, version: Optional[Version], release_task_key: str, child_task_key: str): print(f"* {child_task_key}") self._api.create_issue_link(self.release_task.link_type, release_task_key, child_task_key) if version: self._add_to_release_version(version, child_task_key) def make_release_task(self): print_title("Creating Jira release task") extra_fields = {"components": [{"name": self.release_task.component}]} issue = self._api.create_issue( project=self.release_task.project, summary=self.release_task_name, issuetype={"name": self.release_task.type}, **extra_fields, ) print(f"Created Jira release task: {issue.key}") return issue.key def get_release_task(self): print_title("Searching for Jira release task") query = (f'project = "{self.release_task.project}"' f' AND summary ~ "{self.release_task_name}"' f' AND type = "{self.release_task.type}"') found_issues = self._api.search_issues(query) if not found_issues: print("Did not find existing release task") return self.make_release_task() if len(found_issues) > 1: issues_str = ", ".join([i.key for i in found_issues]) print_error( f"Your release task has not unique name, fix it before using this functionality," f" found issues: {issues_str}") exit(1) release_issue = found_issues[0] print(f"Found Jira release task: {release_issue.key}") return release_issue.key def mark_release_task_done(self, release_task_key): print_title( f'Transition release task "{release_task_key}" from "{self.transition.release_from_status}" to "{self.transition.release_to_status}"' ) release_issue = self._api.issue(release_task_key) print_title( f'Current release task status is "{release_issue.fields.status}"') if (release_issue.fields.status.name.lower() != self.transition.release_from_status.lower()): print_error( f'Release task "{release_task_key}" has inproper status') return transition = self._get_transition(release_issue, self.transition.release_to_status) if not transition: print_error( f'Release task "{release_task_key}" has no transition to "{self.transition.release_to_status}"' ) return self._api.transition_issue(release_issue, transition["id"]) print( f'Release task {release_issue.key} has been transited to status "{transition["name"]}"' ) def mark_children_tasks_done(self, release_task_key): print_title( f'Transition children of "{release_task_key}" from "{self.transition.child_from_status}" to "{self.transition.child_to_status}"' ) query = (f'issue in linkedIssues("{release_task_key}")' f' AND status = "{self.transition.child_from_status}"') found_issues = self._api.search_issues(query) to_status = self.transition.child_to_status.lower() if not found_issues: print("Did not find any task for transition") return for issue in found_issues: transition = self._get_transition(issue, to_status) if not transition: print_error( f'Issue "{issue.key}" does not have transition to status "{self.transition.child_to_status}"' ) continue self._api.transition_issue(issue, transition["id"]) print( f'Task {issue.key} has been transited to status "{transition["name"]}"' )
class Jira(object): """Python-Jira API class""" def __init__(self, args): """Init""" self.jira_server = 'https://company.jira.net' self.jira = JIRA('%s' % self.jira_server, basic_auth=(args.jira_user,args.jira_pswd)) # Arguments self.release_name = args.release_name self.release_date = args.release_date self.project = args.project self.status = args.status self.fix_version = args.fix_version self.tkts_resolved = args.tkts_resolved self.verbose = args.verbose self.issues = [] def search_issues(self): """Return issues searched with JQL""" search_str = 'project = %s AND status = %s AND fixVersion = %s order by lastViewed DESC' % \ (self.project, self.status, self.fix_version) return self.jira.search_issues(search_str) def is_version_exist(self): """Check if version exist""" # New version is in the last element of the list version = self.jira.project_versions(self.project)[-1] if str(version) == self.release_name: return True else: return False def create_version(self): """Create Jira release version""" descrp = [] # Adding tickets summary to release description for tkt in self.issues: descrp.append((tkt.fields.summary).encode('UTF8')) descrp = '. '.join(descrp) self.jira.create_version(self.release_name,\ self.project,\ description=descrp,\ releaseDate=self.release_date,\ startDate=None,\ archived=False,\ released=True) def tickets_resolved_log(self): """Create a file with resolved tickets summary""" version = self.jira.project_versions(self.project)[-1] release_url = '%s/projects/%s/versions/%s/tab/release-report-all-issues/' % (self.jira_server, self.project, str(version.id)) hdr_str = "======================================================\n" \ "The new Fixes or Features added in this release are:\n" \ "\n" \ "%s\n\n" % release_url tail_str = "======================================================" with open(self.tkts_resolved, "a") as tkts_file: tkts_file.write(hdr_str) for tkt in self.issues: tkts_file.write("%s: %s \n" % (tkt.key, tkt.fields.summary)) tkts_file.write(tail_str) def update_issue_fix_version(self): """ Update fixVersion of tickets""" fixVersion = [] fixVersion.append({'name': self.release_name}) for tkt in self.issues: tkt.update(fields={'fixVersions': fixVersion}) def create_release(self): """ Workflow to create release: 1. Search tickets for release 2. Create Jira release version 3. Update fixVersion of tickets 4. Parse tickets resolved to file for release announcment """ self.issues = self.search_issues() version_exist = self.is_version_exist() if version_exist == False: self.create_version() self.update_issue_fix_version() self.tickets_resolved_log() def command(self, args): """Commands for Jira release""" if 'create_release' in args.command: self.create_release() elif 'close_release' in args.command: self.close_release()
from jira import JIRA # You can either use your Jira username and password OR get API token from Jira to connect to # your Jira instance. Getting API token is preferable, because you don't want to expose your plain text # password in python code. # Atlassian has instructions at https://confluence.atlassian.com/cloud/api-tokens-938839638.html # to create api token. options = {"server": "https://your-site.atlassian.net"} jira = JIRA(options, basic_auth=("your-user-name", "api-token")) LIST_OF_PROJECTS = ["PLATFORM", "PAYMENTS", "ENTERPRISE", "DATA"] # Expand this list, if you want to create multiple fix versions at once. # Make sure fixversion and releaseDates lists have same number of values LIST_OF_FIXVERSIONS = ["2019-12-18 US Release"] LIST_OF_RELEASEDATES = ["2019-12-18"] # Loop thru project list and create fix versions for each project. # Don't worry about creating duplicate fix version, JIRA will reject duplicate requests for i in enumerate(LIST_OF_PROJECTS): for j in enumerate(LIST_OF_FIXVERSIONS): newversion = jira.create_version( name=LIST_OF_FIXVERSIONS[j], project=LIST_OF_PROJECTS[i], releaseDate=LIST_OF_RELEASEDATES[j], ) print(LIST_OF_FIXVERSIONS[j] + " is created for project " + LIST_OF_PROJECTS[i])