Skip to content

Commit 7a51e9f

Browse files
Feature/119 add ability to define rls from which to detect changes (#121)
* #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 62f17ab commit 7a51e9f

File tree

11 files changed

+182
-18
lines changed

11 files changed

+182
-18
lines changed

.github/workflows/release_draft.yml

+16-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ on:
2121
tag-name:
2222
description: 'Name of git tag to be created, and then draft release created. Syntax: "v[0-9]+.[0-9]+.[0-9]+".'
2323
required: true
24+
from-tag-name:
25+
description: 'Name of the git tag from which to detect changes from. Default value: latest tag. Syntax: "v[0-9]+.[0-9]+.[0-9]+".'
26+
required: false
2427

2528
jobs:
2629
release-draft:
@@ -37,20 +40,32 @@ jobs:
3740

3841
- name: Check format of received tag
3942
id: check-version-tag
40-
uses: AbsaOSS/version-tag-check@v0.2.0
43+
uses: AbsaOSS/version-tag-check@v0.3.0
4144
env:
4245
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4346
with:
4447
github-repository: ${{ github.repository }}
4548
version-tag: ${{ github.event.inputs.tag-name }}
4649

50+
- name: Check format of received from tag
51+
if: ${{ github.event.inputs.from-tag-name }}
52+
id: check-version-from-tag
53+
uses: AbsaOSS/[email protected]
54+
env:
55+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56+
with:
57+
github-repository: ${{ github.repository }}
58+
version-tag: ${{ github.event.inputs.from-tag-name }}
59+
should-exist: true
60+
4761
- name: Generate Release Notes
4862
id: generate_release_notes
4963
uses: AbsaOSS/generate-release-notes@master
5064
env:
5165
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5266
with:
5367
tag-name: ${{ github.event.inputs.tag-name }}
68+
from-tag-name: ${{ github.event.inputs.from-tag-name }}
5469
chapters: '[
5570
{"title": "No entry 🚫", "label": "duplicate"},
5671
{"title": "No entry 🚫", "label": "invalid"},

action.yml

+5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ inputs:
2727
description: 'Allow duplicity of issue lines in chapters. Scopes: custom, service, both, none.'
2828
required: false
2929
default: 'both'
30+
from-tag-name:
31+
description: 'The tag name of the previous release to use as a start reference point for the current release notes.'
32+
required: false
33+
default: ''
3034
duplicity-icon:
3135
description: 'Icon to be used for duplicity warning. Icon is placed before the record line.'
3236
required: false
@@ -115,6 +119,7 @@ runs:
115119
INPUT_GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
116120
INPUT_TAG_NAME: ${{ inputs.tag-name }}
117121
INPUT_CHAPTERS: ${{ inputs.chapters }}
122+
INPUT_FROM_TAG_NAME: ${{ inputs.from-tag-name }}
118123
INPUT_DUPLICITY_SCOPE: ${{ inputs.duplicity-scope }}
119124
INPUT_DUPLICITY_ICON: ${{ inputs.duplicity-icon }}
120125
INPUT_WARNINGS: ${{ inputs.warnings }}

examples/release_draft.yml

+16-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ on:
55
tag-name:
66
description: 'Name of git tag to be created, and then draft release created. Syntax: "v[0-9]+.[0-9]+.[0-9]+".'
77
required: true
8+
from-tag-name:
9+
description: 'Name of the git tag from which to detect changes from. Default value: latest tag. Syntax: "v[0-9]+.[0-9]+.[0-9]+".'
10+
required: false
811

912
jobs:
1013
release-draft:
@@ -23,21 +26,32 @@ jobs:
2326

2427
- name: Check format of received tag
2528
id: check-version-tag
26-
uses: AbsaOSS/version-tag-check@v0.1.0
29+
uses: AbsaOSS/version-tag-check@v0.3.0
2730
env:
2831
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2932
with:
3033
github-repository: ${{ github.repository }}
31-
branch: 'master'
3234
version-tag: ${{ github.event.inputs.tag-name }}
3335

36+
- name: Check format of received from tag
37+
if: ${{ github.event.inputs.from-tag-name }}
38+
id: check-version-from-tag
39+
uses: AbsaOSS/[email protected]
40+
env:
41+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42+
with:
43+
github-repository: ${{ github.repository }}
44+
version-tag: ${{ github.event.inputs.from-tag-name }}
45+
should-exist: true
46+
3447
- name: Generate Release Notes
3548
id: generate_release_notes
3649
uses: AbsaOSS/[email protected]
3750
env:
3851
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3952
with:
4053
tag-name: ${{ github.event.inputs.tag-name }}
54+
from-tag-name: ${{ github.event.inputs.from-tag-name }}
4155
chapters: '[
4256
{"title": "No entry 🚫", "label": "duplicate"},
4357
{"title": "No entry 🚫", "label": "invalid"},

release_notes_generator/action_inputs.py

+27-5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
RELEASE_NOTES_TITLE,
4444
RELEASE_NOTE_TITLE_DEFAULT,
4545
SUPPORTED_ROW_FORMAT_KEYS,
46+
FROM_TAG_NAME,
4647
)
4748
from release_notes_generator.utils.enums import DuplicityScopeEnum
4849
from release_notes_generator.utils.gh_action import get_action_input
@@ -81,6 +82,21 @@ def get_tag_name() -> str:
8182
"""
8283
return get_action_input(TAG_NAME)
8384

85+
@staticmethod
86+
def get_from_tag_name() -> str:
87+
"""
88+
Get the from-tag name from the action inputs.
89+
"""
90+
return get_action_input(FROM_TAG_NAME, default="")
91+
92+
@staticmethod
93+
def is_from_tag_name_defined() -> bool:
94+
"""
95+
Check if the from-tag name is defined in the action inputs.
96+
"""
97+
value = ActionInputs.get_from_tag_name()
98+
return value.strip() != ""
99+
84100
@staticmethod
85101
def get_chapters_json() -> str:
86102
"""
@@ -120,8 +136,9 @@ def get_skip_release_notes_labels() -> str:
120136
"""
121137
Get the skip release notes label from the action inputs.
122138
"""
123-
user_choice = [item.strip() for item in get_action_input(SKIP_RELEASE_NOTES_LABELS, "").split(",")]
124-
if len(user_choice) > 0:
139+
user_input = get_action_input(SKIP_RELEASE_NOTES_LABELS, "")
140+
user_choice = [item.strip() for item in user_input.split(",")] if user_input else []
141+
if user_choice:
125142
return user_choice
126143
return ["skip-release-notes"]
127144

