Skip to content

Commit

Permalink
#54 - Chapter line formatting - default (#73)
Browse files Browse the repository at this point in the history
#54 - Chapter line formatting - default
- Removed not used record formatter logic. No more needed.
- Introduces action input parameters for row formats and PR: prefix presence.
- Updated unit tests.
- Limited pylint and black to non-test code only.
  • Loading branch information
miroslavpojer authored Sep 25, 2024
1 parent d5751a9 commit 178819b
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 223 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ Generate Release Notes action is dedicated to enhance the quality and organizati
- **Description**: A JSON string defining chapters and corresponding labels for categorization. Each chapter should have a title and a label matching your GitHub issues and PRs.
- **Required**: Yes

### `row-format-issue`
- **Description**: The format of the row for the issue in the release notes. The format can contain placeholders for the issue `number`, `title`, and issues `pull-requests`. The placeholders are case-sensitive.
- **Required**: No
- **Default**: `#{number} _{title}_ in {pull-requests}"`

### `row-format-pr`
- **Description**: The format of the row for the PR in the release notes. The format can contain placeholders for the PR `number`, `title`, and PR `pull-requests`. The placeholders are case-sensitive.
- **Required**: No
- **Default**: `#{number} _{title}_"`

### `row-format-link-pr`
- **Description**: If defined `true`, the PR row will begin with a `"PR: "` string. Otherwise, no prefix will be added.
- **Required**: No
- **Default**: true

### `duplicity-scope`
- **Description**: Set to `custom` to allow duplicity issue lines to be shown only in custom chapters. Options: `custom`, `service`, `both`, `none`.
- **Required**: No
Expand Down
16 changes: 16 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ inputs:
description: 'Print verbose logs.'
required: false
default: 'false'
row-format-issue:
description: 'Format of the issue row in the release notes. Available placeholders: {link}, {title}, {pull-requests}. Placeholders are case-insensitive.'
required: false
default: '#{number} _{title}_ in {pull-requests}'
row-format-pr:
description: 'Format of the pr row in the release notes. Available placeholders: {link}, {title}, {pull-requests}. Placeholders are case-insensitive.'
required: false
default: '#{number} _{title}_'
row-format-link-pr:
description: 'Add prefix "PR:" before link to PR when not linked an Issue.'
required: false
default: 'true'

outputs:
release-notes:
description: 'Generated release notes.'
Expand Down Expand Up @@ -108,6 +121,9 @@ runs:
INPUT_CHAPTERS_TO_PR_WITHOUT_ISSUE: ${{ inputs.chapters-to-pr-without-issue }}
INPUT_VERBOSE: ${{ inputs.verbose }}
INPUT_GITHUB_REPOSITORY: ${{ github.repository }}
INPUT_ROW_FORMAT_ISSUE: ${{ inputs.row-format-issue }}
INPUT_ROW_FORMAT_PR: ${{ inputs.row-format-pr }}
INPUT_ROW_FORMAT_LINK_PR: ${{ inputs.row-format-link-pr }}
run: |
python ${{ github.action_path }}/main.py
shell: bash
1 change: 0 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def run() -> None:
py_github = Github(auth=Auth.Token(token=ActionInputs.get_github_token()), per_page=100)

ActionInputs.validate_inputs()

# Load custom chapters configuration
custom_chapters = CustomChapters(print_empty_chapters=ActionInputs.get_print_empty_chapters()).from_json(
ActionInputs.get_chapters_json()
Expand Down
41 changes: 41 additions & 0 deletions release_notes_generator/action_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,18 @@
CHAPTERS_TO_PR_WITHOUT_ISSUE,
DUPLICITY_SCOPE,
DUPLICITY_ICON,
ROW_FORMAT_LINK_PR,
ROW_FORMAT_ISSUE,
ROW_FORMAT_PR,
)
from release_notes_generator.utils.enums import DuplicityScopeEnum
from release_notes_generator.utils.gh_action import get_action_input
from release_notes_generator.utils.utils import detect_row_format_invalid_keywords

logger = logging.getLogger(__name__)


