Skip to content

Commit

Permalink
Merge pull request #16 from tarurar:tarurar/issue4
Browse files Browse the repository at this point in the history
Tarurar/issue4
  • Loading branch information
tarurar authored Nov 25, 2022
2 parents 5ea4776 + 3bf53e3 commit 3886a47
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 30 deletions.
63 changes: 61 additions & 2 deletions core/nova_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,56 @@
Nova component module
"""

from core.nova_task import NovaTask

from .cvs import CodeRepository
from .nova_status import Status


def get_release_notes_md(
revision_from: str,
revision_to: str,
repo_url: str,
tasks: list[NovaTask]) -> str:
"""Returns release notes for component tasks in markdown format"""
header = '## What\'s changed'

task_notes = [('* ' + task.get_release_notes()) for task in tasks]

change_log_url = get_changelog_url(revision_from, revision_to, repo_url)
change_log = f'**Full change log**: {change_log_url}'

result = [header, *task_notes, '\n', change_log]

return '\n'.join(result)


def get_changelog_url(
revision_from: str,
revision_to: str,
repo_url: str) -> str:
"""Returns changelog url
Revision from should be less than revision to
GitHub format: https://github.com/LykkeBusiness/MT/compare/v2.20.2...v2.21.1
"""
if revision_from is None or revision_to is None or repo_url is None:
return ''

revision_from = revision_from.strip().lower()
revision_to = revision_to.strip().lower()
repo_url = repo_url.strip().lower().rstrip('/')

if not revision_from or not revision_to:
return ''
if not repo_url:
return ''
if revision_from >= revision_to:
return ''

result = f'{repo_url}/compare/{revision_from}...{revision_to}'
return result


class NovaComponent:
"""
Represents Nova component registered in Jira
Expand Down Expand Up @@ -64,9 +110,13 @@ def describe_status(self) -> str:
f' | {tasks_count:>3} tasks'
return description

def get_release_notes(self) -> str:
def get_release_notes(self, revision_from, revision_to) -> str:
"""Returns release notes for component"""
return 'Release notes'
return get_release_notes_md(
revision_from,
revision_to,
self.repo.url,
self.tasks)