@@ -222,6 +239,10 @@ def validate_inputs() -> None:
222239
if not isinstance(tag_name, str) or not tag_name.strip():
223240
errors.append("Tag name must be a non-empty string.")
224241

242+
from_tag_name = ActionInputs.get_from_tag_name()
243+
if not isinstance(from_tag_name, str):
244+
errors.append("From tag name must be a string.")
245+
225246
chapters_json = ActionInputs.get_chapters_json()
226247
try:
227248
json.loads(chapters_json)
@@ -294,9 +315,10 @@ def _detect_row_format_invalid_keywords(row_format: str, row_type: str = "Issue"
294315
cleaned_row_format = row_format
295316
for invalid_keyword in invalid_keywords:
296317
logger.error(
297-
"Invalid `{}` detected in `{}` row format keyword(s) found: {}. Will be removed from string.".format(
298-
invalid_keyword, row_type, ", ".join(invalid_keywords)
299-
)
318+
"Invalid `%s` detected in `%s` row format keyword(s) found: %s. Will be removed from string.",
319+
invalid_keyword,
320+
row_type,
321+
", ".join(invalid_keywords),
300322
)
301323
if clean:
302324
cleaned_row_format = cleaned_row_format.replace(f"{{{invalid_keyword}}}", "")

release_notes_generator/builder.py

-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
logger = logging.getLogger(__name__)
3030

3131

32-
# TODO - reduce to function only after implementing the features. Will be supported more build ways?
3332
# pylint: disable=too-few-public-methods
3433
class ReleaseNotesBuilder:
3534
"""

release_notes_generator/generator.py

+40-7
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
"""
2121

2222
import logging
23+
import sys
2324

2425
from typing import Optional
2526
from github import Github
27+
from github.GitRelease import GitRelease
28+
from github.Repository import Repository
2629

2730
from release_notes_generator.model.custom_chapters import CustomChapters
2831
from release_notes_generator.model.record import Record
@@ -72,15 +75,13 @@ def generate(self) -> Optional[str]:
7275
7376
@return: The generated release notes as a string, or None if the repository could not be found.
7477
"""
78+
# get the repository
7579
repo = self._safe_call(self.github_instance.get_repo)(ActionInputs.get_github_repository())
7680
if repo is None:
7781
return None
7882

79-
rls = self._safe_call(repo.get_latest_release)()
80-
if rls is None:
81-
logger.info("Latest release not found for %s. 1st release for repository!", repo.full_name)
82-
else:
83-
logger.debug("RLS created_at: %s, published_at: %s", rls.created_at, rls.published_at)
83+
# get the latest release
84+
rls: GitRelease = self.get_latest_release(repo)
8485

8586
# default is repository creation date if no releases OR created_at of latest release
8687
since = rls.created_at if rls else repo.created_at
@@ -97,12 +98,12 @@ def generate(self) -> Optional[str]:
9798

9899
# filter out closed Issues before the date
99100
issues = list(
100-
filter(lambda issue: issue.closed_at is not None and issue.closed_at > since, list(issues_all))
101+
filter(lambda issue: issue.closed_at is not None and issue.closed_at >= since, list(issues_all))
101102
)
102103
logger.debug("Count of issues reduced from %d to %d", len(list(issues_all)), len(issues))
103104

104105
# filter out merged PRs and commits before the date
105-
pulls = list(filter(lambda pull: pull.merged_at is not None and pull.merged_at > since, list(pulls_all)))
106+
pulls = list(filter(lambda pull: pull.merged_at is not None and pull.merged_at >= since, list(pulls_all)))
106107
logger.debug("Count of pulls reduced from %d to %d", len(list(pulls_all)), len(pulls))
107108

108109
commits = list(filter(lambda commit: commit.commit.author.date > since, list(commits_all)))
@@ -125,3 +126,35 @@ def generate(self) -> Optional[str]:
125126
)
126127

127128
return release_notes_builder.build()
129+
130+
def get_latest_release(self, repo: Repository) -> Optional[GitRelease]:
131+
"""
132+
Get the latest release of the repository.
133+
134+
@param repo: The repository to get the latest release from.
135+
@return: The latest release of the repository, or None if no releases are found.
136+
"""
137+
if ActionInputs.is_from_tag_name_defined():
138+
logger.info("Getting latest release by from-tag name %s", ActionInputs.get_tag_name())
139+
rls: GitRelease = self._safe_call(repo.get_release)(ActionInputs.get_from_tag_name())
140+
141+
if rls is None:
142+
logger.info("Latest release not found for received tag %s. Ending!", ActionInputs.get_from_tag_name())
143+
sys.exit(1)
144+
145+
else:
146+
logger.info("Getting latest release by time.")
147+
rls: GitRelease = self._safe_call(repo.get_latest_release)()
148+
149+
if rls is None:
150+
logger.info("Latest release not found for %s. 1st release for repository!", repo.full_name)
151+
152+
if rls is not None:
153+
logger.debug(
154+
"Latest release with tag:'%s' created_at: %s, published_at: %s",
155+
rls.tag_name,
156+
rls.created_at,
157+
rls.published_at,
158+
)
159+
160+
return rls

release_notes_generator/record/record_factory.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def create_record_for_issue(r: Repository, i: Issue) -> None:
6868
@return: None
6969
"""
7070
# check for skip labels presence and skip when detected
71-
issue_labels = [label.name for label in issue.labels]
71+
issue_labels = [label.name for label in i.labels]
7272
skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels())
7373
records[i.number] = Record(r, i, skip=skip_record)
7474

