diff --git a/action.yml b/action.yml index eeeec240..5088e954 100644 --- a/action.yml +++ b/action.yml @@ -52,6 +52,21 @@ inputs: description: 'Print service chapters if true.' required: false default: 'true' + hidden-service-chapters: + description: | + List of service chapter titles to hide from output (comma or newline separated). + Title matching is exact and case-sensitive. + Example: "Direct Commits ⚠️, Others - No Topic ⚠️" + Available service chapters: + - "Closed Issues without Pull Request ⚠️" + - "Closed Issues without User Defined Labels ⚠️" + - "Merged PRs without Issue and User Defined Labels ⚠️" + - "Closed PRs without Issue and User Defined Labels ⚠️" + - "Merged PRs Linked to 'Not Closed' Issue ⚠️" + - "Direct Commits ⚠️" + - "Others - No Topic ⚠️" + required: false + default: '' print-empty-chapters: description: 'Print chapters even if they are empty.' required: false @@ -145,6 +160,7 @@ runs: INPUT_DUPLICITY_SCOPE: ${{ inputs.duplicity-scope }} INPUT_DUPLICITY_ICON: ${{ inputs.duplicity-icon }} INPUT_WARNINGS: ${{ inputs.warnings }} + INPUT_HIDDEN_SERVICE_CHAPTERS: ${{ inputs.hidden-service-chapters }} INPUT_PUBLISHED_AT: ${{ inputs.published-at }} INPUT_SKIP_RELEASE_NOTES_LABELS: ${{ inputs.skip-release-notes-labels }} INPUT_PRINT_EMPTY_CHAPTERS: ${{ inputs.print-empty-chapters }} diff --git a/docs/configuration_reference.md b/docs/configuration_reference.md index 73797d68..868fcd1b 100644 --- a/docs/configuration_reference.md +++ b/docs/configuration_reference.md @@ -13,6 +13,7 @@ This page lists all action inputs and outputs with defaults. Grouped for readabi | `published-at` | No | `false` | Use previous release `published_at` timestamp instead of `created_at`. | | `skip-release-notes-labels` | No | `skip-release-notes` | Comma‑separated labels that fully exclude issues/PRs. | | `warnings` | No | `true` | Toggle Service Chapters generation. | +| `hidden-service-chapters` | No | "" | Comma or newline list of service chapter titles to hide from output. Title matching is exact and case-sensitive. Only effective when `warnings: true`. | | `print-empty-chapters` | No | `true` | Print chapter headings even when empty. | | `duplicity-scope` | No | `both` | Where duplicates are allowed: `none`, `custom`, `service`, `both`. Case-insensitive. | | `duplicity-icon` | No | `🔔` | One-character icon prefixed on duplicate rows. | @@ -91,7 +92,8 @@ Controlled by `duplicity-scope` and `duplicity-icon` (see [Duplicity Handling](f | Basic release notes | `tag-name`, `chapters` | | Restrict time window manually | Add `from-tag-name` | | Prefer published timestamp | `published-at: true` | -| Hide diagnostics | `warnings: false` | +| Hide all service chapters | `warnings: false` | +| Hide specific service chapters | `hidden-service-chapters: "Direct Commits ⚠️, Others - No Topic ⚠️"` | | Tight output (no empty headings) | `print-empty-chapters: false` | | Enforce no duplicates | `duplicity-scope: none` | | Enable hierarchy rollups | `hierarchy: true` | diff --git a/docs/features/service_chapters.md b/docs/features/service_chapters.md index 267626be..333df825 100644 --- a/docs/features/service_chapters.md +++ b/docs/features/service_chapters.md @@ -34,11 +34,40 @@ Highlight quality gaps or inconsistencies in the release scope: missing PR for c chapters: | - {"title": "New Features 🎉", "label": "feature"} - {"title": "Bugfixes 🛠", "label": "bug"} - warnings: true # enable service chapters (default) - print-empty-chapters: true # show even when empty (default) - duplicity-scope: "both" # allow duplicates across custom + service + warnings: true # enable service chapters (default) + hidden-service-chapters: '' # hide specific service chapters (default: empty) + print-empty-chapters: true # show even when empty (default) + duplicity-scope: "both" # allow duplicates across custom + service ``` +### Granular Chapter Control +Use `hidden-service-chapters` to selectively hide individual service chapters: + +```yaml +- name: Generate Release Notes + with: + warnings: true + hidden-service-chapters: | + Direct Commits ⚠️ + Others - No Topic ⚠️ +``` + +Or use comma-separated format: +```yaml + hidden-service-chapters: "Direct Commits ⚠️, Others - No Topic ⚠️" +``` + +**Available service chapter titles:** +- `Closed Issues without Pull Request ⚠️` +- `Closed Issues without User Defined Labels ⚠️` +- `Merged PRs without Issue and User Defined Labels ⚠️` +- `Closed PRs without Issue and User Defined Labels ⚠️` +- `Merged PRs Linked to 'Not Closed' Issue ⚠️` +- `Direct Commits ⚠️` +- `Others - No Topic ⚠️` + +**Note:** Title matching is exact and case-sensitive. When `warnings: false`, all service chapters are hidden regardless of the `hidden-service-chapters` setting. + ## Example Result ```markdown ### Closed Issues without Pull Request ⚠️ diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index acf71284..d7e7f018 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -33,6 +33,7 @@ PUBLISHED_AT, VERBOSE, WARNINGS, + HIDDEN_SERVICE_CHAPTERS, RUNNER_DEBUG, PRINT_EMPTY_CHAPTERS, DUPLICITY_SCOPE, @@ -279,6 +280,26 @@ def get_warnings() -> bool: return get_action_input(WARNINGS, "true").lower() == "true" # type: ignore[union-attr] # mypy: string is returned as default + @staticmethod + def get_hidden_service_chapters() -> list[str]: + """ + Get the list of service chapter titles to hide from the action inputs. + Returns a list of chapter titles that should be hidden from output. + """ + hidden_chapters: list[str] = [] + raw = get_action_input(HIDDEN_SERVICE_CHAPTERS, "") + if not isinstance(raw, str): + logger.error("Error: 'hidden-service-chapters' is not a valid string.") + return hidden_chapters + + titles = raw.strip() + if titles: + # Support both comma and newline separators + separator = "," if "," in titles else "\n" + hidden_chapters = [title.strip() for title in titles.split(separator) if title.strip()] + + return hidden_chapters + @staticmethod def get_print_empty_chapters() -> bool: """ @@ -473,6 +494,7 @@ def validate_inputs() -> None: logger.debug("Skip release notes labels: %s", ActionInputs.get_skip_release_notes_labels()) logger.debug("Verbose logging: %s", verbose) logger.debug("Warnings: %s", warnings) + logger.debug("Hidden service chapters: %s", ActionInputs.get_hidden_service_chapters()) logger.debug("Print empty chapters: %s", print_empty_chapters) logger.debug("Release notes title: %s", release_notes_title) logger.debug("CodeRabbit support active: %s", coderabbit_support_active) diff --git a/release_notes_generator/builder/builder.py b/release_notes_generator/builder/builder.py index 325ebb55..6f09c0e4 100644 --- a/release_notes_generator/builder/builder.py +++ b/release_notes_generator/builder/builder.py @@ -69,6 +69,7 @@ def build(self) -> str: print_empty_chapters=self.print_empty_chapters, user_defined_labels=user_defined_labels, used_record_numbers=self.custom_chapters.populated_record_numbers_list, + hidden_chapters=ActionInputs.get_hidden_service_chapters(), ) service_chapters.populate(self.records) diff --git a/release_notes_generator/chapters/service_chapters.py b/release_notes_generator/chapters/service_chapters.py index 308dd3ec..4b166ca1 100644 --- a/release_notes_generator/chapters/service_chapters.py +++ b/release_notes_generator/chapters/service_chapters.py @@ -56,12 +56,14 @@ def __init__( print_empty_chapters: bool = True, user_defined_labels: Optional[list[str]] = None, used_record_numbers: Optional[list[int | str]] = None, + hidden_chapters: Optional[list[str]] = None, ): super().__init__(sort_ascending, print_empty_chapters) self.user_defined_labels = user_defined_labels if user_defined_labels is not None else [] self.sort_ascending = sort_ascending self.used_record_numbers: list[int | str] = used_record_numbers if used_record_numbers is not None else [] + self.hidden_chapters: list[str] = hidden_chapters if hidden_chapters is not None else [] self.chapters = { CLOSED_ISSUES_WITHOUT_PULL_REQUESTS: Chapter( @@ -293,3 +295,25 @@ def duplicity_allowed() -> bool: @return: True if duplicity is allowed, False otherwise. """ return ActionInputs.get_duplicity_scope() in (DuplicityScopeEnum.SERVICE, DuplicityScopeEnum.BOTH) + + def to_string(self) -> str: + """ + Converts the chapters to a string, excluding hidden chapters. + + @return: The chapters as a string. + """ + result = "" + for chapter in self.chapters.values(): + # Skip chapters that are in the hidden list + if chapter.title in self.hidden_chapters: + logger.debug("Skipping hidden service chapter: %s", chapter.title) + continue + + chapter_string = chapter.to_string( + sort_ascending=self.sort_ascending, print_empty_chapters=self.print_empty_chapters + ) + if chapter_string: + result += chapter_string + "\n\n" + + # Note: strip is required to remove leading newline chars when empty chapters are not printed option + return result.strip() diff --git a/release_notes_generator/utils/constants.py b/release_notes_generator/utils/constants.py index 67833b68..81343ce6 100644 --- a/release_notes_generator/utils/constants.py +++ b/release_notes_generator/utils/constants.py @@ -44,6 +44,7 @@ # Features WARNINGS = "warnings" +HIDDEN_SERVICE_CHAPTERS = "hidden-service-chapters" PRINT_EMPTY_CHAPTERS = "print-empty-chapters" # Release notes comment constants diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 063d2529..2f7fdd6e 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -96,7 +96,11 @@ def custom_chapters_not_print_empty_chapters(): # Fixtures for Service Chapters @pytest.fixture def service_chapters(): - return ServiceChapters(sort_ascending=True, print_empty_chapters=True, user_defined_labels=["bug", "enhancement"]) + return ServiceChapters( + sort_ascending=True, + print_empty_chapters=True, + user_defined_labels=["bug", "enhancement"], + ) # Fixtures for GitHub Repository diff --git a/tests/unit/release_notes_generator/chapters/test_service_chapters.py b/tests/unit/release_notes_generator/chapters/test_service_chapters.py index 442959d4..dd022750 100644 --- a/tests/unit/release_notes_generator/chapters/test_service_chapters.py +++ b/tests/unit/release_notes_generator/chapters/test_service_chapters.py @@ -15,6 +15,7 @@ # from release_notes_generator.model.chapter import Chapter +from release_notes_generator.chapters.service_chapters import ServiceChapters from release_notes_generator.utils.constants import ( MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES, CLOSED_ISSUES_WITHOUT_PULL_REQUESTS, @@ -105,3 +106,63 @@ def test_populate_closed_issue_duplicity(service_chapters, record_with_issue_clo assert 0 == len(service_chapters.chapters[CLOSED_ISSUES_WITHOUT_PULL_REQUESTS].rows) assert 0 == len(service_chapters.chapters[CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS].rows) + + +# to_string with hidden chapters + + +def test_to_string_with_no_hidden_chapters(service_chapters, record_with_issue_closed_no_pull): + service_chapters.populate({1: record_with_issue_closed_no_pull}) + result = service_chapters.to_string() + + assert CLOSED_ISSUES_WITHOUT_PULL_REQUESTS in result + assert CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS in result + + +def test_to_string_with_single_hidden_chapter(record_with_issue_closed_no_pull): + service_chapters = ServiceChapters( + sort_ascending=True, + print_empty_chapters=True, + user_defined_labels=["bug", "enhancement"], + hidden_chapters=[CLOSED_ISSUES_WITHOUT_PULL_REQUESTS], + ) + service_chapters.populate({1: record_with_issue_closed_no_pull}) + result = service_chapters.to_string() + + assert CLOSED_ISSUES_WITHOUT_PULL_REQUESTS not in result + assert CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS in result + + +def test_to_string_with_multiple_hidden_chapters(record_with_issue_closed_no_pull): + service_chapters = ServiceChapters( + sort_ascending=True, + print_empty_chapters=True, + user_defined_labels=["bug", "enhancement"], + hidden_chapters=[ + CLOSED_ISSUES_WITHOUT_PULL_REQUESTS, + CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS, + ], + ) + service_chapters.populate({1: record_with_issue_closed_no_pull}) + result = service_chapters.to_string() + + assert CLOSED_ISSUES_WITHOUT_PULL_REQUESTS not in result + assert CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS not in result + # Other empty chapters should still be shown since print_empty_chapters=True + assert MERGED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS in result + + +def test_to_string_with_hidden_chapter_not_in_results(pull_request_record_merged): + service_chapters = ServiceChapters( + sort_ascending=True, + print_empty_chapters=True, + user_defined_labels=["bug", "enhancement"], + hidden_chapters=[CLOSED_ISSUES_WITHOUT_PULL_REQUESTS], # This chapter won't be populated + ) + service_chapters.populate({123: pull_request_record_merged}) + result = service_chapters.to_string() + + # CLOSED_ISSUES_WITHOUT_PULL_REQUESTS shouldn't appear anyway (not populated) + assert CLOSED_ISSUES_WITHOUT_PULL_REQUESTS not in result + # MERGED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS should appear + assert MERGED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS in result diff --git a/tests/unit/release_notes_generator/test_action_inputs.py b/tests/unit/release_notes_generator/test_action_inputs.py index a58b5ff9..b9865127 100644 --- a/tests/unit/release_notes_generator/test_action_inputs.py +++ b/tests/unit/release_notes_generator/test_action_inputs.py @@ -54,6 +54,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_row_format_link_pr", "not_bool", "'row-format-link-pr' value must be a boolean."), ("get_hierarchy", "not_bool", "Hierarchy must be a boolean."), ] @@ -282,6 +283,42 @@ def test_coderabbit_summary_ignore_groups_empty_group_input(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value=",") # Note: this is not valid input which is catched by the validation_inputs() method assert ActionInputs.get_coderabbit_summary_ignore_groups() == ['', ''] + +def test_get_hidden_service_chapters_default(mocker): + mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="") + assert ActionInputs.get_hidden_service_chapters() == [] + +def test_get_hidden_service_chapters_single_comma_separated(mocker): + mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="Direct Commits ⚠️") + assert ActionInputs.get_hidden_service_chapters() == ["Direct Commits ⚠️"] + +def test_get_hidden_service_chapters_multiple_comma_separated(mocker): + mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="Direct Commits ⚠️, Others - No Topic ⚠️") + assert ActionInputs.get_hidden_service_chapters() == ["Direct Commits ⚠️", "Others - No Topic ⚠️"] + +def test_get_hidden_service_chapters_newline_separated(mocker): + mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="Direct Commits ⚠️\nOthers - No Topic ⚠️") + assert ActionInputs.get_hidden_service_chapters() == ["Direct Commits ⚠️", "Others - No Topic ⚠️"] + +def test_get_hidden_service_chapters_with_extra_whitespace(mocker): + mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value=" Direct Commits ⚠️ , Others - No Topic ⚠️ ") + assert ActionInputs.get_hidden_service_chapters() == ["Direct Commits ⚠️", "Others - No Topic ⚠️"] + +def test_get_hidden_service_chapters_int_input(mocker): + mock_log_error = mocker.patch("release_notes_generator.action_inputs.logger.error") + mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value=123) + assert ActionInputs.get_hidden_service_chapters() == [] + mock_log_error.assert_called_once() + assert "hidden-service-chapters' is not a valid string" in mock_log_error.call_args[0][0] + +def test_get_row_format_link_pr_true(mocker): + mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="true") + assert ActionInputs.get_row_format_link_pr() is True + +def test_get_row_format_link_pr_false(mocker): + mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="false") + assert ActionInputs.get_row_format_link_pr() is False + # Mirrored test file for release_notes_generator/generator.py # Extracted from previous aggregated test_release_notes_generator.py