# pylint: disable=too-many-branches, too-many-statements, too-many-locals
class ActionInputs:
"""
A class representing the inputs provided to the GH action.
Expand Down Expand Up @@ -157,6 +162,27 @@ def validate_input(input_value, expected_type: type, error_message: str, error_b
return False
return True

@staticmethod
def get_row_format_issue() -> str:
"""
Get the issue row format for the release notes.
"""
return get_action_input(ROW_FORMAT_ISSUE, "#{number} _{title}_ in {pull-requests}").strip()

@staticmethod
def get_row_format_pr() -> str:
"""
Get the pr row format for the release notes.
"""
return get_action_input(ROW_FORMAT_PR, "#{number} _{title}_").strip()

@staticmethod
def get_row_format_link_pr() -> bool:
"""
Get the value controlling whether the row format should include a 'PR:' prefix when linking to PRs.
"""
return get_action_input(ROW_FORMAT_LINK_PR, "true").lower() == "true"

@staticmethod
def validate_inputs():
"""
Expand Down Expand Up @@ -201,6 +227,21 @@ def validate_inputs():
verbose = ActionInputs.get_verbose()
ActionInputs.validate_input(verbose, bool, "Verbose logging must be a boolean.", errors)

row_format_issue = ActionInputs.get_row_format_issue()
if not isinstance(row_format_issue, str) or not row_format_issue.strip():
errors.append("Issue row format must be a non-empty string.")

errors.extend(detect_row_format_invalid_keywords(row_format_issue))

row_format_pr = ActionInputs.get_row_format_pr()
if not isinstance(row_format_pr, str) or not row_format_pr.strip():
errors.append("PR Row format must be a non-empty string.")

errors.extend(detect_row_format_invalid_keywords(row_format_pr, row_type="PR"))

row_format_link_pr = ActionInputs.get_row_format_link_pr()
ActionInputs.validate_input(row_format_link_pr, bool, "'row-format-link-pr' value must be a boolean.", errors)

# Features
print_empty_chapters = ActionInputs.get_print_empty_chapters()
ActionInputs.validate_input(print_empty_chapters, bool, "Print empty chapters must be a boolean.", errors)
Expand Down
3 changes: 0 additions & 3 deletions release_notes_generator/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import logging
from itertools import chain

from release_notes_generator.record.record_formatter import RecordFormatter
from release_notes_generator.model.custom_chapters import CustomChapters
from release_notes_generator.model.record import Record
from release_notes_generator.model.service_chapters import ServiceChapters
Expand All @@ -43,12 +42,10 @@ def __init__(
self,
records: dict[int, Record],
changelog_url: str,
formatter: RecordFormatter,
custom_chapters: CustomChapters,
):
self.records = records
self.changelog_url = changelog_url
self.formatter = formatter
self.custom_chapters = custom_chapters

self.warnings = ActionInputs.get_warnings()
Expand Down
2 changes: 0 additions & 2 deletions release_notes_generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from typing import Optional
from github import Github

from release_notes_generator.record.record_formatter import RecordFormatter
from release_notes_generator.model.custom_chapters import CustomChapters
from release_notes_generator.model.record import Record
from release_notes_generator.builder import ReleaseNotesBuilder
Expand Down Expand Up @@ -113,7 +112,6 @@ def generate(self) -> Optional[str]:
release_notes_builder = ReleaseNotesBuilder(
records=rls_notes_records,
custom_chapters=self.custom_chapters,
formatter=RecordFormatter(),
changelog_url=changelog_url,
)

Expand Down
46 changes: 18 additions & 28 deletions release_notes_generator/model/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,47 +264,37 @@ def register_commit(self, commit: Commit) -> None:

logger.error("Commit %s not registered in any PR of record %s", commit.sha, self.number)

# TODO in Issue named 'Chapter line formatting - default'
def to_chapter_row(self, row_format="") -> str:
def to_chapter_row(self) -> str:
"""
Converts the record to a row in a chapter.
Converts the record to a string row in a chapter.
@param row_format: The format of the row.
@param increment_in_chapters: A boolean indicating whether to increment the count of chapters.
@return: The record as a row in a chapter.
@return: The record as a row string.
"""
self.increment_present_in_chapters()
row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters() > 1 else ""
format_values = {}

if self.__gh_issue is None:
p = self.__pulls[0]
format_values["number"] = p.number
format_values["title"] = p.title
format_values["authors"] = self.authors if self.authors is not None else ""
format_values["contributors"] = self.contributors if self.contributors is not None else ""

row = f"{row_prefix}PR: #{p.number} _{p.title}_"

