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
4 changes: 4 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ Learned rules
- Must not change externally-visible output strings without updating the contract.

Repo additions
- Test conftest files:
- tests/unit/conftest.py — shared fixtures for all unit tests
- tests/unit/release_notes_generator/builder/conftest.py — builder-scoped fixtures (e.g. autouse mocks that must not bleed into other test packages)
- Must not place a builder-only autouse fixture in the global conftest.
- Project name: generate-release-notes
- Entry points: action.yml, main.py
- Core package: release_notes_generator/
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ jobs:

- name: Run action against real repository and validate output
env:
INPUT_TAG_NAME: 'v0.2.0'
INPUT_GITHUB_REPOSITORY: 'AbsaOSS/generate-release-notes'
INPUT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_CHAPTERS: |
Expand All @@ -209,7 +208,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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ Each feature is documented separately — click a name below to learn configurat
| [CodeRabbit Integration](docs/features/coderabbit_integration.md) | Extraction | Optional extension to Release Notes Extraction, enabling AI-generated summaries when PR notes are missing. |
| [Skip Labels](docs/features/skip_labels.md) | Filtering | Exclude issues/PRs carrying configured labels from all release notes. |
| [Service Chapters](docs/features/service_chapters.md) | Quality & Warnings | Surfaces gaps: issues without PRs, unlabeled items, PRs without notes, etc. |
| [Statistics & Anti-game](docs/features/stats_chapters.md) | Quality & Warnings | Surfaces skip-label usage statistics per PR author, issue author/assignee, type, and label. |
| [Duplicity Handling](docs/features/duplicity_handling.md) | Quality & Warnings | Marks duplicate lines when the same issue appears in multiple chapters. |
| [Tag Range Selection](docs/features/tag_range.md) | Time Range | Chooses scope via `tag-name`/`from-tag-name`. |
| [Date Selection](docs/features/date_selection.md) | Time Range | Chooses scope via timestamps (`published-at` vs `created-at`). |
Expand Down
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ inputs:
description: 'Print service chapters if true.'
required: false
default: 'true'
show-stats-chapters:
description: 'Print Statistics & Anti-game chapters showing skip-label usage stats.'
required: false
default: 'true'
hidden-service-chapters:
description: |
List of service chapter titles to hide from output (comma or newline separated).
Expand Down Expand Up @@ -196,6 +200,7 @@ runs:
INPUT_DUPLICITY_ICON: ${{ inputs.duplicity-icon }}
INPUT_OPEN_HIERARCHY_SUB_ISSUE_ICON: ${{ inputs.open-hierarchy-sub-issue-icon }}
INPUT_WARNINGS: ${{ inputs.warnings }}
INPUT_SHOW_STATS_CHAPTERS: ${{ inputs.show-stats-chapters }}
INPUT_HIDDEN_SERVICE_CHAPTERS: ${{ inputs.hidden-service-chapters }}
INPUT_SERVICE_CHAPTER_ORDER: ${{ inputs.service-chapter-order }}
INPUT_PUBLISHED_AT: ${{ inputs.published-at }}
Expand Down
1 change: 1 addition & 0 deletions 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. |
| `show-stats-chapters` | No | `true` | Toggle Statistics & Anti-game chapters showing skip-label usage stats. When `true` and at least one record was skipped, renders usage tables. Honors `print-empty-chapters` for individual sub-sections. |
| `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`. |
| `print-empty-chapters` | No | `true` | Print chapter headings even when empty. |
Expand Down
3 changes: 2 additions & 1 deletion docs/features/skip_labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ https://github.com/org/repo/compare/v1.5.0...v2.0.0
> (Internal / skipped items not shown.)

## Related Features
- [Service Chapters](./service_chapters.md) – skipped items don’t appear as warnings.
- [Service Chapters](./service_chapters.md) – skipped items don't appear as warnings.
- [Statistics & Anti-game Chapters](./stats_chapters.md) – surfaces skip-label usage statistics per author, type, and label.

