Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/120 change latest tag selection from time to semantic #124

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]
Loading