Skip to content

Commit

Permalink
Merge pull request #105 from csomh/apply-labels-on-close
Browse files Browse the repository at this point in the history
Add support to apply labels when upstream issue is closed
  • Loading branch information
Sid Premkumar authored May 18, 2020
2 parents 82e28ac + 68ccef1 commit 29b9e7e
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 18 deletions.
3 changes: 0 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ python:
- "3.7"
install:
- pip install tox
- pip install flake8
- pip install coveralls
jobs:
include:
- stage: Linting Tests
script: flake8 sync2jira --max-line-length=140
- stage: Unit Tests
script: tox
after_success:
Expand Down
4 changes: 3 additions & 1 deletion docs/source/config-file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ The config file is made up of multiple parts
* Sync title
* :code:`{'transition': True/'CUSTOM_TRANSITION'}`
* Sync status (open/closed), Sync only status/Attempt to transition JIRA ticket to CUSTOM_TRANSITION on upstream closure
* :code:`{'on_close': {'apply_lables': ['label', ...]}}`
* When the upstream issue is closed, apply additional labels on the corresponding Jira ticket.
* :code:`github_markdown`
* If description syncing is turned on, this flag will convert Github markdown to plaintext. This uses the pypandoc module.
* :code:`upstream_id`
Expand Down Expand Up @@ -182,4 +184,4 @@ The config file is made up of multiple parts
* :code:`tags`
* List of tags to look for
* :code:`milestone`
* Upstream milestone status
* Upstream milestone status
2 changes: 1 addition & 1 deletion sync2jira/confluence_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def update_stat_page(self, confluence_data):
if html_text.replace(" ", "") != page_html.replace(" ", ""):
self.update_page(self.page_id, html_text)
except: # noqa E722
log.exception(f"Something went wrong updating confluence!")
log.exception("Something went wrong updating confluence!")

def find_page(self):
""" finds the page with confluence_page_title in confluence_space
Expand Down
80 changes: 72 additions & 8 deletions sync2jira/downstream_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ def _update_jira_issue(existing, issue, client):
log.info("Updating information for upstream issue: %s" % issue.title)

# Get a list of what the user wants to update for the upstream issue
updates = issue.downstream.get('issue_updates', {})
updates = issue.downstream.get('issue_updates', [])

# Update relevant data if needed
# If the user has specified nothing
Expand Down Expand Up @@ -767,6 +767,10 @@ def _update_jira_issue(existing, issue, client):
log.info("Looking for new transition(s)")
_update_transition(client, existing, issue)

# Only execute 'on_close' events for listings that opt-in
log.info("Attempting to update downstream issue on upstream closed event")
_update_on_close(existing, issue, updates)

log.info('Done updating %s!' % issue.title)


Expand Down Expand Up @@ -1060,6 +1064,27 @@ def _update_assignee(client, existing, issue, updates):
confluence_client.update_stat_page(confluence_data)


def _update_jira_labels(issue, labels):
"""Update a Jira issue with 'labels'
Do this only if the current labels would change.
:param jira.resource.Issue issue: Jira issue to be updated
:param list<strings> labels: Lables to be applied on the issue
:returns: None
"""
_labels = sorted(labels)
if _labels == sorted(issue.fields.labels):
return

data = {'labels': _labels}
issue.update(data)
log.info('Updated %s tag(s)' % len(_labels))
if confluence_client.update_stat:
confluence_data = {'Tags': len(_labels)}
confluence_client.update_stat_page(confluence_data)


def _update_tags(updates, existing, issue):
"""
Helper function to sync tags between upstream issue and downstream JIRA issue.
Expand All @@ -1086,13 +1111,7 @@ def _update_tags(updates, existing, issue):
updated_labels = verify_tags(updated_labels)

# Now we can update the JIRA if labels are different
if sorted(updated_labels) != sorted(existing.fields.labels):
data = {'labels': updated_labels}
existing.update(data)
log.info('Updated %s tag(s)' % len(updated_labels))
if confluence_client.update_stat:
confluence_data = {'Tags': len(updated_labels)}
confluence_client.update_stat_page(confluence_data)
_update_jira_labels(existing, updated_labels)


def _update_description(existing, issue):
Expand Down Expand Up @@ -1179,6 +1198,51 @@ def _update_description(existing, issue):
confluence_client.update_stat_page(confluence_data)


def _update_on_close(existing, issue, updates):
"""Update downstream Jira issue when upstream issue was closed
Example update configuration:
[
...,
{
"on_close": {
{
"apply_labels": [
"closed-upstream"
]
}
}
},
...
]
:param jira.resource.Issue existing: existing Jira issue
:param sync2jira.intermediary.Issue issue: Upstream issue
:param dict updates: update configuration
:return: None
"""
on_close_updates = None
for item in updates:
if 'on_close' in item:
on_close_updates = item['on_close']
break

if not on_close_updates:
return

if issue.status != 'Closed':
return

if 'apply_labels' not in on_close_updates:
return

updated_labels = list(
set(existing.fields.labels).union(set(on_close_updates['apply_labels']))
)
log.info("Applying 'on_close' labels to downstrem Jira issue")
_update_jira_labels(existing, updated_labels)


