Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ jobs:
{ title: Bugfixes 🛠, label: bug }
]
INPUT_WARNINGS: 'true'
INPUT_PRINT_EMPTY_CHAPTERS: 'false'
INPUT_PRINT_EMPTY_CHAPTERS: 'true'
INPUT_VERBOSE: 'true'
INPUT_HIERARCHY: 'false'
INPUT_DUPLICITY_SCOPE: 'both'
Expand Down
17 changes: 16 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ inputs:
- "Others - No Topic ⚠️"
required: false
default: ''
service-chapter-exclude:
description: |
YAML mapping of service chapter title to label-exclusion groups.
Each group is a list of labels (AND logic within a group).
Multiple groups per chapter are evaluated with OR logic.
Use the reserved key "*" to define global rules that apply to all service chapters.
Example:
service-chapter-exclude: |
"*":
- [scope:security, type:tech-debt]
Closed Issues without Pull Request ⚠️:
- [scope:security, type:false-positive]
required: false
default: ''
Comment thread
coderabbitai[bot] marked this conversation as resolved.
print-empty-chapters:
description: 'Print chapters even if they are empty.'
required: false
Expand Down Expand Up @@ -128,7 +142,7 @@ inputs:
row-format-hierarchy-issue:
description: 'Format of the hierarchy issue in the release notes. Available placeholders: {type}, {number}, {title}, {author}, {assignees}, {developers}. Placeholders are case-insensitive.'
required: false
default: '{type}: _{title}_ {number}'
default: '{type}: _{title}_ {number} {progress}'
row-format-issue:
description: 'Format of the issue row in the release notes. Available placeholders: {type}, {number}, {title}, {author}, {assignees}, {developers}, {pull-requests}. Placeholders are case-insensitive.'
required: false
Expand Down Expand Up @@ -198,6 +212,7 @@ runs:
INPUT_WARNINGS: ${{ inputs.warnings }}
INPUT_HIDDEN_SERVICE_CHAPTERS: ${{ inputs.hidden-service-chapters }}
INPUT_SERVICE_CHAPTER_ORDER: ${{ inputs.service-chapter-order }}
INPUT_SERVICE_CHAPTER_EXCLUDE: ${{ inputs.service-chapter-exclude }}
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
1 change: 1 addition & 0 deletions docs/configuration_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This page lists all action inputs and outputs with defaults. Grouped for readabi
| `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`. |
| `service-chapter-order` | No | "" | Comma or newline list of service chapter titles controlling display order. Listed titles render first; unlisted titles appended in default order. Title matching is exact and case-sensitive. Only effective when `warnings: true`. |
| `service-chapter-exclude` | No | "" | YAML mapping of service chapter title to label-exclusion groups. Each group is a list of labels (AND logic). Multiple groups per chapter use OR logic. Use the reserved key `"*"` for global rules applied to all service chapters. 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
41 changes: 41 additions & 0 deletions docs/features/service_chapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ The service chapters appear in the following order in the generated release note
warnings: true # enable service chapters (default)
hidden-service-chapters: '' # hide specific service chapters (default: empty)
service-chapter-order: '' # custom display order (default: empty = default order)
service-chapter-exclude: '' # label-exclusion rules per chapter (default: empty)
print-empty-chapters: true # show even when empty (default)
duplicity-scope: "both" # allow duplicates across custom + service
```
Expand Down Expand Up @@ -116,6 +117,46 @@ Or use comma-separated format:

**Note:** Title matching is exact and case-sensitive. The `service-chapter-order` input is independent of `hidden-service-chapters`; hidden chapters are still hidden even if listed in the order.

### Label-Based Exclusion Rules
Use `service-chapter-exclude` to filter out issues/PRs from service chapters by label combinations. Each rule is a group of labels that must **all** be present on a record (AND logic) for the record to be excluded. Multiple groups per chapter are evaluated with OR logic (any group match excludes the record).

**Per-chapter exclusion** — excludes a matching record from that chapter only:
Comment thread
coderabbitai[bot] marked this conversation as resolved.

```yaml
- name: Generate Release Notes
with:
warnings: true
service-chapter-exclude: |
Closed Issues without Pull Request ⚠️:
- [scope:security, type:tech-debt]
- [scope:security, type:false-positive]
Others - No Topic ⚠️:
- [wontfix]
```

**Global exclusion** — use the reserved key `"*"` to exclude matching records from **all** service chapters:

```yaml
- name: Generate Release Notes
with:
warnings: true
service-chapter-exclude: |
"*":
- [scope:security, type:tech-debt]
Closed Issues without Pull Request ⚠️:
- [scope:security, type:false-positive]
```

**Behavior:**
- Within a group, all labels must be present on the issue (AND logic).
- Across groups, any single match is sufficient to exclude (OR logic).
- The `"*"` key applies to every service chapter; a matching record is dropped entirely.
- Per-chapter rules only affect the specified chapter; the record may still appear in other chapters.
- Global exclusion takes precedence over per-chapter rules.
- Label strings that do not appear on any record simply prevent the group from matching.
- Unknown chapter titles are skipped with a warning.
- If omitted or empty, no exclusion is applied.

## Example Result
```markdown
### Closed Issues without User Defined Labels ⚠️
Expand Down
65 changes: 65 additions & 0 deletions release_notes_generator/action_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
WARNINGS,
HIDDEN_SERVICE_CHAPTERS,
SERVICE_CHAPTER_ORDER,
SERVICE_CHAPTER_EXCLUDE,
GLOBAL_EXCLUDE_KEY,
RUNNER_DEBUG,
PRINT_EMPTY_CHAPTERS,
DUPLICITY_SCOPE,
Expand Down Expand Up @@ -417,6 +419,68 @@ def get_service_chapter_order() -> list[str]:

