def _update_fixVersion(updates, existing, issue, client): """ Helper function to sync comments between existing JIRA issue and upstream issue. :param List updates: Downstream updates requested by the user :param jira.resource.Issue existing: Existing JIRA issue :param sync2jira.intermediary.Issue issue: Upstream issue :param jira.client.JIRA client: JIRA client :returns: Nothing """ fix_version = [] # If we are not supposed to overwrite JIRA content try: # For python 3 > if not bool(list(filter(lambda d: "fixVersion" in d, updates))[0]['fixVersion']['overwrite']): # We need to make sure we're not deleting any fixVersions on JIRA # Get all fixVersions for the issue for version in existing.fields.fixVersions: fix_version.append({'name': version.name}) except ValueError: # for python 2.7 if not bool((filter(lambda d: "fixVersion" in d, updates))[0]['fixVersion']['overwrite']): # We need to make sure we're not deleting any fixVersions on JIRA # Get all fixVersions for the issue for version in existing.fields.fixVersions: fix_version.append({'name': version.name}) # Github and Pagure do not allow for multiple fixVersions (milestones) # But JIRA does, that is why we're looping here. Hopefully one # Day Github/Pagure will support multiple fixVersions :0 for version in issue.fixVersion: if version is not None: # Update the fixVersion only if it's already not in JIRA result = filter(lambda v: v['name'] == str(version), fix_version) # If we have a result skip, if not then add it to fix_version if not result or not list(result): fix_version.append({'name': version}) # We don't want to make an API call if the labels are the same jira_labels = [] for label in existing.fields.fixVersions: jira_labels.append({'name': label.name}) res = [i for i in jira_labels if i not in fix_version] + \ [j for j in fix_version if j not in jira_labels] if res: data = {'fixVersions': fix_version} # If the fixVersion is not in JIRA, it will throw an error try: existing.update(data) log.info('Updated %s fixVersion(s)' % len(fix_version)) except JIRAError: log.warning('Error updating the fixVersion. %s is an invalid fixVersion.' % issue.fixVersion) # Add a comment to indicate there was an issue client.add_comment(existing, f"Error updating fixVersion: {issue.fixVersion}")
def _close_as_duplicate(client, duplicate, keeper, config): """ Helper function to close an issue as a duplicate. :param jira.client client: JIRA Client :param jira.resources.Issue duplicate: Duplicate JIRA Issue :param jira.resources.Issue keeper: JIRA issue to keep :param Dict config: Config dict :returns: Nothing """ log.info("Closing %s as duplicate of %s", duplicate.permalink(), keeper.permalink()) if config['sync2jira']['testing']: log.info("Testing flag is true. Skipping actual delete.") return # Find the id of some dropped or done state. transitions = client.transitions(duplicate) transitions = dict([(t['name'], t['id']) for t in transitions]) closed = None preferences = ['Dropped', 'Reject', 'Done', 'Closed', 'Closed (2)', ] for preference in preferences: if preference in transitions: closed = transitions[preference] break text = 'Marking as duplicate of %s' % keeper.key if any([text in comment.body for comment in client.comments(duplicate)]): log.info("Skipping comment. Already present.") else: client.add_comment(duplicate, text) text = '%s is a duplicate.' % duplicate.key if any([text in comment.body for comment in client.comments(keeper)]): log.info("Skipping comment. Already present.") else: client.add_comment(keeper, text) if closed: try: client.transition_issue(duplicate, closed, resolution={'name': 'Duplicate'}) except Exception as e: if "Field 'resolution' cannot be set" in e.response.text: # Try closing without a specific resolution. try: client.transition_issue(duplicate, closed) except Exception: log.exception("Failed to close %r", duplicate.permalink()) else: log.exception("Failed to close %r", duplicate.permalink()) else: log.warning("Unable to find close transition for %r" % duplicate.key)
def _update_comments(client, existing, issue): """ Helper function to sync comments between existing JIRA issue and upstream issue. :param jira.client.JIRA client: JIRA client :param jira.resource.Issue existing: Existing JIRA issue :param sync2jira.intermediary.Issue issue: Upstream issue :returns: Nothing """ # First get all existing comments comments = client.comments(existing) # Remove any comments that have already been added comments_d = _comment_matching(issue.comments, comments) # Loop through the comments that remain for comment in comments_d: # Format and add them comment_body = _comment_format(comment) client.add_comment(existing, comment_body) if len(comments_d) > 0: log.info("Comments synchronization done on %i comments." % len(comments_d))
def _update_transition(client, existing, issue): """ Helper function to update the transition of a downstream JIRA issue. :param jira.client.JIRA client: JIRA client :param jira.resource.Issue existing: Existing JIRA issue :param sync2jira.intermediary.Issue issue: Upstream issue :returns: Nothing """ # Update the issue status in the JIRA description # Format the status formatted_status = "Upstream issue status: %s" % issue.status new_description = existing.fields.description # Bool to indicate if we should update update = False # Check if the issue has the issue status line # First check legacy upstream status so we can update them if "] Upstream issue status:" in existing.fields.description: # Use pattern matching to find and update the status new_description = re.sub( r"\[.*\] Upstream issue status: .*", formatted_status, new_description) update = True # Now check if the status is already present elif formatted_status in existing.fields.description: pass # Then check for upstream status elif "Upstream issue status:" in existing.fields.description: # Use pattern matching to find and update the status new_description = re.sub( r"Upstream issue status: .*", formatted_status, new_description) update = True else: # We can just add this line to the very top new_description = formatted_status + '\n' + new_description update = True if update: # Now we can update the JIRA issue (always need to update this # as there is a timestamp involved) data = {'description': new_description} existing.update(data) log.info('Updated transition') # If the user just inputted True, only update the description # If the user added a custom closed status, attempt to close the # downstream JIRA ticket # First get the closed status from the config file try: # For python 3 > closed_status = list(filter(lambda d: "transition" in d, issue.downstream.get('updates', {})))[0]['transition'] except ValueError: # for python 2.7 closed_status = (filter(lambda d: "transition" in d, issue.downstream.get('updates', {})))[0]['transition'] if closed_status is not True and issue.status == 'Closed' \ and existing.fields.status.name.upper() != closed_status.upper(): # Now we need to update the status of the JIRA issue # First add a comment indicating the change (in case it doesn't go through) hyperlink = f"[Upstream issue|{issue.url}]" comment_body = f"{hyperlink} closed. Attempting transition to {closed_status}." client.add_comment(existing, comment_body) # Ensure that closed_status is a valid choice # Find all possible transactions (i.e. change states) we could `do _change_status(client, existing, closed_status, issue)
def _create_jira_issue(client, issue, config): """ Create a JIRA issue and adds all relevant information in the issue to the JIRA issue. :param jira.client.JIRA client: JIRA client :param sync2jira.intermediary.Issue issue: Issue object :param Dict config: Config dict :returns: Returns JIRA issue that was created :rtype: jira.resources.Issue """ log.info("Creating %r issue for %r", issue.downstream, issue) if config['sync2jira']['testing']: log.info("Testing flag is true. Skipping actual creation.") return custom_fields = issue.downstream.get('custom_fields', {}) default_type = issue.downstream.get('type', "Bug") # Build the description of the JIRA issue if 'description' in issue.downstream.get('updates', {}): description = "Upstream description: {quote}%s{quote}" % issue.content else: description = '' if any('transition' in item for item in issue.downstream.get('updates', {})): # Just add it to the top of the description formatted_status = "Upstream issue status: %s" % issue.status description = formatted_status + '\n' + description if issue.reporter: # Add to the description description = '[%s] Upstream Reporter: %s \n %s' % ( issue.id, issue.reporter['fullname'], description ) # Add the url if requested if 'url' in issue.downstream.get('updates', {}): description = description + f"\nUpstream URL: {issue.url}" kwargs = dict( summary=issue.title, description=description, issuetype=dict(name="Story" if "RFE" in issue.title else default_type), ) if issue.downstream['project']: kwargs['project'] = dict(key=issue.downstream['project']) if issue.downstream.get('component'): # TODO - make this a list in the config kwargs['components'] = [dict(name=issue.downstream['component'])] for key, custom_field in custom_fields.items(): if type(custom_field) is str: kwargs[key] = custom_field.replace("[remote-link]", issue.url) else: kwargs[key] = custom_field # Add labels if needed if 'labels' in issue.downstream.keys(): kwargs['labels'] = issue.downstream['labels'] log.info("Creating issue.") downstream = client.create_issue(**kwargs) # Add Epic link or QA field if present if issue.downstream.get('epic-link', None) or \ issue.downstream.get('qa-contact', None): # Fetch all fields all_fields = client.fields() # Make a map from field name -> field id name_map = {field['name']: field['id'] for field in all_fields} if issue.downstream.get('epic-link', None): # Try to get and update the custom field custom_field = name_map.get('Epic Link', None) if custom_field: try: downstream.update({custom_field: issue.downstream.get('epic-link')}) except JIRAError: client.add_comment(downstream, f"Error adding Epic-Link: {issue.downstream.get('epic-link')}") if issue.downstream.get('qa-contact', None): # Try to get and update the custom field custom_field = name_map.get('QA Contact', None) if custom_field: downstream.update({custom_field: issue.downstream.get('qa-contact')}) # Add upstream issue ID in comment if required if 'upstream_id' in issue.downstream.get('updates', []): comment = f"Creating issue for " \ f"[{issue.upstream}-#{issue.upstream_id}|{issue.url}]" client.add_comment(downstream, comment) remote_link = dict(url=issue.url, title=remote_link_title) _attach_link(client, downstream, remote_link) default_status = issue.downstream.get('default_status', None) if default_status is not None: _change_status(client, downstream, default_status, issue) # Update relevant information (i.e. tags, assignees etc.) if the # User opted in _update_jira_issue(downstream, issue, client) return downstream