release_notes_generator/utils/constants.py

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
GITHUB_REPOSITORY = "GITHUB_REPOSITORY"
2323
GITHUB_TOKEN = "github-token"
2424
TAG_NAME = "tag-name"
25+
FROM_TAG_NAME = "from-tag-name"
2526
CHAPTERS = "chapters"
2627
DUPLICITY_SCOPE = "duplicity-scope"
2728
DUPLICITY_ICON = "duplicity-icon"

release_notes_generator/utils/pull_reuqest_utils.py

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
# limitations under the License.
1515
#
1616

17+
"""
18+
This module contains the PullRequestUtils class which is responsible for extracting information from pull requests.
19+
"""
20+
1721
import re
1822

1923
from github.PullRequest import PullRequest

tests/test_action_inputs.py

+19
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
success_case = {
2424
"get_github_repository": "owner/repo_name",
2525
"get_tag_name": "tag_name",
26+
"get_from_tag_name": "from_tag_name",
2627
"get_chapters_json": '{"chapter": "content"}',
2728
"get_duplicity_scope": "custom",
2829
"get_duplicity_icon": "🔁",
@@ -31,12 +32,14 @@
3132
"get_skip_release_notes_labels": ["skip"],
3233
"get_print_empty_chapters": True,
3334
"get_verbose": True,
35+
"get_release_notes_title": "Success value",
3436
}
3537

