Skip to content

Commit

Permalink
Add an extended action that supports syncing assignee and status.
Browse files Browse the repository at this point in the history
Separates out a couple of key parts of the default action to allow
sub-classes to make minor changes before and after Jira is updated. This
includes:

  * Allowing sub-classes to modify the comments generated for existing
    issues
  * Allowing sub-classes to make changes to the Jira issue after the
    default action is done processing.

Then defines a new action that extends the default and syncs assignee
and status.
  • Loading branch information
Mossop committed Jul 6, 2022
1 parent ecd95dc commit f94edbc
Show file tree
Hide file tree
Showing 5 changed files with 577 additions and 2 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The system reads the action configuration from a YAML file, one per environment.
Below is a full example of an action configuration:
```yaml
action: src.jbi.whiteboard_actions.default
allow_private: false
contact: [example@allizom.com]
description: example configuration
enabled: true
Expand All @@ -31,6 +32,10 @@ A bit more about the different fields...
- string
- default: [src.jbi.whiteboard_actions.default](src/jbi/whiteboard_actions/default.py)
- The specified Python module must be available in the `PYTHONPATH`
- `allow_private` (optional)
- bool [true, false]
- default: false
- If false bugs that are not public will not be synchronized
- `contact`
- list of strings
- If an issue arises with the workflow, communication will be established with these contacts
Expand All @@ -54,6 +59,36 @@ A bit more about the different fields...
[View 'prod' configurations here.](config/config.prod.yaml)


## Extended action
The `src.jbi.whiteboard_actions.extended` action adds some additional features on top of the default.

It will attempt to assign the Jira issue the same person as the bug is assigned to. This relies on
the user using the same email address in both Bugzilla and Jira. If the user does not exist in Jira
then the assignee is cleared from the Jira issue.

The extended action supports setting the Jira issues's status when the Bugzilla status and
resolution change. This is defined using a mapping on a per-project basis configured in the
`status_map` field of the `parameters` field.

An example configuration:
```yaml
action: src.jbi.whiteboard_actions.extended
contact: [example@allizom.com]
description: example configuration
enabled: true
parameters:
jira_project_key: EXMPL
whiteboard_tag: example
status_map:
NEW: "In Progress"
FIXED: "Closed"
```

In this case if the bug changes to the NEW status the action will attempt to set the linked Jira
issue status to "In Progress". If the bug changes to RESOLVED FIXED it will attempt to set the
linked Jira issue status to "Closed". If the bug changes to a status not listed in `status_map` then
no change will be made to the Jira issue.

### Custom Actions
If you're looking for a unique capability for your team's data flow, you can add your own python methods and functionality[...read more here.](src/jbi/whiteboard_actions/README.md)

Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ testpaths = [
ignore='third_party'
ignore-patterns = "tests/*"
extension-pkg-whitelist = "pydantic"
[tool.pylint.SIMILARITIES]
ignore-signatures = "yes"

[tool.isort]
profile = "black"
Expand Down
24 changes: 22 additions & 2 deletions src/jbi/whiteboard_actions/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""

from src.app.environment import get_settings
from src.jbi.bugzilla import BugzillaWebhookRequest
from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
from src.jbi.errors import ActionError
from src.jbi.services import get_bugzilla, get_jira, getbug_as_bugzilla_object

Expand Down Expand Up @@ -56,6 +56,22 @@ def comment_create_or_noop(self, payload: BugzillaWebhookRequest):
)
return {"status": "comment", "jira_response": jira_response}

def jira_comments_for_update( # pylint: disable=no-self-use
self,
payload: BugzillaWebhookRequest,
):
"""Returns the comments to post to Jira for a changed bug"""
return payload.map_as_comments()

def update_issue(
self,
payload: BugzillaWebhookRequest,
bug_obj: BugzillaBug,
linked_issue_key: str,
is_new: bool,
):
"""Allows sub-classes to modify the Jira issue in response to a bug event"""

def bug_create_or_update(
self, payload: BugzillaWebhookRequest
): # pylint: disable=too-many-locals
Expand All @@ -64,7 +80,7 @@ def bug_create_or_update(
linked_issue_key = bug_obj.extract_from_see_also() # type: ignore
if linked_issue_key:
# update
comments = payload.map_as_comments()
comments = self.jira_comments_for_update(payload)
jira_response_update = self.jira_client.update_issue_field(
key=linked_issue_key, fields=bug_obj.map_as_jira_issue()
)
Expand All @@ -76,6 +92,7 @@ def bug_create_or_update(
issue_key=linked_issue_key, comment=comment
)
)
self.update_issue(payload, bug_obj, linked_issue_key, False)
return {
"status": "update",
"jira_responses": [jira_response_update, jira_response_comments],
Expand Down Expand Up @@ -131,6 +148,9 @@ def create_and_link_issue(self, payload, bug_obj):
link_url=bugzilla_url,
title="Bugzilla Ticket",
)

self.update_issue(payload, bug_obj, jira_key_in_response, True)

return {
"status": "create",
"bugzilla_response": bugzilla_response,
Expand Down
119 changes: 119 additions & 0 deletions src/jbi/whiteboard_actions/extended.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Extended action that provides some additional features over the default:
* Updates the Jira assignee when the bug's assignee changes.
* Optionally updates the Jira status when the bug's resolution or status changes.
`init` is required; and requires at minimum the
`whiteboard_tag` and `jira_project_key`. `status_map` is optional.
`init` should return a __call__able
"""
import logging

from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
from src.jbi.whiteboard_actions.default import DefaultExecutor

logger = logging.getLogger(__name__)


def init(whiteboard_tag, jira_project_key, **kwargs):
"""Function that takes required and optional params and returns a callable object"""
return ExtendedExecutor(
whiteboard_tag=whiteboard_tag, jira_project_key=jira_project_key, **kwargs
)


class ExtendedExecutor(DefaultExecutor):
"""Callable class that encapsulates the extended action."""

def __init__(self, **kwargs):
"""Initialize ExtendedExecutor Object"""
super().__init__(**kwargs)
self.status_map = kwargs.get("status_map", {})

def jira_comments_for_update(
self,
payload: BugzillaWebhookRequest,
):
"""Returns the comments to post to Jira for a changed bug"""
return payload.map_as_comments(
status_log_enabled=False, assignee_log_enabled=False
)

def update_issue(
self,
payload: BugzillaWebhookRequest,
bug_obj: BugzillaBug,
linked_issue_key: str,
is_new: bool,
):
changed_fields = payload.event.changed_fields() or []

log_context = {
"bug": {
"id": bug_obj.id,
"status": bug_obj.status,
"resolution": bug_obj.resolution,
"assigned_to": bug_obj.assigned_to,
},
"jira": linked_issue_key,
"changed_fields": changed_fields,
}

def clear_assignee():
# New tickets already have no assignee.
if not is_new:
logger.debug("Clearing assignee", extra=log_context)
self.jira_client.update_issue_field(
key=linked_issue_key, fields={"assignee": None}
)

# If this is a new issue or if the bug's assignee has changed then
# update the assignee.
if is_new or "assigned_to" in changed_fields:
if bug_obj.assigned_to == "nobody@mozilla.org":
clear_assignee()
else:
logger.debug(
"Attempting to update assignee to %s",
bug_obj.assigned_to,
extra=log_context,
)
# Look up this user in Jira
users = self.jira_client.user_find_by_user_string(
query=bug_obj.assigned_to
)
if len(users) == 1:
try:
# There doesn't appear to be an easy way to verify that
# this user can be assigned to this issue, so just try
# and do it.
self.jira_client.update_issue_field(
key=linked_issue_key,
fields={"assignee": {"accountId": users[0]["accountId"]}},
)
except IOError as exception:
logger.debug(
"Setting assignee failed: %s", exception, extra=log_context
)
# If that failed then just fall back to clearing the
# assignee.
clear_assignee()

# If this is a new issue or if the bug's status or resolution has
# changed then update the assignee.
if is_new or "status" in changed_fields or "resolution" in changed_fields:
# We use resolution if one exists or status otherwise.
status = bug_obj.resolution
if status == "":
status = bug_obj.status

if status in self.status_map:
logger.debug(
"Updating Jira status to %s",
self.status_map[status],
extra=log_context,
)
self.jira_client.set_issue_status(
linked_issue_key, self.status_map[status]
)
Loading

0 comments on commit f94edbc

Please sign in to comment.