def create_jira_task(host, user, password, assignee): jira = JIRA(basic_auth=(user, password), server=host) issue = jira.create_issue(project=JIRA_PROJECT, summary=JIRA_ISSUE_SUMMARY, description=JIRA_ISSUE_DESCRIPTION, issuetype={'name': 'Task'}) username = jira.search_users(assignee, maxResults=1)[0].name issue.update(assignee={'name': username}) transition_id = jira.find_transitionid_by_name(issue.id, JIRA_ISSUE_STATUS) jira.transition_issue(issue.id, transition_id)
class JiraClient: def __init__(self, server, username, api_key): """ Client object for invoking JIRA Python API "https://pypi.org/project/jira/" Parameters ---------- server: str The server address and context path to use. Defaults to http://localhost:2990/jira username : str Username to establish a session via HTTP BASIC authentication. api_key : str API Key to establish a session via HTTP BASIC authentication. """ options = {'server': server} self._client = JIRA(options, basic_auth=(username, api_key)) def transition_issue(self, issue_key, status): """ Perform a transition on an issue. Parameters ---------- issue_key : str ID or key of the issue to add the comment to (e.g. IM-1) status : str Name of the transition to perform Raises ------ JiraError If the issue does not exist or you do not have permission to see it. KeyError If transition does not exist for the issue. Returns ------ None """ try: issue = self._client.issue(issue_key) transition_id = self._client.find_transitionid_by_name(issue_key, status) if transition_id is None: raise KeyError( f'Error: Transition "{status}" does not exist for Issue {issue_key}.' ) self._client.transition_issue(issue, transition_id) except JIRAError: logger.error(f'Error: Issue "{issue_key}" does not exist.') raise def add_comment(self, issue_key, comment): """ Add a comment from the current authenticated user on the specified issue and return a Resource for it. Parameters ---------- issue_key : str ID or key of the issue to add the comment to (e.g. IM-1) comment : str Text of the comment to add Raises ------ JiraError If the issue does not exist or you do not have permission to see it. Returns ------ Resource object of <class 'jira.resources.Comment'> with Comment ID. """ try: return self._client.add_comment(issue_key, comment) except JIRAError: logger.error(f'Error: Issue "{issue_key}" does not exist.') raise def get_issue_field(self, issue, field): """ Helper function that returns the value of the requested value for an issue. This function is used in the get_issues() funtion to extract the fields of an issue. Parameters ---------- issue : <class 'jira.resources.Issue'> Issue Object of class <class 'jira.resources.Issue'>. field : str Name of the expected field. (e.g status). Note: for a custom field, use the custom field id. (e.g. customfield_10028) Raises ------ AttributeError If the Field does not exist for the issue. Returns ------ str For a text field : returns a string with the value of the requested field. (e.g. 'Error') For a field with multiple options : returns a string of comma separated values from all the options available in the field. (for e.g 'User1,User2') """ try: field_value = getattr(issue.fields, field) if isinstance(field_value, list): return (','.join(map(str, field_value))) else: return str(field_value) except AttributeError: logger.error(f'Field "{field}" does not exist for issue "{issue}."') raise def get_issues(self, project, fields=DEFAULT_FIELDS): """ Returns a Pandas DataFrame of all JIRA Issues for the requested project with values for all requested fields. Parameters ---------- project : str Project name or key to pull issues from. fields : list, optional A list of fields to be extracted for each issue in the project. (default is ['created', 'creator', 'assignee', 'status', 'issuetype', 'priority', 'summary', 'description', 'resolution', 'resolutiondate']) Note: for a custom field, use the custom field id. (e.g. customfield_10028) Raises ------ AttributeError If the Field does not exist for the issue. Returns ------ pandas DataFrame A pandas DataFrame of all issues for the requested project with values for all the requested fields. The column header of the resulting DataFrame is of the form: If "fields" is defined: ['project', 'key', field1, field2 , field3....] (default) If "fields" is not defined: ['project', 'key' ,'created', 'creator', 'assignee','status', 'issuetype', 'priority', 'summary', 'description', 'resolution', 'resolutiondate'] """ block_num = 0 block_size = 1000 # define base header list header_list = ['project', 'key'] # extend the base header list to include requested fields header_list.extend(fields) # initialize lists all_issues = [] issues_list = [] # Retrieve all issues from JIRA Project issues = self._client.search_issues(f'project={project}', 0, block_size) while len(issues) > 0: all_issues.extend(issues) block_num += 1 issues = self._client.search_issues( f'project={project}', block_num * block_size, block_size ) # Loop over all issues and extract project, issue and requested fields defined by the # parameter fields for issue in all_issues: issue_details = [project, issue.key] extract_fields = list(map(lambda n: self.get_issue_field(issue, n), fields)) issue_details.extend(extract_fields) issues_list.append(issue_details) df_issues = pd.DataFrame(issues_list, columns=header_list, index=None) return df_issues
action('Creating branch ' + bold(branch)) try: if os.environ.get('JBRANCH_NO_SWITCH'): subprocess.check_output(['git', 'branch', branch], stderr=subprocess.STDOUT) else: subprocess.check_output(['git', 'checkout', '-b', branch], stderr=subprocess.STDOUT) except OSError as e: error(str(e)) except subprocess.CalledProcessError as e: error(e.output.rstrip()) else: success() transition = os.environ.get('JBRANCH_TRANSITION') if transition: action('Triggering transition: ' + bold(transition)) transition_id = jira.find_transitionid_by_name(issue, transition) if transition_id is None: error('the transition {0} is not available for this issue'.format( bold(transition))) try: jira.transition_issue(issue, transition_id) except JIRAError as jira_error: error(jira_error.text) else: success()
class Jira(object): """JIRA objects and operations.""" def __init__(self, url, username, token, project, max_results): self.jira = JIRA(url, basic_auth=(username, token)) self.project = project self.max_results = max_results def search_issues(self, keyword, **kwargs): """ Search Jira issues. Filtering by keyword is not done on JIRA query becasue it does not support filtering both summary and key by string values. """ keyword = keyword.lower() query_parameters = [] if kwargs.get('type'): query_parameters.append('type = "{}"'.format(kwargs.get('type'))) query = ' OR'.join(query_parameters) + ' order by created desc' issues = self.jira.search_issues(query, maxResults=self.max_results) matching_issues = [] for issue in issues: if keyword in issue.key.lower( ) or keyword in issue.fields.summary.lower(): matching_issues.append(issue) return matching_issues def get_issue_by_key(self, key): """Get issue by key""" try: return self.jira.issue(key) except JIRAError as e: if e.status_code == 404: raise click.UsageError( 'The specified JIRA issue: {}, does not exist.'.format( key)) raise def create_issue(self, fields): return self.jira.create_issue(fields=fields) def get_resolution_by_name(self, name): resolutions = self.jira.resolutions() for r in resolutions: if r.name == name: return r.id return None def get_transition(self, issue, name): return self.jira.find_transitionid_by_name(issue, name) def transition_issue(self, issue, transition): transition_id = self.get_transition(issue, transition) if transition_id: self.jira.transition_issue(issue, transition_id) def assign_issue(self, issue, assignee): self.jira.assign_issue(issue, assignee)
class JiraLocal(object): def __init__(self, jira_url, auth_user, auth_token, rule, jira_fields_dict): self.jira_instance = JIRA(jira_url, basic_auth=(auth_user, auth_token)) self.jira_config = rule['jira_config'] self.jira_fields_dict = jira_fields_dict self.jira_issue_id_field_key = jira_fields_dict[ rule['jira_config']["jira_issue_id_field"]] self.log = Logger(rule=rule) return def get_jira_issues(self, project_key, halo_issues): jira_issues_dict = {} with ThreadPoolExecutor(max_workers=os.cpu_count() * 2) as executor: future_to_issue_id = { executor.submit(self.get_jira_issues_for_halo_issue, issue["id"], project_key): issue["id"] for issue in halo_issues } for future in as_completed(future_to_issue_id): issue_id = future_to_issue_id[future] jira_issues = future.result() jira_issues_dict[issue_id] = jira_issues return jira_issues_dict def get_jira_epics_or_issues(self, project_keys, issuetype, dict_format=True): if isinstance(project_keys, str): project_keys = [project_keys] jira_issues_dict = defaultdict(list) jira_issues = self.jira_instance.search_issues( f'project in ({", ".join(x for x in project_keys)}) AND ' f'resolution = Unresolved AND ' f'issuetype={issuetype} AND ' f'"{self.jira_config["jira_issue_id_field"]}" is not EMPTY', maxResults=False) if not dict_format: return jira_issues for issue in jira_issues: jira_issues_dict[issue.raw["fields"][ self.jira_issue_id_field_key]].append(issue) return jira_issues_dict def get_jira_issues_for_halo_issue(self, issue_id, project_key): results = self.jira_instance.search_issues( f'project="{project_key}" AND ' f'"{self.jira_config["jira_issue_id_field"]}"~{issue_id} AND ' f'issuetype="{self.jira_config["jira_issue_type"]}"') return results def create_jira_epic(self, group_key_hash, group_key_str, project_key): # Get IDs for epic fields epic_dict = { 'project': { 'key': project_key }, 'summary': group_key_str, self.jira_fields_dict["Epic Name"]: group_key_str, 'description': group_key_str, 'issuetype': { 'name': 'Epic' }, self.jira_issue_id_field_key: group_key_hash } epic = self.jira_instance.create_issue(fields=epic_dict) return epic def create_jira_issue(self, issue, epic, jira_fields_dict, fields, project_key): epic_link = None if epic: epic_link = epic.key summary, description, field_mapping = self.prepare_issue( issue, fields, jira_fields_dict) issue_dict = { 'project': { 'key': project_key }, 'issuetype': { 'name': self.jira_config['jira_issue_type'] }, self.jira_issue_id_field_key: issue["id"], self.jira_fields_dict["Epic Link"]: epic_link, 'summary': summary, 'description': description } issue_dict.update(field_mapping) self.log.info(f"Creating issue: {issue['id']}") self.jira_instance.create_issue(fields=issue_dict) def update_jira_issue(self, issue, jira_issues, jira_fields_dict, fields): self.log.info(f"Updating issue: {issue['id']}") for jira_issue in jira_issues: summary, description, field_mapping = self.prepare_issue( issue, fields, jira_fields_dict) issue_dict = {'summary': summary, 'description': description} issue_dict.update(field_mapping) jira_issue.update(fields=issue_dict) if issue["status"] == "resolved": self.transition_issue(jira_issue, self.jira_config["issue_status_closed"]) elif jira_issue.raw["fields"]["status"][ "name"] == self.jira_config["issue_status_closed"]: self.transition_issue( jira_issue, self.jira_config["issue_status_reopened"]) def transition_issue(self, issue, transition_name): self.log.info(f"Transitioning issue {issue.key} to {transition_name}") transition_id = self.jira_instance.find_transitionid_by_name( issue, transition_name) try: self.jira_instance.transition_issue(issue, transition_id) except JIRAError: self.log.error( f"Could not transition Jira Issue '{issue.key}' " f"from {issue.raw['fields']['status']['name']} to {transition_name}" ) def prepare_issue(self, issue, fields, jira_fields_dict): asset_formatted = Formatter.format_object(issue["asset_type"], issue.pop("asset")) finding_formatted = Formatter.format_object("findings", issue.pop("findings")) issue_formatted = Formatter.format_object("issue", issue) summary = Formatter.format_summary(issue) description = issue_formatted + asset_formatted + finding_formatted description = description[:32759] + '{code}\n\n' dynamic_map = fields.get("mapping") or {} static = fields.get("static") or {} field_mapping = map_fields(dynamic_map, static, issue, jira_fields_dict) return summary, description, field_mapping def push_issues(self, issues, jira_epics_dict, jira_issues_dict, jira_fields_dict, fields, project_key=None): with ThreadPoolExecutor(max_workers=os.cpu_count() * 2) as executor: for issue in issues: jira_issues = jira_issues_dict.get(issue["id"]) if jira_issues: executor.submit(self.update_jira_issue, issue, jira_issues, jira_fields_dict, fields) else: groupby_key = issue.pop("groupby_key", "") epic = jira_epics_dict.get(groupby_key) executor.submit(self.create_jira_issue, issue, epic, jira_fields_dict, fields, project_key) def cleanup_epics(self, project_keys): jira_issues = self.get_jira_epics_or_issues( project_keys, self.jira_config["jira_issue_type"], dict_format=False) epics_set = set(issue.raw["fields"][self.jira_fields_dict["Epic Link"]] for issue in jira_issues) jira_epics = self.get_jira_epics_or_issues(project_keys, "Epic", dict_format=False) with ThreadPoolExecutor(max_workers=os.cpu_count() * 2) as executor: for epic in jira_epics: if epic.key not in epics_set: self.log.info(f"Deleting epic: {epic.key}") executor.submit(self.transition_issue, epic, self.jira_config["issue_status_closed"])
class Jira(object): """JIRA objects and operations.""" def __init__(self, instance, project, workflow, connection_user, max_results=50): self.workflow = workflow self.project = project self.username = instance.credentials.username self.max_results = max_results self.jira = JIRA(instance.url, basic_auth=(connection_user, instance.credentials.token)) def search_issues(self, keyword, **kwargs): """ Search Jira issues. Filtering by keyword is not done on JIRA query becasue it does not support filtering both summary and key by string values. """ keyword = keyword.lower() query_parameters = [] if kwargs.get("types"): for type in kwargs.get("types"): query_parameters.append('type = "{}"'.format(type)) query = " OR ".join(query_parameters) + " order by created desc" issues = self.jira.search_issues(query, maxResults=self.max_results) matching_issues = [] for issue in issues: if keyword in issue.key.lower( ) or keyword in issue.fields.summary.lower(): matching_issues.append(self._convert_to_issue(issue)) return matching_issues def get_issue_by_key(self, key): """Get issue by key""" try: jira_issue = self.jira.issue(key) return self._convert_to_issue(jira_issue) except JIRAError as e: if e.status_code == 404: raise click.UsageError( "The specified JIRA issue: {}, does not exist.".format( key)) raise def create_issue(self, fields): jira_issue = self.jira.create_issue(fields=fields) return self._convert_to_issue(jira_issue) def get_resolution_by_name(self, name): resolutions = self.jira.resolutions() for r in resolutions: if r.name == name: return r.id return None def get_transition(self, issue, name): return self.jira.find_transitionid_by_name(issue, name) def transition_issue(self, issue, transition): transition_id = self.get_transition(issue, transition) if transition_id: self.jira.transition_issue(issue, transition_id) def assign_issue(self, issue, assignee): self.jira.assign_issue(issue, assignee) def make_action(self, action, issue): issue.status = action.next_state # Get jira issue to perform transitions jira_issue = self.jira.issue(issue.key) for transition in action.transitions: self.transition_issue(jira_issue, transition) if action.assign_to_user: self.assign_issue(jira_issue, self.username) return issue def _convert_to_issue(self, jira_issue): """Convert to simplified issue.""" i = Issue( jira_issue.key, jira_issue.fields.summary, self._get_type(jira_issue), self._get_status(jira_issue), ) i.subtasks = self._get_subtasks(jira_issue) return i def _get_type(self, jira_issue): jira_type = jira_issue.fields.issuetype.name for type_mapping in self.workflow.types: if type_mapping.mapping == jira_type: return type_mapping.issue_type raise Exception(f"Unable to map issue type: {jira_type}") def _get_status(self, jira_issue): jira_status = jira_issue.fields.status.name for status_mapping in self.workflow.statuses: if jira_status in status_mapping.mapping: return status_mapping.status raise Exception(f"Unable to map issue status: {jira_status}") def _get_subtasks(self, jira_issue): try: return [ self._convert_to_issue(subtask) for subtask in jira_issue.fields.subtasks ] except AttributeError as e: return []