def verify_tags(tags):
"""
Helper function to ensure tag are JIRA ready :).
Expand Down
79 changes: 75 additions & 4 deletions tests/test_downstream_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ def setUp(self):
{'tags': {'overwrite': False}},
{'fixVersion': {'overwrite': False}},
{'assignee': {'overwrite': True}}, 'description', 'title',
{'transition': 'CUSTOM TRANSITION'}
{'transition': 'CUSTOM TRANSITION'},
{'on_close': {"apply_labels": ["closed-upstream"]}}
],
'owner': 'mock_owner'
}
Expand All @@ -79,6 +80,7 @@ def setUp(self):
{'fixVersion': {'overwrite': False}},
{'assignee': {'overwrite': True}}, 'description', 'title',
{'transition': 'CUSTOM TRANSITION'},
{'on_close': {"apply_labels": ["closed-upstream"]}}
]

# Mock Jira transition
Expand Down Expand Up @@ -674,9 +676,11 @@ def test_sync_with_jira_no_matching(self,
@mock.patch(PATH + '_update_fixVersion')
@mock.patch(PATH + '_update_transition')
@mock.patch(PATH + '_update_assignee')
@mock.patch(PATH + '_update_on_close')
@mock.patch('jira.client.JIRA')
def test_update_jira_issue(self,
mock_client,
mock_update_on_close,
mock_update_assignee,
mock_update_transition,
mock_update_fixVersion,
Expand Down Expand Up @@ -724,6 +728,7 @@ def test_update_jira_issue(self,
self.mock_downstream,
self.mock_issue
)
mock_update_on_close.assert_called_once()

@mock.patch(PATH + 'confluence_client')
@mock.patch('jira.client.JIRA')
Expand Down Expand Up @@ -1009,7 +1014,7 @@ def test_update_tags(self,
"""
# Set up return values
mock_label_matching.return_value = 'mock_updated_labels'
mock_verify_tags.return_value = 'mock_verified_tags'
mock_verify_tags.return_value = ['mock_verified_tags']
mock_confluence_client.update_stat = True

# Call the function
Expand All @@ -1025,8 +1030,8 @@ def test_update_tags(self,
self.mock_downstream.fields.labels
)
mock_verify_tags.assert_called_with('mock_updated_labels')
self.mock_downstream.update.assert_called_with({'labels': 'mock_verified_tags'})
mock_confluence_client.update_stat_page.assert_called_with({'Tags': 18})
self.mock_downstream.update.assert_called_with({'labels': ['mock_verified_tags']})
mock_confluence_client.update_stat_page.assert_called_with({'Tags': 1})

@mock.patch(PATH + 'verify_tags')
@mock.patch(PATH + '_label_matching')
Expand Down Expand Up @@ -1705,3 +1710,69 @@ def test_update_url_update(self,
self.mock_downstream.update.assert_called_with(
{'description':
f"\nUpstream URL: {self.mock_issue.url}\n"})

@mock.patch(PATH + 'confluence_client')
def test_update_on_close_update(self,
mock_confluence_client):
"""
This function tests '_update_on_close' where there is an
"apply_labels" configuration, and labels need to be updated.
"""
# Set up return values
mock_confluence_client.update_stat = True
self.mock_downstream.fields.description = ""
self.mock_issue.status = 'Closed'
updates = [{"on_close": {"apply_labels": ["closed-upstream"]}}]

# Call the function
d._update_on_close(self.mock_downstream, self.mock_issue, updates)

# Assert everything was called correctly
self.mock_downstream.update.assert_called_with(
{'labels':
["closed-upstream", "tag3", "tag4"]})

def test_update_on_close_no_change(self):
"""
This function tests '_update_on_close' where there is an
"apply_labels" configuration but there is no update required.
"""
# Set up return values
self.mock_issue.status = 'Closed'
updates = [{"on_close": {"apply_labels": ["tag4"]}}]

# Call the function
d._update_on_close(self.mock_downstream, self.mock_issue, updates)

# Assert everything was called correctly
self.mock_downstream.update.assert_not_called()

def test_update_on_close_no_action(self):
"""
This function tests '_update_on_close' where there is no
"apply_labels" configuration.
"""
# Set up return values
self.mock_issue.status = 'Closed'
updates = [{"on_close": {"some_other_action": None}}]

# Call the function
d._update_on_close(self.mock_downstream, self.mock_issue, updates)

# Assert everything was called correctly
self.mock_downstream.update.assert_not_called()

def test_update_on_close_no_config(self):
"""
This function tests '_update_on_close' where there is no
configuration for close events.
"""
# Set up return values
self.mock_issue.status = 'Closed'
updates = ["description"]

# Call the function
d._update_on_close(self.mock_downstream, self.mock_issue, updates)

# Assert everything was called correctly
self.mock_downstream.update.assert_not_called()
10 changes: 9 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py37
envlist = py37,lint

[testenv]
passenv = TRAVIS TRAVIS_*
Expand All @@ -21,3 +21,11 @@ sitepackages = False
commands =
coverage run -m pytest {posargs} --ignore=tests/integration_tests
# Add the following line locally to get an HTML report --cov-report html:htmlcov-py37

[testenv:lint]
skip_install = true
basepython = python3.7
deps =
flake8
commands =
flake8 sync2jira --max-line-length=140

0 comments on commit 29b9e7e

Please sign in to comment.