From 217946762aa4dce801f28b5f0d1de11b53698bd0 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 9 Sep 2025 14:27:42 +0200 Subject: [PATCH 1/4] #165 - Introduce regimes - prepare default one - Implemented default regime. --- README.md | 53 ++++-- action.yml | 5 + release_notes_generator/action_inputs.py | 13 ++ release_notes_generator/builders/__init__.py | 0 .../builders/base_builder.py | 53 ++++++ .../default_builder.py} | 19 +- release_notes_generator/generator.py | 27 ++- .../record/default_record_factory.py | 165 ++++++++++++++++++ .../record/record_factory.py | 135 +------------- .../test_release_notes_builder.py | 56 +++--- 10 files changed, 327 insertions(+), 199 deletions(-) create mode 100644 release_notes_generator/builders/__init__.py create mode 100644 release_notes_generator/builders/base_builder.py rename release_notes_generator/{builder.py => builders/default_builder.py} (80%) create mode 100644 release_notes_generator/record/default_record_factory.py diff --git a/README.md b/README.md index 40dae642..6f2596ec 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ - [Motivation](#motivation) - [Requirements](#requirements) - [Inputs](#inputs) + - [Feature controls](#feature-controls) + - [Regimes](#regimes) - [Outputs](#outputs) - [Usage Example](#usage-example) - [Features](#features) @@ -40,24 +42,25 @@ Generate Release Notes action is dedicated to enhance the quality and organizati ## Inputs -| Name | Description | Required | Default | -|----------------|-----------------------------------------------------------------------------------------------------------------------------|----------|-----------------------------------------| -| `GITHUB_TOKEN` | Your GitHub token for authentication. Store it as a secret and reference it in the workflow file as secrets.GITHUB_TOKEN. | Yes | | -| `tag-name` | The name of the tag for which you want to generate release notes. This should be the same as the tag name used in the release workflow. | Yes | | -| `from-tag-name` | The name of the tag from which you want to generate release notes. | No | '' | -| `chapters` | An YAML array defining chapters and corresponding labels for categorization. Each chapter should have a title and a label matching your GitHub issues and PRs. | Yes | | -| `row-format-issue` | 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. | No | `"{number} _{title}_ in {pull-requests}"` | -| `row-format-pr` | The format of the row for the PR in the release notes. The format can contain placeholders for the PR `number`, and `title`. The placeholders are case-sensitive. | No | `"{number} _{title}_"` | -| `row-format-link-pr` | If defined `true`, the PR row will begin with a `"PR: "` string. Otherwise, no prefix will be added. | No | true | -| `duplicity-scope` | Set to `custom` to allow duplicity issue lines to be shown only in custom chapters. Options: `custom`, `service`, `both`, `none`. | No | `both` | -| `duplicity-icon` | The icon used to indicate duplicity issue lines in the release notes. Icon will be placed at the beginning of the line. | No | `🔔` | -| `published-at` | Set to true to enable the use of the `published-at` timestamp as the reference point for searching closed issues and PRs, instead of the `created-at` date of the latest release. If first release, repository creation date is used. | No | false | -| `skip-release-notes-labels` | List labels used for detection if issues or pull requests are ignored in the Release Notes generation process. Example: `skip-release-notes, question`. | No | `skip-release-notes` | -| `verbose` | Set to true to enable verbose logging for detailed output during the action's execution. | No | false | -| `release-notes-title` | The title of the release notes section in the PR description. | No | `[Rr]elease [Nn]otes:` | -| `coderabbit-support-active` | Enable CodeRabbit support. If true, the action will use CodeRabbit to generate release notes. | No | false | -| `coderabbit-release-notes-title` | The title of the CodeRabbit summary in the PR body. Value supports regex. | No | `Summary by CodeRabbit` | -| `coderabbit-summary-ignore-groups` | List of "group names" to be ignored by release notes detection logic. Example: `Documentation, Tests, Chores, Bug Fixes`. | No | '' | +| Name | Description | Required | Default | +|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------------------| +| `GITHUB_TOKEN` | Your GitHub token for authentication. Store it as a secret and reference it in the workflow file as secrets.GITHUB_TOKEN. | Yes | | +| `tag-name` | The name of the tag for which you want to generate release notes. This should be the same as the tag name used in the release workflow. | Yes | | +| `from-tag-name` | The name of the tag from which you want to generate release notes. | No | '' | +| `chapters` | An YAML array defining chapters and corresponding labels for categorization. Each chapter should have a title and a label matching your GitHub issues and PRs. | Yes | | +| `regime` | Controls the regime of the action. Options: `default`. See more about the [Regimes](#regimes). | No | `default` | +| `row-format-issue` | 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. | No | `"{number} _{title}_ in {pull-requests}"` | +| `row-format-pr` | The format of the row for the PR in the release notes. The format can contain placeholders for the PR `number`, and `title`. The placeholders are case-sensitive. | No | `"{number} _{title}_"` | +| `row-format-link-pr` | If defined `true`, the PR row will begin with a `"PR: "` string. Otherwise, no prefix will be added. | No | true | +| `duplicity-scope` | Set to `custom` to allow duplicity issue lines to be shown only in custom chapters. Options: `custom`, `service`, `both`, `none`. | No | `both` | +| `duplicity-icon` | The icon used to indicate duplicity issue lines in the release notes. Icon will be placed at the beginning of the line. | No | `🔔` | +| `published-at` | Set to true to enable the use of the `published-at` timestamp as the reference point for searching closed issues and PRs, instead of the `created-at` date of the latest release. If first release, repository creation date is used. | No | false | +| `skip-release-notes-labels` | List labels used for detection if issues or pull requests are ignored in the Release Notes generation process. Example: `skip-release-notes, question`. | No | `skip-release-notes` | +| `verbose` | Set to true to enable verbose logging for detailed output during the action's execution. | No | false | +| `release-notes-title` | The title of the release notes section in the PR description. | No | `[Rr]elease [Nn]otes:` | +| `coderabbit-support-active` | Enable CodeRabbit support. If true, the action will use CodeRabbit to generate release notes. | No | false | +| `coderabbit-release-notes-title` | The title of the CodeRabbit summary in the PR body. Value supports regex. | No | `Summary by CodeRabbit` | +| `coderabbit-summary-ignore-groups` | List of "group names" to be ignored by release notes detection logic. Example: `Documentation, Tests, Chores, Bug Fixes`. | No | '' | > **Notes** > - `skip-release-notes-labels` @@ -78,6 +81,20 @@ Generate Release Notes action is dedicated to enhance the quality and organizati > - `warnings` > - **Disabling this feature will hide service chapter showing direct commits!** These cannot be visible in custom chapters as they do not have labels! +### Regimes + +### Default regime + +The basic regime for this action. + +- **Data management** + - The issue type is not used. It can lead to placing Epic and other issues without linked PR into service chapters. If you need to use issue type use another regime. +- **Release notes** + - Organized by custom chapters defined by user using labels. + - Used these `types of rows`: + - Issue with/without linked Pull Request(s). + - Pull Request without linked Issue. + - Direct commits (without Issue and Pull Request). ## Outputs The output of the action is a markdown string containing the release notes for the specified tag. This string can be used in subsequent steps to publish the release notes to a file, create a GitHub release, or send notifications. diff --git a/action.yml b/action.yml index 384278ce..f4a27c46 100644 --- a/action.yml +++ b/action.yml @@ -32,6 +32,10 @@ inputs: description: 'The tag name of the previous release to use as a start reference point for the current release notes.' required: false default: '' + regime: + description: 'Regime of the release notes generation. Options: default.' + required: false + default: 'default' duplicity-icon: description: 'Icon to be used for duplicity warning. Icon is placed before the record line.' required: false @@ -133,6 +137,7 @@ runs: INPUT_TAG_NAME: ${{ inputs.tag-name }} INPUT_CHAPTERS: ${{ inputs.chapters }} INPUT_FROM_TAG_NAME: ${{ inputs.from-tag-name }} + INPUT_REGIME: ${{ inputs.regime }} INPUT_DUPLICITY_SCOPE: ${{ inputs.duplicity-scope }} INPUT_DUPLICITY_ICON: ${{ inputs.duplicity-icon }} INPUT_WARNINGS: ${{ inputs.warnings }} diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index dbec0058..22501886 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -63,6 +63,8 @@ class ActionInputs: A class representing the inputs provided to the GH action. """ + REGIME_DEFAULT = "default" + _row_format_issue = None _row_format_pr = None _row_format_link_pr = None @@ -160,6 +162,13 @@ def get_chapters() -> list[dict[str, str]]: return chapters + @staticmethod + def get_regime() -> str: + """ + Get the regime parameter value from the action inputs. + """ + return get_action_input("regime", "default") # type: ignore[return-value] # default defined + @staticmethod def get_duplicity_scope() -> DuplicityScopeEnum: """ @@ -365,6 +374,9 @@ def validate_inputs() -> None: if not isinstance(duplicity_icon, str) or not duplicity_icon.strip() or len(duplicity_icon) != 1: errors.append("Duplicity icon must be a non-empty string and have a length of 1.") + regime = ActionInputs.get_regime() + ActionInputs.validate_input(regime, str, "Regime must be a string.", errors) + warnings = ActionInputs.get_warnings() ActionInputs.validate_input(warnings, bool, "Warnings must be a boolean.", errors) @@ -421,6 +433,7 @@ def validate_inputs() -> None: logger.debug("Tag name: %s", tag_name) logger.debug("From tag name: %s", from_tag_name) logger.debug("Chapters: %s", chapters) + logger.debug("Regime: %s", regime) logger.debug("Published at: %s", published_at) logger.debug("Skip release notes labels: %s", ActionInputs.get_skip_release_notes_labels()) logger.debug("Verbose logging: %s", verbose) diff --git a/release_notes_generator/builders/__init__.py b/release_notes_generator/builders/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/release_notes_generator/builders/base_builder.py b/release_notes_generator/builders/base_builder.py new file mode 100644 index 00000000..e579f0f0 --- /dev/null +++ b/release_notes_generator/builders/base_builder.py @@ -0,0 +1,53 @@ +# +# Copyright 2023 ABSA Group Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +This module contains the ReleaseNotesBuilder class which is responsible for building of the release notes. +""" +from abc import ABCMeta, abstractmethod + +from release_notes_generator.action_inputs import ActionInputs +from release_notes_generator.model.custom_chapters import CustomChapters +from release_notes_generator.model.record import Record + + +class ReleaseNotesBuilder(metaclass=ABCMeta): + """ + A class representing the Release Notes Builder. + The class is responsible for building the release notes based on the records, changelog URL, formatter, and custom + chapters. + """ + + def __init__( + self, + records: dict[int | str, Record], + changelog_url: str, + custom_chapters: CustomChapters, + ): + self.records = records + self.changelog_url = changelog_url + self.custom_chapters = custom_chapters + + self.warnings = ActionInputs.get_warnings() + self.print_empty_chapters = ActionInputs.get_print_empty_chapters() + + @abstractmethod + def build(self) -> str: + """ + Build the release notes based on the records, changelog URL, formatter, and custom chapters. + + @return: The release notes as a string. + """ diff --git a/release_notes_generator/builder.py b/release_notes_generator/builders/default_builder.py similarity index 80% rename from release_notes_generator/builder.py rename to release_notes_generator/builders/default_builder.py index 65636ebf..fe163215 100644 --- a/release_notes_generator/builder.py +++ b/release_notes_generator/builders/default_builder.py @@ -21,34 +21,19 @@ import logging from itertools import chain -from release_notes_generator.model.custom_chapters import CustomChapters -from release_notes_generator.model.record import Record +from release_notes_generator.builders.base_builder import ReleaseNotesBuilder from release_notes_generator.model.service_chapters import ServiceChapters -from release_notes_generator.action_inputs import ActionInputs logger = logging.getLogger(__name__) -class ReleaseNotesBuilder: +class DefaultReleaseNotesBuilder(ReleaseNotesBuilder): """ A class representing the Release Notes Builder. The class is responsible for building the release notes based on the records, changelog URL, formatter, and custom chapters. """ - def __init__( - self, - records: dict[int | str, Record], - changelog_url: str, - custom_chapters: CustomChapters, - ): - self.records = records - self.changelog_url = changelog_url - self.custom_chapters = custom_chapters - - self.warnings = ActionInputs.get_warnings() - self.print_empty_chapters = ActionInputs.get_print_empty_chapters() - def build(self) -> str: """ Build the release notes based on the records, changelog URL, formatter, and custom chapters. diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index 85ebde30..898165f1 100644 --- a/release_notes_generator/generator.py +++ b/release_notes_generator/generator.py @@ -28,9 +28,11 @@ from release_notes_generator.filter import FilterByRelease from release_notes_generator.miner import DataMiner from release_notes_generator.action_inputs import ActionInputs -from release_notes_generator.builder import ReleaseNotesBuilder +from release_notes_generator.builders.base_builder import ReleaseNotesBuilder +from release_notes_generator.builders.default_builder import DefaultReleaseNotesBuilder from release_notes_generator.model.custom_chapters import CustomChapters from release_notes_generator.model.record import Record +from release_notes_generator.record.default_record_factory import DefaultRecordFactory from release_notes_generator.record.record_factory import RecordFactory from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter from release_notes_generator.utils.utils import get_change_url @@ -90,14 +92,25 @@ def generate(self) -> Optional[str]: assert data_filtered_by_release.repository is not None, "Repository must not be None" - rls_notes_records: dict[int | str, Record] = RecordFactory.generate( + # get record factory instance in dependency on selected regime + record_factory: RecordFactory = DefaultRecordFactory() + # This is a placeholder for future regimes - will be added in following issue + # match ActionInputs.get_regime(): + # case "TODO": + # record_factory = TBD + + rls_notes_records: dict[int | str, Record] = record_factory.generate( github=self._github_instance, data=data_filtered_by_release ) - release_notes_builder = ReleaseNotesBuilder( - records=rls_notes_records, - custom_chapters=self.custom_chapters, + return self._get_rls_notes_builder(rls_notes_records, changelog_url, self.custom_chapters).build() + + def _get_rls_notes_builder( + self, records: dict[int | str, Record], changelog_url: str, custom_chapters: CustomChapters + ) -> ReleaseNotesBuilder: + + return DefaultReleaseNotesBuilder( + records=records, + custom_chapters=custom_chapters, changelog_url=changelog_url, ) - - return release_notes_builder.build() diff --git a/release_notes_generator/record/default_record_factory.py b/release_notes_generator/record/default_record_factory.py new file mode 100644 index 00000000..872fadc4 --- /dev/null +++ b/release_notes_generator/record/default_record_factory.py @@ -0,0 +1,165 @@ +# +# Copyright 2023 ABSA Group Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +This module contains the RecordFactory class which is responsible for generating records for release notes. +""" + +import logging +from typing import cast + +from github import Github +from github.Issue import Issue +from github.PullRequest import PullRequest +from github.Commit import Commit + +from release_notes_generator.model.commit_record import CommitRecord +from release_notes_generator.model.issue_record import IssueRecord +from release_notes_generator.model.mined_data import MinedData +from release_notes_generator.action_inputs import ActionInputs +from release_notes_generator.model.pull_request_record import PullRequestRecord +from release_notes_generator.model.record import Record +from release_notes_generator.record.record_factory import RecordFactory + +from release_notes_generator.utils.decorators import safe_call_decorator +from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter +from release_notes_generator.utils.pull_request_utils import get_issues_for_pr, extract_issue_numbers_from_body + +logger = logging.getLogger(__name__) + + +class DefaultRecordFactory(RecordFactory): + """ + A class used to generate records for release notes. + """ + + def generate(self, github: Github, data: MinedData) -> dict[int | str, Record]: + """ + Generate records for release notes. + Parameters: + github (GitHub): The GitHub instance to generate records for. + data (MinedData): The MinedData instance containing repository, issues, pull requests, and commits. + Returns: + dict[int|str, Record]: A dictionary of records where the key is the issue or pull request number. + """ + + def register_pull_request(pull: PullRequest, skip_rec: bool) -> None: + detected_issues = extract_issue_numbers_from_body(pull) + logger.debug("Detected issues - from body: %s", detected_issues) + detected_issues.update(safe_call(get_issues_for_pr)(pull_number=pull.number)) + logger.debug("Detected issues - final: %s", detected_issues) + + for parent_issue_number in detected_issues: + # create an issue record if not present for PR parent + if parent_issue_number not in records: + logger.warning( + "Detected PR %d linked to issue %d which is not in the list of received issues. " + "Fetching ...", + pull.number, + parent_issue_number, + ) + parent_issue = ( + safe_call(data.repository.get_issue)(parent_issue_number) if data.repository else None + ) + if parent_issue is not None: + DefaultRecordFactory._create_record_for_issue(records, parent_issue) + + if parent_issue_number in records: + cast(IssueRecord, records[parent_issue_number]).register_pull_request(pull) + logger.debug("Registering PR %d: %s to Issue %d", pull.number, pull.title, parent_issue_number) + else: + logger.debug( + "Registering stand-alone PR %d: %s as mentioned Issue %d not found.", + pull.number, + pull.title, + parent_issue_number, + ) + + records: dict[int | str, Record] = {} + rate_limiter = GithubRateLimiter(github) + safe_call = safe_call_decorator(rate_limiter) + + logger.debug("Registering issues to records...") + for issue in data.issues: + DefaultRecordFactory._create_record_for_issue(records, issue) + + logger.debug("Registering pull requests to records...") + for pull in data.pull_requests: + pull_labels = [label.name for label in pull.get_labels()] + skip_record: bool = any(item in pull_labels for item in ActionInputs.get_skip_release_notes_labels()) + + if not safe_call(get_issues_for_pr)(pull_number=pull.number) and not extract_issue_numbers_from_body(pull): + records[pull.number] = PullRequestRecord(pull, skip=skip_record) + logger.debug("Created record for PR %d: %s", pull.number, pull.title) + else: + logger.debug("Registering pull number: %s, title : %s", pull.number, pull.title) + register_pull_request(pull, skip_record) + + logger.debug("Registering commits to records...") + detected_direct_commits_count = sum( + not DefaultRecordFactory._register_commit_to_record(records, commit) for commit in data.commits + ) + + logger.info( + "Generated %d records from %d issues and %d PRs, with %d commits detected.", + len(records), + len(data.issues), + len(data.pull_requests), + detected_direct_commits_count, + ) + return records + + @staticmethod + def _register_commit_to_record(records: dict[int | str, Record], commit: Commit) -> bool: + """ + Register a commit to a record. + + @param commit: The commit to register. + @return: True if the commit was registered to a record, False otherwise + """ + for record in records.values(): + if isinstance(record, IssueRecord): + rec_i = cast(IssueRecord, record) + for number in rec_i.get_pull_request_numbers(): + pr = rec_i.get_pull_request(number) + if pr and pr.merge_commit_sha == commit.sha: + rec_i.register_commit(pr, commit) + return True + + elif isinstance(record, PullRequestRecord): + rec_pr = cast(PullRequestRecord, record) + if rec_pr.is_commit_sha_present(commit.sha): + rec_pr.register_commit(commit) + return True + + records[commit.sha] = CommitRecord(commit=commit) + logger.debug("Created record for direct commit %s: %s", commit.sha, commit.commit.message) + return False + + @staticmethod + def _create_record_for_issue(records: dict[int | str, Record], i: Issue) -> None: + """ + Create a record for an issue. + + @param i: Issue instance. + @return: None + """ + # check for skip labels presence and skip when detected + issue_labels = [label.name for label in i.get_labels()] + skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels()) + records[i.number] = IssueRecord(issue=i, skip=skip_record) + + logger.debug("Created record for issue %d: %s", i.number, i.title) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index be497a9e..cf06b4b9 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -17,149 +17,28 @@ """ This module contains the RecordFactory class which is responsible for generating records for release notes. """ - -import logging -from typing import cast +from abc import ABCMeta, abstractmethod from github import Github -from github.Issue import Issue -from github.PullRequest import PullRequest -from github.Commit import Commit -from release_notes_generator.model.commit_record import CommitRecord -from release_notes_generator.model.issue_record import IssueRecord from release_notes_generator.model.mined_data import MinedData -from release_notes_generator.action_inputs import ActionInputs -from release_notes_generator.model.pull_request_record import PullRequestRecord from release_notes_generator.model.record import Record -from release_notes_generator.utils.decorators import safe_call_decorator -from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter -from release_notes_generator.utils.pull_request_utils import get_issues_for_pr, extract_issue_numbers_from_body - -logger = logging.getLogger(__name__) - -class RecordFactory: +class RecordFactory(metaclass=ABCMeta): """ A class used to generate records for release notes. """ - @staticmethod - def generate(github: Github, data: MinedData) -> dict[int | str, Record]: + @abstractmethod + def generate(self, github: Github, data: MinedData) -> dict[int | str, Record]: """ Generate records for release notes. + Parameters: - github (GitHub): The GitHub instance to generate records for. + github (Github): GitHub instance. data (MinedData): The MinedData instance containing repository, issues, pull requests, and commits. + Returns: dict[int|str, Record]: A dictionary of records where the key is the issue or pull request number. """ - - def register_pull_request(pull: PullRequest, skip_rec: bool) -> None: - detected_issues = extract_issue_numbers_from_body(pull) - logger.debug("Detected issues - from body: %s", detected_issues) - detected_issues.update(safe_call(get_issues_for_pr)(pull_number=pull.number)) - logger.debug("Detected issues - final: %s", detected_issues) - - for parent_issue_number in detected_issues: - # create an issue record if not present for PR parent - if parent_issue_number not in records: - logger.warning( - "Detected PR %d linked to issue %d which is not in the list of received issues. " - "Fetching ...", - pull.number, - parent_issue_number, - ) - parent_issue = ( - safe_call(data.repository.get_issue)(parent_issue_number) if data.repository else None - ) - if parent_issue is not None: - RecordFactory._create_record_for_issue(records, parent_issue) - - if parent_issue_number in records: - cast(IssueRecord, records[parent_issue_number]).register_pull_request(pull) - logger.debug("Registering PR %d: %s to Issue %d", pull.number, pull.title, parent_issue_number) - else: - logger.debug( - "Registering stand-alone PR %d: %s as mentioned Issue %d not found.", - pull.number, - pull.title, - parent_issue_number, - ) - - records: dict[int | str, Record] = {} - rate_limiter = GithubRateLimiter(github) - safe_call = safe_call_decorator(rate_limiter) - - logger.debug("Registering issues to records...") - for issue in data.issues: - RecordFactory._create_record_for_issue(records, issue) - - logger.debug("Registering pull requests to records...") - for pull in data.pull_requests: - pull_labels = [label.name for label in pull.get_labels()] - skip_record: bool = any(item in pull_labels for item in ActionInputs.get_skip_release_notes_labels()) - - if not safe_call(get_issues_for_pr)(pull_number=pull.number) and not extract_issue_numbers_from_body(pull): - records[pull.number] = PullRequestRecord(pull, skip=skip_record) - logger.debug("Created record for PR %d: %s", pull.number, pull.title) - else: - logger.debug("Registering pull number: %s, title : %s", pull.number, pull.title) - register_pull_request(pull, skip_record) - - logger.debug("Registering commits to records...") - detected_direct_commits_count = sum( - not RecordFactory._register_commit_to_record(records, commit) for commit in data.commits - ) - - logger.info( - "Generated %d records from %d issues and %d PRs, with %d commits detected.", - len(records), - len(data.issues), - len(data.pull_requests), - detected_direct_commits_count, - ) - return records - - @staticmethod - def _register_commit_to_record(records: dict[int | str, Record], commit: Commit) -> bool: - """ - Register a commit to a record. - - @param commit: The commit to register. - @return: True if the commit was registered to a record, False otherwise - """ - for record in records.values(): - if isinstance(record, IssueRecord): - rec_i = cast(IssueRecord, record) - for number in rec_i.get_pull_request_numbers(): - pr = rec_i.get_pull_request(number) - if pr and pr.merge_commit_sha == commit.sha: - rec_i.register_commit(pr, commit) - return True - - elif isinstance(record, PullRequestRecord): - rec_pr = cast(PullRequestRecord, record) - if rec_pr.is_commit_sha_present(commit.sha): - rec_pr.register_commit(commit) - return True - - records[commit.sha] = CommitRecord(commit=commit) - logger.debug("Created record for direct commit %s: %s", commit.sha, commit.commit.message) - return False - - @staticmethod - def _create_record_for_issue(records: dict[int | str, Record], i: Issue) -> None: - """ - Create a record for an issue. - - @param i: Issue instance. - @return: None - """ - # check for skip labels presence and skip when detected - issue_labels = [label.name for label in i.get_labels()] - skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels()) - records[i.number] = IssueRecord(issue=i, skip=skip_record) - - logger.debug("Created record for issue %d: %s", i.number, i.title) diff --git a/tests/release_notes/test_release_notes_builder.py b/tests/release_notes/test_release_notes_builder.py index 3eeaaef3..c16ec2bc 100644 --- a/tests/release_notes/test_release_notes_builder.py +++ b/tests/release_notes/test_release_notes_builder.py @@ -14,10 +14,8 @@ # limitations under the License. # -import json - from release_notes_generator.model.custom_chapters import CustomChapters -from release_notes_generator.builder import ReleaseNotesBuilder +from release_notes_generator.builders.default_builder import DefaultReleaseNotesBuilder # pylint: disable=pointless-string-statement """ @@ -323,7 +321,7 @@ def test_build_no_data(): expected_release_notes = RELEASE_NOTES_NO_DATA - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={}, # empty record data set changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters, @@ -340,7 +338,7 @@ def test_build_no_data_no_warnings(mocker): expected_release_notes = RELEASE_NOTES_NO_DATA_NO_WARNING - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={}, # empty record data set changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters, @@ -359,7 +357,7 @@ def test_build_no_data_no_warnings_no_empty_chapters(mocker): expected_release_notes = RELEASE_NOTES_NO_DATA_NO_WARNING_NO_EMPTY_CHAPTERS - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_no_empty_chapters, @@ -377,7 +375,7 @@ def test_build_no_data_no_empty_chapters(mocker): expected_release_notes = RELEASE_NOTES_NO_DATA_NO_EMPTY_CHAPTERS - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_no_empty_chapters, @@ -549,7 +547,7 @@ def test_build_closed_issue_with_one_custom_label( rec = record_with_issue_closed_two_pulls mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -569,7 +567,7 @@ def test_build_closed_issue_with_more_custom_labels_duplicity_reduction_on( rec.issue.title = "I1+bug-enhancement" mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -587,7 +585,7 @@ def test_build_closed_issue_service_chapter_without_pull_request_and_user_define rec = record_with_issue_closed_no_pull mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -605,7 +603,7 @@ def test_build_merged_pr_service_chapter_without_issue_and_user_labels( rec = pull_request_record_merged mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -623,7 +621,7 @@ def test_build_closed_pr_service_chapter_without_issue_and_user_labels( rec = pull_request_record_closed mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -641,7 +639,7 @@ def test_build_open_issue_with_merged_pr_service_chapter_linked_to_not_closed_is rec = record_with_issue_open_two_pulls_closed mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -657,7 +655,7 @@ def test_build_open_issue(custom_chapters_not_print_empty_chapters, record_with_ rec = record_with_issue_open_no_pull mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -673,7 +671,7 @@ def test_build_closed_issue(custom_chapters_not_print_empty_chapters, record_wit rec = record_with_issue_closed_no_pull mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -692,7 +690,7 @@ def test_build_reopened_issue(custom_chapters_not_print_empty_chapters, record_w rec.issue.state_reason = "reopened" mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -711,7 +709,7 @@ def test_build_closed_not_planned_issue( rec.issue.state_reason = "not_planned" mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -730,7 +728,7 @@ def test_build_closed_issue_with_user_labels_no_prs( rec._labels = {"bug", "breaking-changes"} mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -750,7 +748,7 @@ def test_build_closed_issue_with_prs_without_user_label( rec.issue.title = "I1" mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -768,7 +766,7 @@ def test_build_open_pr_without_issue( rec = pull_request_record_open mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -786,7 +784,7 @@ def test_build_merged_pr_without_issue_ready_for_review( rec = pull_request_record_merged mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -804,7 +802,7 @@ def test_build_closed_pr_without_issue_ready_for_review( rec = pull_request_record_closed mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -823,7 +821,7 @@ def test_build_closed_pr_without_issue_non_draft( rec.pull_request.draft = False mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -844,7 +842,7 @@ def test_merged_pr_without_issue_with_more_user_labels_duplicity_reduction_on( rec._labels = {"bug", "enhancement"} mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -862,7 +860,7 @@ def test_merged_pr_with_open_init_issue_mention( records = record_with_two_issue_open_two_pulls_closed mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records=records, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -880,7 +878,7 @@ def test_merged_pr_with_closed_issue_mention_without_user_labels( rec = record_with_issue_closed_one_pull mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -898,7 +896,7 @@ def test_merged_pr_with_closed_issue_mention_with_user_labels( rec = record_with_issue_closed_one_pull_merged mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -915,7 +913,7 @@ def test_merged_pr_with_closed_issue_mention_with_user_labels_with_skip_label_on rec = record_with_issue_closed_one_pull_merged_skip mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, @@ -933,7 +931,7 @@ def test_build_closed_pr_service_chapter_without_issue_with_skip_label_on_pr( rec = pull_request_record_closed_with_skip_label mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) - builder = ReleaseNotesBuilder( + builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, changelog_url=DEFAULT_CHANGELOG_URL, custom_chapters=custom_chapters_not_print_empty_chapters, From 3565619747666b3433038aa85abb7e442aceadef Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 9 Sep 2025 15:14:18 +0200 Subject: [PATCH 2/4] #165 - Introduce regimes - prepare default one - Implemented default regime. --- .../{builders => builder}/__init__.py | 0 .../{builders => builder}/base_builder.py | 0 .../{builders => builder}/default_builder.py | 2 +- release_notes_generator/generator.py | 4 +- tests/release_notes/test_record_factory.py | 31 +++++------ .../test_release_notes_builder.py | 54 +++++++++---------- tests/test_release_notes_generator.py | 6 +-- 7 files changed, 49 insertions(+), 48 deletions(-) rename release_notes_generator/{builders => builder}/__init__.py (100%) rename release_notes_generator/{builders => builder}/base_builder.py (100%) rename release_notes_generator/{builders => builder}/default_builder.py (97%) diff --git a/release_notes_generator/builders/__init__.py b/release_notes_generator/builder/__init__.py similarity index 100% rename from release_notes_generator/builders/__init__.py rename to release_notes_generator/builder/__init__.py diff --git a/release_notes_generator/builders/base_builder.py b/release_notes_generator/builder/base_builder.py similarity index 100% rename from release_notes_generator/builders/base_builder.py rename to release_notes_generator/builder/base_builder.py diff --git a/release_notes_generator/builders/default_builder.py b/release_notes_generator/builder/default_builder.py similarity index 97% rename from release_notes_generator/builders/default_builder.py rename to release_notes_generator/builder/default_builder.py index fe163215..11c72de8 100644 --- a/release_notes_generator/builders/default_builder.py +++ b/release_notes_generator/builder/default_builder.py @@ -21,7 +21,7 @@ import logging from itertools import chain -from release_notes_generator.builders.base_builder import ReleaseNotesBuilder +from release_notes_generator.builder.base_builder import ReleaseNotesBuilder from release_notes_generator.model.service_chapters import ServiceChapters logger = logging.getLogger(__name__) diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index 898165f1..fecd9997 100644 --- a/release_notes_generator/generator.py +++ b/release_notes_generator/generator.py @@ -28,8 +28,8 @@ from release_notes_generator.filter import FilterByRelease from release_notes_generator.miner import DataMiner from release_notes_generator.action_inputs import ActionInputs -from release_notes_generator.builders.base_builder import ReleaseNotesBuilder -from release_notes_generator.builders.default_builder import DefaultReleaseNotesBuilder +from release_notes_generator.builder.base_builder import ReleaseNotesBuilder +from release_notes_generator.builder.default_builder import DefaultReleaseNotesBuilder from release_notes_generator.model.custom_chapters import CustomChapters from release_notes_generator.model.record import Record from release_notes_generator.record.default_record_factory import DefaultRecordFactory diff --git a/tests/release_notes/test_record_factory.py b/tests/release_notes/test_record_factory.py index 7c390694..70a4cd66 100644 --- a/tests/release_notes/test_record_factory.py +++ b/tests/release_notes/test_record_factory.py @@ -28,6 +28,7 @@ from release_notes_generator.model.issue_record import IssueRecord from release_notes_generator.model.mined_data import MinedData from release_notes_generator.model.pull_request_record import PullRequestRecord +from release_notes_generator.record.default_record_factory import DefaultRecordFactory from release_notes_generator.record.record_factory import RecordFactory @@ -171,7 +172,7 @@ def mock_get_issues_for_pr(pull_number: int) -> list[int]: return [] def test_generate_with_issues_and_pulls_and_commits(mocker, mock_repo): - mocker.patch("release_notes_generator.record.record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) + mocker.patch("release_notes_generator.record.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) mock_github_client = mocker.Mock(spec=Github) issue1, issue2, pr1, pr2, commit1, commit2 = setup_issues_pulls_commits(mocker) @@ -188,7 +189,7 @@ def test_generate_with_issues_and_pulls_and_commits(mocker, mock_repo): data.commits = [commit1, commit2, commit3] data.repository = mock_repo - records = RecordFactory.generate(mock_github_client, data) + records = DefaultRecordFactory().generate(mock_github_client, data) # Check if records for issues and PRs were created assert len(records) == 3 @@ -208,8 +209,8 @@ def test_generate_with_issues_and_pulls_and_commits(mocker, mock_repo): assert commit1 == rec_i1.get_commit(101, 'abc123') def test_generate_with_issues_and_pulls_and_commits_with_skip_labels(mocker, mock_repo): - mocker.patch("release_notes_generator.builder.ActionInputs.get_skip_release_notes_labels", return_value=["skip-release-notes"]) - mocker.patch("release_notes_generator.record.record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) + mocker.patch("release_notes_generator.record.default_record_factory.ActionInputs.get_skip_release_notes_labels", return_value=["skip-release-notes"]) + mocker.patch("release_notes_generator.record.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) mock_github_client = mocker.Mock(spec=Github) issue1, issue2, pr1, pr2, commit1, commit2 = setup_issues_pulls_commits(mocker) @@ -228,7 +229,7 @@ def test_generate_with_issues_and_pulls_and_commits_with_skip_labels(mocker, moc data.pull_requests = [pr1, pr2] data.commits = [commit1, commit2, commit3] - records = RecordFactory.generate(mock_github_client,data) + records = DefaultRecordFactory().generate(mock_github_client,data) # Check if records for issues and PRs were created assert len(records) == 3 @@ -277,8 +278,8 @@ def test_generate_with_no_commits(mocker, mock_repo): data.commits = [] # No commits data.repository = mock_repo - mocker.patch("release_notes_generator.record.record_factory.get_issues_for_pr", return_value=[2]) - records = RecordFactory.generate(mock_github_client, data) + mocker.patch("release_notes_generator.record.default_record_factory.get_issues_for_pr", return_value=[2]) + records = DefaultRecordFactory().generate(mock_github_client, data) assert 2 == len(records) @@ -312,8 +313,8 @@ def test_generate_with_no_commits_with_wrong_issue_number_in_pull_body_mention(m data.commits = [] # No commits data.repository = mock_repo - mocker.patch("release_notes_generator.record.record_factory.get_issues_for_pr", return_value=[2]) - records = RecordFactory.generate(mock_github_client, data) + mocker.patch("release_notes_generator.record.default_record_factory.get_issues_for_pr", return_value=[2]) + records = DefaultRecordFactory().generate(mock_github_client, data) assert 2 == len(records) @@ -342,7 +343,7 @@ def mock_get_issues_for_pr_no_issues(pull_number: int) -> list[int]: def test_generate_with_no_issues(mocker, request): - mocker.patch("release_notes_generator.record.record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator_no_issues) + mocker.patch("release_notes_generator.record.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator_no_issues) mock_github_client = mocker.Mock(spec=Github) data = MinedData() pr1, pr2, commit1, commit2 = setup_no_issues_pulls_commits(mocker) @@ -351,7 +352,7 @@ def test_generate_with_no_issues(mocker, request): data.repository = request.getfixturevalue("mock_repo") data.issues = [] # No issues - records = RecordFactory.generate(mock_github_client, data) + records = DefaultRecordFactory().generate(mock_github_client, data) # Verify the record creation assert 2 == len(records) @@ -373,8 +374,8 @@ def test_generate_with_no_issues(mocker, request): assert commit2 == rec_pr2.get_commit('def456') def test_generate_with_no_issues_skip_labels(mocker, request): - mocker.patch("release_notes_generator.builder.ActionInputs.get_skip_release_notes_labels", return_value=["skip-release-notes", "another-skip-label"]) - mocker.patch("release_notes_generator.record.record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator_no_issues) + mocker.patch("release_notes_generator.record.default_record_factory.ActionInputs.get_skip_release_notes_labels", return_value=["skip-release-notes", "another-skip-label"]) + mocker.patch("release_notes_generator.record.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator_no_issues) mock_github_client = mocker.Mock(spec=Github) data = MinedData() pr1, pr2, commit1, commit2 = setup_no_issues_pulls_commits(mocker) @@ -393,7 +394,7 @@ def test_generate_with_no_issues_skip_labels(mocker, request): data.repository = request.getfixturevalue("mock_repo") data.issues = [] # No issues - records = RecordFactory.generate(mock_github_client, data) + records = DefaultRecordFactory().generate(mock_github_client, data) # Verify the record creation assert 2 == len(records) @@ -423,7 +424,7 @@ def test_generate_with_no_pulls(mocker, mock_repo): data.repository = mock_repo data.pull_requests = [] # No pull requests data.commits = [] # No commits - records = RecordFactory.generate(mock_github_client, data) + records = DefaultRecordFactory().generate(mock_github_client, data) # Verify the record creation assert 2 == len(records) diff --git a/tests/release_notes/test_release_notes_builder.py b/tests/release_notes/test_release_notes_builder.py index c16ec2bc..8f552c16 100644 --- a/tests/release_notes/test_release_notes_builder.py +++ b/tests/release_notes/test_release_notes_builder.py @@ -15,7 +15,7 @@ # from release_notes_generator.model.custom_chapters import CustomChapters -from release_notes_generator.builders.default_builder import DefaultReleaseNotesBuilder +from release_notes_generator.builder.default_builder import DefaultReleaseNotesBuilder # pylint: disable=pointless-string-statement """ @@ -334,7 +334,7 @@ def test_build_no_data(): def test_build_no_data_no_warnings(mocker): custom_chapters = CustomChapters() custom_chapters.from_yaml_array(default_chapters) - mocker.patch("release_notes_generator.builder.ActionInputs.get_warnings", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_warnings", return_value=False) expected_release_notes = RELEASE_NOTES_NO_DATA_NO_WARNING @@ -352,8 +352,8 @@ def test_build_no_data_no_warnings_no_empty_chapters(mocker): custom_chapters_no_empty_chapters = CustomChapters() custom_chapters_no_empty_chapters.from_yaml_array(default_chapters) custom_chapters_no_empty_chapters.print_empty_chapters = False - mocker.patch("release_notes_generator.builder.ActionInputs.get_warnings", return_value=False) - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_warnings", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) expected_release_notes = RELEASE_NOTES_NO_DATA_NO_WARNING_NO_EMPTY_CHAPTERS @@ -371,7 +371,7 @@ def test_build_no_data_no_empty_chapters(mocker): custom_chapters_no_empty_chapters = CustomChapters() custom_chapters_no_empty_chapters.from_yaml_array(default_chapters) custom_chapters_no_empty_chapters.print_empty_chapters = False - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) expected_release_notes = RELEASE_NOTES_NO_DATA_NO_EMPTY_CHAPTERS @@ -545,7 +545,7 @@ def test_build_closed_issue_with_one_custom_label( ): expected_release_notes = RELEASE_NOTES_DATA_CUSTOM_CHAPTERS_ONE_LABEL rec = record_with_issue_closed_two_pulls - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -565,7 +565,7 @@ def test_build_closed_issue_with_more_custom_labels_duplicity_reduction_on( rec = record_with_issue_closed_two_pulls rec.issue.labels.append(MockLabel("enhancement")) rec.issue.title = "I1+bug-enhancement" - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -583,7 +583,7 @@ def test_build_closed_issue_service_chapter_without_pull_request_and_user_define ): expected_release_notes = RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_ISSUE_NO_PR_NO_USER_LABELS rec = record_with_issue_closed_no_pull - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -601,7 +601,7 @@ def test_build_merged_pr_service_chapter_without_issue_and_user_labels( ): expected_release_notes = RELEASE_NOTES_DATA_SERVICE_CHAPTERS_MERGED_PR_NO_ISSUE_NO_USER_LABELS rec = pull_request_record_merged - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -619,7 +619,7 @@ def test_build_closed_pr_service_chapter_without_issue_and_user_labels( ): expected_release_notes = RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_PR_NO_ISSUE_NO_USER_LABELS rec = pull_request_record_closed - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -637,7 +637,7 @@ def test_build_open_issue_with_merged_pr_service_chapter_linked_to_not_closed_is ): expected_release_notes = RELEASE_NOTES_DATA_SERVICE_CHAPTERS_OPEN_ISSUE_AND_MERGED_PR_NO_USER_LABELS rec = record_with_issue_open_two_pulls_closed - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -653,7 +653,7 @@ def test_build_open_issue_with_merged_pr_service_chapter_linked_to_not_closed_is def test_build_open_issue(custom_chapters_not_print_empty_chapters, record_with_issue_open_no_pull, mocker): expected_release_notes = RELEASE_NOTES_NO_DATA_NO_WARNING_NO_EMPTY_CHAPTERS rec = record_with_issue_open_no_pull - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -669,7 +669,7 @@ def test_build_open_issue(custom_chapters_not_print_empty_chapters, record_with_ def test_build_closed_issue(custom_chapters_not_print_empty_chapters, record_with_issue_closed_no_pull, mocker): expected_release_notes = RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_ISSUE_NO_PR_NO_USER_LABELS rec = record_with_issue_closed_no_pull - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -688,7 +688,7 @@ def test_build_reopened_issue(custom_chapters_not_print_empty_chapters, record_w expected_release_notes = RELEASE_NOTES_NO_DATA_NO_WARNING_NO_EMPTY_CHAPTERS rec = record_with_issue_open_no_pull rec.issue.state_reason = "reopened" - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -707,7 +707,7 @@ def test_build_closed_not_planned_issue( expected_release_notes = RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_ISSUE_NO_PR_NO_USER_LABELS rec = record_with_issue_closed_no_pull rec.issue.state_reason = "not_planned" - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -726,7 +726,7 @@ def test_build_closed_issue_with_user_labels_no_prs( expected_release_notes = RELEASE_NOTES_DATA_CLOSED_ISSUE_NO_PR_WITH_USER_LABELS rec = record_with_issue_closed_no_pull rec._labels = {"bug", "breaking-changes"} - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -746,7 +746,7 @@ def test_build_closed_issue_with_prs_without_user_label( rec = record_with_issue_closed_two_pulls rec._labels = {"label1", "label2"} rec.issue.title = "I1" - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -764,7 +764,7 @@ def test_build_open_pr_without_issue( ): expected_release_notes = RELEASE_NOTES_DATA_OPEN_PR_WITHOUT_ISSUE rec = pull_request_record_open - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -782,7 +782,7 @@ def test_build_merged_pr_without_issue_ready_for_review( ): expected_release_notes = RELEASE_NOTES_DATA_SERVICE_CHAPTERS_MERGED_PR_NO_ISSUE_NO_USER_LABELS rec = pull_request_record_merged - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -800,7 +800,7 @@ def test_build_closed_pr_without_issue_ready_for_review( ): expected_release_notes = RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_PR_NO_ISSUE_NO_USER_LABELS rec = pull_request_record_closed - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -819,7 +819,7 @@ def test_build_closed_pr_without_issue_non_draft( expected_release_notes = RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_PR_NO_ISSUE_NO_USER_LABELS rec = pull_request_record_closed rec.pull_request.draft = False - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -840,7 +840,7 @@ def test_merged_pr_without_issue_with_more_user_labels_duplicity_reduction_on( expected_release_notes = RELEASE_NOTES_DATA_MERGED_PR_WITH_USER_LABELS_DUPLICITY_REDUCTION_ON rec = pull_request_record_merged rec._labels = {"bug", "enhancement"} - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -858,7 +858,7 @@ def test_merged_pr_with_open_init_issue_mention( ): expected_release_notes = RELEASE_NOTES_DATA_MERGED_PRS_WITH_OPEN_ISSUES records = record_with_two_issue_open_two_pulls_closed - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records=records, @@ -876,7 +876,7 @@ def test_merged_pr_with_closed_issue_mention_without_user_labels( ): expected_release_notes = RELEASE_NOTES_DATA_CLOSED_ISSUE_WITH_MERGED_PRS_WITHOUT_USER_LABELS rec = record_with_issue_closed_one_pull - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -894,7 +894,7 @@ def test_merged_pr_with_closed_issue_mention_with_user_labels( ): expected_release_notes = RELEASE_NOTES_DATA_CLOSED_ISSUE_WITH_MERGED_PRS_WITH_USER_LABELS rec = record_with_issue_closed_one_pull_merged - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -911,7 +911,7 @@ def test_merged_pr_with_closed_issue_mention_with_user_labels_with_skip_label_on ): expected_release_notes = RELEASE_NOTES_DATA_CLOSED_ISSUE_WITH_MERGED_PRS_WITH_USER_LABELS_WITH_SKIP_LABEL rec = record_with_issue_closed_one_pull_merged_skip - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, @@ -929,7 +929,7 @@ def test_build_closed_pr_service_chapter_without_issue_with_skip_label_on_pr( ): expected_release_notes = RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_PR_NO_ISSUE_SKIP_USER_LABELS rec = pull_request_record_closed_with_skip_label - mocker.patch("release_notes_generator.builder.ActionInputs.get_print_empty_chapters", return_value=False) + mocker.patch("release_notes_generator.builder.base_builder.ActionInputs.get_print_empty_chapters", return_value=False) builder = DefaultReleaseNotesBuilder( records={rec.record_id: rec}, diff --git a/tests/test_release_notes_generator.py b/tests/test_release_notes_generator.py index d13ac952..1103f65f 100644 --- a/tests/test_release_notes_generator.py +++ b/tests/test_release_notes_generator.py @@ -67,7 +67,7 @@ def test_generate_release_notes_latest_release_not_found( mock_pull_closed_with_rls_notes_102.merged_at = mock_repo.created_at + timedelta(days=7) mocker.patch("release_notes_generator.miner.DataMiner.get_latest_release", return_value=None) - mocker.patch("release_notes_generator.record.record_factory.get_issues_for_pr", return_value=[]) + mocker.patch("release_notes_generator.record.default_record_factory.get_issues_for_pr", return_value=[]) mock_rate_limit = mocker.Mock() mock_rate_limit.rate.remaining = 1000 github_mock.get_rate_limit.return_value = mock_rate_limit @@ -112,7 +112,7 @@ def test_generate_release_notes_latest_release_found_by_created_at( 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.miner.DataMiner.get_latest_release", return_value=mock_git_release) - mocker.patch("release_notes_generator.record.record_factory.get_issues_for_pr", return_value=[]) + mocker.patch("release_notes_generator.record.default_record_factory.get_issues_for_pr", return_value=[]) mock_rate_limit = mocker.Mock() mock_rate_limit.rate.remaining = 1000 @@ -164,7 +164,7 @@ def test_generate_release_notes_latest_release_found_by_published_at( 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.miner.DataMiner.get_latest_release", return_value=mock_git_release) - mocker.patch("release_notes_generator.record.record_factory.get_issues_for_pr", return_value=[]) + mocker.patch("release_notes_generator.record.default_record_factory.get_issues_for_pr", return_value=[]) mock_rate_limit = mocker.Mock() mock_rate_limit.rate.remaining = 1000 From 983164942f6cf9578329121f016f6b630cf45988 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 9 Sep 2025 15:17:09 +0200 Subject: [PATCH 3/4] Fixed black. --- release_notes_generator/action_inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index 22501886..123c7589 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -167,7 +167,7 @@ def get_regime() -> str: """ Get the regime parameter value from the action inputs. """ - return get_action_input("regime", "default") # type: ignore[return-value] # default defined + return get_action_input("regime", "default") # type: ignore[return-value] # default defined @staticmethod def get_duplicity_scope() -> DuplicityScopeEnum: From bb11d127bada1c83dd9bf520cbe3d5d07040218d Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 9 Sep 2025 15:27:33 +0200 Subject: [PATCH 4/4] Manual test and improved unit tests. --- release_notes_generator/action_inputs.py | 2 ++ tests/test_action_inputs.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index 123c7589..8ad92019 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -376,6 +376,8 @@ def validate_inputs() -> None: regime = ActionInputs.get_regime() ActionInputs.validate_input(regime, str, "Regime must be a string.", errors) + if regime not in [ActionInputs.REGIME_DEFAULT]: + errors.append(f"Regime '{regime}' is not supported.") warnings = ActionInputs.get_warnings() ActionInputs.validate_input(warnings, bool, "Warnings must be a boolean.", errors) diff --git a/tests/test_action_inputs.py b/tests/test_action_inputs.py index 28d8afea..8eb2373c 100644 --- a/tests/test_action_inputs.py +++ b/tests/test_action_inputs.py @@ -25,6 +25,7 @@ "get_tag_name": "tag_name", "get_from_tag_name": "from_tag_name", "get_chapters": [{"title": "Title", "label": "Label"}], + "get_regime": "default", "get_duplicity_scope": "custom", "get_duplicity_icon": "🔁", "get_warnings": True, @@ -55,6 +56,7 @@ ("get_release_notes_title", "", "Release Notes title must be a non-empty string and have non-zero length."), ("get_coderabbit_release_notes_title", "", "CodeRabbit Release Notes title must be a non-empty string and have non-zero length."), ("get_coderabbit_summary_ignore_groups", [""], "CodeRabbit summary ignore groups must be a non-empty string and have non-zero length."), + ("get_regime", "not_supported", "Regime 'not_supported' is not supported."), ]