class NovaEmptyComponent(NovaComponent):
Expand All @@ -77,6 +127,15 @@ class NovaEmptyComponent(NovaComponent):
"""

default_component_name = 'n/a'
component_names = [default_component_name, 'multiple components']

def __init__(self):
super().__init__(NovaEmptyComponent.default_component_name, None)

@classmethod
def parse(cls, component_name: str):
"""Parses component name"""
normalized = component_name.strip().lower()
if normalized in NovaEmptyComponent.component_names:
return NovaEmptyComponent()
return None
2 changes: 1 addition & 1 deletion core/nova_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def __init__(self, project, version, delivery):
self.version = version
self.delivery = delivery
self.project = project
self.components = []
self.components: list[NovaComponent] = []

def __str__(self):
return 'Nova ' + str(self.version) + ". Delivery " + str(self.delivery)
Expand Down
26 changes: 23 additions & 3 deletions core/nova_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
class NovaTask:
"""Nova task"""

@staticmethod
def map_jira_issue_status(status):
@classmethod
def map_jira_issue_status(cls, status):
"""Maps Jira issue status to Nova task status"""
match status:
case 'Selected For Release':
Expand All @@ -26,9 +26,15 @@ def map_jira_issue_status(status):
case _:
return Status.UNDEFINED

def __init__(self, name, status):
def __init__(self, name: str, status: Status, summary: str = ''):
if not name:
raise ValueError('Task name is not defined')
if status is None:
raise ValueError('Task status is not defined')

self._name = name
self._status = status
self._summary = summary

@property
def status(self):
Expand All @@ -39,3 +45,17 @@ def status(self):
def name(self):
"""Task name"""
return self._name

@property
def summary(self):
"""Task summary"""
return self._summary

def get_release_notes(self) -> str:
"""Returns release notes for task"""
key = self._name.strip().upper()
summary = self._summary.split(
']')[-1].strip().lstrip('[').rstrip('.').strip().capitalize()
ending = '' if summary.endswith('.') else '.'

return f'{key}: {summary}{ending}'
4 changes: 2 additions & 2 deletions jira_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ class JiraService:
ready_for_release_status = 'Selected For Release'
done_status = 'Done'

@staticmethod
def get_issue_component(issue):
@classmethod
def get_issue_component(cls, issue):
"""Returns JIRA issue component name"""
try:
return issue.fields.components[0].name
Expand Down
16 changes: 10 additions & 6 deletions jira_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

from typing import Optional
from urllib.parse import urlparse

import validators
from jira.resources import Issue

from core.cvs import GitCloudService, CodeRepository
from core.nova_task import NovaTask
from core.cvs import CodeRepository, GitCloudService
from core.nova_component import NovaComponent, NovaEmptyComponent
from core.nova_status import Status
from core.nova_task import NovaTask


def parse_jira_cmp_descr(descr: str) -> tuple[
Expand Down Expand Up @@ -52,7 +54,7 @@ def build_jql(project: str, fix_version='', component='') -> str:
return jql


def parse_jira_issue(issue: object) -> NovaTask:
def parse_jira_issue(issue: Issue) -> NovaTask:
"""
Parse Jira issue.
"""
Expand All @@ -73,7 +75,7 @@ def parse_jira_issue(issue: object) -> NovaTask:
raise ValueError(
f'[{issue.key}] has invalid status [{issue.fields.status.name}]')

return NovaTask(issue.key, status)
return NovaTask(issue.key, status, issue.fields.summary)


def parse_jira_component(cmp: object) -> NovaComponent:
Expand All @@ -86,8 +88,10 @@ def parse_jira_component(cmp: object) -> NovaComponent:
raise ValueError('Component has no name')
if cmp.name is None:
raise ValueError('Component name is empty')
if cmp.name.strip().lower() == NovaEmptyComponent.default_component_name:
return NovaEmptyComponent()

empty_component = NovaEmptyComponent.parse(cmp.name)
if empty_component is not None:
return empty_component
if not hasattr(cmp, 'description'):
raise ValueError(f'Component [{cmp.name}] has no description')
if cmp.description is None or cmp.description.strip() == '':
Expand Down
2 changes: 1 addition & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def choose_component_from_release(rel: NovaRelease) -> NovaComponent:
"""Choose component from release"""
print('\n')
component_name = input(
'Please, select component to release or press \'q\': ')
'Please, select component to release or press \'q\' (you can specify name partially): ')
if component_name == 'q':
return None
cmp = rel.get_component_by_name(component_name)
Expand Down
46 changes: 37 additions & 9 deletions release_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
from git import Tag
from github import Github
from jira import JIRA, JIRAError
from core.nova_status import Status

import github_utils as gu
import jira_utils as ju
from core.cvs import GitCloudService
from core.nova_component import NovaComponent
from core.nova_release import NovaRelease
from core.nova_status import Status


class ReleaseManager:
Expand Down Expand Up @@ -85,26 +85,54 @@ def release_component(self, release: NovaRelease, component: NovaComponent):
if repo is None:
raise Exception(f'Cannot get repository {component.repo.url}')

# this call return a list of Tag class instances (not GitTag class instances)
tags = repo.get_tags()
tag = ReleaseManager.choose_existing_tag(list(tags)[:5])
top5_tags = list(tags)[:5]
print(f'Please, choose a tag to release component [{component.name}]')
tag = ReleaseManager.choose_existing_tag(top5_tags)
if tag is None:
tag_name = ReleaseManager.input_tag_name()
if tag_name is None:
return
sha = repo.get_branch('master').commit.sha
git_tag = repo.create_git_tag(
tag_name, release.get_title(), sha, 'commit')
git_tag_name = repo.create_git_tag(
tag_name, release.get_title(), sha, 'commit').tag
else:
git_tag = tag
git_tag_name = tag.name

print(
f'Please, choose a tag of previous component [{component.name}] release')
previous_tag = ReleaseManager.choose_existing_tag(top5_tags)
if previous_tag is None:
logging.warning(
'Previous release tag was not specified, auto-detection will be used')
# TODO: what if we do not have items in the list after filtration?
auto_detected_previous_tag = list(filter(
lambda t: t.name != git_tag_name, top5_tags))[0]
previous_tag_name = auto_detected_previous_tag.name
logging.info('Auto-detected previous release tag: %s',
previous_tag_name)
else:
previous_tag_name = previous_tag.name

git_release = repo.create_git_release(
git_tag.tag, release.get_title(), component.get_release_notes())
git_tag_name,
release.get_title(),
component.get_release_notes(previous_tag_name, git_tag_name))
if git_release is None:
raise Exception(f'Could not create release for tag {git_tag.tag}')
raise Exception(f'Could not create release for tag {git_tag_name}')

for task in component.tasks:
self.__j.transition_issue(
task.name, 'Done', comment=f'{release.get_title()} released')
try:
self.__j.transition_issue(
task.name,
'Done',
comment=f'{release.get_title()} released')
except JIRAError as error:
logging.warning(
'Could not transition issue %s due to error: %s',
task.name,
error.text)

@classmethod
def input_tag_name(cls) -> str:
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
GitPython==3.1.29
jira==3.4.1
PyGithub==1.56
PyGithub==1.57
pytest==7.2.0
validators==0.20.0
7 changes: 4 additions & 3 deletions tests/test_jira_issues_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ def __init__(self, name):
class MockFields:
""" Mock Jira issue fields"""

def __init__(self, components: list, status_name: str):
def __init__(self, components: list, status_name: str, summary: str):
self.components = list(map(MockComponent, components))
self.status = MockStatus(status_name)
self.summary = summary


class MockStatus:
Expand All @@ -30,8 +31,8 @@ def __init__(self, name: str):
class MockIssue:
""" Mock Jira issue """

def __init__(self, key: str, components: list, status_name: str):
self.fields = MockFields(components, status_name)
def __init__(self, key: str, components: list, status_name: str, summary: str = ''):
self.fields = MockFields(components, status_name, summary)
self.key = key


Expand Down
Loading

0 comments on commit 3886a47

Please sign in to comment.