Example #1
0
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}")
Example #2
0
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)
Example #3
0
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))
Example #4
0
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)
Example #5
0
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