Skip to content

Commit c8e5252

Browse files
committed
#120 - Change latest tag selection from time to semantic
- Replaced time ordering by semantic one when no from tag defined.
1 parent 669302e commit c8e5252

File tree

4 files changed

+146
-11
lines changed

4 files changed

+146
-11
lines changed

release_notes_generator/generator.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,22 @@
2121

2222
import logging
2323
import sys
24-
2524
from typing import Optional
25+
import semver
26+
2627
from github import Github
2728
from github.GitRelease import GitRelease
2829
from github.Repository import Repository
2930

31+
from release_notes_generator.action_inputs import ActionInputs
32+
from release_notes_generator.builder import ReleaseNotesBuilder
3033
from release_notes_generator.model.custom_chapters import CustomChapters
3134
from release_notes_generator.model.record import Record
32-
from release_notes_generator.builder import ReleaseNotesBuilder
3335
from release_notes_generator.record.record_factory import RecordFactory
34-
from release_notes_generator.action_inputs import ActionInputs
3536
from release_notes_generator.utils.constants import ISSUE_STATE_ALL
36-
3737
from release_notes_generator.utils.decorators import safe_call_decorator
38-
from release_notes_generator.utils.utils import get_change_url
3938
from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter
39+
from release_notes_generator.utils.utils import get_change_url
4040

4141
logger = logging.getLogger(__name__)
4242

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

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

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

160162
return rls
163+
164+
def __get_latest_semantic_release(self, releases) -> Optional[GitRelease]:
165+
published_releases = [release for release in releases if not release.draft and not release.prerelease]
166+
latest_version: Optional[semver.Version] = None
167+
rls: Optional[GitRelease] = None
168+
169+
for release in published_releases:
170+
try:
171+
version_str = release.tag_name.lstrip("v")
172+
current_version: Optional[semver.Version] = semver.VersionInfo.parse(version_str)
173+
except ValueError:
174+
logger.debug("Skipping invalid value of version tag: %s", release.tag_name)
175+
continue
176+
except TypeError:
177+
logger.debug("Skipping invalid type of version tag: %s", release.tag_name)
178+
continue
179+
180+
if latest_version is None or current_version > latest_version:
181+
latest_version = current_version
182+
rls = release
183+
184+
return rls

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ PyGithub==1.59.0
55
pylint==3.2.6
66
requests==2.31.0
77
black==24.8.0
8+
semver==3.0.2

tests/conftest.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import pytest
2222

2323
from github import Github
24+
from github.GitRelease import GitRelease
2425
from github.Issue import Issue
2526
from github.PullRequest import PullRequest
2627
from github.Rate import Rate
@@ -83,11 +84,24 @@ def mock_repo(mocker):
8384
# Fixtures for GitHub Release(s)
8485
@pytest.fixture
8586
def mock_git_release(mocker):
86-
release = mocker.Mock()
87+
release = mocker.Mock(spec=GitRelease)
8788
release.tag_name = "v1.0.0"
8889
return release
8990

9091

92+
@pytest.fixture
93+
def mock_git_releases(mocker):
94+
release_1 = mocker.Mock(spec=GitRelease)
95+
release_1.tag_name = "v1.0.0"
96+
release_1.draft = False
97+
release_1.prerelease = False
98+
release_2 = mocker.Mock(spec=GitRelease)
99+
release_2.tag_name = "v2.0.0"
100+
release_2.draft = False
101+
release_2.prerelease = False
102+
return [release_1, release_2]
103+
104+
91105
@pytest.fixture
92106
def rate_limiter(mocker, request):
93107
mock_github_client = mocker.Mock(spec=Github)