3638
failure_cases = [
3739
("get_github_repository", "", "Owner and Repo must be a non-empty string."),
3840
("get_github_repository", "owner/", "Owner and Repo must be a non-empty string."),
3941
("get_tag_name", "", "Tag name must be a non-empty string."),
42+
("get_from_tag_name", 1, "From tag name must be a string."),
4043
("get_chapters_json", "invalid_json", "Chapters JSON must be a valid JSON string."),
4144
("get_warnings", "not_bool", "Warnings must be a boolean."),
4245
("get_published_at", "not_bool", "Published at must be a boolean."),
@@ -46,6 +49,7 @@
4649
("get_duplicity_icon", "Oj", "Duplicity icon must be a non-empty string and have a length of 1."),
4750
("get_row_format_issue", "", "Issue row format must be a non-empty string."),
4851
("get_row_format_pr", "", "PR Row format must be a non-empty string."),
52+
("get_release_notes_title", "", "Release Notes title must be a non-empty string and have non-zero length."),
4953
]
5054

5155

@@ -123,14 +127,21 @@ def test_get_skip_release_notes_label(mocker):
123127
mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="skip-release-notes")
124128
assert ActionInputs.get_skip_release_notes_labels() == ["skip-release-notes"]
125129

130+
131+
def test_get_skip_release_notes_label_not_defined(mocker):
132+
mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="")
133+
assert ActionInputs.get_skip_release_notes_labels() == ["skip-release-notes"]
134+
126135
def test_get_skip_release_notes_labels(mocker):
127136
mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="skip-release-notes, another-skip-label")
128137
assert ActionInputs.get_skip_release_notes_labels() == ["skip-release-notes", "another-skip-label"]
129138

139+
130140
def test_get_skip_release_notes_labels_no_space(mocker):
131141
mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="skip-release-notes,another-skip-label")
132142
assert ActionInputs.get_skip_release_notes_labels() == ["skip-release-notes", "another-skip-label"]
133143

144+
134145
def test_get_print_empty_chapters(mocker):
135146
mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="true")
136147
assert ActionInputs.get_print_empty_chapters() is True
@@ -200,3 +211,11 @@ def test_clean_row_format_invalid_keywords_nested_braces():
200211
actual_format = ActionInputs._detect_row_format_invalid_keywords(row_format, clean=True)
201212
assert expected_format == actual_format
202213

214+
215+
def test_release_notes_title_default():
216+
assert ActionInputs.get_release_notes_title() == "[Rr]elease [Nn]otes:"
217+
218+
219+
def test_release_notes_title_custom(mocker):
220+
mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="Custom Title")
221+
assert ActionInputs.get_release_notes_title() == "Custom Title"

0 commit comments

Comments
 (0)