Skip to content

Commit

Permalink
Feature/120 change latest tag selection from time to semantic (#124)
Browse files Browse the repository at this point in the history
* #119 - Add ability to define RLS from which to detect changes
- Implemented new inpout to define from tag.
- Newly user is able to control start point for Release Notes generation.
  • Loading branch information
miroslavpojer authored Dec 9, 2024
1 parent 7a51e9f commit 303a57a
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 9 deletions.
37 changes: 31 additions & 6 deletions release_notes_generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,21 @@
import sys

from typing import Optional
import semver

from github import Github
from github.GitRelease import GitRelease
from github.Repository import Repository

from release_notes_generator.action_inputs import ActionInputs
from release_notes_generator.builder import ReleaseNotesBuilder
from release_notes_generator.model.custom_chapters import CustomChapters
from release_notes_generator.model.record import Record
from release_notes_generator.builder import ReleaseNotesBuilder
from release_notes_generator.record.record_factory import RecordFactory
from release_notes_generator.action_inputs import ActionInputs
from release_notes_generator.utils.constants import ISSUE_STATE_ALL

from release_notes_generator.utils.decorators import safe_call_decorator
from release_notes_generator.utils.utils import get_change_url
from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter
from release_notes_generator.utils.utils import get_change_url

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -134,6 +135,7 @@ def get_latest_release(self, repo: Repository) -> Optional[GitRelease]:
@param repo: The repository to get the latest release from.
@return: The latest release of the repository, or None if no releases are found.
"""
# check if from-tag name is defined
if ActionInputs.is_from_tag_name_defined():
logger.info("Getting latest release by from-tag name %s", ActionInputs.get_tag_name())
rls: GitRelease = self._safe_call(repo.get_release)(ActionInputs.get_from_tag_name())
Expand All @@ -143,8 +145,9 @@ def get_latest_release(self, repo: Repository) -> Optional[GitRelease]:
sys.exit(1)

else:
logger.info("Getting latest release by time.")
rls: GitRelease = self._safe_call(repo.get_latest_release)()
logger.info("Getting latest release by semantic ordering (could not be the last one by time).")
gh_releases: list = list(self._safe_call(repo.get_releases)())
rls: GitRelease = self.__get_latest_semantic_release(gh_releases)

if rls is None:
logger.info("Latest release not found for %s. 1st release for repository!", repo.full_name)
Expand All @@ -158,3 +161,25 @@ def get_latest_release(self, repo: Repository) -> Optional[GitRelease]:
)

return rls

def __get_latest_semantic_release(self, releases) -> Optional[GitRelease]:
published_releases = [release for release in releases if not release.draft and not release.prerelease]
latest_version: Optional[semver.Version] = None
rls: Optional[GitRelease] = None

for release in published_releases:
try:
version_str = release.tag_name.lstrip("v")
current_version: Optional[semver.Version] = semver.VersionInfo.parse(version_str)
except ValueError:
logger.debug("Skipping invalid value of version tag: %s", release.tag_name)
continue
except TypeError:
logger.debug("Skipping invalid type of version tag: %s", release.tag_name)
continue

if latest_version is None or current_version > latest_version:
latest_version = current_version
rls = release

return rls
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ PyGithub==1.59.0
pylint==3.2.6
requests==2.31.0
black==24.8.0
semver==3.0.2
16 changes: 15 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import pytest

from github import Github
from github.GitRelease import GitRelease
from github.Issue import Issue
from github.PullRequest import PullRequest
from github.Rate import Rate
Expand Down Expand Up @@ -83,11 +84,24 @@ def mock_repo(mocker):
# Fixtures for GitHub Release(s)
@pytest.fixture
def mock_git_release(mocker):
release = mocker.Mock()
release = mocker.Mock(spec=GitRelease)
release.tag_name = "v1.0.0"
return release


@pytest.fixture
def mock_git_releases(mocker):
release_1 = mocker.Mock(spec=GitRelease)
release_1.tag_name = "v1.0.0"
release_1.draft = False
release_1.prerelease = False
release_2 = mocker.Mock(spec=GitRelease)
release_2.tag_name = "v2.0.0"
release_2.draft = False
release_2.prerelease = False
return [release_1, release_2]


@pytest.fixture
def rate_limiter(mocker, request):
mock_github_client = mocker.Mock(spec=Github)
Expand Down
101 changes: 99 additions & 2 deletions tests/test_release_notes_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_generate_release_notes_latest_release_not_found(
mock_pull_closed_with_rls_notes_101.merged_at = mock_repo.created_at + timedelta(days=2)
mock_pull_closed_with_rls_notes_102.merged_at = mock_repo.created_at + timedelta(days=7)

github_mock.get_repo().get_latest_release.return_value = None
mocker.patch("release_notes_generator.generator.ReleaseNotesGenerator.get_latest_release", return_value=None)

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
Expand Down Expand Up @@ -107,9 +107,9 @@ def test_generate_release_notes_latest_release_found_by_created_at(
mock_pull_closed_with_rls_notes_101.merged_at = mock_repo.created_at + timedelta(days=2)
mock_pull_closed_with_rls_notes_102.merged_at = mock_repo.created_at + timedelta(days=7)

github_mock.get_repo().get_latest_release.return_value = mock_git_release
mock_git_release.created_at = mock_repo.created_at + timedelta(days=5)
mock_git_release.published_at = mock_repo.created_at + timedelta(days=5)
mocker.patch("release_notes_generator.generator.ReleaseNotesGenerator.get_latest_release", return_value=mock_git_release)

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
Expand Down Expand Up @@ -158,6 +158,7 @@ def test_generate_release_notes_latest_release_found_by_published_at(
github_mock.get_repo().get_latest_release.return_value = mock_git_release
mock_git_release.created_at = mock_repo.created_at + timedelta(days=5)
mock_git_release.published_at = mock_repo.created_at + timedelta(days=5)
mocker.patch("release_notes_generator.generator.ReleaseNotesGenerator.get_latest_release", return_value=mock_git_release)

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
Expand Down Expand Up @@ -223,3 +224,99 @@ def test_get_latest_release_from_tag_name_defined_release_exists(mocker, mock_re
mock_exit.assert_not_called()
assert mock_log_info.called_with(1)
assert ('Getting latest release by from-tag name %s', None) == mock_log_info.call_args_list[0][0]


def test_get_latest_release_from_tag_name_not_defined_no_release(mocker, mock_repo):
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")

github_mock = mocker.Mock(spec=Github)
github_mock.get_repo.return_value = mock_repo

mock_repo.get_releases.return_value = []

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
github_mock.get_rate_limit.return_value = mock_rate_limit

release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))

latest_release = release_notes_generator.get_latest_release(mock_repo)

assert latest_release is None
assert mock_log_info.called_with(2)
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
assert ('Latest release not found for %s. 1st release for repository!', 'org/repo') == mock_log_info.call_args_list[1][0]


def test_get_latest_release_from_tag_name_not_defined_2_releases(mocker, mock_repo, mock_git_releases):
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")

github_mock = mocker.Mock(spec=Github)
github_mock.get_repo.return_value = mock_repo

mock_repo.get_releases.return_value = mock_git_releases

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
github_mock.get_rate_limit.return_value = mock_rate_limit

release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))

latest_release = release_notes_generator.get_latest_release(mock_repo)

assert latest_release is not None
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]


def test_get_latest_release_from_tag_name_not_defined_2_releases_value_error(mocker, mock_repo, mock_git_releases):
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")
mock_log_debug = mocker.patch("release_notes_generator.generator.logger.debug")

github_mock = mocker.Mock(spec=Github)
github_mock.get_repo.return_value = mock_repo

mock_repo.get_releases.return_value = mock_git_releases

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
github_mock.get_rate_limit.return_value = mock_rate_limit

release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))
mocker.patch("semver.Version.parse", side_effect=ValueError)

latest_release = release_notes_generator.get_latest_release(mock_repo)

assert latest_release is None
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
assert ('Latest release not found for %s. 1st release for repository!', 'org/repo') == mock_log_info.call_args_list[1][0]
assert ('Skipping invalid value of version tag: %s', 'v1.0.0') == mock_log_debug.call_args_list[0][0]
assert ('Skipping invalid value of version tag: %s', 'v2.0.0') == mock_log_debug.call_args_list[1][0]


def test_get_latest_release_from_tag_name_not_defined_2_releases_type_error(mocker, mock_repo, mock_git_releases):
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")
mock_log_debug = mocker.patch("release_notes_generator.generator.logger.debug")

github_mock = mocker.Mock(spec=Github)
github_mock.get_repo.return_value = mock_repo

mock_repo.get_releases.return_value = mock_git_releases

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
github_mock.get_rate_limit.return_value = mock_rate_limit

release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))
mocker.patch("semver.Version.parse", side_effect=TypeError)

latest_release = release_notes_generator.get_latest_release(mock_repo)

assert latest_release is None
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
assert ('Latest release not found for %s. 1st release for repository!', 'org/repo') == mock_log_info.call_args_list[1][0]
assert ('Skipping invalid type of version tag: %s', 'v1.0.0') == mock_log_debug.call_args_list[0][0]
assert ('Skipping invalid type of version tag: %s', 'v2.0.0') == mock_log_debug.call_args_list[1][0]

0 comments on commit 303a57a

Please sign in to comment.