Skip to content

Commit f94edbc

Browse files
committed
Add an extended action that supports syncing assignee and status.
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.
1 parent ecd95dc commit f94edbc

File tree

5 files changed

+577
-2
lines changed

5 files changed

+577
-2
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The system reads the action configuration from a YAML file, one per environment.
1818
Below is a full example of an action configuration:
1919
```yaml
2020
action: src.jbi.whiteboard_actions.default
21+
allow_private: false
2122
contact: [example@allizom.com]
2223
description: example configuration
2324
enabled: true
@@ -31,6 +32,10 @@ A bit more about the different fields...
3132
- string
3233
- default: [src.jbi.whiteboard_actions.default](src/jbi/whiteboard_actions/default.py)
3334
- The specified Python module must be available in the `PYTHONPATH`
35+
- `allow_private` (optional)
36+
- bool [true, false]
37+
- default: false
38+
- If false bugs that are not public will not be synchronized
3439
- `contact`
3540
- list of strings
3641
- If an issue arises with the workflow, communication will be established with these contacts
@@ -54,6 +59,36 @@ A bit more about the different fields...
5459
[View 'prod' configurations here.](config/config.prod.yaml)
5560

5661

62+
## Extended action
63+
The `src.jbi.whiteboard_actions.extended` action adds some additional features on top of the default.
64+
65+
It will attempt to assign the Jira issue the same person as the bug is assigned to. This relies on
66+
the user using the same email address in both Bugzilla and Jira. If the user does not exist in Jira
67+
then the assignee is cleared from the Jira issue.
68+
69+
The extended action supports setting the Jira issues's status when the Bugzilla status and
70+
resolution change. This is defined using a mapping on a per-project basis configured in the
71+
`status_map` field of the `parameters` field.
72+
73+
An example configuration:
74+
```yaml
75+
action: src.jbi.whiteboard_actions.extended
76+
contact: [example@allizom.com]
77+
description: example configuration
78+
enabled: true
79+
parameters:
80+
jira_project_key: EXMPL
81+
whiteboard_tag: example
82+
status_map:
83+
NEW: "In Progress"
84+
FIXED: "Closed"
85+
```
86+
87+
In this case if the bug changes to the NEW status the action will attempt to set the linked Jira
88+
issue status to "In Progress". If the bug changes to RESOLVED FIXED it will attempt to set the
89+
linked Jira issue status to "Closed". If the bug changes to a status not listed in `status_map` then
90+
no change will be made to the Jira issue.
91+
5792
### Custom Actions
5893
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)
5994

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ testpaths = [
5656
ignore='third_party'
5757
ignore-patterns = "tests/*"
5858
extension-pkg-whitelist = "pydantic"
59+
[tool.pylint.SIMILARITIES]
60+
ignore-signatures = "yes"
5961

6062
[tool.isort]
6163
profile = "black"

src/jbi/whiteboard_actions/default.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"""
88

99
from src.app.environment import get_settings
10-
from src.jbi.bugzilla import BugzillaWebhookRequest
10+
from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
1111
from src.jbi.errors import ActionError
1212
from src.jbi.services import get_bugzilla, get_jira, getbug_as_bugzilla_object
1313

@@ -56,6 +56,22 @@ def comment_create_or_noop(self, payload: BugzillaWebhookRequest):
5656
)
5757
return {"status": "comment", "jira_response": jira_response}
5858

59+
def jira_comments_for_update( # pylint: disable=no-self-use
60+
self,
61+
payload: BugzillaWebhookRequest,
62+
):
63+
"""Returns the comments to post to Jira for a changed bug"""
64+
return payload.map_as_comments()
65+
66+
def update_issue(
67+
self,
68+
payload: BugzillaWebhookRequest,
69+
bug_obj: BugzillaBug,
70+
linked_issue_key: str,
71+
is_new: bool,
72+
):
73+
"""Allows sub-classes to modify the Jira issue in response to a bug event"""
74+
5975
def bug_create_or_update(
6076
self, payload: BugzillaWebhookRequest
6177
): # pylint: disable=too-many-locals
@@ -64,7 +80,7 @@ def bug_create_or_update(
6480
linked_issue_key = bug_obj.extract_from_see_also() # type: ignore
6581
if linked_issue_key:
6682
# update
67-
comments = payload.map_as_comments()
83+
comments = self.jira_comments_for_update(payload)
6884
jira_response_update = self.jira_client.update_issue_field(
6985
key=linked_issue_key, fields=bug_obj.map_as_jira_issue()
7086
)
@@ -76,6 +92,7 @@ def bug_create_or_update(
7692
issue_key=linked_issue_key, comment=comment
7793
)
7894
)
95+
self.update_issue(payload, bug_obj, linked_issue_key, False)
7996
return {
8097
"status": "update",
8198
"jira_responses": [jira_response_update, jira_response_comments],
@@ -131,6 +148,9 @@ def create_and_link_issue(self, payload, bug_obj):
131148
link_url=bugzilla_url,
132149
title="Bugzilla Ticket",
133150
)
151+
152+
self.update_issue(payload, bug_obj, jira_key_in_response, True)
153+
134154
return {
135155
"status": "create",
136156
"bugzilla_response": bugzilla_response,
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Extended action that provides some additional features over the default:
3+
* Updates the Jira assignee when the bug's assignee changes.
4+
* Optionally updates the Jira status when the bug's resolution or status changes.
5+
6+
`init` is required; and requires at minimum the
7+
`whiteboard_tag` and `jira_project_key`. `status_map` is optional.
8+
9+
`init` should return a __call__able
10+
"""
11+
import logging
12+
13+
from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
14+
from src.jbi.whiteboard_actions.default import DefaultExecutor
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
def init(whiteboard_tag, jira_project_key, **kwargs):
20+
"""Function that takes required and optional params and returns a callable object"""
21+
return ExtendedExecutor(
22+
whiteboard_tag=whiteboard_tag, jira_project_key=jira_project_key, **kwargs
23+
)
24+
25+
26+
class ExtendedExecutor(DefaultExecutor):
27+
"""Callable class that encapsulates the extended action."""
28+
29+
def __init__(self, **kwargs):
30+
"""Initialize ExtendedExecutor Object"""
31+
super().__init__(**kwargs)
32+
self.status_map = kwargs.get("status_map", {})
33+
34+
def jira_comments_for_update(
35+
self,
36+
payload: BugzillaWebhookRequest,
37+
):
38+
"""Returns the comments to post to Jira for a changed bug"""
39+
return payload.map_as_comments(
40+
status_log_enabled=False, assignee_log_enabled=False
41+
)
42+
43+
def update_issue(
44+
self,
45+
payload: BugzillaWebhookRequest,
46+
bug_obj: BugzillaBug,
47+
linked_issue_key: str,
48+
is_new: bool,
49+
):
50+
changed_fields = payload.event.changed_fields() or []
51+
52+
log_context = {
53+
"bug": {
54+
"id": bug_obj.id,
55+
"status": bug_obj.status,
56+
"resolution": bug_obj.resolution,
57+
"assigned_to": bug_obj.assigned_to,
58+
},
59+
"jira": linked_issue_key,
60+
"changed_fields": changed_fields,
61+
}
62+
63+
def clear_assignee():
64+
# New tickets already have no assignee.
65+
if not is_new:
66+
logger.debug("Clearing assignee", extra=log_context)
67+
self.jira_client.update_issue_field(
68+
key=linked_issue_key, fields={"assignee": None}
69+
)
70+
71+
# If this is a new issue or if the bug's assignee has changed then
72+
# update the assignee.
73+
if is_new or "assigned_to" in changed_fields:
74+
if bug_obj.assigned_to == "nobody@mozilla.org":
75+
clear_assignee()
76+
else:
77+
logger.debug(
78+
"Attempting to update assignee to %s",
79+
bug_obj.assigned_to,
80+
extra=log_context,
81+
)
82+
# Look up this user in Jira
83+
users = self.jira_client.user_find_by_user_string(
84+
query=bug_obj.assigned_to
85+
)
86+
if len(users) == 1:
87+
try:
88+
# There doesn't appear to be an easy way to verify that
89+
# this user can be assigned to this issue, so just try
90+
# and do it.
91+
self.jira_client.update_issue_field(
92+
key=linked_issue_key,
93+
fields={"assignee": {"accountId": users[0]["accountId"]}},
94+
)
95+
except IOError as exception:
96+
logger.debug(
97+
"Setting assignee failed: %s", exception, extra=log_context
98+
)
99+
# If that failed then just fall back to clearing the
100+
# assignee.
101+
clear_assignee()
102+
103+
# If this is a new issue or if the bug's status or resolution has
104+
# changed then update the assignee.
105+
if is_new or "status" in changed_fields or "resolution" in changed_fields:
106+
# We use resolution if one exists or status otherwise.
107+
status = bug_obj.resolution
108+
if status == "":
109+
status = bug_obj.status
110+
111+
if status in self.status_map:
112+
logger.debug(
113+
"Updating Jira status to %s",
114+
self.status_map[status],
115+
extra=log_context,
116+
)
117+
self.jira_client.set_issue_status(
118+
linked_issue_key, self.status_map[status]
119+
)

0 commit comments

Comments
 (0)