Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
4 changes: 3 additions & 1 deletion docs/configuration_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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` |
Expand Down
35 changes: 32 additions & 3 deletions docs/features/service_chapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ⚠️
Expand Down
22 changes: 22 additions & 0 deletions release_notes_generator/action_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
PUBLISHED_AT,
VERBOSE,
WARNINGS,
HIDDEN_SERVICE_CHAPTERS,
RUNNER_DEBUG,
PRINT_EMPTY_CHAPTERS,
DUPLICITY_SCOPE,
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions release_notes_generator/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
24 changes: 24 additions & 0 deletions release_notes_generator/chapters/service_chapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method has much more logic than just to_string. I do suggest something like serialize_chapters.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method override parent method to_string to keep the expected contract.

"""
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()
1 change: 1 addition & 0 deletions release_notes_generator/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

# Features
WARNINGS = "warnings"
HIDDEN_SERVICE_CHAPTERS = "hidden-service-chapters"
PRINT_EMPTY_CHAPTERS = "print-empty-chapters"

# Release notes comment constants
Expand Down
6 changes: 5 additions & 1 deletion tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
37 changes: 37 additions & 0 deletions tests/unit/release_notes_generator/test_action_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."),
]

Expand Down Expand Up @@ -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

Expand Down