return ordered

@staticmethod
def get_service_chapter_exclude() -> dict[str, list[list[str]]]:
"""
Get label-exclusion rules for service chapters from the action inputs.

Returns:
Mapping of chapter title (or ``"*"`` for global) to a list of label
groups. Each group is a list of label strings (AND logic within a
group, OR logic across groups).
"""
valid_titles = set(DEFAULT_SERVICE_CHAPTER_ORDER)

raw = get_action_input(SERVICE_CHAPTER_EXCLUDE, "")
if not isinstance(raw, str):
logger.error("Error: 'service-chapter-exclude' is not a valid string.")
return {}

raw = raw.strip()
if not raw:
return {}

try:
parsed = yaml.safe_load(raw)
if parsed is None:
return {}
if not isinstance(parsed, dict):
logger.error("Error: 'service-chapter-exclude' input is not a valid YAML mapping.")
return {}
except yaml.YAMLError as exc:
logger.error("Error parsing 'service-chapter-exclude' input: %s", exc)
return {}

result: dict[str, list[list[str]]] = {}
for title, groups in parsed.items():
title_str = str(title)

if title_str != GLOBAL_EXCLUDE_KEY and title_str not in valid_titles:
logger.warning("Unknown service chapter title '%s' in 'service-chapter-exclude'. Skipping.", title_str)
continue

if not isinstance(groups, list):
logger.warning(
"Value for '%s' in 'service-chapter-exclude' is not a list. Skipping.",
title_str,
)
continue

validated_groups: list[list[str]] = []
for group in groups:
if not isinstance(group, list):
logger.warning(
"Group entry under '%s' in 'service-chapter-exclude' is not a list: %s. Skipping.",
title_str,
group,
)
continue
validated_groups.append([str(label) for label in group])

result[title_str] = validated_groups

return result

@staticmethod
def get_print_empty_chapters() -> bool:
"""
Expand Down Expand Up @@ -612,6 +676,7 @@ def validate_inputs() -> None:
logger.debug("CodeRabbit summary ignore groups: %s", coderabbit_summary_ignore_groups)
logger.debug("Hidden service chapters: %s", ActionInputs.get_hidden_service_chapters())
logger.debug("Service chapter order: %s", ActionInputs.get_service_chapter_order())
logger.debug("Service chapter exclude: %s", ActionInputs.get_service_chapter_exclude())
logger.debug("Super chapters (raw): %s", get_action_input(SUPER_CHAPTERS, default=""))

@staticmethod
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 @@ -72,6 +72,7 @@ def build(self) -> str:
used_record_numbers=self.custom_chapters.populated_record_numbers_list,
hidden_chapters=ActionInputs.get_hidden_service_chapters(),
chapter_order=ActionInputs.get_service_chapter_order(),
chapter_exclude=ActionInputs.get_service_chapter_exclude(),
)
service_chapters.populate(self.records)

Expand Down
Loading