# Issue can have more authors (as multiple PRs can be present)
if self.authors is not None:
row = f"{row}, implemented by {self.authors}"

if self.contributors is not None:
row = f"{row}, contributed by {self.contributors}"

if self.contains_release_notes:
return f"{row}\n{self.get_rls_notes()}"
pr_prefix = "PR: " if ActionInputs.get_row_format_link_pr() else ""
row = f"{row_prefix}{pr_prefix}" + ActionInputs.get_row_format_pr().format(**format_values)

else:
row = f"{row_prefix}#{self.__gh_issue.number} _{self.__gh_issue.title}_"

if self.authors is not None:
row = f"{row}, implemented by {self.authors}"

if self.contributors is not None:
row = f"{row}, contributed by {self.contributors}"
format_values["number"] = self.__gh_issue.number
format_values["title"] = self.__gh_issue.title
format_values["pull-requests"] = self.pr_links if len(self.__pulls) > 0 else ""
format_values["authors"] = self.authors if self.authors is not None else ""
format_values["contributors"] = self.contributors if self.contributors is not None else ""

if len(self.__pulls) > 0:
row = f"{row} in {self.pr_links}"
row = f"{row_prefix}" + ActionInputs.get_row_format_issue().format(**format_values)

if self.contains_release_notes:
row = f"{row}\n{self.get_rls_notes()}"
if self.contains_release_notes:
row = f"{row}\n{self.get_rls_notes()}"

return row

Expand Down
94 changes: 0 additions & 94 deletions release_notes_generator/record/record_formatter.py

This file was deleted.

10 changes: 4 additions & 6 deletions release_notes_generator/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
SKIP_RELEASE_NOTES_LABEL = "skip-release-notes-label"
VERBOSE = "verbose"
RUNNER_DEBUG = "RUNNER_DEBUG"
ROW_FORMAT_ISSUE = "row-format-issue"
ROW_FORMAT_PR = "row-format-pr"
ROW_FORMAT_LINK_PR = "row-format-link-pr"
SUPPORTED_ROW_FORMAT_KEYS = ["number", "title", "pull-requests"]

# Features
WARNINGS = "warnings"
Expand Down Expand Up @@ -58,9 +62,3 @@
MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES: str = "Merged PRs Linked to 'Not Closed' Issue ⚠️"

OTHERS_NO_TOPIC: str = "Others - No Topic ⚠️"

# Record formatter patterns
DEFAULT_ISSUE_PATTERN: str = (
"- #{number} _{title}_ implemented by {developers} in {pull_requests}\n" "{release_note_rows}"
)
DEFAULT_PULL_REQUESTS_PATTERN: str = "[#{number}]({url})"
20 changes: 20 additions & 0 deletions release_notes_generator/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@
"""

import logging
import re

from typing import Optional

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

from release_notes_generator.utils.constants import SUPPORTED_ROW_FORMAT_KEYS

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -53,3 +57,19 @@ def get_change_url(
changelog_url = f"https://github.com/{repo.full_name}/compare/{rls.tag_name}...{tag_name}"

return changelog_url


def detect_row_format_invalid_keywords(row_format: str, row_type: str = "Issue") -> list[str]:
"""
Detects invalid keywords in the row format.
@param row_format: The row format to be checked for invalid keywords.
@param row_type: The type of row format. Default is "Issue".
@return: A list of errors if invalid keywords are found, otherwise an empty list.
"""
errors = []
keywords_in_braces = re.findall(r"\{(.*?)\}", row_format)
invalid_keywords = [keyword for keyword in keywords_in_braces if keyword not in SUPPORTED_ROW_FORMAT_KEYS]
if invalid_keywords:
errors.append(f"Invalid {row_type} row format keyword(s) found: {', '.join(invalid_keywords)}")
return errors
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,10 @@ def record_with_no_issue_one_pull_closed_no_rls_notes(request):
record = Record(repo=request.getfixturevalue("mock_repo"))
record.register_pull_request(request.getfixturevalue("mock_pull_no_rls_notes"))
return record


@pytest.fixture
def mock_logging_setup(mocker):
"""Fixture to mock the basic logging setup using pytest-mock."""
mock_log_config = mocker.patch("logging.basicConfig")
yield mock_log_config
Loading

0 comments on commit 178819b

Please sign in to comment.