Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
de66dc7
feat: add super-chapters support for grouping chapters by label
miroslavpojer Apr 2, 2026
8895bc2
feat: implement super-chapters functionality with uncategorized fallback
miroslavpojer Apr 2, 2026
b5b068c
fix: improve unclaimed IDs handling and normalize label input in Cust…
miroslavpojer Apr 2, 2026
d9291bc
Fixed review comments.
miroslavpojer Apr 2, 2026
d0250fd
Fix review comments.
miroslavpojer Apr 2, 2026
8679c2a
feat: enhance hierarchy issue rendering logic for open and closed par…
miroslavpojer Apr 2, 2026
30754f3
feat: add super-chapters support for grouping chapters by label
miroslavpojer Apr 2, 2026
203a8cf
Fixed after merge issues.
miroslavpojer Apr 6, 2026
b93b0db
Implemented partial split by super chapter label.
miroslavpojer Apr 6, 2026
abfb0ff
fix: simplify comment in CustomChapters class for clarity
miroslavpojer Apr 6, 2026
20cf902
Merge branch 'master' into fature/99-Super-chapters
miroslavpojer Apr 6, 2026
cd6cadc
refactor: streamline method calls in CustomChapters and HierarchyIssu…
miroslavpojer Apr 6, 2026
bafb0eb
Fixed review comments.
miroslavpojer Apr 6, 2026
583978f
Self-review and reduction of pylint exceptions.
miroslavpojer Apr 6, 2026
69d5b2d
Fixed black.
miroslavpojer Apr 6, 2026
4666b0d
refactor: update type hints in CustomChapters and HierarchyIssueRecor…
miroslavpojer Apr 6, 2026
2514d44
Fix review notes.
miroslavpojer Apr 6, 2026
f72bcc5
Fix review comments.
miroslavpojer Apr 6, 2026
2da077c
Fix review comments.
miroslavpojer Apr 6, 2026
8382e55
Fixed review comments.
miroslavpojer Apr 6, 2026
85774a5
Fixed review comment.
miroslavpojer Apr 6, 2026
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
8 changes: 8 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ Testing
- Must not access private members (names starting with `_`) of the class under test directly in tests.
- Must place shared test helper functions and factory fixtures in the nearest `conftest.py` and reuse them across tests.
- Must annotate pytest fixture parameters with `MockerFixture` (from `pytest_mock`) and return types with `Callable[..., T]` (from `collections.abc`) when the fixture returns a factory function.
- Prefer TDD workflow:
- Must create or update `SPEC.md` in the relevant package directory before writing any code, listing scenarios, inputs, and expected outputs.
- Must propose the full set of test cases (name + one-line intent + input summary + expected output summary) and wait for user confirmation before writing any code.
- Must be ready to add, remove, or rename tests based on user feedback before proceeding.
- Must write all failing tests first (red), then implement until all pass (green).
- Must cover all distinct combinations; each test must state its scenario in the docstring.
- Must update `SPEC.md` after all tests pass with the confirmed test case table (name + intent + input + expected output).
- Must not add comments outside test methods in `test_*.py` files; use section-header comments (`# --- section ---`) only to separate logical groups of tests.