tests/test_release_notes_generator.py

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def test_generate_release_notes_latest_release_not_found(
6666
mock_pull_closed_with_rls_notes_101.merged_at = mock_repo.created_at + timedelta(days=2)
6767
mock_pull_closed_with_rls_notes_102.merged_at = mock_repo.created_at + timedelta(days=7)
6868

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

7171
mock_rate_limit = mocker.Mock()
7272
mock_rate_limit.core.remaining = 1000
@@ -108,9 +108,9 @@ def test_generate_release_notes_latest_release_found_by_created_at(
108108
mock_pull_closed_with_rls_notes_101.merged_at = mock_repo.created_at + timedelta(days=2)
109109
mock_pull_closed_with_rls_notes_102.merged_at = mock_repo.created_at + timedelta(days=7)
110110

111-
github_mock.get_repo().get_latest_release.return_value = mock_git_release
112111
mock_git_release.created_at = mock_repo.created_at + timedelta(days=5)
113112
mock_git_release.published_at = mock_repo.created_at + timedelta(days=5)
113+
mocker.patch("release_notes_generator.generator.ReleaseNotesGenerator.get_latest_release", return_value=mock_git_release)
114114

115115
mock_rate_limit = mocker.Mock()
116116
mock_rate_limit.core.remaining = 1000
@@ -156,9 +156,9 @@ def test_generate_release_notes_latest_release_found_by_published_at(
156156
mock_pull_closed_with_rls_notes_101.merged_at = mock_repo.created_at + timedelta(days=2)
157157
mock_pull_closed_with_rls_notes_102.merged_at = mock_repo.created_at + timedelta(days=7)
158158

159-
github_mock.get_repo().get_latest_release.return_value = mock_git_release
160159
mock_git_release.created_at = mock_repo.created_at + timedelta(days=5)
161160
mock_git_release.published_at = mock_repo.created_at + timedelta(days=5)
161+
mocker.patch("release_notes_generator.generator.ReleaseNotesGenerator.get_latest_release", return_value=mock_git_release)
162162

163163
mock_rate_limit = mocker.Mock()
164164
mock_rate_limit.core.remaining = 1000
@@ -201,6 +201,102 @@ def test_get_latest_release_from_tag_name_defined_no_release(mocker, mock_repo):
201201
assert ('Latest release not found for received tag %s. Ending!', '') == mock_log_info.call_args_list[1][0]
202202

203203

204+
def test_get_latest_release_from_tag_name_not_defined_no_release(mocker, mock_repo):
205+
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
206+
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")
207+
208+
github_mock = mocker.Mock(spec=Github)
209+
github_mock.get_repo.return_value = mock_repo
210+
211+
mock_repo.get_releases.return_value = []
212+
213+
mock_rate_limit = mocker.Mock()
214+
mock_rate_limit.core.remaining = 1000
215+
github_mock.get_rate_limit.return_value = mock_rate_limit
216+
217+
release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))
218+
219+
latest_release = release_notes_generator.get_latest_release(mock_repo)
220+
221+
assert latest_release is None
222+
assert mock_log_info.called_with(2)
223+
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
224+
assert ('Latest release not found for %s. 1st release for repository!', 'org/repo') == mock_log_info.call_args_list[1][0]
225+
226+
227+
def test_get_latest_release_from_tag_name_not_defined_2_releases(mocker, mock_repo, mock_git_releases):
228+
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
229+
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")
230+
231+
github_mock = mocker.Mock(spec=Github)
232+
github_mock.get_repo.return_value = mock_repo
233+
234+
mock_repo.get_releases.return_value = mock_git_releases
235+
236+
mock_rate_limit = mocker.Mock()
237+
mock_rate_limit.core.remaining = 1000
238+
github_mock.get_rate_limit.return_value = mock_rate_limit
239+
240+
release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))
241+
242+
latest_release = release_notes_generator.get_latest_release(mock_repo)
243+
244+
assert latest_release is not None
245+
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
246+
247+
248+
def test_get_latest_release_from_tag_name_not_defined_2_releases_value_error(mocker, mock_repo, mock_git_releases):
249+
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
250+
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")
251+
mock_log_debug = mocker.patch("release_notes_generator.generator.logger.debug")
252+
253+
github_mock = mocker.Mock(spec=Github)
254+
github_mock.get_repo.return_value = mock_repo
255+
256+
mock_repo.get_releases.return_value = mock_git_releases
257+
258+
mock_rate_limit = mocker.Mock()
259+
mock_rate_limit.core.remaining = 1000
260+
github_mock.get_rate_limit.return_value = mock_rate_limit
261+
262+
release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))
263+
mocker.patch("semver.Version.parse", side_effect=ValueError)
264+
265+
latest_release = release_notes_generator.get_latest_release(mock_repo)
266+
267+
assert latest_release is None
268+
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
269+
assert ('Latest release not found for %s. 1st release for repository!', 'org/repo') == mock_log_info.call_args_list[1][0]
270+
assert ('Skipping invalid value of version tag: %s', 'v1.0.0') == mock_log_debug.call_args_list[0][0]
271+
assert ('Skipping invalid value of version tag: %s', 'v2.0.0') == mock_log_debug.call_args_list[1][0]
272+
273+
274+
def test_get_latest_release_from_tag_name_not_defined_2_releases_type_error(mocker, mock_repo, mock_git_releases):
275+
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
276+
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")
277+
mock_log_debug = mocker.patch("release_notes_generator.generator.logger.debug")
278+
279+
github_mock = mocker.Mock(spec=Github)
280+
github_mock.get_repo.return_value = mock_repo
281+
282+
mock_repo.get_releases.return_value = mock_git_releases
283+
284+
mock_rate_limit = mocker.Mock()
285+
mock_rate_limit.core.remaining = 1000
286+
github_mock.get_rate_limit.return_value = mock_rate_limit
287+
288+
release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))
289+
mocker.patch("semver.Version.parse", side_effect=TypeError)
290+
291+
latest_release = release_notes_generator.get_latest_release(mock_repo)
292+
293+
assert latest_release is None
294+
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
295+
assert ('Latest release not found for %s. 1st release for repository!', 'org/repo') == mock_log_info.call_args_list[1][0]
296+
assert ('Skipping invalid type of version tag: %s', 'v1.0.0') == mock_log_debug.call_args_list[0][0]
297+
assert ('Skipping invalid type of version tag: %s', 'v2.0.0') == mock_log_debug.call_args_list[1][0]
298+
299+
204300
def test_get_latest_release_from_tag_name_defined_release_exists(mocker, mock_repo):
205301
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=True)
206302
mock_exit = mocker.patch("sys.exit")

0 commit comments

Comments
 (0)