def main(username, password, origin_project, target_project, verbose): jira = JIRA(JIRA_SERVER_URL, basic_auth=(username, password)) _origin_project_versions = jira.project_versions(origin_project) origin_project_versions = [ version for version in _origin_project_versions if version_pattern.match(version.name) is not None ] _target_project_versions = jira.project_versions(target_project) target_project_versions = [ version for version in _target_project_versions if version_pattern.match(version.name) is not None ] if verbose: click.echo("\nOriginal project versions") for version in origin_project_versions: click.echo("- {}".format(version.name)) click.echo("\nTarget project versions") for version in origin_project_versions: click.echo("- {}".format(version.name)) # Put diff of original and target versions_to_creation = get_versions_for_creation( origin_project_versions, target_project_versions ) create_versions(jira, target_project, versions_to_creation)
class TestVersions(unittest.TestCase): def setUp(self): self.jira = JIRA(options=dict(server=TEST_URL, verify=False), basic_auth=(TEST_USERNAME, TEST_PASSWORD)) self.purge_test_versions() def tearDown(self): self.purge_test_versions() def get_version(self, name): versions = [ _ for _ in self.jira.project_versions('KB') if _.name == 'test-1' ] return versions[0] if len(versions) == 1 else None def purge_test_versions(self): for _ in self.jira.project_versions('KB'): if _.name.startswith('test-'): _.delete() def test_create_version(self): result = CliRunner().invoke( topcli, ['version', 'create', 'KB', 'test-1', '-d', 'Some description']) self.assertEqual(result.exit_code, 0) def test_delete_version(self): result = CliRunner().invoke(topcli, ['version', 'create', 'KB', 'test-1']) self.assertEqual(result.exit_code, 0) self.assertIsNotNone(self.get_version('test-1')) result = CliRunner().invoke(topcli, ['version', 'delete', 'KB', 'test-1']) self.assertEqual(result.exit_code, 0) self.assertIsNone(self.get_version('test-1')) def test_update_version(self): result = CliRunner().invoke( topcli, ['version', 'create', 'KB', 'test-1', '-d', 'before']) before = self.get_version('test-1') self.assertEqual(before.description, 'before') result = CliRunner().invoke( topcli, ['version', 'update', 'KB', 'test-1', '-d', 'after']) self.assertEqual(result.exit_code, 0) after = self.get_version('test-1') self.assertEqual(after.description, 'after') def test_search_version(self): result = CliRunner().invoke( topcli, ['version', 'create', 'KB', 'test-1', '-d', 'Some description']) result = CliRunner().invoke(topcli, ['version', 'search', 'KB']) self.assertEqual(result.exit_code, 0) self.assertIn('test-1', result.output) self.assertIn('Some description', result.output)
def jira_prj_versions(project, version_range): (from_str, to_str) = version_range.split('-', 1) from_ver = parse_version(from_str) to_ver = parse_version(to_str) if from_ver == None or to_ver == None: raise ValueError("Cannot parse range %s" % version_range) if type(from_ver) != type(to_ver): raise ValueError("Inconsistent types in range %s" % version_range) jira = JIRA(server="https://jira.opendaylight.org") prj = jira.project(project) versions = set() for version in jira.project_versions(prj): name = version.name ver = parse_version(name) if type(ver) == type(from_ver) and ver >= from_ver and ver <= to_ver: versions.add(name) if len(versions) == 0: raise ValueError("No versions selected for project %s range %s" % (project, version_range)) versions = list(versions) versions.sort() versions = ", ".join(versions) return (jira, prj, from_str, to_str, versions)
def GET(self): gl.GL_WEBINPUT = web.input() projectname = gl.GL_WEBINPUT.product_name productline_list = gl.GL_DB.query("select distinct version from crashinfo where appName='" + projectname + "'") # select *, count(distinct name) from table group by name result = [] result_g_version = [] result_jira_version = [] #jira = JIRA(server='http://127.0.0.1:1194', basic_auth=('wangyang', 'qwerty123456')) jira = JIRA(server=jira_server, basic_auth=(user_accout, user_pass)) for name in project: if name in projectname: jira_name = project[name]["projectname"] versions = jira.project_versions(jira.project(jira_name)) for item in productline_list: item_dict = {"game_version": item.version} result_g_version.append(item_dict) [result_jira_version.append({"jira_version": v.name}) for v in reversed(versions)] result.append(result_g_version) result.append(result_jira_version) encodedjson = json.dumps(result) return encodedjson
class TestVersions(unittest.TestCase): def setUp(self): self.jira = JIRA(options=dict(server=TEST_URL, verify=False), basic_auth=(TEST_USERNAME, TEST_PASSWORD)) self.purge_test_versions() def tearDown(self): self.purge_test_versions() def get_version(self, name): versions = [_ for _ in self.jira.project_versions('KB') if _.name == 'test-1'] return versions[0] if len(versions) == 1 else None def purge_test_versions(self): for _ in self.jira.project_versions('KB'): if _.name.startswith('test-'): _.delete() def test_create_version(self): result = CliRunner().invoke(topcli, ['version', 'create', 'KB', 'test-1', '-d', 'Some description']) self.assertEqual(result.exit_code, 0) def test_delete_version(self): result = CliRunner().invoke(topcli, ['version', 'create', 'KB', 'test-1']) self.assertEqual(result.exit_code, 0) self.assertIsNotNone(self.get_version('test-1')) result = CliRunner().invoke(topcli, ['version', 'delete', 'KB', 'test-1']) self.assertEqual(result.exit_code, 0) self.assertIsNone(self.get_version('test-1')) def test_update_version(self): result = CliRunner().invoke(topcli, ['version', 'create', 'KB', 'test-1', '-d', 'before']) before = self.get_version('test-1') self.assertEqual(before.description, 'before') result = CliRunner().invoke(topcli, ['version', 'update', 'KB', 'test-1', '-d', 'after']) self.assertEqual(result.exit_code, 0) after = self.get_version('test-1') self.assertEqual(after.description, 'after') def test_search_version(self): result = CliRunner().invoke(topcli, ['version', 'create', 'KB', 'test-1', '-d', 'Some description']) result = CliRunner().invoke(topcli, ['version', 'search', 'KB']) self.assertEqual(result.exit_code, 0) self.assertIn('test-1', result.output) self.assertIn('Some description', result.output)
def get_releases(client): jira = JIRA(client.url, basic_auth=(client.username, client.get_password())) for project in jira.projects(): clear_query = Release.objects.filter(client=client).filter( project=project.key) existing_versions = [] for version in jira.project_versions(project): if version.archived or version.released: continue existing_versions.append(version.name) release = (Release.objects.filter(client=client).filter( project=project.key).filter(version=version.name)).first() if not release: release = Release() print(project.key, version.name) release.project = project.key release.version = version.name release.client = client release.save() release.statuses.all().delete() issues = jira.search_issues("project=%s AND fixVersion=%s" % (version.projectId, version.id)) for key, value in STATUSES.items(): statuses = value['statuses'] found_issues = [ issue for issue in issues if issue.fields.status.name in statuses ] release_status = ReleaseStatus() release_status.release = release release_status.status = key release_status.style = value['style'] release_status.count = len(found_issues) release_status.save() clear_query.exclude(version__in=existing_versions).all().delete()
class Api(object): versions = None def __init__(self): self.j = JIRA( server=settings.JIRA_URL, basic_auth=(settings.JIRA_USERNAME, settings.JIRA_PASSWORD), ) self.project = self.j.project(settings.JIRA_PROJECT) def get_versions(self): return self.j.project_versions(self.project) def get_issues(self, version_name, username): if self.versions is None: self.versions = self.get_versions() version = next( (v for v in self.versions if version_name in v.name and '1C' not in v.name and '1С' not in v.name and 'WWW' not in v.name ), None ) if version is None: return [] else: jql = 'project=VIP ' \ 'AND resolution=Unresolved ' \ 'AND fixVersion="{fixVersion}"' \ 'AND assignee in ({assignee})'.format( fixVersion=version.name, assignee=username, ) return self.j.search_issues(jql)
class Util(object): """ """ def __init__(self, **kwargs): """ """ if 'config' in kwargs: self._config = kwargs['config'] else: logging.critical("config was not defined") raise Exception("config was not defined") if 'username' in kwargs: self._username = kwargs['username'] else: logging.critical("username was not defined") raise Exception("username was not defined") if 'password' in kwargs: self._password = kwargs['password'] else: logging.critical("password was not defined") raise Exception("password was not defined") if 'project' in kwargs: self._project = kwargs['project'] else: if 'project' in self._config: self._project = self._config['project'] logging.info( "project was set to '%s' from the configuration file" % self._project) else: self._project = DEFAULT_PROJECT logging.info("project was set to default '%s'" % self._project) if 'base_url' in kwargs: self._base_url = kwargs['base_url'] else: if 'base_url' in self._config: self._base_url = self._config['base_url'] logging.info( "base_url was set to '%s' from the configuration file" % self._base_url) else: self._base_url = DEFAULT_BASE_URL logging.info("base_url was set to default '%s'" % self._base_url) if 'add_missing_watchers' in kwargs: self._add_missing_watchers = kwargs['add_missing_watchers'] else: if 'add_missing_watchers' in self._config: self._add_missing_watchers = self._config[ 'add_missing_watchers'] logging.info( "add_missing_watchers was set to '%s' from the configuration file" % self._add_missing_watchers) else: self._add_missing_watchers = DEFAULT_ADD_MISSING_WATCHERS logging.info("add_missing_watchers was set to default '%s'" % self._add_missing_watchers) self._jira = None self._jra = None self._initialize() def setProject(self, project): """ :param project: :return: """ self._project = project def setAddMissingWatchers(self, add_missing_watchers): """ :param add_missing_watchers: :return: """ self._add_missing_watchers = add_missing_watchers def _initialize(self): """ :return: """ print("Attempting to connect to JIRA at '%s'" % self._base_url) self._jira = JIRA(self._base_url, basic_auth=(self._username, self._password)) print("Attempting to retrieve info for project '%s'" % self._project) self._jra = self._jira.project(self._project) def getReport(self): """ :return: """ self.report_misc() self.report_components() self.report_roles() self.report_versions() self.report_open_issues() def report_misc(self): """ :return: """ print(Fore.BLUE + "Project name '%s'" % self._jra.name) print(Fore.BLUE + "Project lead '%s'" % self._jra.lead.displayName) print(Style.RESET_ALL) def report_components(self): """ :return: """ components = self._jira.project_components(self._jra) if len(components) > 0: print(Fore.BLUE + "Here are the components") print(Style.RESET_ALL) for c in components: print(c.name) else: print(Fore.RED + "There are no components") print(Style.RESET_ALL) def report_roles(self): """ :return: """ roles = self._jira.project_roles(self._jra) if len(roles) > 0: print(Fore.BLUE + "Here are the roles") print(Style.RESET_ALL) for r in roles: print(r) else: print(Fore.RED + "There are no roles") print(Style.RESET_ALL) def report_versions(self): """ :return: """ versions = self._jira.project_versions(self._jra) if len(versions) > 0: print(Fore.BLUE + "Here are the versions") print(Style.RESET_ALL) for v in reversed(versions): print(v.name) else: print(Fore.RED + "There are no versions") print(Style.RESET_ALL) def report_watchers(self, issue): """ :param issue: :return: """ watcher = self._jira.watchers(issue) print("Issue '%s' has '%d' watcher(s)" % (issue.key, watcher.watchCount)) current_watchers_email = {} for watcher in watcher.watchers: current_watchers_email[watcher.emailAddress] = True print("'%s' - '%s'" % (watcher, watcher.emailAddress)) # watcher is instance of jira.resources.User: # print(watcher.emailAddress) for watcher_email in self._config['members_email_lookup']: if not watcher_email in current_watchers_email: print(Fore.RED + "member '%s' needs to be added as a watcher to '%s'" % (watcher_email, issue.key)) username = self._config['members_email_lookup'][watcher_email] print("Need to add username '%s'" % username) print(Style.RESET_ALL) if self._add_missing_watchers: self._jira.add_watcher(issue, username) print("Exiting") sys.exit(0) print(Style.RESET_ALL) def checkWatchers(self): """ :return: """ issues = self._jira.search_issues('project= LO AND status != Done', maxResults=DEFAULT_MAX_RESULTS) if len(issues) > 0: for issue in issues: self.report_watchers(issue) def report_open_issues(self): issues = self._jira.search_issues('project= LO AND status != Done', maxResults=DEFAULT_MAX_RESULTS) if len(issues) > 0: print(Fore.BLUE + "Found the following '%d' open issues" % len(issues)) print(Style.RESET_ALL) for issue in issues: summary = issue.fields.summary id = issue.id key = issue.key print("id '%s' key '%s' summary : '%s'" % (id, key, summary)) if DEFAULT_REPORT_WATCHERS: self._report_watchers(issue) print(Style.RESET_ALL) def getComments(self, key): """ :param key: :return: """ logging.info("Attempting to retrieve the issue with key '%s'" % key) issues = self._jira.search_issues('key = ' + key) if len(issues) > 1: raise Exception("Expected only one issue for '%s' but found '%d'" % (key, len(issues))) if len(issues) == 1: # comments = issues[0].fields.comment.comments # comments = issues[0].raw['fields']['comment']['comments'] comments = self._jira.comments(issues[0]) if len(comments) > 0: print("Found the following '%d' comments" % len(comments)) comment_ctr = 0 for comment_id in comments: print("-----------------------------------") comment_ctr += 1 comment = self._jira.comment(key, comment_id) author = comment.author.displayName date_created = comment.created body = comment.body print(Fore.BLUE + "%d. author '%s' date '%s'" % (comment_ctr, author, date_created)) print(Style.RESET_ALL) print(body)
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 JiraSession(object): def __init__(self, server, account, password, verify=True): """ Init Jira Session :param server: :param account: :param password: :param verify: """ self.__server = server self.__account = account self.__password = password self.__jira_opts = { 'server': self.__server, 'verify': verify, } self.__session = JIRA(self.__jira_opts, basic_auth=(self.__account, self.__password)) def __enter__(self): assert self.__session.current_user() == self.__account return self def __exit__(self, exc_type, exc_val, exc_tb): self.__session.close() def get_user(self): """ Get jira user :return: """ logging.info(u'Get JIRA Current User') return self.__session.current_user() def search_issues(self, jql): """ Search issues via JQL :param jql: :return: """ logging.info(u'JIRA Search: %s' % jql) return self.__session.search_issues(jql_str=jql, maxResults=128, json_result=True) # def get_issue_count(self, jql: str, issue_summary: dict, issue_key: str): # """ # Search issues via JQL and return count # :param jql: # :param issue_summary: # :param issue_key: # :return: # """ # logging.info(u'JIRA Issue Count: %s' % jql) # issue_summary[issue_key] = int(self.search_issues(jql).get('total')) # return True def get_projects(self): """ Get jira projects :return: <key, name, id> """ logging.info(u'Get JIRA Projects') return self.__session.projects() def get_sprints(self): """ Get jira sprints :return: <name, id> """ logging.info(u'Get JIRA Sprints') jira_sprints = list() for board in self.__session.boards(): _sprints = self.__session.sprints(board.id) jira_sprints = jira_sprints + _sprints return jira_sprints def get_issue_fields(self): """ Get jira fields :return: [{'name':'','id':''}] """ logging.info(u'Get JIRA Fields') _fields = list() for _field in self.__session.fields(): _fields.append({'name': _field['name'], 'id': _field['id']}) return _fields def get_issue_types(self): """ Get jira issue types :return: <name, id> """ logging.info(u'Get JIRA Issue Types') return self.__session.issue_types() def get_issue_statuses(self): """ Get issue statuses :return: <name, id> """ logging.info(u'Get JIRA Issue Statuses') return self.__session.statuses() def get_project_versions(self, pid: str): """ Get project versions :param pid: :return: [<name, id>] """ logging.info(u'Get JIRA Project %s Versions' % pid) return self.__session.project_versions(project=pid)
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
def create_bug_issue(self, channel, summary, description, component, version, labels, user=JIRA_USER, passwd=JIRA_PASS, project=JIRA_PROJECT): """ Creates a bug issue on Jira :param channel: The channel to notify :param summary: The title summary :param description: Description field :param component: Component bug affects :param version: Version this bug affects :param labels: Labels to attach to the issue :param user: User to report bug as :param passwd: Password :param project: Jira project """ if user and passwd and project: try: jira = JIRA(server='https://issues.voltdb.com/', basic_auth=(user, passwd)) except: self.logger.exception('Could not connect to Jira') return else: self.logger.error( 'Did not provide either a Jira user, a Jira password or a Jira project' ) return # Check for existing bug with same summary existing = jira.search_issues('summary ~ \'%s\'' % summary) if len(existing) > 0: # Already reported self.logger.info('OLD: Already reported issue with summary "' + summary + '"') return issue_dict = { 'project': project, 'summary': summary, 'description': description, 'issuetype': { 'name': 'Bug' }, 'labels': labels } jira_component = None components = jira.project_components(project) for c in components: if c.name == component: jira_component = {'name': c.name, 'id': c.id} break if jira_component: issue_dict['components'] = [jira_component] else: # Components is still a required field issue_dict['components'] = ['Core'] jira_version = None versions = jira.project_versions(project) for v in versions: if v.name == version: jira_version = {'name': v.name, 'id': v.id} break if jira_version: issue_dict['versions'] = [jira_version] else: # Versions is still a required field issue_dict['versions'] = ['DEPLOY-Integration'] new_issue = jira.create_issue(fields=issue_dict) self.logger.info('NEW: Reported issue with summary "' + summary + '"') if self.connect_to_slack(): self.post_message( channel, 'Opened issue at https://issues.voltdb.com/browse/' + new_issue.key)
item = top_ten.pop() prj_name = item[0] issue_count = item[1] print("Project name : {name}, Assigned Count : {count}".format( name=prj_name, count=issue_count)) #project 상세 정보 표출 if prj_name == "BTVD": prj = jira.project(prj_name) print("=" * 30) print("Project name : %s" % prj.name) print("Project leader name : %s" % prj.lead.displayName) components = jira.project_components(prj) print("Components : " + str([c.name for c in components])) print("Roles : " + str(jira.project_roles(prj))) versions = jira.project_versions(prj) print("Versions : " + str([v.name for v in reversed(versions)])) print("=" * 30) else: continue break_loop = False for issue in issues: print("Issue summary : " + issue.fields.summary) watcher = jira.watchers(issue) if watcher.watchCount > 1: print("Issue has {} watchers(s)".format(watcher.watchCount)) for watcher in watcher.watchers: print(watcher) print(watcher.emailAddress) break_loop = True
user="******", passwd="jabra2020", database="projects") c = mydb.cursor() alt = list() c.execute("SELECT * FROM projects WHERE status = 'Active' AND jama != 0") myresult = c.fetchall() for project in myresult: alt.append(project[0]) c.close() mydb.close() # Check the project status for project in projects: if project.key in alt: logData("Checking status for " + project.name + "") checkStatus(project) logData("Done checking status for " + project.name + "") logData("Processing data for " + project.name + "") logData("Fetching periods for " + project.name + "") project.periods = jira.project_versions(project.key) logData("Fetching meta for " + project.name + "") queryJiraMeta(project) logData("Fetching data for " + project.name + "") queryJira(project) logData("Saving projects in db") saveProjects() elapsed = timeit.default_timer() - start_time logData("Done fetching & parsing data from Jira. Elapsed time: %s" % elapsed)
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()
class jira_handler: """ Jira处理类。 【备注】:目前存在Jira方法与Issue对象混合在一起的不足。 【改进】:将Jira方法与Issue对象实体分离,各自进行类定义。 """ def __init__(self, project_name): self.mongo_db = mongodb_class.mongoDB(project_name) self.jira = JIRA('http://172.16.60.13:8080', basic_auth=('shenwei','sw64419')) self.gh = GreenHopper({'server': 'http://172.16.60.13:8080'}, basic_auth=('shenwei', 'sw64419')) self.name = project_name self.project = self.jira.project(self.name) self.pj_name = u"%s" % self.project.name self.pj_manager = u"%s" % self.project.lead.displayName """获取项目版本信息 """ _versions = self.jira.project_versions(self.name) self.version = {} self.sprints = self._get_sprints() for _v in _versions: _key = (u"%s" % _v).replace('.', '^') if not self.version.has_key(_key): self.version[_key] = {} self.version[_key][u"id"] = _v.id self.version[_key]['startDate'] = "" self.version[_key]['releaseDate'] = "" if 'startDate' in dir(_v): self.version[_key]['startDate'] = _v.startDate if 'releaseDate' in dir(_v): self.version[_key]['releaseDate'] = _v.releaseDate self.mongo_db.handler("project", "update", {"version": _key}, dict({"version": _key}, **self.version[_key])) self.issue = None def _get_board(self): _boards = self.jira.boards() for _b in _boards: if self.name.upper() in _b.name.upper(): return _b.id return None def transDate(self, str): print("---> transDate [%s] <---" % str) if str != None and str != u'无': _s = str.\ replace(u'十一月', '11').\ replace(u'十二月', '12').\ replace(u'一月', '1').\ replace(u'二月', '2').\ replace(u'三月', '3').\ replace(u'四月', '4').\ replace(u'五月', '5').\ replace(u'六月', '6').\ replace(u'七月', '7').\ replace(u'八月', '8').\ replace(u'九月', '9').\ replace(u'十月', '10') _time = time.strptime(_s, '%d/%m/%y') return time.strftime('%Y-%m-%d', _time) else: return "" def _get_sprints(self): """ 获取看板内sprint列表 :return: sprint列表 [ name, startDate, endDate, state ] """ _list = [] _b_id = self._get_board() if type(_b_id) is not types.NoneType: _sprints = self.jira.sprints(_b_id) for _s in _sprints: _sprint = self.jira.sprint(_s.id) _data = {'name': _s.name, 'startDate': self.transDate(_sprint.startDate.split(' ')[0]), 'endDate': self.transDate(_sprint.endDate.split(' ')[0]), 'state': _s.state } _list.append(_data) return _list return None def get_sprints(self): return self.sprints def get_current_sprint(self): """ 获取本阶段sprint名称 :return: 返回状态为ACTIVE的sprint的名称 """ if type(self.sprints) is not types.NoneType: for _s in self.sprints: if _s['state'] == 'ACTIVE': return (_s['name'], _s['startDate'], _s['endDate']), _next _next = (_s['name'], _s['startDate'], _s['endDate']) return None def get_sprint(self): """ 获取当前Issue的sprint定义 :return: sprint定义 """ if "customfield_10501" in self.issue.raw['fields'] and \ type(self.issue.fields.customfield_10501) is not types.NoneType: return u'%s' % (",".join(item.split('name=')[1].split(',')[0] for item in self.issue.fields.customfield_10501)) # return u'%s' % self.issue.fields.customfield_10501[0].split('name=')[1].split(',')[0] return None def get_versions(self): _v = {} for _k in self.version: _key = (u"%s" % _k).replace('^', '.') _v[_key] = self.version[_k] return _v def get_pj_info(self): return {'pj_name': self.pj_name, 'pj_manager': self.pj_manager} def set_issue_by_name(self, issue_id): self.issue = self.jira.issue(issue_id) def print_green_hopper(self): _f = self.gh.fields() for __f in _f: __cns = __f['clauseNames'] print('-' * 8) for _n in __cns: print u"name: %s" % _n print "id: ", u"%s" % __f['id'] print "name: ", u"%s" % __f['name'] def get_story_point(self): """ 获取Issue(story)的预置成本, 1 point = 4 hours :return: 预置成本 """ if "customfield_10304" in self.issue.raw['fields'] and \ type(self.issue.fields.customfield_10304) is not types.NoneType: return self.issue.fields.customfield_10304 return None def get_task_time(self): return {"agg_time": self.issue.fields.aggregatetimeestimate, "org_time": self.issue.fields.timeoriginalestimate, "spent_time": self.issue.fields.timespent} def get_landmark(self): if len(self.issue.fields.fixVersions) > 0: return u"%s" % self.issue.fields.fixVersions[0] if len(self.issue.fields.versions) > 0: # print self.show_name(), " version: %s" % self.issue.fields.versions[0] return u"%s" % self.issue.fields.versions[0] return "" def get_desc(self): return self.issue.fields.summary def show_name(self): return str(self.issue) def get_type(self): return u"%s" % self.issue.fields.issuetype def get_status(self): """ 获取Issue的状态,待办、处理中、待测试、测试中、完成 :return: """ return u"%s" % self.issue.fields.status def get_subtasks(self): """ 收集issue的相关子任务的issue :return: 相关issue字典 """ link = {} if not link.has_key(self.show_name()): link[self.show_name()] = [] _task_issues = self.issue.fields.subtasks for _t in _task_issues: link[self.show_name()].append(u"%s" % _t) return link def get_child_requirement(self): link = [] jql = "issue in childrenOfParentRequirement('%s')" % self.show_name() # print jql tot = 0 while True: issues = self.jira.search_issues(jql, maxResults=100, startAt=tot) for issue in issues: link.append(issue.key) if len(issues) == 100: tot += 100 else: break return link def get_epic_link(self, jql): print(">>> get_epic_link<%s>" % jql) total = 0 _issue_name = self.show_name() task_link = {_issue_name: []} while True: issues = self.jira.search_issues(jql, maxResults=100, startAt=total) for issue in issues: self.issue = issue self.sync_issue() """收集epic相关的story和任务""" task_link[_issue_name].append(self.show_name()) if len(issues) == 100: total += 100 else: break print task_link self.set_issue_by_name(_issue_name) return task_link def get_link(self): """ 收集issue的相关issue :return: 相关issue字典 """ link = {self.show_name(): []} """兼容以前: 与story相关的task是通过issulelinks关联的""" _task_issues = self.issue.fields.issuelinks for _t in _task_issues: if "outwardIssue" in dir(_t): """该story相关的任务""" link[self.show_name()].append(u"%s" % _t.outwardIssue) if "inwardIssue" in dir(_t): """该story相关的任务""" link[self.show_name()].append(u"%s" % _t.inwardIssue) """采用synapseRT插件后对需求的管理""" _task_issues = self.get_child_requirement() for _t in _task_issues: link[self.show_name()].append(_t) return link def sync_issue(self): """ 同步issue数据,同时完成重要参量的变更日志。 :return: """ _components = u"%s" % (', '.join(comp.name for comp in self.issue.fields.components)) _key = u"%s" % self.show_name() _time = self.get_task_time() _epic_link = None if "customfield_11300" in self.issue.raw['fields'] and \ type(self.issue.fields.customfield_11300) is not types.NoneType: _epic_link = self.issue.raw['fields']["customfield_11300"] _issue = {u"%s" % self.show_name(): { "issue_type": self.get_type(), "created": self.issue.fields.created, "updated": self.issue.fields.updated, "lastViewed": self.issue.fields.lastViewed, "users": self.get_users(), "status": self.get_status(), "landmark": self.get_landmark(), "point": self.get_story_point(), "agg_time": _time['agg_time'], "org_time": _time['org_time'], "summary": self.issue.fields.summary, "spent_time": _time['spent_time'], "sprint": self.get_sprint(), "epic_link": _epic_link, "components": _components }} _old_issue = self.mongo_db.handler("issue", "find_one", {"issue": _key}) if _old_issue is None: self.mongo_db.handler("issue", "update", {"issue": _key}, dict({"issue": _key}, **_issue[_key])) else: _change = False for _item in ['issue_type','created','updated','users','status', 'landmark','point','agg_time','org_time', 'summary','spent_time','sprint','epic_link']: if _old_issue[_item] != _issue[_key][_item]: _log = {"issue_id": _key, "key": _item, "old": _old_issue[_item], "new": _issue[_key][_item]} self.write_log(_log) _change = True _change = True if _change: self.mongo_db.handler("issue", "update", {"issue": _key}, dict({"issue": _key}, **_issue[_key])) def get_issue_link(self): _link = self.get_link() print "---> get_issue_link: ", _link return _link def sync_issue_link(self): _key = u"%s" % self.show_name() _link = self.get_link() print "sync_issue_link: ", _link self.mongo_db.handler("issue_link", "update", {"issue": _key}, dict({"issue": _key}, **_link)) return _link def show_issue(self): """ 显示issue信息 :return: """ print(u"[%s]" % self.show_name()), print u"类型:%s" % self.get_type(), print(u'状态:%s' % self.get_status()), print u"里程碑:%s" % self.get_landmark() def get_users(self): """ 获取访问issue的用户 2018.3.1:改为 经办人 assignee :return: watcher = self.jira.watchers(self.issue) _user = u"%s" % (', '.join(watcher.displayName for watcher in watcher.watchers)) """ if type(self.issue.raw['fields']["assignee"]) is types.NoneType: return None return (u"%s" % self.issue.raw['fields']["assignee"]['displayName']).replace(' ', '') def write_log(self, info): print "---<write_log>---: ",info self.mongo_db.handler("log", "insert", info) def write_worklog(self, info): """ 写入或更新 工作日志记录。 :param info: 新的日志数据 :return: """ if not info.has_key("comment"): return _search = {'issue': info['issue'], 'id': info['id']} self.mongo_db.handler('worklog', 'update', _search, info) def clear_worklog(self, worklog_id): """ 清除"不存在"的记录! :param worklog_id: 存在的worklog_id :return: """ _set = {'timeSpentSeconds': 0} if len(worklog_id) > 0: _search = {"issue": self.show_name(), "id": {"$not": {"$in": worklog_id}}} else: _search = {"issue": self.show_name()} _one = self.mongo_db.handler('worklog', 'find_one', _search) if _one is None: return # self.mongo_db.handler('worklog', 'remove', _search) """保留原记录,将其用时值设置为0,以便事后跟踪 """ self.mongo_db.handler('worklog', 'update', _search, _set) def sync_worklog(self): """ 获取指定 issue 的工作日志记录。 - 2018.4.2:针对以前有的,但现在没有的 日志记录 的处理?!清除其spent时间 :return: """ worklogs = self.jira.worklogs(self.show_name()) wl = {} _id = [] for worklog in worklogs: wl['issue'] = self.show_name() wl['author'] = u'%s' % worklog.author wl['comment'] = u'%s' % worklog.comment wl['timeSpent'] = worklog.timeSpent wl['timeSpentSeconds'] = worklog.timeSpentSeconds wl['updated'] = worklog.updated wl['created'] = worklog.created wl['started'] = worklog.started wl['id'] = worklog.id _id.append(worklog.id) self.write_worklog(wl) """同时同步Issue的变动日志""" """ 因worklog可随意更改或删除, 有必要实时清除多余的记录!""" self.clear_worklog(_id) self.sync_changelog() def scan_task_by_sprint(self, sprint): """ 通过sprint获取Issue,以便获取它们的 工作日志 :param sprint: 当前的sprint名称 :return: Issue列表 """ jql_sql = u'project=%s AND Sprint = "%s" ORDER BY created DESC' %\ (self.name, sprint) total = 0 tasks = [] while True: issues = self.jira.search_issues(jql_sql, maxResults=100, startAt=total) for issue in issues: self.issue = issue """同步issue""" self.sync_issue() self.sync_worklog() self.sync_issue_link() tasks.append(self.show_name()) if len(issues) == 100: total += 100 else: break return tasks def scan_epic(self, bg_date): """ 扫描project收集epic信息 :param bg_date: 起始日期,如 2018-1-31 :param issue_type:指定issue类型 :return: 按issue类型进行统计值kv_sum,issue链kv_link,相关任务链task_link """ jql_sql = u'project=%s AND issuetype=epic AND created >= %s ORDER BY created DESC' % ( self.name, bg_date) total = 0 story_link = [] while True: issues = self.jira.search_issues(jql_sql, maxResults=100, startAt=total) for issue in issues: self.issue = issue """同步issue""" # self.show_issue() self.sync_issue() """收集epic相关的story和任务""" _jql = u'project=%s AND "Epic Link"=%s AND created >= %s ORDER BY created DESC' % \ (self.name, self.show_name(), bg_date) _link = self.get_epic_link(_jql) """同步epic的link""" # print "--> epic link: ", dict({"issue": self.show_name()}, **_link) self.mongo_db.handler("issue_link", "update", {"issue": self.show_name()}, dict({"issue": self.show_name()}, **_link)) story_link += _link[self.show_name()] if len(issues) == 100: total += 100 else: break return story_link def scan_story(self, bg_date): """ 按 project 获取其下所有 story 数据。 :param bg_date: 开始搜索的日期 :return: """ jql_sql = u'project=%s AND issuetype=story AND created >= %s ORDER BY created DESC' % ( self.name, bg_date) total = 0 task_link = [] while True: issues = self.jira.search_issues(jql_sql, maxResults=100, startAt=total) for issue in issues: self.issue = issue """同步issue""" # self.show_issue() self.sync_issue() """收集story相关的任务""" _link = self.sync_issue_link() """同步epic的link""" self.mongo_db.handler("issue_link", "update", {"issue": self.show_name()}, dict({"issue": self.show_name()}, **_link)) task_link += _link[self.show_name()] if len(issues) == 100: total += 100 else: break return task_link def scan_task(self, bg_date): """ 按 project 获取其下所有与执行相关的 issue 数据。 :param bg_date: 开始搜索的日期 :return: """ jql_sql = u'project=%s AND ( issuetype=task OR' \ u' issuetype=任务 OR' \ u' issuetype=故障 OR' \ u' issuetype=Bug OR' \ u' issuetype=Sub-task OR' \ u' issuetype=子任务 ) AND' \ u' created >= %s ORDER BY created DESC' % (self.name, bg_date) print jql_sql total = 0 task_link = [] while True: issues = self.jira.search_issues(jql_sql, maxResults=100, startAt=total) for issue in issues: self.issue = issue """同步issue""" self.show_issue() self.sync_issue() # self.sync_changelog() self.sync_worklog() task_link.append(self.show_name()) if len(issues) == 100: total += 100 else: break return task_link def sync_changelog(self): """ 获取指定 issue 的 变更日志记录 :return: """ issue = self.jira.issue(self.show_name(), expand='changelog') changelog = issue.changelog for history in changelog.histories: for item in history.items: _data = {'issue': self.show_name(), 'field': item.field, 'author': u"%s" % history.author, 'date': history.created, 'old': getattr(item, 'fromString'), 'new': getattr(item, 'toString') } if self.mongo_db.handler('changelog', 'find_one', _data) is None: self.mongo_db.handler('changelog', 'insert', _data)
def report_issue(self, build): try: jira = JIRA(server='https://issues.voltdb.com/', basic_auth=(JIRA_USER, JIRA_PASS)) except: logging.exception('Could not connect to Jira') return build_report_url = self.jhost + '/job/' + job + '/' + str(build) + '/api/python' build_report = eval(self.read_url(build_report_url)) build_url = build_report.get('url') build_result = build_report.get('result') if build_result == 'SUCCESS': # only generate Jira issue if the test fails print 'No new issue created. Build ' + str(build) + 'resulted in: ' + build_result return summary_url = self.jhost + '/job/' + job + '/' + str(build) + '/artifact/tests/sqlgrammar/summary.out' summary_report = self.read_url(summary_url) pframe_split = summary_report.split('Problematic frame:') pframe_split = pframe_split[1].split('C') pframe_split = pframe_split[1].split(']') pframe_split = pframe_split[1].split('#') pframe = pframe_split[0].strip() summary = job + ':' + str(build) + ' - ' + pframe # search_issues gets a parsing error on (), so escape it. existing = jira.search_issues('summary ~ \'%s\'' % summary.replace('()','\\\\(\\\\)',10)) if len(existing) > 0: print 'No new Jira issue created. Build ' + str(build) + ' has already been reported.' return 'Already reported' old_issue = '' existing = jira.search_issues('summary ~ \'%s\'' % pframe_split[0].strip().replace('()','\\\\(\\\\)',10)) for issue in existing: if str(issue.fields.status) != 'Closed' and u'grammar-gen' in issue.fields.labels: old_issue = issue build_artifacts = build_report.get('artifacts')[0] pid_fileName = build_artifacts['fileName'] pid_url = build_url + 'artifact/' + pid_fileName query_split = summary_report.split('(or it was never started??), after SQL statement:') crash_query = query_split[1] hash_split = summary_report.split('#', 1) hash_split = hash_split[1].split('# See problematic frame for where to report the bug.') sigsegv_message = hash_split[0] + '# See problematic frame for where to report the bug.\n#' description = job + ' build ' + str(build) + ' : ' + str(build_result) + '\n' \ + 'Jenkins build: ' + build_url + ' \n \n' \ + 'DDL: ' + 'https://github.com/VoltDB/voltdb/blob/master/tests/sqlgrammar/DDL.sql' + ' \n \n' \ + 'hs_err_pid: ' + pid_url + ' \n \n' \ + 'SIGSEGV Message: \n' + '#' + sigsegv_message + ' \n \n' \ + 'Query that Caused the Crash: ' + crash_query description = description.replace('#', '\#') labels = ['grammar-gen'] component = 'Core' components = jira.project_components(JIRA_PROJECT) jira_component = {} for c in components: if c.name == component: jira_component = { 'name': c.name, 'id': c.id } break current_version_raw = str(self.read_url('https://raw.githubusercontent.com/VoltDB/voltdb/master/version.txt')) current_version_float = float(current_version_raw) current_version = 'V' + current_version_raw current_version = current_version.strip() next_version = current_version_float + .1 next_version = str(next_version) next_version = 'V' + next_version next_version = next_version[:4] jira_versions = jira.project_versions(JIRA_PROJECT) this_version = {} new_version = {} for v in jira_versions: if str(v.name) == current_version: this_version = { 'name': v.name, 'id': v.id } if str(v.name) == next_version: new_version = { 'name': v.name, 'id': v.id } issue_dict = { 'project': JIRA_PROJECT, 'summary': summary, 'description': description, 'issuetype': {'name': 'Bug'}, 'priority': {'name': 'Blocker'}, 'labels': labels, 'customfield_10430': {'value': 'CORE team'}, 'components': [jira_component] } if new_version: issue_dict['versions'] = [new_version] issue_dict['fixVersions'] = [new_version] elif this_version: issue_dict['versions'] = [this_version] issue_dict['fixVersions'] = [this_version] if old_issue: new_comment = jira.add_comment(old_issue, description) print 'JIRA-action: New comment on issue: ' + str(old_issue) + ' created for failure on build ' + str(build) else: new_issue = jira.create_issue(fields=issue_dict) print 'JIRA-action: New issue ' + new_issue.key + ' created for failure on build ' + str(build)
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
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')
def create_bug_issue(self, channel, summary, description, component, version, labels, attachments={}, user=JIRA_USER, passwd=JIRA_PASS, project=JIRA_PROJECT, DRY_RUN=False): """ Creates a bug issue on Jira :param channel: The channel to notify :param summary: The title summary :param description: Description field :param component: Component bug affects :param version: Version this bug affects :param labels: Labels to attach to the issue :param user: User to report bug as :param passwd: Password :param project: Jira project """ def add_attachments(jira, ticketId, attachments): for file in attachments: urlretrieve(attachments[file], file) jira.add_attachment(ticketId, os.getcwd() + '/' + file, file) os.unlink(file) if user and passwd and project: try: jira = JIRA(server='https://issues.voltdb.com/', basic_auth=(user, passwd), options=dict(verify=False)) except: self.logger.exception('Could not connect to Jira') return else: self.logger.error( 'Did not provide either a Jira user, a Jira password or a Jira project' ) return # Check for existing bugs for the same test case, if there are any, suppress filing another test_case = summary.split(' ')[0] existing = jira.search_issues( 'summary ~ \'%s\' and labels = automatic and status != Closed' % test_case) if len(existing) > 0: self.logger.info('Found open issue(s) for "' + test_case + '" ' + ' '.join([k.key for k in existing])) # Check if new failure is on different job than existing ticket, if so comments job = summary.split()[-2] existing_ticket = jira.issue(existing[0].id) if job not in existing_ticket.fields.summary: comments = jira.comments(existing[0].id) for comment in comments: # Check for existing comment for same job, if there are any, suppress commenting another if job in comment.body: self.logger.info('Found existing comment(s) for "' + job + '" on open issue') return self.logger.info( 'Commenting about separate job failure for %s on open issue' % test_case) if not DRY_RUN: jira.add_comment(existing[0].id, summary + '\n\n' + description) add_attachments(jira, existing[0].id, attachments) return issue_dict = { 'project': project, 'summary': summary, 'description': description, 'issuetype': { 'name': 'Bug' }, 'labels': labels } jira_component = None components = jira.project_components(project) for c in components: if c.name == component: jira_component = {'name': c.name, 'id': c.id} break if jira_component: issue_dict['components'] = [jira_component] else: # Components is still a required field issue_dict['components'] = ['Core'] jira_version = None versions = jira.project_versions(project) version = 'V' + version for v in versions: if str(v.name) == version.strip(): jira_version = {'name': v.name, 'id': v.id} break if jira_version: issue_dict['versions'] = [jira_version] else: # Versions is still a required field issue_dict['versions'] = ['DEPLOY-Integration'] issue_dict['fixVersions'] = [{'name': 'Backlog'}] issue_dict['priority'] = {'name': 'Blocker'} self.logger.info("Filing ticket: %s" % summary) if not DRY_RUN: new_issue = jira.create_issue(fields=issue_dict) add_attachments(jira, new_issue.id, attachments) #self.logger.info('NEW: Reported issue with summary "' + summary + '"') if self.connect_to_slack(): self.post_message( channel, 'Opened issue at https://issues.voltdb.com/browse/' + new_issue.key) suite = summary.split('.')[-3] # Find all tickets within same test suite and link them link_tickets = jira.search_issues( 'summary ~ \'%s\' and labels = automatic and status != Closed and reporter in (voltdbci)' % suite) for ticket in link_tickets: jira.create_issue_link('Related', new_issue.key, ticket) else: new_issue = None return new_issue
def create_bug_issue(self, channel, summary, description, component, version, labels, user=JIRA_USER, passwd=JIRA_PASS, project=JIRA_PROJECT): """ Creates a bug issue on Jira :param channel: The channel to notify :param summary: The title summary :param description: Description field :param component: Component bug affects :param version: Version this bug affects :param labels: Labels to attach to the issue :param user: User to report bug as :param passwd: Password :param project: Jira project """ if user and passwd and project: try: jira = JIRA(server='https://issues.voltdb.com/', basic_auth=(user, passwd)) except: self.logger.exception('Could not connect to Jira') return else: self.logger.error('Did not provide either a Jira user, a Jira password or a Jira project') return # Check for existing bug with same summary existing = jira.search_issues('summary ~ \'%s\'' % summary) if len(existing) > 0: # Already reported self.logger.info('OLD: Already reported issue with summary "' + summary + '"') return issue_dict = { 'project': project, 'summary': summary, 'description': description, 'issuetype': { 'name': 'Bug' }, 'labels': labels } jira_component = None components = jira.project_components(project) for c in components: if c.name == component: jira_component = { 'name': c.name, 'id': c.id } break if jira_component: issue_dict['components'] = [jira_component] else: # Components is still a required field issue_dict['components'] = ['Core'] jira_version = None versions = jira.project_versions(project) version = 'V' + version for v in versions: if str(v.name) == version.strip(): jira_version = { 'name': v.name, 'id': v.id } break if jira_version: issue_dict['versions'] = [jira_version] else: # Versions is still a required field issue_dict['versions'] = ['DEPLOY-Integration'] issue_dict['fixVersions'] = [{'name':'Backlog'}] issue_dict['priority'] = {'name': 'Blocker'} new_issue = jira.create_issue(fields=issue_dict) self.logger.info('NEW: Reported issue with summary "' + summary + '"') if self.connect_to_slack(): self.post_message(channel, 'Opened issue at https://issues.voltdb.com/browse/' + new_issue.key)
class JiraClient: def __init__(self, email, token, server=SERVER): self.client = JIRA(server=server, basic_auth=(email, token), max_retries=MAX_RETRIES, timeout=4) def get_issues(self, start_at=0, query='', limit=ISSUES_COUNT): return self.client.search_issues( query, fields='key, summary, timetracking, status, assignee', startAt=start_at, maxResults=limit) def log_work(self, issue, time_spent, start_date, comment, adjust_estimate=None, new_estimate=None, reduce_by=None): self.client.add_worklog(issue=issue, timeSpent=time_spent, adjustEstimate=adjust_estimate, newEstimate=new_estimate, reduceBy=reduce_by, started=start_date, comment=comment) def get_possible_resolutions(self): resolutions = self.client.resolutions() possible_resolutions = [resolution.name for resolution in resolutions] return possible_resolutions def get_possible_versions(self, issue): all_projects = self.client.projects() current_project_key = issue.key.split('-')[0] for id, project in enumerate(all_projects): if project.key == current_project_key: current_project_id = id versions = self.client.project_versions( all_projects[current_project_id]) possible_versions = [version.name for version in versions] return possible_versions @staticmethod def get_remaining_estimate(issue): try: existing_estimate = issue.fields.timetracking.raw[ 'remainingEstimate'] except (AttributeError, TypeError, KeyError): existing_estimate = "0m" return existing_estimate @staticmethod def get_original_estimate(issue): try: original_estimate = issue.fields.timetracking.originalEstimate except JIRAError as e: return e.text except (AttributeError, TypeError): return "You should establish estimate first" return original_estimate def issue(self, key): return self.client.issue(key)
def create_bug_issue(self, channel, summary, description, component, version, labels, user=JIRA_USER, passwd=JIRA_PASS, project=JIRA_PROJECT, DRY_RUN=False): """ Creates a bug issue on Jira :param channel: The channel to notify :param summary: The title summary :param description: Description field :param component: Component bug affects :param version: Version this bug affects :param labels: Labels to attach to the issue :param user: User to report bug as :param passwd: Password :param project: Jira project """ if user and passwd and project: try: jira = JIRA(server='https://issues.voltdb.com/', basic_auth=(user, passwd), options=dict(verify=False)) except: self.logger.exception('Could not connect to Jira') return else: self.logger.error( 'Did not provide either a Jira user, a Jira password or a Jira project' ) return # Check for existing bugs for the same test case, if there are any, suppress filing another test_case = summary.split(' ')[0] existing = jira.search_issues( 'summary ~ \'%s\' and labels = automatic and status != Closed' % test_case) if len(existing) > 0: # Already reported self.logger.info('Found open issue(s) for "' + test_case + '" ' + ' '.join([k.key for k in existing])) return issue_dict = { 'project': project, 'summary': summary, 'description': description, 'issuetype': { 'name': 'Bug' }, 'labels': labels } jira_component = None components = jira.project_components(project) for c in components: if c.name == component: jira_component = {'name': c.name, 'id': c.id} break if jira_component: issue_dict['components'] = [jira_component] else: # Components is still a required field issue_dict['components'] = ['Core'] jira_version = None versions = jira.project_versions(project) version = 'V' + version for v in versions: if str(v.name) == version.strip(): jira_version = {'name': v.name, 'id': v.id} break if jira_version: issue_dict['versions'] = [jira_version] else: # Versions is still a required field issue_dict['versions'] = ['DEPLOY-Integration'] issue_dict['fixVersions'] = [{'name': 'Backlog'}] issue_dict['priority'] = {'name': 'Blocker'} self.logger.info("Filing ticket: %s" % summary) if not DRY_RUN: new_issue = jira.create_issue(fields=issue_dict) #self.logger.info('NEW: Reported issue with summary "' + summary + '"') if self.connect_to_slack(): self.post_message( channel, 'Opened issue at https://issues.voltdb.com/browse/' + new_issue.key) else: new_issue = None return new_issue
def report_issue(self, build): try: jira = JIRA(server='https://issues.voltdb.com/', basic_auth=(JIRA_USER, JIRA_PASS), options=dict(verify=False)) except: logging.exception('Could not connect to Jira') return build_report_url = self.jhost + '/job/' + job + '/' + str( build) + '/api/python' build_report = eval(self.read_url(build_report_url)) build_url = build_report.get('url') build_result = build_report.get('result') if build_result == 'SUCCESS': # only generate Jira issue if the test fails print 'No new issue created. Build ' + str( build) + 'resulted in: ' + build_result return summary_url = self.jhost + '/job/' + job + '/' + str( build) + '/artifact/summary.out' summary_report = self.read_url(summary_url) pframe_split = summary_report.split('Problematic frame:') pframe_split = pframe_split[1].split('C') pframe_split = pframe_split[1].split(']') pframe_split = pframe_split[1].split('#') pframe = pframe_split[0].strip() summary = job + ':' + str(build) + ' - ' + pframe # search_issues gets a parsing error on (), so escape it. existing = jira.search_issues('summary ~ \'%s\'' % summary.replace('()', '\\\\(\\\\)', 10)) if len(existing) > 0: print 'No new Jira issue created. Build ' + str( build) + ' has already been reported.' return 'Already reported' old_issue = '' existing = jira.search_issues( 'summary ~ \'%s\'' % pframe_split[0].strip().replace('()', '\\\\(\\\\)', 10)) for issue in existing: if str(issue.fields.status ) != 'Closed' and u'grammar-gen' in issue.fields.labels: old_issue = issue build_artifacts = build_report.get('artifacts')[0] pid_fileName = build_artifacts['fileName'] pid_url = build_url + 'artifact/' + pid_fileName query_split = summary_report.split( '(or it was never started??), after SQL statement:') crash_query = query_split[1] hash_split = summary_report.split('#', 1) hash_split = hash_split[1].split( '# See problematic frame for where to report the bug.') sigsegv_message = hash_split[ 0] + '# See problematic frame for where to report the bug.\n#' description = job + ' build ' + str(build) + ' : ' + str(build_result) + '\n' \ + 'Jenkins build: ' + build_url + ' \n \n' \ + 'DDL: ' + 'https://github.com/VoltDB/voltdb/blob/master/tests/sqlgrammar/DDL.sql' + ' \n \n' \ + 'hs_err_pid: ' + pid_url + ' \n \n' \ + 'SIGSEGV Message: \n' + '#' + sigsegv_message + ' \n \n' \ + 'Query that Caused the Crash: ' + crash_query description = description.replace('#', '\#') labels = ['grammar-gen'] component = 'Core' components = jira.project_components(JIRA_PROJECT) jira_component = {} for c in components: if c.name == component: jira_component = {'name': c.name, 'id': c.id} break current_version_raw = str( self.read_url( 'https://raw.githubusercontent.com/VoltDB/voltdb/master/version.txt' )) current_version_float = float(current_version_raw) current_version = 'V' + current_version_raw current_version = current_version.strip() next_version = current_version_float + .1 next_version = str(next_version) next_version = 'V' + next_version next_version = next_version[:4] jira_versions = jira.project_versions(JIRA_PROJECT) this_version = {} new_version = {} for v in jira_versions: if str(v.name) == current_version: this_version = {'name': v.name, 'id': v.id} if str(v.name) == next_version: new_version = {'name': v.name, 'id': v.id} issue_dict = { 'project': JIRA_PROJECT, 'summary': summary, 'description': description, 'issuetype': { 'name': 'Bug' }, 'priority': { 'name': 'Blocker' }, 'labels': labels, 'customfield_10430': { 'value': 'CORE team' }, 'components': [jira_component] } if new_version: issue_dict['versions'] = [new_version] issue_dict['fixVersions'] = [new_version] elif this_version: issue_dict['versions'] = [this_version] issue_dict['fixVersions'] = [this_version] if old_issue: new_comment = jira.add_comment(old_issue, description) print 'JIRA-action: New comment on issue: ' + str( old_issue) + ' created for failure on build ' + str(build) else: new_issue = jira.create_issue(fields=issue_dict) print 'JIRA-action: New issue ' + new_issue.key + ' created for failure on build ' + str( build)
class JiraProject: """ Client in charge to retrieve all issues and comments for a given JIRA project """ def __init__(self, jira_url, project_key): self.jira_url = jira_url self.jira_client = JIRA( options={'server': self.jira_url, 'verify': False}, validate=False) self.project_key = project_key def get_comments(self, issue): return self.jira_client.issue(issue.key, expand='comments') def get_project_versions(self): return self.jira_client.project_versions(self.project_key) @staticmethod def get_attachments(issue): try: return issue.fields.attachment except AttributeError: return None @staticmethod def get_assignee(issue): try: return issue.fields.assignee.name except AttributeError: return None @staticmethod def get_creation_datetime(issue): return issue.fields.created @staticmethod def get_fix_version(issue): try: fix_versions = issue.fields.fixVersions if len(fix_versions) > 0: return fix_versions[0].name else: return None except AttributeError: return None @staticmethod def get_priority(issue): try: return issue.fields.priority.name except AttributeError: return None @staticmethod def get_resolution(issue): if issue.fields.resolution is not None: return issue.fields.resolution.name else: return None @staticmethod def get_title(issue): return issue.fields.summary @staticmethod def get_type(issue): return issue.fields.issuetype.name @staticmethod def is_closed(issue): return issue.fields.resolution is not None def get_issues(self): start_index = 0 max_nb_results = 100 result = [] # while start_index < max_nb_results: while True: issues = self.jira_client.search_issues( 'project=' + self.project_key, startAt=start_index, maxResults=max_nb_results) result.extend(issues) if len(issues) == 0 or len(issues) < max_nb_results: break else: start_index += max_nb_results return sorted(result, key=lambda issue: int( issue.key[issue.key.index('-') + 1:])) def get_attachment_information(self): start_index = 0 max_nb_results = 100 result = [] # while start_index < max_nb_results \ while True: issues = self.jira_client.search_issues( 'project=' + self.project_key, fields='attachment', startAt=start_index, maxResults=max_nb_results) for issue in issues: a = self.get_attachments(issue) if a is not None and len(a) > 0: [result.append((issue.key, v.id)) for v in a] if len(issues) == 0 or len(issues) < max_nb_results: break else: start_index += max_nb_results return result def get_attachment(self, attachment_id): return self.jira_client.attachment(attachment_id)
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': '', 'statusCategory': ['To Do', 'In Progress'], 'text': '' } self.reset_filters() self.userconfig_newissue = { 'assignee': '', 'component': '', 'fixVersion': '', 'issuetype': 'Bug', 'priority': '', 'status': '', } 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.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 ''' # 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() else: password = self.vira_servers[server]['password'] # Connect to jira server try: self.jira = JIRA(options={ 'server': server, 'verify': cert_verify, }, basic_auth=(username, password), timeout=5) 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: raise e 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( "'null'", "Null").replace("'Unassigned'", "Null").replace(f"text in ({selection})", f"text ~ {selection}") def get_assign_issue(self): ''' Menu to select users ''' self.get_users() def get_assignees(self): ''' Get my issues with JQL ''' self.get_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 popup menu for a list of components ''' for component in self.jira.project_components( self.userconfig_filter['project']): print(component.name) def get_component(self): ''' Build a vim popup menu for a list of components ''' self.get_components() def get_epics(self): ''' Get my issues with JQL ''' for issue in self.query_issues(issuetypes="Epic"): print(issue["key"] + ' - ' + issue["fields"]['summary']) def get_issue(self, issue): ''' Get single issue by isuue 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 get_projects(self): ''' Build a vim popup menu for a list of projects ''' for project in self.jira.projects(): print(project) def get_priority(self): ''' Build a vim popup 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 ''' # Edit summary self.prompt_type = prompt_type 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'] return summary + self.prompt_text_commented # 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 = '' return description + self.prompt_text_commented # Prepare dynamic variables for prompt text query = 'ORDER BY updated DESC' issues = self.jira.search_issues(query, fields='assignee, reporter', json_result='True', maxResults=-1) # Determine cloud/server jira id = 'accountId' if issues['issues'][0]['fields']['reporter'].get( 'accountId') else 'name' users = set() for issue in issues['issues']: user = str(issue['fields']['reporter']['displayName'] ) + ' ~ ' + issue['fields']['reporter'][id] users.add(user) if type(issue['fields']['assignee']) == dict: user = str(issue['fields']['assignee']['displayName'] ) + ' ~ ' + issue['fields']['assignee'][id] users.add(user) self.prompt_text_commented = f''' # --------------------------------- # 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: {users} ''' # Add comment if self.prompt_type == 'add_comment': return self.prompt_text_commented # Edit comment if self.prompt_type == 'edit_comment': self.active_comment = self.jira.comment(active_issue, comment_id) return self.active_comment.body + self.prompt_text_commented 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} ''' return 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}''' def get_report(self): ''' Print a report for the given issue ''' # Get passed issue content active_issue = vim.eval("g:vira_active_issue") issues = self.jira.search_issues( 'issue = "' + active_issue + '"', # fields='*', fields=','.join([ 'summary', 'comment', 'component', 'description', 'issuetype', 'priority', 'status', 'created', 'updated', 'assignee', 'reporter', 'fixVersion', 'customfield_10106' ]), 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 = issue['created'][0:10] + ' ' + issues['issues'][0]['fields'][ 'created'][11:16] updated = issue['updated'][0:10] + ' ' + issues['issues'][0]['fields'][ 'updated'][11:16] 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']]) description = str(issue.get('description')) comments = '' idx = 0 for idx, comment in enumerate((issue['comment']['comments'])): comments += ''.join([ comment['author']['displayName'] + ' @ ' + # comment['body'] + '\n}}}\n' comment['updated'][0:10] + ' ' + comment['updated'][11:16] + ' {{{2\n' + comment['body'] + '\n}}}\n' ]) comments = ''.join(['Old Comments {{{1\n' ]) + comments if idx > 3 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 ] 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) % 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 ' ']) # 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} │ │ Component │ {component}{component_spaces} │ │ Version │ {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) return report.format(**locals()) def get_reporters(self): ''' Get my issues with JQL ''' self.get_users() def get_servers(self): ''' Get list of servers ''' for server in self.vira_servers.keys(): print(server) 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.get_versions() 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) users = [] for issue in issues["issues"]: id = str(issue['fields']['reporter']['self']).split("=")[1] user = issue['fields']['reporter']['displayName'] if user + ' ~ ' + id not in users: users.append(user + ' ~ ' + str(id)) for user in sorted(users): print(user) print('Unassigned') def get_versions(self): ''' Build a vim popup menu for a list of versions ''' for version in self.jira.project_versions( self.userconfig_filter['project']): print(version.name) print('null') def load_project_config(self): ''' Load project configuration for the current git repo 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 repo = run_command( 'git rev-parse --show-toplevel')['stdout'].strip().split('/')[-1] # If curren't repo doesn't exist, use __default__ project config if it exists if not self.vira_projects.get(repo): if self.vira_projects.get('__default__'): repo = '__default__' else: 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 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 updated DESC' 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', 'Status': 'ViraSetStatus', 'Type': 'ViraSetType', 'Version': 'ViraSetVersion', 'Summary': 'ViraEditSummary', } 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 if field == 'Summary': self.report_lines[idx + 2] = command self.report_lines[idx + 3] = command continue description_len = description.count('\n') + 3 for x in range(18, 18 + description_len): self.report_lines[x] = 'ViraEditDescription' offset = 2 if len(issue['comment']['comments']) > 4 else 1 comment_line = 18 + 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 write_jira(self): ''' Write to jira Can be issue name, description, comment, etc... ''' # User input issue = vim.eval('g:vira_active_issue') input_stripped = vim.eval('g:vira_input_text').replace( self.prompt_text_commented.strip(), '').strip() # Check if anything was actually entered by user if input_stripped == '': print("No vira actions performed") return if self.prompt_type == 'add_comment': return self.jira.add_comment(issue, input_stripped) if self.prompt_type == 'edit_comment': return self.active_comment.update(body=input_stripped) elif self.prompt_type == 'summary': return self.jira.issue(issue).update(summary=input_stripped) elif self.prompt_type == 'description': return self.jira.issue(issue).update(description=input_stripped) elif self.prompt_type == 'issue': return self.create_issue(input_stripped)
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()
class jira_handler: def __init__(self, project_name): self.mongo_db = mongodb_class.mongoDB() self.jira = JIRA('http://172.16.60.13:8080', basic_auth=('shenwei', 'sw64419')) self.gh = GreenHopper({'server': 'http://172.16.60.13:8080'}, basic_auth=('shenwei', 'sw64419')) self.name = project_name self.project = self.jira.project(self.name) self.pj_name = u"%s" % self.project.name self.pj_manager = u"%s" % self.project.lead.displayName """获取项目版本信息 """ _versions = self.jira.project_versions(self.name) self.version = {} for _v in _versions: _key = (u"%s" % _v).replace('.', '^') if not self.version.has_key(_key): self.version[_key] = {} self.version[_key][u"id"] = _v.id self.version[_key]['startDate'] = "" self.version[_key]['releaseDate'] = "" if 'startDate' in dir(_v): self.version[_key]['startDate'] = _v.startDate if 'releaseDate' in dir(_v): self.version[_key]['releaseDate'] = _v.releaseDate if self.mongo_db.get_count("project", {"version": _key}) > 0: self.mongo_db.handler( "project", "update", {"version": _key}, dict({"version": _key}, **self.version[_key])) else: _val = dict({"version": _key}, **self.version[_key]) print _val self.mongo_db.handler("project", "insert", _val) self.issue = None def _get_board(self): _boards = self.jira.boards() for _b in _boards: if self.name in _b.name: return _b.id return None def get_current_sprint(self): """ 获取本阶段sprint名称 :return: 返回状态为ACTIVE的sprint的名称 """ _b_id = self._get_board() if type(_b_id) is not types.NoneType: _sprints = self.jira.sprints(_b_id) for _s in _sprints: if _s.state == 'ACTIVE': return _s.name return None def get_sprint(self): if "customfield_10501" in self.issue.raw['fields'] and \ type(self.issue.fields.customfield_10501) is not types.NoneType: return u'%s' % self.issue.fields.customfield_10501[0].split( 'name=')[1].split(',')[0] return None def get_versions(self): _v = {} for _k in self.version: _key = (u"%s" % _k).replace('^', '.') _v[_key] = self.version[_k] return _v def get_pj_info(self): return {'pj_name': self.pj_name, 'pj_manager': self.pj_manager} def set_issue_by_name(self, issue_id): self.issue = self.jira.issue(issue_id) def print_green_hopper(self): _f = self.gh.fields() for __f in _f: __cns = __f['clauseNames'] print('-' * 8) for _n in __cns: print u"name: %s" % _n print "id: ", u"%s" % __f['id'] print "name: ", u"%s" % __f['name'] def get_story_point(self): """ 获取Issue(story)的预置成本, 1 point = 4 hours :return: 预置成本 """ if "customfield_10304" in self.issue.raw['fields'] and \ type(self.issue.fields.customfield_10304) is not types.NoneType: return self.issue.fields.customfield_10304 return None def get_task_time(self): return { "agg_time": self.issue.fields.aggregatetimeestimate, "org_time": self.issue.fields.timeoriginalestimate, "spent_time": self.issue.fields.timespent } def get_landmark(self): if len(self.issue.fields.fixVersions) > 0: return u"%s" % self.issue.fields.fixVersions[0] if len(self.issue.fields.versions) > 0: print self.show_name( ), " version: %s" % self.issue.fields.versions[0] return u"%s" % self.issue.fields.versions[0] return "" def get_desc(self): return u"%s" % self.issue.fields.summary def show_name(self): return u"%s" % str(self.issue) def get_type(self): return u"%s" % self.issue.fields.issuetype def get_status(self): return u"%s" % self.issue.fields.status def get_subtasks(self): """ 收集issue的相关子任务的issue :return: 相关issue字典 """ link = {} if not link.has_key(self.show_name()): link[self.show_name()] = [] _task_issues = self.issue.fields.subtasks for _t in _task_issues: link[self.show_name()].append(u"%s" % _t) return link def get_child_requirement(self): link = [] jql = "issue in childrenOfParentRequirement('%s')" % self.show_name() # print jql tot = 0 while True: issues = self.jira.search_issues(jql, maxResults=100, startAt=tot) for issue in issues: link.append(issue.key) if len(issues) == 100: tot += 100 else: break return link def get_link(self): """ 收集issue的相关issue :return: 相关issue字典 """ link = {} if self.show_name() not in link: link[self.show_name()] = [] """兼容以前: 与story相关的task是通过issulelinks关联的""" _task_issues = self.issue.fields.issuelinks for _t in _task_issues: if "outwardIssue" in dir(_t): """该story相关的任务""" link[self.show_name()].append(u"%s" % _t.outwardIssue) if "inwardIssue" in dir(_t): """该story相关的任务""" link[self.show_name()].append(u"%s" % _t.inwardIssue) """采用synapseRT插件后对需求的管理""" _task_issues = self.get_child_requirement() for _t in _task_issues: link[self.show_name()].append(_t) return link def show_issue(self): """ 显示issue信息 :return: """ print("[%s]-%s" % (self.show_name(), self.get_desc())), print u"类型:%s" % self.get_type(), print(u'状态:%s' % self.get_status()), print u"里程碑:%s" % self.get_landmark(), _time = self.get_task_time() """ if type(_time['agg_time']) is types.NoneType: _time['agg_time'] = "" if type(_time['org_time']) is types.NoneType: _time['org_time'] = "" if type(_time["spent_time"]) is types.NoneType: _time["spent_time"] = "" """ if "customfield_11300" in self.issue.raw['fields'] and \ type(self.issue.fields.customfield_11300) is not types.NoneType: _epic_link = self.issue.raw['fields']["customfield_11300"] else: _epic_link = None _issue = { u"%s" % self.show_name(): { "issue_type": self.get_type(), "created": self.issue.fields.created, "updated": self.issue.fields.updated, "lastViewed": self.issue.fields.lastViewed, "users": self.get_users(), "status": self.get_status(), "landmark": self.get_landmark(), "point": self.get_story_point(), "agg_time": _time['agg_time'], "org_time": _time['org_time'], "summary": self.issue.fields.summary, "spent_time": _time['spent_time'], "sprint": self.get_sprint(), "epic_link": _epic_link } } _key = u"%s" % self.show_name() if self.mongo_db.get_count("issue", {"issue": _key}) > 0: self.mongo_db.handler("issue", "update", {"issue": _key}, dict({"issue": _key}, **_issue[_key])) else: self.mongo_db.handler("issue", "insert", dict({"issue": _key}, **_issue[_key])) if self.mongo_db.get_count("issue_link", {"issue": _key}) > 0: self.mongo_db.handler("issue_link", "update", {"issue": _key}, dict({"issue": _key}, **self.get_link())) else: self.mongo_db.handler("issue_link", "insert", dict({"issue": _key}, **self.get_link())) return _issue def get_users(self): """ 获取访问issue的用户 2018.3.1:改为 经办人 assignee :return: watcher = self.jira.watchers(self.issue) _user = u"%s" % (', '.join(watcher.displayName for watcher in watcher.watchers)) """ if type(self.issue.raw['fields']["assignee"]) is types.NoneType: return None return (u"%s" % self.issue.raw['fields']["assignee"]['displayName']).replace( ' ', '') def write_log(self, info): self.mongo_db.handler("log", "insert", info) def write_worklog(self, info): _search = { 'issue': info['issue'], 'author': info['author'], 'updated': info['updated'] } self.mongo_db.handler('worklog', 'update', _searchinfo) def sync_worklog(self): worklogs = self.jira.worklogs(self.show_name()) wl = {} for worklog in worklogs: wl['issue'] = self.show_name() wl['author'] = u'%s' % worklog.author wl['comment'] = u'%s' % worklog.comment wl['timeSpent'] = worklog.timeSpent wl['timeSpentSeconds'] = worklog.timeSpentSeconds wl['updated'] = worklog.updated self.write_worklog(wl) def scan_issue(self, bg_date, keys, version): """ 扫描project收集相关版本的issue信息 :param bg_date: 起始日期,如 2018-1-31 :param keys: 关键字,[u'story', u'故事'] :param version: 版本,里程碑 :return: 按issue类型进行统计值kv_sum,issue链kv_link,相关任务链task_link """ jql_sql = u'project=%s AND created >= %s ORDER BY created DESC' % ( self.name, bg_date) total = 0 kv_sum = {} kv_link = {} task_link = {} while True: issues = self.jira.search_issues(jql_sql, maxResults=100, startAt=total) for issue in issues: if (u"%s" % issue.fields.issuetype) not in keys: continue self.issue = issue if ((u"%s" % issue.fields.issuetype) == 'story' and (self.get_landmark() == version or u'入侵' in self.issue.fields.summary)) or \ (u"%s" % issue.fields.issuetype) in [u'epic', u'improvement', u'New Feature', u'改进', u'新功能']: """收集story相关的任务""" task_link.update(self.get_link()) _type = self.get_type() _status = self.get_status() if not kv_sum.has_key(_type): kv_sum[_type] = 0 kv_link[_type] = {} kv_sum[_type] += 1 if not (kv_link[_type]).has_key(_status): (kv_link[_type])[_status] = [] (kv_link[_type])[_status].append(self.show_name()) if len(issues) == 100: total += 100 else: break return kv_sum, kv_link, task_link