Skip to content

Commit 303a57a

Browse files
Feature/120 change latest tag selection from time to semantic (#124)
* #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.
1 parent 7a51e9f commit 303a57a

File tree

4 files changed

+146
-9
lines changed

4 files changed

+146
-9
lines changed

release_notes_generator/generator.py

+31-6
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,21 @@
2323
import sys
2424

2525
from typing import Optional
26+
import semver
27+
2628
from github import Github
2729
from github.GitRelease import GitRelease
2830
from github.Repository import Repository
2931

32+
from release_notes_generator.action_inputs import ActionInputs
33+
from release_notes_generator.builder import ReleaseNotesBuilder
3034
from release_notes_generator.model.custom_chapters import CustomChapters
3135
from release_notes_generator.model.record import Record
32-
from release_notes_generator.builder import ReleaseNotesBuilder
3336
from release_notes_generator.record.record_factory import RecordFactory
34-
from release_notes_generator.action_inputs import ActionInputs
3537
from release_notes_generator.utils.constants import ISSUE_STATE_ALL
36-
3738
from release_notes_generator.utils.decorators import safe_call_decorator
38-
from release_notes_generator.utils.utils import get_change_url
3939
from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter
40+
from release_notes_generator.utils.utils import get_change_url
4041

4142
logger = logging.getLogger(__name__)
4243

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

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

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

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

requirements.txt

+1
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

+15-1
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

+99-2
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
@@ -107,9 +107,9 @@ def test_generate_release_notes_latest_release_found_by_created_at(
107107
mock_pull_closed_with_rls_notes_101.merged_at = mock_repo.created_at + timedelta(days=2)
108108
mock_pull_closed_with_rls_notes_102.merged_at = mock_repo.created_at + timedelta(days=7)
109109

110-
github_mock.get_repo().get_latest_release.return_value = mock_git_release
111110
mock_git_release.created_at = mock_repo.created_at + timedelta(days=5)
112111
mock_git_release.published_at = mock_repo.created_at + timedelta(days=5)
112+
mocker.patch("release_notes_generator.generator.ReleaseNotesGenerator.get_latest_release", return_value=mock_git_release)
113113

114114
mock_rate_limit = mocker.Mock()
115115
mock_rate_limit.core.remaining = 1000
@@ -158,6 +158,7 @@ def test_generate_release_notes_latest_release_found_by_published_at(
158158
github_mock.get_repo().get_latest_release.return_value = mock_git_release
159159
mock_git_release.created_at = mock_repo.created_at + timedelta(days=5)
160160
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)
161162

162163
mock_rate_limit = mocker.Mock()
163164
mock_rate_limit.core.remaining = 1000
@@ -223,3 +224,99 @@ def test_get_latest_release_from_tag_name_defined_release_exists(mocker, mock_re
223224
mock_exit.assert_not_called()
224225
assert mock_log_info.called_with(1)
225226
assert ('Getting latest release by from-tag name %s', None) == mock_log_info.call_args_list[0][0]
227+
228+
229+
def test_get_latest_release_from_tag_name_not_defined_no_release(mocker, mock_repo):
230+
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
231+
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")
232+
233+
github_mock = mocker.Mock(spec=Github)
234+
github_mock.get_repo.return_value = mock_repo
235+
236+
mock_repo.get_releases.return_value = []
237+
238+
mock_rate_limit = mocker.Mock()
239+
mock_rate_limit.core.remaining = 1000
240+
github_mock.get_rate_limit.return_value = mock_rate_limit
241+
242+
release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))
243+
244+
latest_release = release_notes_generator.get_latest_release(mock_repo)
245+
246+
assert latest_release is None
247+
assert mock_log_info.called_with(2)
248+
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
249+
assert ('Latest release not found for %s. 1st release for repository!', 'org/repo') == mock_log_info.call_args_list[1][0]
250+
251+
252+
def test_get_latest_release_from_tag_name_not_defined_2_releases(mocker, mock_repo, mock_git_releases):
253+
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
254+
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")
255+
256+
github_mock = mocker.Mock(spec=Github)
257+
github_mock.get_repo.return_value = mock_repo
258+
259+
mock_repo.get_releases.return_value = mock_git_releases
260+
261+
mock_rate_limit = mocker.Mock()
262+
mock_rate_limit.core.remaining = 1000
263+
github_mock.get_rate_limit.return_value = mock_rate_limit
264+
265+
release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))
266+
267+
latest_release = release_notes_generator.get_latest_release(mock_repo)
268+
269+
assert latest_release is not None
270+
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
271+
272+
273+
def test_get_latest_release_from_tag_name_not_defined_2_releases_value_error(mocker, mock_repo, mock_git_releases):
274+
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
275+
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")
276+
mock_log_debug = mocker.patch("release_notes_generator.generator.logger.debug")
277+
278+
github_mock = mocker.Mock(spec=Github)
279+
github_mock.get_repo.return_value = mock_repo
280+
281+
mock_repo.get_releases.return_value = mock_git_releases
282+
283+
mock_rate_limit = mocker.Mock()
284+
mock_rate_limit.core.remaining = 1000
285+
github_mock.get_rate_limit.return_value = mock_rate_limit
286+
287+
release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))
288+
mocker.patch("semver.Version.parse", side_effect=ValueError)
289+
290+
latest_release = release_notes_generator.get_latest_release(mock_repo)
291+
292+
assert latest_release is None
293+
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
294+
assert ('Latest release not found for %s. 1st release for repository!', 'org/repo') == mock_log_info.call_args_list[1][0]
295+
assert ('Skipping invalid value of version tag: %s', 'v1.0.0') == mock_log_debug.call_args_list[0][0]
296+
assert ('Skipping invalid value of version tag: %s', 'v2.0.0') == mock_log_debug.call_args_list[1][0]
297+
298+
299+
def test_get_latest_release_from_tag_name_not_defined_2_releases_type_error(mocker, mock_repo, mock_git_releases):
300+
mocker.patch("release_notes_generator.action_inputs.ActionInputs.is_from_tag_name_defined", return_value=False)
301+
mock_log_info = mocker.patch("release_notes_generator.generator.logger.info")
302+
mock_log_debug = mocker.patch("release_notes_generator.generator.logger.debug")
303+
304+
github_mock = mocker.Mock(spec=Github)
305+
github_mock.get_repo.return_value = mock_repo
306+
307+
mock_repo.get_releases.return_value = mock_git_releases
308+
309+
mock_rate_limit = mocker.Mock()
310+
mock_rate_limit.core.remaining = 1000
311+
github_mock.get_rate_limit.return_value = mock_rate_limit
312+
313+
release_notes_generator = ReleaseNotesGenerator(github_mock, CustomChapters(print_empty_chapters=True))
314+
mocker.patch("semver.Version.parse", side_effect=TypeError)
315+
316+
latest_release = release_notes_generator.get_latest_release(mock_repo)
317+
318+
assert latest_release is None
319+
assert ('Getting latest release by semantic ordering (could not be the last one by time).',) == mock_log_info.call_args_list[0][0]
320+
assert ('Latest release not found for %s. 1st release for repository!', 'org/repo') == mock_log_info.call_args_list[1][0]
321+
assert ('Skipping invalid type of version tag: %s', 'v1.0.0') == mock_log_debug.call_args_list[0][0]
322+
assert ('Skipping invalid type of version tag: %s', 'v2.0.0') == mock_log_debug.call_args_list[1][0]

0 commit comments

Comments
 (0)