← [Back to Feature Tutorials](../../README.md#feature-tutorials)
109 changes: 109 additions & 0 deletions docs/features/stats_chapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Feature: Statistics & Anti-game Chapters

## Purpose
Surface skip-label usage statistics so that maintainers can audit who and what is being excluded from release notes via skip labels. Discourages overuse of the `skip-release-notes` label by making the practice visible.

## How It Works
- Enabled when input `show-stats-chapters` is `true` (default). When `false`, stats chapters are omitted entirely.
- Iterates **all** records, including those with `skip=True`, to compute totals and skip counts.
- The chapter is only rendered when at least one record in the release window carries a skip label. If nothing was skipped, the entire section is omitted regardless of `print-empty-chapters`.
- Honors `print-empty-chapters` for individual sub-sections: when `true` (default), sub-sections where no records were skipped but records exist are still shown (with a zero skipped count). When `false`, only sub-sections with at least one skipped record are rendered.
- Rendered after Service Chapters (or after Custom Chapters if `warnings: false`).

### Sub-sections

The chapter contains four fixed sub-sections in the following order:

#### 1. PR Authors
Compares PR authors and their usage of the skip label.

| Column | Description |
|--------|-------------|
| Author | PR author (`@login`) or `(no author)` |
| Total PRs | All PRs by that author |
| Skipped PRs | PRs by that author carrying a skip label |

#### 2. Issue Authors / Assignees
Compares issue authors and assignees and their usage of the skip label. A person appearing as both author and assignee of different issues is counted once per issue involvement.

| Column | Description |
|--------|-------------|
| Author / Assignee | Person (`@login`) or `(no author)` |
| Total Issues | Issues where this person is author or assignee |
| Skipped Issues | Skipped issues where this person is author or assignee |

#### 3. Issue Labels
Compares non-skip labels on issues and their usage of the skip label.

| Column | Description |
|--------|-------------|
| Label | Non-skip label on the issue, or `(no label)` |
| Total Issues | Issues carrying that label |
| Skipped Issues | Skipped issues carrying that label |

#### 4. PR Labels
Compares non-skip labels on PRs and their usage of the skip label.

| Column | Description |
|--------|-------------|
| Label | Non-skip label on the PR, or `(no label)` |
| Total PRs | PRs carrying that label |
| Skipped PRs | Skipped PRs carrying that label |

All sub-sections sort rows by skipped count descending, then by name ascending. Skip-label names are excluded from label buckets.

## Configuration
```yaml
- name: Generate Release Notes
id: release_notes_scrapper
uses: AbsaOSS/generate-release-notes@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag-name: "v2.0.0"
chapters: |
- {"title": "Features", "label": "feature"}
- {"title": "Fixes", "label": "bug"}
show-stats-chapters: true # enabled by default
print-empty-chapters: true # show sub-sections even with zero skips
```

To disable:
```yaml
show-stats-chapters: false
```

## Example Result
```markdown
### Skip Release Notes Label Usage ⚠️
#### PR Authors
| Author | Total PRs | Skipped PRs |
|--------|-----------|-------------|
| @alice | 5 | 3 |
| @bob | 2 | 0 |

#### Issue Authors / Assignees
| Author / Assignee | Total Issues | Skipped Issues |
|--------------------|--------------|----------------|
| @alice | 6 | 3 |
| @bob | 4 | 2 |

#### Issue Labels
| Label | Total Issues | Skipped Issues |
|-------|--------------|----------------|
| bug | 6 | 4 |
| enhancement | 3 | 1 |
| (no label) | 2 | 2 |

#### PR Labels
| Label | Total PRs | Skipped PRs |
|-------|-----------|-------------|
| bug | 2 | 1 |
| (no label) | 1 | 1 |
```

## Related Features
- [Skip Labels](./skip_labels.md) – defines which labels mark records as skipped.
- [Service Chapters](./service_chapters.md) – quality diagnostics (skipped records are excluded there).

← [Back to Feature Tutorials](../../README.md#feature-tutorials)
12 changes: 12 additions & 0 deletions release_notes_generator/action_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
SERVICE_CHAPTER_ORDER,
RUNNER_DEBUG,
PRINT_EMPTY_CHAPTERS,
SHOW_STATS_CHAPTERS,
DUPLICITY_SCOPE,
DUPLICITY_ICON,
OPEN_HIERARCHY_SUB_ISSUE_ICON,
Expand Down Expand Up @@ -424,6 +425,13 @@ def get_print_empty_chapters() -> bool:
"""
return get_action_input(PRINT_EMPTY_CHAPTERS, "true").lower() == "true"

@staticmethod
def get_show_stats_chapters() -> bool:
"""
Get the show stats chapters parameter value from the action inputs.
"""
return get_action_input(SHOW_STATS_CHAPTERS, "true").lower() == "true"

Comment on lines +428 to +434
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Invalid show-stats-chapters values are silently accepted as False.

get_show_stats_chapters() coerces any non-"true" value to False, so the validation at Line 599-600 can never fail for malformed inputs (e.g., "treu"), causing silent behavior changes.

💡 Proposed fix (strict bool parsing with validation)
@@
     `@staticmethod`
     def get_show_stats_chapters() -> bool:
         """
         Get the show stats chapters parameter value from the action inputs.
         """
-        return get_action_input(SHOW_STATS_CHAPTERS, "true").lower() == "true"
+        raw = (get_action_input(SHOW_STATS_CHAPTERS, "true") or "").strip().lower()
+        return raw == "true"
@@
-        show_stats_chapters = ActionInputs.get_show_stats_chapters()
-        ActionInputs.validate_input(show_stats_chapters, bool, "Show stats chapters must be a boolean.", errors)
+        show_stats_chapters_raw = (get_action_input(SHOW_STATS_CHAPTERS, "true") or "").strip().lower()
+        if show_stats_chapters_raw not in ("true", "false"):
+            errors.append("Show stats chapters must be 'true' or 'false'.")
+        show_stats_chapters = show_stats_chapters_raw == "true"

As per coding guidelines, **/*.py: Centralize parsing and validation in one input layer.

Also applies to: 599-600

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@release_notes_generator/action_inputs.py` around lines 428 - 434,
get_show_stats_chapters currently coerces any non-"true" string to False,
masking malformed inputs like "treu" and preventing downstream validation in
release_notes_generator.action_inputs; update get_show_stats_chapters to perform
strict parsing and validation at the input layer: read the raw value via
get_action_input(SHOW_STATS_CHAPTERS, "true"), validate it is exactly "true" or
"false" (case-insensitive), and return the corresponding bool; if the value is
invalid, raise a ValueError or call the existing input validation helper so the
error surfaces early (centralize parsing/validation here rather than relying on
later checks).

@staticmethod
def validate_input(input_value, expected_type: type, error_message: str, error_buffer: list) -> bool:
"""
Expand Down Expand Up @@ -588,6 +596,9 @@ def validate_inputs() -> None:
print_empty_chapters = ActionInputs.get_print_empty_chapters()
ActionInputs.validate_input(print_empty_chapters, bool, "Print empty chapters must be a boolean.", errors)

show_stats_chapters = ActionInputs.get_show_stats_chapters()
ActionInputs.validate_input(show_stats_chapters, bool, "Show stats chapters must be a boolean.", errors)

# Log errors if any
if errors:
for error in errors:
Expand All @@ -606,6 +617,7 @@ def validate_inputs() -> None:
logger.debug("Verbose logging: %s", verbose)
logger.debug("Warnings: %s", warnings)
logger.debug("Print empty chapters: %s", print_empty_chapters)
logger.debug("Show stats chapters: %s", show_stats_chapters)
logger.debug("Release notes title: %s", release_notes_title)
logger.debug("CodeRabbit support active: %s", coderabbit_support_active)
logger.debug("CodeRabbit release notes title: %s", coderabbit_release_notes_title)
Expand Down
20 changes: 14 additions & 6 deletions release_notes_generator/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from release_notes_generator.action_inputs import ActionInputs
from release_notes_generator.chapters.custom_chapters import CustomChapters
from release_notes_generator.chapters.service_chapters import ServiceChapters
from release_notes_generator.chapters.stats_chapters import StatsChapters
from release_notes_generator.model.record.record import Record

logger = logging.getLogger(__name__)
Expand All @@ -49,6 +50,7 @@ def __init__(

self.warnings = ActionInputs.get_warnings()
self.print_empty_chapters = ActionInputs.get_print_empty_chapters()
self.show_stats_chapters = ActionInputs.get_show_stats_chapters()

def build(self) -> str:
"""
Expand Down Expand Up @@ -77,13 +79,19 @@ def build(self) -> str:

service_chapters_str = service_chapters.to_string()
if len(service_chapters_str) > 0:
release_notes = (
f"""{user_defined_chapters_str}\n\n{service_chapters_str}\n\n"""
f"""#### Full Changelog\n{self.changelog_url}\n"""
)
body = f"""{user_defined_chapters_str}\n\n{service_chapters_str}"""
else:
release_notes = f"""{user_defined_chapters_str}\n\n#### Full Changelog\n{self.changelog_url}\n"""
body = user_defined_chapters_str
else:
release_notes = f"""{user_defined_chapters_str}\n\n#### Full Changelog\n{self.changelog_url}\n"""
body = user_defined_chapters_str

if self.show_stats_chapters:
stats_chapters = StatsChapters(print_empty_chapters=self.print_empty_chapters)
stats_chapters.populate(self.records)
stats_str = stats_chapters.to_string()
if stats_str:
body = body.rstrip("\n") + "\n\n" + stats_str

release_notes = f"""{body}\n\n#### Full Changelog\n{self.changelog_url}\n"""

return release_notes.lstrip()
Loading
Loading