Tooling
- Must format with Black (pyproject.toml).
Expand Down
9 changes: 9 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ inputs:
At most one `catch-open-hierarchy` chapter allowed; duplicates are warned and ignored.
required: false
default: ''
super-chapters:
description: |
YAML array of super-chapter definitions that group regular chapters by label.
Required keys: `title`, `label` or `labels`.
Records matching a super-chapter label are rendered inside that super-chapter.
A record can appear in multiple super-chapters.
required: false
default: ''
duplicity-scope:
description: 'Allow duplicity of issue lines in chapters. Scopes: custom, service, both, none.'
required: false
Expand Down Expand Up @@ -181,6 +189,7 @@ runs:
INPUT_GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
INPUT_TAG_NAME: ${{ inputs.tag-name }}
INPUT_CHAPTERS: ${{ inputs.chapters }}
INPUT_SUPER_CHAPTERS: ${{ inputs.super-chapters }}
INPUT_FROM_TAG_NAME: ${{ inputs.from-tag-name }}
INPUT_HIERARCHY: ${{ inputs.hierarchy }}
INPUT_DUPLICITY_SCOPE: ${{ inputs.duplicity-scope }}
Expand Down
1 change: 1 addition & 0 deletions docs/configuration_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ This page lists all action inputs and outputs with defaults. Grouped for readabi
| `row-format-issue` | No | `{type}: {number} _{title}_ developed by {developers} in {pull-requests}` | Template for issue rows. |
| `row-format-pr` | No | `{number} _{title}_ developed by {developers}` | Template for PR rows. |
| `row-format-link-pr` | No | `true` | If true adds `PR:` prefix when a PR is listed without an issue. |
| `super-chapters` | No | "" | YAML multi-line list of super-chapter entries (`title` + `label`/`labels`). Groups regular chapters under higher-level headings by label. See [Super Chapters](features/custom_chapters.md#super-chapters). |

> CodeRabbit summaries must already be present in the PR body (produced by your own CI/App setup). This action only parses existing summaries; it does not configure or call CodeRabbit.

Expand Down
110 changes: 110 additions & 0 deletions docs/features/custom_chapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,116 @@ A `catch-open-hierarchy` chapter can also be `hidden: true` to silently track op
- Duplicate `catch-open-hierarchy: true` chapters are reduced to the first; a warning is logged for the rest.
- When `hierarchy: false`, a warning is logged once at populate time: `"catch-open-hierarchy has no effect when hierarchy is disabled"`.

## Super Chapters

**Super chapters** group regular chapters under higher-level headings based on a separate label. This is useful in monorepo or multi-module projects where the same chapter structure (Features, Bugfixes, …) should appear once per component.

### Configuration

Define super chapters via the `super-chapters` input — a YAML array with `title` and `label`/`labels`:

```yaml
with:
super-chapters: |
- title: "Atum Server"
label: "atum-server"
- title: "Atum Agent"
labels: "atum-agent, atum-agent-spark"
chapters: |
- {"title": "Enhancements", "label": "enhancement"}
- {"title": "Bugfixes", "label": "bug"}
```

### Rendering

When super chapters are configured the output uses `##` headings for super chapters and `###` for regular chapters nested inside:

```markdown
## Atum Server
### Enhancements
- #10 _Streaming API_ developed by @alice in #12

### Bugfixes
- #15 _Fix timeout_ developed by @bob in #16

## Atum Agent
### Enhancements
- #20 _Checkpointing_ developed by @carol in #22
```

### Behavior
- A record is placed under a super-chapter if it carries at least one label matching the super-chapter's labels.
- A record can appear in **multiple** super-chapters if its labels match more than one.
- Within each super-chapter, records are routed to regular chapters by the normal label-matching rules.
- Empty super chapters (no matching records) respect the `print-empty-chapters` setting:
- `print-empty-chapters: true` → header is printed with `No entries detected.`
- `print-empty-chapters: false` → header is omitted entirely
- `## Uncategorized` is only emitted when there are actually unmatched records; `print-empty-chapters` has no effect on it.
- When no super chapters are configured, output is flat (unchanged from previous behavior).

### Hierarchy Split (with `hierarchy: true`)

With `hierarchy: true`, super-chapter matching uses each hierarchy record's **full aggregated label set** — own labels plus all descendant labels recursively at every depth. So an Epic matches a super chapter even when the relevant label lives only on a grandchild Task (e.g. Epic → Feature → Task).

The record is then split by which descendants belong to which super chapter:

| Descendants | Output |
|---|---|
| All match one super chapter | Record appears in that super chapter only |
| None match any super chapter | Record appears in `## Uncategorized` only |
| Some match, some don't | Record appears in the matching super chapter **and** in `## Uncategorized`, each showing only its own subset |
| Match multiple super chapters | Record appears in each matching super chapter with its relevant subset |
| Match multiple SCs + some unmatched | Record appears in each matching super chapter and in `## Uncategorized` |

#### Example

Epic #1 has Task #2 (`scope:frontend`) and Task #3 (`scope:backend`):

```yaml
super-chapters: |
- title: "Frontend"
label: "scope:frontend"
- title: "Backend"
label: "scope:backend"
chapters: |
- {"title": "New Features", "labels": "feature"}
```

```markdown
## Frontend
### New Features
- Epic: _Add user authentication_ #1
- #2 _Build login form_

## Backend
### New Features
- Epic: _Add user authentication_ #1
- #3 _Add JWT endpoint_
```

If Epic #1 also had Task #4 with no super-chapter label, it would additionally appear in `## Uncategorized` with only Task #4.

#### 3-level depth

The same split works when the matching label is on a grandchild. Epic #1 → Feature #2 → Task #3 (`scope:security`):

```markdown
## Security
### New Features
- Epic: _Add authentication_ #1
- Feature: _Auth flow_ #2
- #3 _Add JWT endpoint_
```

Feature #2 has no `scope:security` label of its own, but its aggregated set includes it via Task #3, so it is routed to the Security super chapter.

Children within each rendered node are sorted **ascending by issue number**.

### Validation
- Entries missing `title` or `label`/`labels` are skipped with a warning.
- Non-dict entries are skipped with a warning.
- Empty labels after normalization cause the entry to be skipped with a warning.

## Related Features
- [Duplicity Handling](./duplicity_handling.md) – governs multi-chapter visibility.
- [Release Notes Extraction](./release_notes_extraction.md) – provides the change increment lines.
Expand Down
1 change: 1 addition & 0 deletions docs/features/issue_hierarchy_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Resulting output (hierarchy issues only — sub-issue rows use `row-format-issue
- [Custom Row Formats](./custom_row_formats.md) – controls hierarchy line rendering.
- [Service Chapters](./service_chapters.md) – flags missing change increments if hierarchy parents lack qualifying sub-issues.
- [Duplicity Handling](./duplicity_handling.md) – duplicate hierarchy items can be icon-prefixed if allowed.
- [Super Chapters](./custom_chapters.md#super-chapters) – hierarchy records split across super chapters when descendants carry different super-chapter labels.

← [Back to Feature Tutorials](../../README.md#feature-tutorials)

67 changes: 61 additions & 6 deletions release_notes_generator/action_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@
import os
import sys
import re

from typing import Any
import yaml

from release_notes_generator.utils.constants import (
GITHUB_REPOSITORY,
GITHUB_TOKEN,
TAG_NAME,
CHAPTERS,
SUPER_CHAPTERS,
PUBLISHED_AT,
VERBOSE,
WARNINGS,
Expand Down Expand Up @@ -59,7 +60,7 @@
)
from release_notes_generator.utils.enums import DuplicityScopeEnum
from release_notes_generator.utils.gh_action import get_action_input
from release_notes_generator.utils.utils import normalize_version_tag
from release_notes_generator.utils.utils import normalize_labels, normalize_version_tag

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -141,7 +142,7 @@ def get_from_tag_name() -> str:
Get the from-tag name from the action inputs.
"""
raw = get_action_input(FROM_TAG_NAME, default="")
return normalize_version_tag(raw) # type: ignore[arg-type]
return normalize_version_tag(raw)

@staticmethod
def is_from_tag_name_defined() -> bool:
Expand All @@ -166,11 +167,64 @@ def get_chapters() -> list[dict[str, str]]:
logger.error("Error: 'chapters' input is not a valid YAML list.")
return []
except yaml.YAMLError as exc:
logger.error("Error parsing 'chapters' input: {%s}", exc)
logger.error("Error parsing 'chapters' input: %s", exc)
return []

return chapters

@staticmethod
def get_super_chapters() -> list[dict[str, Any]]:
"""
Get list of validated super-chapter definitions from the action inputs.

Each returned entry is guaranteed to have:
- 'title': str
- 'labels': list[str] (non-empty, normalized)

Invalid entries (non-dict, missing title, missing/empty labels) are skipped
with a warning log.
"""
# Get the 'super-chapters' input from environment variables
super_chapters_input: str = get_action_input(SUPER_CHAPTERS, default="")

# Parse the received string back to YAML array input.
try:
raw_list = yaml.safe_load(super_chapters_input)
if raw_list is None:
return []
if not isinstance(raw_list, list):
logger.error("Error: 'super-chapters' input is not a valid YAML list.")
return []
except yaml.YAMLError as exc:
logger.error("Error parsing 'super-chapters' input: %s", exc)
return []

result: list[dict[str, Any]] = []
for entry in raw_list:
if not isinstance(entry, dict):
logger.warning("Skipping super-chapter definition with invalid type %s: %s", type(entry), entry)
continue
if "title" not in entry:
logger.warning("Skipping super-chapter without title key: %s", entry)
continue
title = entry["title"]
if not isinstance(title, str) or not title.strip():
logger.warning("Skipping super-chapter with invalid title value: %r", title)
continue

raw_labels = entry.get("labels", entry.get("label"))
if raw_labels is None:
logger.warning("Super-chapter '%s' has no 'label' or 'labels' key; skipping", title)
continue
normalized = normalize_labels(raw_labels)
if not normalized:
logger.warning("Super-chapter '%s' labels definition empty after normalization; skipping", title)
continue

result.append({"title": title, "labels": normalized})
logger.debug("Validated super-chapter '%s' with labels: %s", title, normalized)
return result

@staticmethod
def get_hierarchy() -> bool:
"""
Expand Down Expand Up @@ -351,8 +405,7 @@ def get_print_empty_chapters() -> bool:
"""
Get the print empty chapters parameter value from the action inputs.
"""
return get_action_input(PRINT_EMPTY_CHAPTERS, "true").lower() == "true" # type: ignore[union-attr]
# mypy: string is returned as default
return get_action_input(PRINT_EMPTY_CHAPTERS, "true").lower() == "true"

@staticmethod
def validate_input(input_value, expected_type: type, error_message: str, error_buffer: list) -> bool:
Expand Down Expand Up @@ -540,6 +593,8 @@ 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())
super_chapters = ActionInputs.get_super_chapters()
logger.debug("Super chapters: %s", super_chapters)
Comment on lines +596 to +597
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

validate_inputs() calls ActionInputs.get_super_chapters() only to log the value at debug level. Since get_super_chapters() performs YAML parsing/validation (and emits warnings/errors), this can duplicate validation logs and work because super chapters are also parsed later when building CustomChapters. Consider removing this call, or caching the parsed/validated super-chapters result in ActionInputs so it’s parsed once and logging doesn’t re-trigger validation side effects.

Suggested change
super_chapters = ActionInputs.get_super_chapters()
logger.debug("Super chapters: %s", super_chapters)

Copilot uses AI. Check for mistakes.

@staticmethod
def _detect_row_format_invalid_keywords(row_format: str, row_type: str = "Issue", clean: bool = False) -> str:
Expand Down
Loading