Skip to content

Conversation

miroslavpojer
Copy link
Collaborator

@miroslavpojer miroslavpojer commented Sep 9, 2025

Release Notes:

  • Introduced optional hierarchy-based release notes generation (enable via “hierarchy” input).
  • Internals reworked to support hierarchy mode and improved filtering/record creation.

Summary by CodeRabbit

  • New Features

    • Optional hierarchy mode for release notes plus a new hierarchy issue row-format option.
  • Bug Fixes

    • Improved issue fetching, deduplication and filtering to avoid duplicated entries.
  • Refactor

    • Internal builder/record generation reworked to support hierarchy-driven output.
  • Documentation

    • README updated: removes old regime docs, adds hierarchy usage, examples and warnings.
  • Chores

    • Action renamed to “Release notes scraper” and top-level inputs/wiring updated.
  • Tests

    • Expanded test coverage for hierarchy, factories and builder scenarios.

Fixes #167
Fixes #81

- Introduced new regime `issue-hierarchy`
@miroslavpojer miroslavpojer self-assigned this Sep 9, 2025
Copy link

coderabbitai bot commented Sep 9, 2025

Walkthrough

Replaces the prior "regime" concept with a boolean hierarchy mode: new inputs and row-format for hierarchy issues; hierarchy-aware record factories and record types; refactors builder/chapters and factories; updates mining/filtering, constants, action metadata, imports, and tests to support hierarchical release notes generation.

Changes

Cohort / File(s) Summary
Config
/.pylintrc
Relaxed DESIGN thresholds: max-branches 12→13, max-locals 15→20, max-returns 6→7.
Action metadata & docs
/action.yml, /README.md
Removed regime input; added hierarchy (bool) and row-format-hierarchy-issue; renamed action to "Release notes scraper"; updated PYTHONPATH wiring and README examples/inputs to document hierarchy.
Entrypoint
/main.py
Updated CustomChapters import path.
Inputs & constants
/release_notes_generator/action_inputs.py, /release_notes_generator/utils/constants.py
Replaced regime API with get_hierarchy(); added ROW_FORMAT_HIERARCHY_ISSUE and per-row-type supported-key sets; added get_row_format_hierarchy_issue() and per-type validation.
Builder & chapters
/release_notes_generator/builder/base_builder.py, /release_notes_generator/builder/builder.py, /release_notes_generator/chapters/base_chapters.py, /release_notes_generator/chapters/custom_chapters.py, /release_notes_generator/chapters/service_chapters.py
Removed abstract base builder file; consolidated/renamed builder class; added since property to BaseChapters; adjusted imports, deduplication guards, to_chapter_row call semantics, and to_string to skip empty chapters.
Model changes
/release_notes_generator/model/*.py
Expanded Record API to accept labels and abstract get_labels; changed to_chapter_row(add_into_chapters=True) across records; added HierarchyIssueRecord and SubIssueRecord; updated Issue/Pull/Commit records for label APIs, duplication handling, and hierarchy rendering.
Factories
/release_notes_generator/record/factory/record_factory.py, .../default_record_factory.py, .../issue_hierarchy_record_factory.py
RecordFactory.generate now takes data; DefaultRecordFactory is instance-based (__init__(github)) with internal state; added IssueHierarchyRecordFactory to assemble hierarchical records and bind PRs/commits.
Generator & selection
/release_notes_generator/generator.py
Chooses factory via _get_record_factory using ActionInputs.get_hierarchy(); sets custom_chapters.since; updated imports and inline ReleaseNotesBuilder invocation.
Mining & filtering
/release_notes_generator/miner.py, /release_notes_generator/filter.py
_get_issues fetches union of updated-since + open issues with deduplication; FilterByRelease now uses default vs hierarchy-aware issue filtering helpers.
Utilities
/release_notes_generator/utils/pull_request_utils.py
get_issues_for_pr now returns set[int], GraphQL query/parse updated, cache size 1024, improved error handling; extract_issue_numbers_from_body unchanged.
Tests
/tests/** (notable: tests/conftest.py, tests/release_notes/**, tests/utils/test_pull_request_utils.py, tests/test_action_inputs.py, tests/test_filter.py)
Added many fixtures and new tests for hierarchy factory and builder; updated imports and expectations (sets vs lists), adjusted mocks for get_issues_for_pr and CustomChapters path; removed an older builder test module and added new comprehensive builder tests.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor GH_Action as GitHub Action
  participant Gen as Generator
  participant AI as ActionInputs
  participant Min as Miner
  participant Fil as FilterByRelease
  participant Fact as RecordFactory
  participant Chap as CustomChapters
  participant Bld as ReleaseNotesBuilder

  GH_Action->>Gen: generate()
  Gen->>AI: get_hierarchy()
  AI-->>Gen: bool
  Gen->>Min: mine data (issues, PRs, commits)
  Min-->>Gen: MinedData (with since)
  Gen->>Chap: set since(data.since)
  Gen->>Fil: filter(data)
  Fil-->>Gen: filtered data
  Gen->>Fact: _get_record_factory(hierarchy) → instantiate appropriate factory
  Fact->>Fact: generate(data)
  Fact-->>Gen: records
  Gen->>Bld: ReleaseNotesBuilder(records, changelog_url, chapters).build()
  Bld-->>GH_Action: release notes string
Loading
sequenceDiagram
  autonumber
  participant IH as IssueHierarchyRecordFactory
  participant PR as PullRequest
  participant API as GitHub API
  participant Rec as Records

  loop Process issues
    IH->>IH: classify as HierarchyIssueRecord or SubIssueRecord
  end
  loop Process PRs
    IH->>API: fetch linked issues (body + GraphQL)
    API-->>IH: set(issue_numbers)
    IH->>IH: associate PR and commits to issues
  end
  IH->>IH: rebind sub-issues to parents
  IH->>IH: order_hierarchy_levels()
  IH-->>Rec: hierarchical records map
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • Zejnilovic
  • lsulak
  • OlivieFranklova

Poem

I hop through issues, neat and spry,
I nest each bug beneath its sky.
Epics bloom, and features sing,
Subtasks find their cozy ring.
With hierarchy true, release notes gleam—🥕

Pre-merge checks and finishing touches

❌ Failed checks (4 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title references adding a “regime” and includes the issue number prefix, but the PR actually removes the regime concept and introduces a hierarchy mode for release notes. It thus misrepresents the primary change and includes unnecessary noise. It does not clearly summarize the addition of hierarchy-based release note generation. Update the title to concisely reflect the main change—such as “Add hierarchy-based release notes generation”—and remove the issue number prefix to improve clarity.
Out of Scope Changes Check ⚠️ Warning The PR includes changes to the .pylintrc thresholds and renames the action’s display name from “scrapper” to “scraper,” neither of which relates to the hierarchy or epic-support objectives. These modifications introduce unrelated noise to the feature delivery. Extract the lint configuration adjustments and the naming correction into a separate housekeeping PR so that this feature PR remains focused solely on hierarchy support.
Description Check ⚠️ Warning The PR description provides a brief “Release Notes” section but does not follow the repository’s required template because it is missing the mandated “## Checklist” and “## Additional Context” sections and lacks a clear summary block. Without the checklist, essential review tasks may be overlooked. The deviation from the template reduces clarity and completeness. Adopt the repository’s PR template by adding the “## Checklist” with all required items, include an “## Additional Context” section, and insert a concise summary at the top to ensure all template sections are filled out.
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Linked Issues Check ✅ Passed The changes introduce a new “hierarchy” input, replace the old regime logic, add HierarchyIssueRecord and SubIssueRecord classes, extend the record factory, adjust filtering and builder flows, and update documentation to support epics and sub-issue hierarchies. These updates directly fulfill the objectives of Issue #167 by enabling hierarchy-based release note generation and address Issue #81 by grouping issues under epics.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/167-Introduce-regime-for-hierarchical-release-notes

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d294f3 and 294ed5e.

📒 Files selected for processing (1)
  • release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (8)
release_notes_generator/model/commit_record.py (3)
  • CommitRecord (13-75)
  • commit (45-50)
  • get_labels (72-73)
release_notes_generator/model/hierarchy_issue_record.py (7)
  • HierarchyIssueRecord (16-154)
  • order_hierarchy_levels (142-154)
  • get_labels (71-84)
  • sub_issues (47-51)
  • sub_hierarchy_issues (54-58)
  • level (30-34)
  • level (37-44)
release_notes_generator/model/issue_record.py (6)
  • IssueRecord (16-328)
  • issue (63-68)
  • get_labels (81-83)
  • register_pull_request (192-199)
  • register_commit (201-215)
  • issue_type (71-77)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/model/pull_request_record.py (3)
  • PullRequestRecord (15-290)
  • get_labels (107-109)
  • register_commit (168-175)
release_notes_generator/model/sub_issue_record.py (2)
  • SubIssueRecord (15-36)
  • issue (27-34)
release_notes_generator/record/factory/default_record_factory.py (4)
  • DefaultRecordFactory (44-170)
  • generate (55-126)
  • register_pull_request (64-96)
  • _create_record_for_issue (154-170)
release_notes_generator/utils/pull_request_utils.py (2)
  • get_issues_for_pr (54-74)
  • extract_issue_numbers_from_body (31-50)
🔇 Additional comments (12)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (12)

17-40: LGTM! Clear module documentation and imports.

The module documentation correctly describes the hierarchical issue factory purpose, and the imports are comprehensive and well-organized.


43-54: LGTM! Well-structured initialization.

The class extends DefaultRecordFactory appropriately and initializes the necessary tracking structures for issue registration, sub-issue parent mappings, and commit tracking.


55-103: LGTM! Comprehensive generation workflow.

The generate method implements a clear multi-stage workflow: registers hierarchical issues, registers non-hierarchical issues, links PRs and commits, registers direct commits, builds hierarchy, and provides useful logging. The logic flow is well-structured and handles all necessary record types.


105-147: LGTM! Robust PR and commit registration.

The method properly handles PR-to-issue linking by combining API results with body extraction, includes proper None safety from previous reviews, implements multi-issue attachment logic, and creates standalone PR records when no issues are linked.


148-157: Missing SubIssue record creation at discovery time.

Based on the previous review comments that were marked as addressed, sub-issues should be created as SubIssueRecord instances at discovery time to preserve SubIssue typing. However, the current implementation only registers parent mappings without creating records.

The previous review correctly identified that SubIssueRecord expects a SubIssue instance, so the records should be materialized during discovery.


165-184: LGTM! Proper hierarchy issue record creation.

The method correctly creates HierarchyIssueRecord instances, handles skip labels, and maintains registration tracking with appropriate logging.


185-194: LGTM! Comprehensive label aggregation.

The method properly combines issue labels with issue type, includes appropriate null checks, and normalizes the type name to lowercase for consistency.


195-201: LGTM! Delegated issue record creation.

The method properly delegates to the parent class implementation while maintaining registration tracking.


202-210: Type signature inconsistency with SubIssueRecord constructor.

Based on the relevant code snippets, SubIssueRecord expects a SubIssue instance, but this method signature accepts Issue. This can cause runtime errors when SubIssue-specific properties are accessed.

The previous review comments correctly identified this signature mismatch and the need to tighten the type signature to SubIssue.


211-248: LGTM! Robust hierarchy building with termination safety.

The method implements proper termination logic to prevent infinite recursion, handles progress tracking, and correctly processes different record types. The fix for infinite recursion from previous reviews has been properly implemented.


249-262: LGTM! Clean hierarchy level ordering.

The method properly filters for top-level HierarchyIssueRecord instances and delegates level ordering to each record, enabling proper hierarchical rendering.


158-164: Missing import for SubIssue type and potential type mismatch.

The method signature on Line 202 suggests _create_record_for_sub_issue should accept a SubIssue, but the current logic may be passing Issue objects. Additionally, the import statement is missing SubIssue from the github.Issue module.

Apply this diff to add the missing import:

 from github import Github
-from github.Issue import Issue
+from github.Issue import Issue, SubIssue
 from github.PullRequest import PullRequest

Also, ensure that Line 161 passes the correct type or handles the conversion appropriately.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@miroslavpojer miroslavpojer marked this pull request as ready for review September 22, 2025 14:52
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
release_notes_generator/utils/pull_request_utils.py (1)

31-33: Remove lru_cache on function taking PullRequest (unhashable).

Using lru_cache with a PullRequest instance key will raise TypeError at runtime. Cache by PR number or drop caching.

Apply:

-@lru_cache(maxsize=None)
-def extract_issue_numbers_from_body(pr: PullRequest) -> set[int]:
+def extract_issue_numbers_from_body(pr: PullRequest) -> set[int]:
release_notes_generator/builder/builder.py (1)

67-73: Wrong attribute: populated_record_numbers vs populated_record_numbers_list.

BaseChapters exposes populated_record_numbers_list. Current code likely AttributeError at runtime.

-                used_record_numbers=self.custom_chapters.populated_record_numbers,
+                used_record_numbers=self.custom_chapters.populated_record_numbers_list,
tests/conftest.py (1)

176-189: Spec’d Mock for Issue blocks non-existent attributes (get_sub_issues, type)

mocker.Mock(spec=Issue) will raise when assigning get_sub_issues or .type, which are not on PyGithub’s Issue. This can break fixture creation.

Two options:

  • Drop spec where you need extra attrs.
  • Or introduce a stub with required API and spec against it.

Minimal fix here:

-    issue = mocker.Mock(spec=Issue)
+    issue = mocker.Mock()  # do not spec to allow test-only attrs like get_sub_issues/type

Repeat for fixtures that set get_sub_issues or .type.

🧹 Nitpick comments (61)
release_notes_generator/model/commit_record.py (1)

68-71: Silence Ruff ARG002: unused parameter

Rename the parameter to underscore to indicate it’s intentionally unused.

-    def get_rls_notes(self, line_marks: Optional[list[str]] = None) -> str:
+    def get_rls_notes(self, _line_marks: Optional[list[str]] = None) -> str:
         # Hint: direct commits does not support release notes
         return ""
release_notes_generator/chapters/base_chapters.py (1)

91-92: Tiny style tweak: truthiness over len(...)

Prefer a direct truthiness check.

-            if len(chapter_string) > 0:
+            if chapter_string:
                 result += chapter_string + "\n\n"
tests/release_notes/chapters/test_base_chapters.py (1)

17-17: Import path update — OK. Consider adding a test for since.

Nice move to the new module path. Suggest adding a focused test for the since property default and setter.

from datetime import datetime

def test_since_default_and_setter():
    chapters = Chapters()
    assert chapters.since == datetime.min

    now = datetime(2024, 1, 1, 0, 0, 0)
    chapters.since = now
    assert chapters.since == now
release_notes_generator/record/factory/record_factory.py (2)

17-19: Fix module docstring to name the correct class

Docstring mentions DefaultRecordFactory, but this module defines RecordFactory.

-"""
-This module contains the DefaultRecordFactory class which is responsible for generating
-"""
+"""
+This module contains the RecordFactory base class used to generate records.
+"""

20-27: Unused logger

logger is defined but not used. Either remove it or log within this module (e.g., during abstract method calls).

release_notes_generator/model/record.py (2)

36-41: Avoid re-computing labels when empty; use a None sentinel

With the current caching, records that truly have no labels (e.g., CommitRecord) will call get_labels() on every access. Use Optional[list[str]] and a None sentinel.

-        self._labels = labels if labels is not None else []
+        self._labels: Optional[list[str]] = labels
@@
-        if len(self._labels) == 0:
-            self._labels = self.get_labels()
-
-        return self._labels
+        if self._labels is None:
+            self._labels = self.get_labels()
+        return self._labels

Also applies to: 65-69


120-126: Docstring return type mismatch

The docstring says set[str] but the API uses list[str].

-        Returns:
-            set[str]: A list of labels associated with the record.
+        Returns:
+            list[str]: Labels associated with the record.
release_notes_generator/action_inputs.py (3)

172-180: Simplify boolean parsing; remove dead None check.

get_action_input(..., "false") never returns None. Also normalize whitespace.

Apply this diff:

-        val = get_action_input("hierarchy", "false")
-        if val is None:
-            return False
-        return val.lower() == "true"
+        val = get_action_input("hierarchy", "false")
+        return str(val).strip().lower() == "true"

308-322: Validate hierarchy row format and remove trailing space in the default.

  • Default contains a trailing space.
  • validate_inputs() doesn’t validate the new hierarchy row format; add parity with issue/PR.

Apply these diffs:

-                    ROW_FORMAT_HIERARCHY_ISSUE, "{type}: _{title}_ {number} "
+                    ROW_FORMAT_HIERARCHY_ISSUE, "{type}: _{title}_ {number}"

And in validate_inputs() after PR row checks:

-        ActionInputs._detect_row_format_invalid_keywords(row_format_pr, row_type="PR")
+        ActionInputs._detect_row_format_invalid_keywords(row_format_pr, row_type=ActionInputs.ROW_TYPE_PR)
+
+        row_format_hier_issue = ActionInputs.get_row_format_hierarchy_issue()
+        if not isinstance(row_format_hier_issue, str) or not row_format_hier_issue.strip():
+            errors.append("Hierarchy Issue row format must be a non-empty string.")
+        ActionInputs._detect_row_format_invalid_keywords(
+            row_format_hier_issue, row_type=ActionInputs.ROW_TYPE_HIERARCHY_ISSUE
+        )

483-491: Add a safe default for unknown row_type.

Unknown types currently yield an empty allow‑list, stripping all keys. Default to Issue keys and warn.

Apply this diff:

         match row_type:
             case ActionInputs.ROW_TYPE_ISSUE:
                 supported_row_format_keys = SUPPORTED_ROW_FORMAT_KEYS_ISSUE
             case ActionInputs.ROW_TYPE_PR:
                 supported_row_format_keys = SUPPORTED_ROW_FORMAT_KEYS_PULL_REQUEST
             case ActionInputs.ROW_TYPE_HIERARCHY_ISSUE:
                 supported_row_format_keys = SUPPORTED_ROW_FORMAT_KEYS_HIERARCHY_ISSUE
+            case _:
+                logger.warning(
+                    "Unknown row_type '%s' in _detect_row_format_invalid_keywords; defaulting to Issue keys.",
+                    row_type,
+                )
+                supported_row_format_keys = SUPPORTED_ROW_FORMAT_KEYS_ISSUE
release_notes_generator/model/issue_record.py (2)

16-16: Unused logger.

logger is defined but not used. Either remove it or add useful debug logs (e.g., when formatting rows).


233-239: Minor: simplify empty check.

Use truthiness for the dict; avoids building a .values() view.

Apply this diff:

-        if len(self._pull_requests.values()) == 0:
+        if not self._pull_requests:
             return []
tests/release_notes/chapters/test_custom_chapters.py (1)

73-85: Test may no longer reflect populate() semantics (pulls count).

CustomChapters.populate() now derives PR count via pull_requests_count() for Issue‑like records; generic Record mocks default to 1, so this test can give false positives.

Consider changing the mock to an IssueRecord spec and stub pull_requests_count():

-from release_notes_generator.chapters.custom_chapters import CustomChapters
+from release_notes_generator.chapters.custom_chapters import CustomChapters
+from release_notes_generator.model.issue_record import IssueRecord
@@
-def test_populate_no_pulls_count(custom_chapters, mocker):
-    record1 = mocker.Mock(spec=Record)
-    record1.labels = ["bug"]
-    record1.pulls_count = 0
-    record1.is_present_in_chapters = False
+def test_populate_no_pulls_count(custom_chapters, mocker):
+    record1 = mocker.Mock(spec=IssueRecord)
+    record1.labels = ["bug"]
+    record1.pull_requests_count.return_value = 0
+    record1.is_present_in_chapters = False
tests/test_action_inputs.py (2)

28-28: Add negative case for hierarchy validation.

We validate hierarchy is boolean; include it in failure_cases.

Apply this diff:

 failure_cases = [
@@
-    ("get_duplicity_icon", "Oj", "Duplicity icon must be a non-empty string and have a length of 1."),
+    ("get_duplicity_icon", "Oj", "Duplicity icon must be a non-empty string and have a length of 1."),
+    ("get_hierarchy", "not_bool", "Hierarchy must be a boolean."),
 ]

267-277: Add a test for hierarchy row format cleaning.

Covers {ROW_FORMAT_HIERARCHY_ISSUE} invalid keys and logging under the HierarchyIssue row type.

Apply this diff:

 def test_clean_row_format_invalid_keywords_nested_braces():
@@
     assert expected_format == actual_format
+
+def test_get_row_format_hierarchy_issue_cleans_invalid_keywords(mocker, caplog):
+    caplog.set_level(logging.ERROR)
+    mocker.patch(
+        "release_notes_generator.action_inputs.get_action_input",
+        return_value="{type}: _{title}_ {number} {invalid}",
+    )
+    fmt = ActionInputs.get_row_format_hierarchy_issue()
+    assert "{invalid}" not in fmt
+    assert any("HierarchyIssue" in rec.getMessage() for rec in caplog.records)
tests/utils/test_pull_reuqest_utils.py (1)

128-129: Set-based assertions: LGTM.

Matches the new set[int] return type and preserves cache behavior checks.

Nit: consider renaming the file to tests/utils/test_pull_request_utils.py to fix the “reuqest” typo.

Also applies to: 155-156, 214-214, 245-246

show_only.md (2)

2-31: Fix Markdown list style/indentation (MD004, MD007).

Normalize bullets to dashes and 2-space indentation to satisfy markdownlint and render correctly.

Apply this diff:

- -   - None: _HI302 open_ #302
-- _Release Notes_:
-    - Hierarchy level release note
-- #451 _SI451 open_ in #150
-    - Hierarchy level release note
-    - Fixed bug
-    - Improved performance
-    + More nice code
-        * Awesome architecture
- -   - None: _HI303 open_ #303
-- _Release Notes_:
-    - Hierarchy level release note
-- #452 _SI452 open_ in #151
-    - Hierarchy level release note
-    - Fixed bug
-    - Improved performance
-    + More nice code
-        * Awesome architecture
- -   - None: _HI304 open_ #304
-- _Release Notes_:
-    - Hierarchy level release note
-- None: _HI350 open_ #350
-    - _Release Notes_:
-        - Sub-hierarchy level release note
-    - #453 _SI453 open_ in #152
-        - Hierarchy level release note
-        - Fixed bug
-        - Improved performance
-        + More nice code
-            * Awesome architecture
+- None: _HI302 open_ #302
+  - _Release Notes_:
+    - Hierarchy level release note
+  - #451 _SI451 open_ in #150
+    - Hierarchy level release note
+    - Fixed bug
+    - Improved performance
+    - More nice code
+      - Awesome architecture
+- None: _HI303 open_ #303
+  - _Release Notes_:
+    - Hierarchy level release note
+  - #452 _SI452 open_ in #151
+    - Hierarchy level release note
+    - Fixed bug
+    - Improved performance
+    - More nice code
+      - Awesome architecture
+- None: _HI304 open_ #304
+  - _Release Notes_:
+    - Hierarchy level release note
+- None: _HI350 open_ #350
+  - _Release Notes_:
+    - Sub-hierarchy level release note
+  - #453 _SI453 open_ in #152
+    - Hierarchy level release note
+    - Fixed bug
+    - Improved performance
+    - More nice code
+      - Awesome architecture

1-32: Consider relocating this generated sample under docs/examples.

Keeps root clean and clarifies that this is illustrative output.

release_notes_generator/chapters/service_chapters.py (2)

147-149: Docstring param name mismatch.

Update @param nr to @param record_id.

Apply this diff:

-        @param nr: The number of the record.
+        @param record_id: The number of the record.

59-60: Speed up membership checks with a set.

used_record_numbers is used for frequent membership tests; consider a companion set to make these O(1) while preserving the list for ordering if needed.

Example:

self._used_record_numbers_set: set[int | str] = set(self.used_record_numbers)
# keep both in sync on append
tests/release_notes/record/factory/test_default_record_factory.py (1)

173-175: Tests updated to new factory path and constructor look good.

Patches and expectations match the refactor to DefaultRecordFactory(mock_github_client).

You can DRY the repeated safe_call_decorator patches via a fixture returning the desired wrapper.

Also applies to: 210-213, 280-282, 315-317, 344-346, 376-378, 426-427

release_notes_generator/generator.py (1)

106-119: Factory selection is correct; tweak docstring for accuracy.

Replace “RecordFactory” with “DefaultRecordFactory” in the return description.

Apply this diff:

-        Returns:
-            RecordFactory: An instance of either IssueHierarchyRecordFactory or RecordFactory.
+        Returns:
+            RecordFactory: An instance of either IssueHierarchyRecordFactory or DefaultRecordFactory.
README.md (1)

51-51: Mention {type} placeholder for hierarchy rows.

Description currently lists only number and title, but default uses {type} too.

Apply this diff:

-| `row-format-hierarchy-issue`       | The format of the row for the hierarchy issue in the release notes. The format can contain placeholders for the issue `number` and `title`. The placeholders are case-sensitive.                                                                                                                                                                                                                                                                                                                                                                                                                                                 | No | `"{type}: _{title}_ {number}"`            |  
+| `row-format-hierarchy-issue`       | The format of the row for the hierarchy issue in the release notes. Placeholders: `type`, `number`, `title` (case-sensitive).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | No | `"{type}: _{title}_ {number}"`            |  
release_notes_generator/utils/pull_request_utils.py (5)

36-38: Docstring return type is wrong.

Says “list” but function returns set[int].

-    @return: The numbers of the issues mentioned in the body of the pull request as a list of integers.
+    @return: The numbers of the issues mentioned in the body of the pull request as a set of integers.

48-50: Drop hardcoded debug print (leaks to stdout).

Use logging if needed; magic PR 3645 shouldn’t be special‑cased.

-    if pr.number == 3645:
-        print(f"PR #{pr.number} - Extracted issue numbers from body: {issue_numbers}")

56-56: Nit: fix typo in docstring.

-    """Update the placeholder values and formate the graphQL query"""
+    """Update the placeholder values and format the GraphQL query"""

76-78: Drop leftover API debug print.

-    if pull_number == 3645:
-        print(f"PR #{pull_number} - Extracted issue numbers from GitHub API: {numbers}")

55-55: Optional: bound cache size for API calls.

Unbounded cache can grow over time. 1024 is plenty here.

-@lru_cache(maxsize=None)
+@lru_cache(maxsize=1024)
 def get_issues_for_pr(pull_number: int) -> set[int]:
tests/release_notes/model/test_commit_record.py (4)

1-5: Remove unused imports; avoid fixture shadowing.

These trigger Ruff F811 and are unnecessary.

-import types
-
-from tests.conftest import mock_commit
-

56-56: Silence ARG005 by not naming unused arg.

-        lambda self: True,
+        lambda _: True,

59-59: Silence ARG005 for unused parameter in lambda.

-    monkeypatch.setattr(rec, "get_rls_notes", lambda line_marks=None: "Extra release notes.")
+    monkeypatch.setattr(rec, "get_rls_notes", lambda _line_marks=None: "Extra release notes.")

7-11: Drop dead test helper.

DummyInnerCommit is unused.

-class DummyInnerCommit:
-    def __init__(self, sha: str, message: str):
-        self.sha = sha
-        self.message = message
-
tests/release_notes/model/test_record.py (2)

49-51: Use the parameter to satisfy linters.

-    def to_chapter_row(self, add_into_chapters: bool = True):
-        return f"Row for {self._record_id}"
+    def to_chapter_row(self, add_into_chapters: bool = True):
+        _ = add_into_chapters
+        return f"Row for {self._record_id}"

52-56: Mark unused argument as used.

-    def get_rls_notes(self, line_marks=None):
-        return self._rls_notes
+    def get_rls_notes(self, line_marks=None):
+        _ = line_marks
+        return self._rls_notes
release_notes_generator/filter.py (2)

21-24: Remove unused Optional import.

-from typing import Optional
-
-from release_notes_generator.action_inputs import ActionInputs
+from release_notes_generator.action_inputs import ActionInputs

140-149: Docstring doesn’t match implementation.

The code doesn’t “always include certain types”; it includes open or recently closed issues.

-        """
-        Hierarchy filtering for issues: filter out closed issues before the release date,
-        but always include issues of certain types (e.g., "Epic", "Story").
-        """
+        """
+        Hierarchy filtering for issues: include issues closed since the release date
+        or still open at generation time.
+        """
release_notes_generator/model/hierarchy_issue_record.py (2)

1-3: Fix module docstring (wrong class name).

-"""
-A module that defines the IssueRecord class, which represents an issue record in the release notes.
-"""
+"""
+A module that defines the HierarchyIssueRecord class for hierarchical issue rendering.
+"""

26-27: Forward reference in annotation may break on older Python.

Use a quoted name to avoid evaluating before class definition.

-        self._sub_hierarchy_issues: dict[int, HierarchyIssueRecord] = {}
+        self._sub_hierarchy_issues: dict[int, "HierarchyIssueRecord"] = {}
release_notes_generator/model/pull_request_record.py (2)

77-104: Remove unreachable contributor code (always returns []).

contributors currently returns [] and the rest is dead code. Either implement or remove the unreachable block to avoid confusion.

Apply:

-        return []
-
-        # TODO - contributors have to be managed in future, make a research if get_commit can return some
-        #  new information in compare to mined ones
-        # Get unique contributors from commits
-        contributors = set()
-        # for commit in self._pull_request.get_commits():
-        #     if commit.author and commit.author.login:
-        #         contributors.add(f"@{commit.author.login}")
-        #
-        # # Remove the main author if they're also in contributors
-        # if self._pull_request.user and self._pull_request.user.login:
-        #     author_name = f"@{self._pull_request.user.login}"
-        #     if author_name in contributors:
-        #         contributors.remove(author_name)
-        #
-        # return sorted(list(contributors))
+        return []
+        # TODO: Implement contributors once the source of truth (PR vs commits) is finalized.

110-122: Format authors/contributors as strings, not lists.

If custom row formats ever include {authors}/{contributors}, the current list objects will render poorly.

Apply:

-        format_values["authors"] = self.authors if self.authors is not None else ""
-        format_values["contributors"] = self.contributors
+        format_values["authors"] = " ".join(self.authors) if self.authors else ""
+        format_values["contributors"] = " ".join(self.contributors) if self.contributors else ""
tests/release_notes/builder/test_release_notes_builder.py (3)

24-25: Deduplicate MockLabel (reuse the one from conftest).

Avoid drifting duplicates by importing MockLabel from conftest.

Apply:

-from tests.conftest import mock_safe_call_decorator
+from tests.conftest import mock_safe_call_decorator, MockLabel
@@
-# pylint: disable=too-few-public-methods
-class MockLabel:
-    def __init__(self, name):
-        self.name = name
+

Also applies to: 77-81


1300-1302: Fix debug prints (current output shows tuples).

Use f-strings to print actual strings.

Apply:

-    print("Actual:\n%s", actual_release_notes)
-    print("Expected:\n%s", expected_release_notes)
+    print(f"Actual:\n{actual_release_notes}")
+    print(f"Expected:\n{expected_release_notes}")

1835-1839: Tighten temporary debug prints.

Minor clean-up for readability.

Apply:

-    print("XXX")
-    print(actual_release_notes)
-    print("XXX")
+    print(f"XXX\n{actual_release_notes}\nXXX")
action.yml (5)

17-18: Typo: “scrapper” → “scraper”.

Apply:

-name: 'Release notes scrapper'
+name: 'Release notes scraper'

79-83: Include {type} in placeholder list.

Default uses {type} but description omits it.

Apply:

-  row-format-hierarchy-issue:
-    description: 'Format of the hierarchy issue in the release notes. Available placeholders: {number}, {title}. Placeholders are case-insensitive.'
+  row-format-hierarchy-issue:
+    description: 'Format of the hierarchy issue in the release notes. Available placeholders: {number}, {title}, {type}. Placeholders are case-insensitive.'

88-91: PR row placeholders: remove {pull-requests}.

PR rows don’t render that placeholder.

Apply:

-  row-format-pr:
-    description: 'Format of the pr row in the release notes. Available placeholders: {number}, {title}, {pull-requests}. Placeholders are case-insensitive.'
+  row-format-pr:
+    description: 'Format of the pr row in the release notes. Available placeholders: {number}, {title}. Placeholders are case-insensitive.'

131-136: PYTHONPATH export doesn’t persist across steps.

Write to GITHUB_ENV so the next step inherits it, or drop the step if unnecessary.

Apply:

     - name: Set PROJECT_ROOT and update PYTHONPATH
       run: |
         ACTION_ROOT="${{ github.action_path }}"
-        export PYTHONPATH="${PYTHONPATH}:${ACTION_ROOT}/release_notes_generator"
+        echo "PYTHONPATH=${PYTHONPATH}:${ACTION_ROOT}/release_notes_generator" >> "$GITHUB_ENV"
       shell: bash

35-39: Back-compat with legacy “regime” input.

Switching to hierarchy is good, but existing users passing regime may break. Recommend accepting regime as deprecated and mapping it in ActionInputs.get_hierarchy().

Proposed change outside this file (release_notes_generator/action_inputs.py):

def get_hierarchy() -> bool:
    val = get_action_input("hierarchy", None)
    if val is None:
        # Back-compat: regime='issue-hierarchy' implies hierarchy
        regime = get_action_input("regime", "")
        return regime.strip().lower() == "issue-hierarchy"
    return val.strip().lower() == "true"
release_notes_generator/record/factory/default_record_factory.py (3)

17-19: Incomplete module docstring.

Finish the sentence for clarity.

Apply:

-"""
-This module contains the DefaultRecordFactory class which is responsible for generating
-"""
+"""
+DefaultRecordFactory builds Record objects (issues, pulls, commits) from mined GitHub data.
+"""

153-171: Stale docstring params.

Method no longer accepts ‘records’; update doc.

Apply:

-        Parameters:
-            records (dict[int|str, Record]): The dictionary of records to add the issue record to.
-            issue (Issue): The issue to create a record for.
-            issue_labels (Optional[list[str]]): Optional set of labels for the issue. If not provided, labels will be
-                fetched from the issue.
-        Returns:
-            None
+        Parameters:
+            issue (Issue): The issue to create a record for.
+            issue_labels (Optional[list[str]]): Labels for the issue; fetched from GitHub if not provided.
+        Returns: None

65-97: Unused parameter skip_rec.

skip_rec is never used in register_pull_request; remove it or apply it when creating standalone PR records in fallback paths.

release_notes_generator/record/factory/issue_hierarchy_record_factory.py (2)

17-19: Module docstring references the wrong class.

Update to reflect IssueHierarchyRecordFactory.

Apply:

-"""
-This module contains the DefaultRecordFactory class which is responsible for generating
-"""
+"""
+IssueHierarchyRecordFactory builds hierarchical issue records (Epics/Features/Tasks) and associates PRs/commits.
+"""

241-243: Recursive rebind: safe but consider an iterative loop.

Current recursion is fine; if the chain is very deep, an iterative while loop over self.__sub_issue_parents would avoid recursion depth concerns.

tests/conftest.py (8)

48-54: Safe-call test shim looks good

Wrapper selectively swaps get_issues_for_pr; simple and readable for tests. Consider functools.wraps if you later need metadata preserved.


55-59: Silence ARG001 and allow keyword-only invocation

Rename the param to underscore to satisfy Ruff while keeping signature compatible with pull_number=... calls.

Apply:

-def mock_get_issues_for_pr(pull_number: int) -> set[int]:
+def mock_get_issues_for_pr(_pull_number: int) -> set[int]:
     # if pull_number == 150:
     #     return [451]
     return set()

226-241: Literal “/n” instead of newline “\n” in body

Use “\n” so release-notes parsing isn’t tripped by a stray “/n”.

-    issue.body = "I400 open/nRelease Notes:\n- Hierarchy level release note"
+    issue.body = "I400 open\nRelease Notes:\n- Hierarchy level release note"

Apply across similar fixtures.


243-258: Same “/n” newline typo in sub-issue body

Fix as above to ensure parsers see the header on a new line.

-    issue.body = "I450 closed/nRelease Notes:\n- Hierarchy level release note"
+    issue.body = "I450 closed\nRelease Notes:\n- Hierarchy level release note"

260-275: Same “/n” typo in hierarchy issue body

Replace “/n” with “\n”.

-    issue.body = "I300 open/nRelease Notes:\n- Hierarchy level release note"
+    issue.body = "I300 open\nRelease Notes:\n- Hierarchy level release note"

710-755: Comment vs. data mismatch; label case choices

  • Comment says “enhancement to one of the PRs” but only bug is assigned. Either add enhancement to a PR or update the comment.
  • Label names are lower-case (“epic”, “feature”), which is fine; just ensure tests don’t rely on case-sensitive comparisons.

Suggested tweak to match the comment:

@@
-    data.pull_requests[0].get_labels.return_value = [l_bug]
+    data.pull_requests[0].get_labels.return_value = [l_bug]
+    data.pull_requests[1].get_labels.return_value = [l_enh]

820-860: Inconsistent IssueType casing (“feature” vs “Feature”)

If code normalizes type names, fine; otherwise prefer consistent capitalization to avoid brittle assertions.

-    t_feature = mocker.Mock(spec=IssueType)
-    t_feature.name = "feature"
+    t_feature = mocker.Mock(spec=IssueType)
+    t_feature.name = "Feature"

907-937: Avoid mutating protected _labels; use ctor’s issue_labels

HierarchyIssueRecord accepts issue_labels; prefer that over touching _labels.

-    rec_epic_issue = HierarchyIssueRecord(issue=request.getfixturevalue("mock_open_hierarchy_issue_epic"))     # nr:200
-    rec_epic_issue._labels = ["epic"]  # override labels to have epic label
+    rec_epic_issue = HierarchyIssueRecord(
+        issue=request.getfixturevalue("mock_open_hierarchy_issue_epic"),
+        issue_labels=["epic"]
+    )  # nr:200

Also, nit: the TODO in Line 902 is stale; consider removing.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 958877a and f0bd99a.

📒 Files selected for processing (38)
  • .pylintrc (2 hunks)
  • README.md (2 hunks)
  • action.yml (4 hunks)
  • main.py (2 hunks)
  • release_notes_generator/action_inputs.py (8 hunks)
  • release_notes_generator/builder/base_builder.py (0 hunks)
  • release_notes_generator/builder/builder.py (1 hunks)
  • release_notes_generator/chapters/base_chapters.py (4 hunks)
  • release_notes_generator/chapters/custom_chapters.py (2 hunks)
  • release_notes_generator/chapters/service_chapters.py (5 hunks)
  • release_notes_generator/filter.py (4 hunks)
  • release_notes_generator/generator.py (3 hunks)
  • release_notes_generator/miner.py (1 hunks)
  • release_notes_generator/model/commit_record.py (1 hunks)
  • release_notes_generator/model/hierarchy_issue_record.py (1 hunks)
  • release_notes_generator/model/issue_record.py (5 hunks)
  • release_notes_generator/model/pull_request_record.py (2 hunks)
  • release_notes_generator/model/record.py (3 hunks)
  • release_notes_generator/model/sub_issue_record.py (1 hunks)
  • release_notes_generator/record/factory/default_record_factory.py (6 hunks)
  • release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1 hunks)
  • release_notes_generator/record/factory/record_factory.py (1 hunks)
  • release_notes_generator/utils/constants.py (1 hunks)
  • release_notes_generator/utils/pull_request_utils.py (3 hunks)
  • show_only.md (1 hunks)
  • tests/conftest.py (9 hunks)
  • tests/release_notes/builder/test_release_notes_builder.py (1 hunks)
  • tests/release_notes/chapters/test_base_chapters.py (1 hunks)
  • tests/release_notes/chapters/test_custom_chapters.py (1 hunks)
  • tests/release_notes/model/test_commit_record.py (1 hunks)
  • tests/release_notes/model/test_record.py (2 hunks)
  • tests/release_notes/record/factory/test_default_record_factory.py (12 hunks)
  • tests/release_notes/record/factory/test_issue_hierarchy_record_factory.py (1 hunks)
  • tests/release_notes/test_release_notes_builder.py (0 hunks)
  • tests/test_action_inputs.py (1 hunks)
  • tests/test_filter.py (1 hunks)
  • tests/test_release_notes_generator.py (1 hunks)
  • tests/utils/test_pull_reuqest_utils.py (4 hunks)
💤 Files with no reviewable changes (2)
  • release_notes_generator/builder/base_builder.py
  • tests/release_notes/test_release_notes_builder.py
🧰 Additional context used
🧬 Code graph analysis (28)
tests/release_notes/chapters/test_custom_chapters.py (1)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
release_notes_generator/utils/pull_request_utils.py (1)
tests/utils/test_pull_reuqest_utils.py (6)
  • json (104-115)
  • json (143-152)
  • json (164-165)
  • json (178-180)
  • json (194-203)
  • json (224-233)
tests/release_notes/record/factory/test_default_record_factory.py (2)
release_notes_generator/record/factory/default_record_factory.py (2)
  • DefaultRecordFactory (44-170)
  • generate (56-125)
tests/conftest.py (1)
  • mock_safe_call_decorator (48-53)
release_notes_generator/chapters/service_chapters.py (3)
release_notes_generator/chapters/base_chapters.py (2)
  • BaseChapters (28-112)
  • add_row (67-78)
release_notes_generator/model/issue_record.py (2)
  • record_id (46-47)
  • to_chapter_row (102-125)
release_notes_generator/model/hierarchy_issue_record.py (1)
  • to_chapter_row (87-140)
tests/utils/test_pull_reuqest_utils.py (1)
release_notes_generator/utils/pull_request_utils.py (1)
  • get_issues_for_pr (55-79)
release_notes_generator/model/pull_request_record.py (3)
release_notes_generator/model/record.py (4)
  • labels (59-68)
  • get_labels (121-126)
  • to_chapter_row (109-118)
  • added_into_chapters (138-144)
release_notes_generator/model/hierarchy_issue_record.py (2)
  • get_labels (71-84)
  • to_chapter_row (87-140)
release_notes_generator/model/issue_record.py (2)
  • get_labels (84-85)
  • to_chapter_row (102-125)
release_notes_generator/model/sub_issue_record.py (2)
release_notes_generator/model/issue_record.py (2)
  • IssueRecord (19-330)
  • issue (66-71)
release_notes_generator/model/record.py (1)
  • skip (54-56)
release_notes_generator/record/factory/record_factory.py (5)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/model/record.py (1)
  • Record (29-196)
release_notes_generator/record/factory/default_record_factory.py (1)
  • generate (56-125)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1)
  • generate (55-103)
release_notes_generator/generator.py (1)
  • generate (70-104)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (9)
release_notes_generator/model/commit_record.py (3)
  • CommitRecord (13-75)
  • commit (45-50)
  • get_labels (72-73)
release_notes_generator/model/hierarchy_issue_record.py (7)
  • HierarchyIssueRecord (16-154)
  • order_hierarchy_levels (142-154)
  • get_labels (71-84)
  • sub_issues (47-51)
  • sub_hierarchy_issues (54-58)
  • level (30-34)
  • level (37-44)
release_notes_generator/model/issue_record.py (6)
  • IssueRecord (19-330)
  • issue (66-71)
  • get_labels (84-85)
  • register_pull_request (194-201)
  • register_commit (203-217)
  • issue_type (74-80)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/action_inputs.py (2)
  • ActionInputs (64-504)
  • get_skip_release_notes_labels (211-219)
release_notes_generator/model/pull_request_record.py (3)
  • PullRequestRecord (15-289)
  • get_labels (107-108)
  • register_commit (167-174)
release_notes_generator/model/sub_issue_record.py (2)
  • SubIssueRecord (17-41)
  • issue (34-39)
release_notes_generator/record/factory/default_record_factory.py (4)
  • DefaultRecordFactory (44-170)
  • generate (56-125)
  • register_pull_request (65-95)
  • _create_record_for_issue (153-170)
release_notes_generator/utils/pull_request_utils.py (2)
  • get_issues_for_pr (55-79)
  • extract_issue_numbers_from_body (32-51)
release_notes_generator/miner.py (2)
release_notes_generator/model/issue_record.py (2)
  • IssueRecord (19-330)
  • issue (66-71)
release_notes_generator/action_inputs.py (1)
  • get_published_at (203-208)
release_notes_generator/model/commit_record.py (5)
release_notes_generator/model/hierarchy_issue_record.py (2)
  • to_chapter_row (87-140)
  • get_labels (71-84)
release_notes_generator/model/issue_record.py (3)
  • to_chapter_row (102-125)
  • get_rls_notes (127-155)
  • get_labels (84-85)
release_notes_generator/model/pull_request_record.py (3)
  • to_chapter_row (110-128)
  • get_rls_notes (130-151)
  • get_labels (107-108)
release_notes_generator/model/record.py (6)
  • to_chapter_row (109-118)
  • added_into_chapters (138-144)
  • present_in_chapters (146-152)
  • contains_release_notes (183-196)
  • get_rls_notes (129-135)
  • get_labels (121-126)
release_notes_generator/action_inputs.py (2)
  • ActionInputs (64-504)
  • get_duplicity_icon (196-200)
main.py (3)
tests/conftest.py (1)
  • custom_chapters (63-69)
release_notes_generator/generator.py (1)
  • custom_chapters (61-63)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
release_notes_generator/model/issue_record.py (6)
release_notes_generator/model/sub_issue_record.py (1)
  • issue (34-39)
release_notes_generator/model/record.py (4)
  • skip (54-56)
  • get_labels (121-126)
  • to_chapter_row (109-118)
  • added_into_chapters (138-144)
release_notes_generator/model/hierarchy_issue_record.py (2)
  • get_labels (71-84)
  • to_chapter_row (87-140)
release_notes_generator/model/pull_request_record.py (2)
  • get_labels (107-108)
  • to_chapter_row (110-128)
release_notes_generator/model/commit_record.py (2)
  • get_labels (72-73)
  • to_chapter_row (54-66)
tests/release_notes/model/test_record.py (2)
  • get_labels (55-56)
  • to_chapter_row (49-50)
tests/release_notes/model/test_commit_record.py (2)
tests/conftest.py (1)
  • mock_commit (573-578)
release_notes_generator/model/commit_record.py (6)
  • CommitRecord (13-75)
  • record_id (27-28)
  • is_closed (31-32)
  • is_open (35-36)
  • authors (39-40)
  • to_chapter_row (54-66)
release_notes_generator/generator.py (7)
release_notes_generator/builder/builder.py (2)
  • ReleaseNotesBuilder (32-86)
  • build (52-86)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
release_notes_generator/record/factory/default_record_factory.py (2)
  • DefaultRecordFactory (44-170)
  • generate (56-125)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (2)
  • IssueHierarchyRecordFactory (43-256)
  • generate (55-103)
release_notes_generator/record/factory/record_factory.py (2)
  • RecordFactory (29-45)
  • generate (35-42)
release_notes_generator/chapters/base_chapters.py (2)
  • since (52-61)
  • since (64-65)
release_notes_generator/action_inputs.py (2)
  • ActionInputs (64-504)
  • get_hierarchy (172-179)
release_notes_generator/builder/builder.py (5)
release_notes_generator/action_inputs.py (3)
  • ActionInputs (64-504)
  • get_warnings (276-281)
  • get_print_empty_chapters (284-289)
tests/conftest.py (2)
  • custom_chapters (63-69)
  • service_chapters (86-87)
release_notes_generator/generator.py (1)
  • custom_chapters (61-63)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
release_notes_generator/chapters/service_chapters.py (1)
  • ServiceChapters (43-270)
tests/release_notes/model/test_record.py (1)
release_notes_generator/model/record.py (4)
  • skip (54-56)
  • labels (59-68)
  • to_chapter_row (109-118)
  • get_labels (121-126)
tests/release_notes/builder/test_release_notes_builder.py (5)
release_notes_generator/builder/builder.py (2)
  • ReleaseNotesBuilder (32-86)
  • build (52-86)
tests/conftest.py (19)
  • custom_chapters (63-69)
  • mock_safe_call_decorator (48-53)
  • MockLabel (44-46)
  • custom_chapters_not_print_empty_chapters (73-81)
  • record_with_issue_closed_two_pulls (901-905)
  • record_with_issue_closed_no_pull (868-869)
  • pull_request_record_merged (977-980)
  • pull_request_record_closed (1002-1005)
  • record_with_issue_open_two_pulls_closed (947-951)
  • record_with_issue_open_no_pull (864-865)
  • pull_request_record_open (984-987)
  • record_with_issue_closed_one_pull (880-883)
  • record_with_issue_closed_one_pull_merged (887-890)
  • record_with_issue_closed_one_pull_merged_skip (894-897)
  • pull_request_record_closed_with_skip_label (1009-1015)
  • mined_data_isolated_record_types_no_labels_no_type_defined (582-707)
  • mined_data_isolated_record_types_with_labels_no_type_defined (711-754)
  • mined_data_isolated_record_types_no_labels_with_type_defined (758-817)
  • mined_data_isolated_record_types_with_labels_with_type_defined (821-859)
release_notes_generator/chapters/custom_chapters.py (2)
  • CustomChapters (37-90)
  • from_yaml_array (75-90)
release_notes_generator/record/factory/default_record_factory.py (1)
  • DefaultRecordFactory (44-170)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (2)
  • IssueHierarchyRecordFactory (43-256)
  • generate (55-103)
release_notes_generator/model/record.py (5)
tests/release_notes/model/test_record.py (3)
  • labels (42-43)
  • get_labels (55-56)
  • to_chapter_row (49-50)
release_notes_generator/model/hierarchy_issue_record.py (2)
  • get_labels (71-84)
  • to_chapter_row (87-140)
release_notes_generator/model/issue_record.py (2)
  • get_labels (84-85)
  • to_chapter_row (102-125)
release_notes_generator/model/pull_request_record.py (2)
  • get_labels (107-108)
  • to_chapter_row (110-128)
release_notes_generator/model/commit_record.py (2)
  • get_labels (72-73)
  • to_chapter_row (54-66)
release_notes_generator/model/hierarchy_issue_record.py (3)
release_notes_generator/action_inputs.py (3)
  • ActionInputs (64-504)
  • get_duplicity_icon (196-200)
  • get_row_format_hierarchy_issue (309-321)
release_notes_generator/model/issue_record.py (5)
  • issue (66-71)
  • pull_requests_count (219-225)
  • get_labels (84-85)
  • get_pr_links (227-239)
  • get_rls_notes (127-155)
release_notes_generator/model/record.py (4)
  • get_labels (121-126)
  • labels (59-68)
  • contains_release_notes (183-196)
  • get_rls_notes (129-135)
release_notes_generator/filter.py (2)
release_notes_generator/action_inputs.py (1)
  • get_hierarchy (172-179)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
tests/conftest.py (8)
release_notes_generator/model/commit_record.py (3)
  • CommitRecord (13-75)
  • get_labels (72-73)
  • commit (45-50)
release_notes_generator/model/hierarchy_issue_record.py (6)
  • HierarchyIssueRecord (16-154)
  • get_labels (71-84)
  • level (30-34)
  • level (37-44)
  • sub_hierarchy_issues (54-58)
  • sub_issues (47-51)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/chapters/service_chapters.py (1)
  • ServiceChapters (43-270)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
release_notes_generator/model/sub_issue_record.py (2)
  • SubIssueRecord (17-41)
  • issue (34-39)
tests/release_notes/record/factory/test_default_record_factory.py (5)
  • mock_safe_call_decorator (159-164)
  • wrapper (160-163)
  • wrapper (334-337)
  • wrapper (439-442)
  • mock_get_issues_for_pr (166-171)
release_notes_generator/record/factory/default_record_factory.py (1)
  • register_pull_request (65-95)
tests/release_notes/record/factory/test_issue_hierarchy_record_factory.py (6)
release_notes_generator/model/commit_record.py (1)
  • CommitRecord (13-75)
release_notes_generator/model/hierarchy_issue_record.py (6)
  • HierarchyIssueRecord (16-154)
  • pull_requests_count (60-69)
  • sub_hierarchy_issues (54-58)
  • sub_issues (47-51)
  • level (30-34)
  • level (37-44)
release_notes_generator/model/issue_record.py (1)
  • IssueRecord (19-330)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (2)
  • IssueHierarchyRecordFactory (43-256)
  • generate (55-103)
tests/conftest.py (5)
  • mock_safe_call_decorator (48-53)
  • mined_data_isolated_record_types_no_labels_no_type_defined (582-707)
  • mined_data_isolated_record_types_with_labels_no_type_defined (711-754)
  • mined_data_isolated_record_types_no_labels_with_type_defined (758-817)
  • mined_data_isolated_record_types_with_labels_with_type_defined (821-859)
release_notes_generator/action_inputs.py (1)
release_notes_generator/utils/gh_action.py (1)
  • get_action_input (26-38)
tests/release_notes/chapters/test_base_chapters.py (1)
release_notes_generator/chapters/base_chapters.py (1)
  • BaseChapters (28-112)
tests/test_release_notes_generator.py (3)
tests/conftest.py (1)
  • custom_chapters (63-69)
release_notes_generator/generator.py (1)
  • custom_chapters (61-63)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
release_notes_generator/record/factory/default_record_factory.py (6)
release_notes_generator/record/factory/record_factory.py (2)
  • RecordFactory (29-45)
  • generate (35-42)
release_notes_generator/generator.py (2)
  • rate_limiter (66-68)
  • generate (70-104)
release_notes_generator/utils/github_rate_limiter.py (1)
  • GithubRateLimiter (30-76)
release_notes_generator/utils/decorators.py (1)
  • safe_call_decorator (51-82)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (2)
  • generate (55-103)
  • _create_record_for_issue (196-201)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/chapters/custom_chapters.py (6)
release_notes_generator/action_inputs.py (1)
  • ActionInputs (64-504)
release_notes_generator/chapters/base_chapters.py (2)
  • BaseChapters (28-112)
  • add_row (67-78)
release_notes_generator/model/hierarchy_issue_record.py (3)
  • HierarchyIssueRecord (16-154)
  • pull_requests_count (60-69)
  • to_chapter_row (87-140)
release_notes_generator/model/issue_record.py (4)
  • IssueRecord (19-330)
  • record_id (46-47)
  • pull_requests_count (219-225)
  • to_chapter_row (102-125)
release_notes_generator/model/record.py (5)
  • Record (29-196)
  • record_id (72-77)
  • labels (59-68)
  • is_present_in_chapters (45-51)
  • to_chapter_row (109-118)
release_notes_generator/model/sub_issue_record.py (1)
  • SubIssueRecord (17-41)
🪛 markdownlint-cli2 (0.18.1)
show_only.md

2-2: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


4-4: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


6-6: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


7-7: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


8-8: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


9-9: Unordered list style
Expected: dash; Actual: plus

(MD004, ul-style)


9-9: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


10-10: Unordered list style
Expected: dash; Actual: asterisk

(MD004, ul-style)


10-10: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


11-11: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


13-13: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


15-15: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


16-16: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


17-17: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


18-18: Unordered list style
Expected: dash; Actual: plus

(MD004, ul-style)


18-18: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


19-19: Unordered list style
Expected: dash; Actual: asterisk

(MD004, ul-style)


19-19: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


20-20: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


25-25: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


26-26: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


27-27: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


28-28: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


29-29: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


30-30: Unordered list style
Expected: dash; Actual: plus

(MD004, ul-style)


30-30: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


31-31: Unordered list style
Expected: dash; Actual: asterisk

(MD004, ul-style)


31-31: Unordered list indentation
Expected: 6; Actual: 12

(MD007, ul-indent)

🪛 Ruff (0.13.1)
release_notes_generator/model/commit_record.py

68-68: Unused method argument: line_marks

(ARG002)

tests/release_notes/model/test_commit_record.py

12-12: Redefinition of unused mock_commit from line 4

(F811)


19-19: Redefinition of unused mock_commit from line 4

(F811)


23-23: Redefinition of unused mock_commit from line 4

(F811)


28-28: Redefinition of unused mock_commit from line 4

(F811)


39-39: Redefinition of unused mock_commit from line 4

(F811)


51-51: Redefinition of unused mock_commit from line 4

(F811)


56-56: Unused lambda argument: self

(ARG005)


59-59: Unused lambda argument: line_marks

(ARG005)


63-63: Redefinition of unused mock_commit from line 4

(F811)

tests/release_notes/model/test_record.py

49-49: Unused method argument: add_into_chapters

(ARG002)


52-52: Unused method argument: line_marks

(ARG002)

tests/conftest.py

55-55: Unused function argument: pull_number

(ARG001)

🪛 GitHub Actions: Check PR Release Notes in Description
main.py

[error] 1-1: Release notes presence check failed. Release notes title '(?:[Rr]elease [Nn]otes:|Summary by CodeRabbit)' not found in pull request body. Command: python /home/runner/work/_actions/AbsaOSS/release-notes-presence-check/v0.3.0/main.py

Comment on lines 135 to 139
# Derive 'since' from release
data.since = data.release.created_at
if ActionInputs.get_published_at():
data.since = data.release.published_at

Copy link

@coderabbitai coderabbitai bot Sep 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

❓ Verification inconclusive

Handle None published_at to avoid passing None to since.

If published-at is true and the latest release has published_at is None (draft/unpublished), since=None may break filtering.

Apply this diff:

-        data.since = data.release.created_at
-        if ActionInputs.get_published_at():
-            data.since = data.release.published_at
+        # Prefer published_at when requested and available, otherwise fallback to created_at
+        prefer_published = ActionInputs.get_published_at()
+        data.since = (
+            data.release.published_at
+            if prefer_published and getattr(data.release, "published_at", None)
+            else data.release.created_at
+        )

Also log which timestamp was used for traceability.


Use published_at only when present — avoid setting data.since to None

File: release_notes_generator/miner.py (lines 135-139)

-        data.since = data.release.created_at
-        if ActionInputs.get_published_at():
-            data.since = data.release.published_at
+        # Prefer published_at when requested and available, otherwise fallback to created_at
+        prefer_published = ActionInputs.get_published_at()
+        data.since = (
+            data.release.published_at
+            if prefer_published and getattr(data.release, "published_at", None)
+            else data.release.created_at
+        )

Also log which timestamp was used for traceability.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In release_notes_generator/miner.py around lines 135 to 139, don't blindly
assign data.release.published_at because it may be None; instead set data.since
= data.release.published_at only if published_at is present/truthy, otherwise
keep data.since = data.release.created_at, and add a log statement indicating
which timestamp (published_at or created_at) was used for traceability; ensure
the log includes the actual timestamp value and use the module's logger.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Used.

Copy link

Choose a reason for hiding this comment

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

@miroslavpojer Great! Thanks for implementing the fix to handle the potential None published_at scenario. This ensures data.since always has a valid timestamp value for the filtering logic.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines 29 to 34
def __init__(self, issue: Issue, issue_labels: Optional[list[str]] = None, skip: bool = False):
super().__init__(skip=skip)

self._issue: Issue = issue
self._issue_type: Optional[str] = None
self._labels = issue_labels if issue_labels is not None else []

Copy link

@coderabbitai coderabbitai bot Sep 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

issue_labels parameter is ignored; use as a fallback.

You accept issue_labels but get_labels() always reads GitHub labels. Prefer provided labels when present (helps factories/tests and offline scenarios).

Apply this diff:

     def get_labels(self) -> list[str]:
-        return [label.name for label in self._issue.get_labels()]
+        # Prefer explicit labels if provided; otherwise read from GitHub.
+        if self._labels:
+            return self._labels
+        return [label.name for label in self._issue.get_labels()]

Also applies to: 84-86

🤖 Prompt for AI Agents
In release_notes_generator/model/issue_record.py around lines 29-34 (and also
adjust lines 84-86), the constructor stores issue_labels but the class currently
always reads labels from the GitHub Issue; change logic so provided issue_labels
are used when present as a fallback for offline/tests. Update get_labels() (and
any other label-accessing code at lines 84-86) to return self._labels if
self._labels is non-empty, otherwise fetch and return labels from the Issue
object; ensure the constructor still defaults self._labels to an empty list when
None is passed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

Copy link

Choose a reason for hiding this comment

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

@miroslavpojer Thank you for the update! However, looking at the current code in the file, the get_labels() method on lines 84-86 still appears to directly read from the GitHub issue:

def get_labels(self) -> list[str]:
    return [label.name for label in self._issue.get_labels()]

This means the issue_labels parameter passed to the constructor is still not being used as a fallback. Could you please push your changes so I can verify the fix, or let me know if you need any clarification on the suggested implementation?

🐰💭

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

I'll check the current implementation to verify that the fix has been applied correctly.

@miroslavpojer I can see that the get_labels() method still hasn't been updated to use the issue_labels parameter as a fallback. The current implementation on lines 84-86 is:

def get_labels(self) -> list[str]:
    self._labels = [label.name for label in list(self._issue.get_labels())]
    return self.labels

This still always fetches from GitHub and ignores any issue_labels passed to the constructor. The fix should be:

def get_labels(self) -> list[str]:
    # Use provided labels if available, otherwise fetch from GitHub
    if self._labels:
        return self._labels
    self._labels = [label.name for label in list(self._issue.get_labels())]
    return self._labels

Could you please ensure your changes are committed and pushed to the branch? The current code in the PR doesn't reflect the fix yet.

🐰🔍


🏁 Script executed:

#!/bin/bash
# Check the current implementation of the get_labels method in IssueRecord
cat release_notes_generator/model/issue_record.py | sed -n '80,90p'

Length of output: 354


@miroslavpojer I can see that the fix hasn't been applied yet. The current get_labels() method still directly fetches from GitHub and ignores the issue_labels parameter:

def get_labels(self) -> list[str]:
    self._labels = [label.name for label in list(self._issue.get_labels())]
    return self.labels

For the fix to work properly, you need to update it to:

def get_labels(self) -> list[str]:
    # Use provided labels if available, otherwise fetch from GitHub
    if self._labels:
        return self._labels
    self._labels = [label.name for label in list(self._issue.get_labels())]
    return self._labels

Also, in the constructor, you should change:

self._labels = issue_labels if issue_labels is not None else []

to:

self._labels = issue_labels if issue_labels is not None else None

This way, get_labels() can distinguish between "no labels provided" (None) and "empty labels provided" ([]).

Please commit and push these changes when ready, and I'll verify the implementation.

🐰📝

Comment on lines 23 to 31
def __init__(self, sub_issue: SubIssue | Issue, issue_labels: Optional[list[str]] = None, skip: bool = False):
super().__init__(sub_issue, issue_labels, skip)

self._labels = issue_labels if issue_labels is not None else []

self._pull_requests: dict[int, PullRequest] = {}
self._commits: dict[int, dict[str, Commit]] = {}

# properties - override IssueRecord properties
Copy link

@coderabbitai coderabbitai bot Sep 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Tighten type to SubIssue and remove redundant state reinit.

Accepting Issue breaks the issue property type guarantee and can mislead downstream code; reinitializing fields duplicates IssueRecord init.

Apply this diff:

-class SubIssueRecord(IssueRecord):
+class SubIssueRecord(IssueRecord):
@@
-    def __init__(self, sub_issue: SubIssue | Issue, issue_labels: Optional[list[str]] = None, skip: bool = False):
-        super().__init__(sub_issue, issue_labels, skip)
-
-        self._labels = issue_labels if issue_labels is not None else []
-
-        self._pull_requests: dict[int, PullRequest] = {}
-        self._commits: dict[int, dict[str, Commit]] = {}
+    def __init__(self, sub_issue: SubIssue, issue_labels: Optional[list[str]] = None, skip: bool = False):
+        super().__init__(sub_issue, issue_labels, skip)
@@
-    def issue(self) -> SubIssue:
+    def issue(self) -> SubIssue:
@@
-        return cast(SubIssue, self._issue)
+        # Defensive: runtime guarantee
+        if not isinstance(self._issue, SubIssue):
+            raise TypeError("SubIssueRecord.issue is expected to be a SubIssue")
+        return self._issue

Also applies to: 33-39

🤖 Prompt for AI Agents
In release_notes_generator/model/sub_issue_record.py around lines 23-31 (also
apply same change to lines 33-39), tighten the constructor signature to accept
SubIssue (not Issue) so the stored issue keeps the SubIssue type guarantee, and
remove the redundant reinitialization of instance fields that the base
IssueRecord already sets (do not reassign self._labels, self._pull_requests, or
self._commits after calling super()); keep the super().__init__(...) call and
only perform any SubIssue-specific initialization if strictly necessary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

Copy link

Choose a reason for hiding this comment

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

@miroslavpojer Thank you for addressing those issues! The type tightening to SubIssue and removal of redundant field initialization will make the code cleaner and more type-safe.

(ノ◕ヮ◕)ノ*:・゚✧


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines 299 to 320
@pytest.fixture
def mock_open_hierarchy_issue_feature(mocker):
issue_type = mocker.Mock(spec=IssueType)
issue_type.name = "Feature"

issue = mocker.Mock(spec=Issue)
issue.state = IssueRecord.ISSUE_STATE_OPEN
issue.number = 201
issue.title = "HI201 open"
issue.state_reason = None
issue.body = "HI201 open/nRelease Notes:\n- Feature level release note"
issue.type = issue_type
issue.created_at = datetime.now()

label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "label3"
issue.get_labels.return_value = [label1, label2]

return issue

Copy link

@coderabbitai coderabbitai bot Sep 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Spec Issue + extra attrs; newline typo

Same concerns as above; also fix the body newline.

-    issue = mocker.Mock(spec=Issue)
+    issue = mocker.Mock()
-    issue.body = "HI201 open/nRelease Notes:\n- Feature level release note"
+    issue.body = "HI201 open\nRelease Notes:\n- Feature level release note"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.fixture
def mock_open_hierarchy_issue_feature(mocker):
issue_type = mocker.Mock(spec=IssueType)
issue_type.name = "Feature"
issue = mocker.Mock(spec=Issue)
issue.state = IssueRecord.ISSUE_STATE_OPEN
issue.number = 201
issue.title = "HI201 open"
issue.state_reason = None
issue.body = "HI201 open/nRelease Notes:\n- Feature level release note"
issue.type = issue_type
issue.created_at = datetime.now()
label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "label3"
issue.get_labels.return_value = [label1, label2]
return issue
@pytest.fixture
def mock_open_hierarchy_issue_feature(mocker):
issue_type = mocker.Mock(spec=IssueType)
issue_type.name = "Feature"
issue = mocker.Mock()
issue.state = IssueRecord.ISSUE_STATE_OPEN
issue.number = 201
issue.title = "HI201 open"
issue.state_reason = None
issue.body = "HI201 open\nRelease Notes:\n- Feature level release note"
issue.type = issue_type
issue.created_at = datetime.now()
label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "label3"
issue.get_labels.return_value = [label1, label2]
return issue
🤖 Prompt for AI Agents
In tests/conftest.py around lines 299 to 320, the issue and label mocks
currently use mocker.Mock(spec=...) which can allow adding attributes not
present on the real objects and the issue.body contains a typo "/n" instead of a
newline; update the mocks to use spec_set (mocker.Mock(spec_set=Issue) and
spec_set=IssueType, spec_set=MockLabel) so only real attributes can be set, only
assign attributes that exist on the Issue/IssueType/MockLabel specs (remove any
extras), and fix the body string to use a real newline sequence ("\nRelease
Notes...") instead of "/n".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This works.

Copy link

Choose a reason for hiding this comment

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

@miroslavpojer Great! I'm glad the fix worked correctly. Using spec_set instead of spec helps catch attribute assignment errors early, and fixing that newline typo ensures the mock data is properly formatted for testing.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines +322 to +342
@pytest.fixture
def mock_closed_issue_type_task(mocker):
issue_type = mocker.Mock(spec=IssueType)
issue_type.name = "Task"

issue = mocker.Mock(spec=Issue)
issue.state = IssueRecord.ISSUE_STATE_CLOSED
issue.title = "Do this task"
issue.number = 202
issue.body = "Some issue body text"
issue.type = issue_type
issue.created_at = datetime.now()

label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "label2"
issue.get_labels.return_value = [label1, label2]

return issue

Copy link

@coderabbitai coderabbitai bot Sep 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Spec Issue + assigning .type

Setting .type on a spec’d PyGithub Issue is unsafe. Drop the spec (or use a stub) for fixtures that manipulate .type.

-    issue = mocker.Mock(spec=Issue)
+    issue = mocker.Mock()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.fixture
def mock_closed_issue_type_task(mocker):
issue_type = mocker.Mock(spec=IssueType)
issue_type.name = "Task"
issue = mocker.Mock(spec=Issue)
issue.state = IssueRecord.ISSUE_STATE_CLOSED
issue.title = "Do this task"
issue.number = 202
issue.body = "Some issue body text"
issue.type = issue_type
issue.created_at = datetime.now()
label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "label2"
issue.get_labels.return_value = [label1, label2]
return issue
@pytest.fixture
def mock_closed_issue_type_task(mocker):
issue_type = mocker.Mock(spec=IssueType)
issue_type.name = "Task"
issue = mocker.Mock()
issue.state = IssueRecord.ISSUE_STATE_CLOSED
issue.title = "Do this task"
issue.number = 202
issue.body = "Some issue body text"
issue.type = issue_type
issue.created_at = datetime.now()
label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "label2"
issue.get_labels.return_value = [label1, label2]
return issue
🤖 Prompt for AI Agents
In tests/conftest.py around lines 322 to 342, the fixture uses
mocker.Mock(spec=Issue) and then assigns a .type attribute which is unsafe with
a spec; replace the spec'd mock with a non-spec mock or a simple stub for Issue
(e.g., remove spec=Issue or use mocker.Mock() / a small simple object) so you
can freely set .type, and similarly avoid spec=MockLabel on labels if you need
to mutate them; update the fixture to create plain mocks/stubs before returning
the issue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This works.

Copy link

Choose a reason for hiding this comment

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

@miroslavpojer Great! I'm glad the fix worked for you. Removing the spec=Issue parameter allows the mock to be freely modified with attributes like .type without running into spec validation issues. This approach gives you the flexibility needed for comprehensive test fixtures while still maintaining clean, working tests.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines +344 to +361
@pytest.fixture
def mock_closed_issue_type_none(mocker):
issue = mocker.Mock(spec=Issue)
issue.state = IssueRecord.ISSUE_STATE_CLOSED
issue.title = "Do this issue"
issue.number = 204
issue.body = "Some sub issue body text"
issue.type = None
issue.created_at = datetime.now()

label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "label2"
issue.get_labels.return_value = [label1, label2]

return issue

Copy link

@coderabbitai coderabbitai bot Sep 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Spec Issue + assigning .type

Same as prior comment.

-    issue = mocker.Mock(spec=Issue)
+    issue = mocker.Mock()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.fixture
def mock_closed_issue_type_none(mocker):
issue = mocker.Mock(spec=Issue)
issue.state = IssueRecord.ISSUE_STATE_CLOSED
issue.title = "Do this issue"
issue.number = 204
issue.body = "Some sub issue body text"
issue.type = None
issue.created_at = datetime.now()
label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "label2"
issue.get_labels.return_value = [label1, label2]
return issue
@pytest.fixture
def mock_closed_issue_type_none(mocker):
issue = mocker.Mock()
issue.state = IssueRecord.ISSUE_STATE_CLOSED
issue.title = "Do this issue"
issue.number = 204
issue.body = "Some sub issue body text"
issue.type = None
issue.created_at = datetime.now()
label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "label2"
issue.get_labels.return_value = [label1, label2]
return issue
🤖 Prompt for AI Agents
In tests/conftest.py around lines 344 to 361, the test creates a mock with
spec=Issue and then assigns issue.type directly which can violate the spec or be
unclear; instead create the mock with the attribute set at construction (e.g.
mocker.Mock(spec=Issue, type=None)) or use create_autospec/create_autospec(...,
instance=True) and set the attribute via the constructor, or remove the direct
assignment if Issue does not define .type; update the fixture to set the type in
the mock constructor (or omit it) so the mock conforms to the Issue spec.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This works.

Copy link

Choose a reason for hiding this comment

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

@miroslavpojer Great! I'm glad the fix resolved the spec issue. Removing spec=Issue from the mock allows for direct assignment of the type attribute without violating the mock specification constraints.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines +363 to +383
@pytest.fixture
def mock_closed_issue_type_bug(mocker):
issue_type = mocker.Mock(spec=IssueType)
issue_type.name = "Bug"

issue = mocker.Mock(spec=Issue)
issue.state = IssueRecord.ISSUE_STATE_CLOSED
issue.title = "Fix the bug"
issue.number = 203
issue.body = "Some issue body text\nRelease Notes:\n- Fixed bug\n- Improved performance\n+ More nice code\n * Awesome architecture"
issue.type = issue_type
issue.created_at = datetime.now()

label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "bug"
issue.get_labels.return_value = [label1, label2]

return issue

Copy link

@coderabbitai coderabbitai bot Sep 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Spec Issue + assigning .type; body contains release-notes block

Drop spec to allow .type, keep release-notes text unchanged.

-    issue = mocker.Mock(spec=Issue)
+    issue = mocker.Mock()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.fixture
def mock_closed_issue_type_bug(mocker):
issue_type = mocker.Mock(spec=IssueType)
issue_type.name = "Bug"
issue = mocker.Mock(spec=Issue)
issue.state = IssueRecord.ISSUE_STATE_CLOSED
issue.title = "Fix the bug"
issue.number = 203
issue.body = "Some issue body text\nRelease Notes:\n- Fixed bug\n- Improved performance\n+ More nice code\n * Awesome architecture"
issue.type = issue_type
issue.created_at = datetime.now()
label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "bug"
issue.get_labels.return_value = [label1, label2]
return issue
@pytest.fixture
def mock_closed_issue_type_bug(mocker):
issue_type = mocker.Mock(spec=IssueType)
issue_type.name = "Bug"
issue = mocker.Mock()
issue.state = IssueRecord.ISSUE_STATE_CLOSED
issue.title = "Fix the bug"
issue.number = 203
issue.body = "Some issue body text\nRelease Notes:\n- Fixed bug\n- Improved performance\n+ More nice code\n * Awesome architecture"
issue.type = issue_type
issue.created_at = datetime.now()
label1 = mocker.Mock(spec=MockLabel)
label1.name = "label1"
label2 = mocker.Mock(spec=MockLabel)
label2.name = "bug"
issue.get_labels.return_value = [label1, label2]
return issue
🤖 Prompt for AI Agents
In tests/conftest.py around lines 363-383 the test creates `issue =
mocker.Mock(spec=Issue)` then assigns `issue.type`, which fails because the mock
is spec'd to Issue; remove the `spec=Issue` (create a plain mock) so you can
assign `.type`, and keep the `issue.body` text exactly as shown (do not alter
the release-notes block or its formatting); leave the label mocks as they are.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This works.

Copy link

Choose a reason for hiding this comment

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

@miroslavpojer Great to hear that the fix works! The mock spec removal allows for the flexible property assignment needed in your tests while maintaining the proper test structure.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
release_notes_generator/model/record.py (1)

175-181: contain_all_labels() logic is incorrect (requires equal lengths).

The method should check that all specified labels are present (subset), not that lengths match.

Apply this diff:

-        if len(self.labels) != len(labels):
-            return False
-
-        for lbl in self.labels:
-            if lbl not in labels:
-                return False
-        return True
+        return all(lbl in self.labels for lbl in labels)
🧹 Nitpick comments (21)
.pylintrc (1)

307-319: Raising complexity thresholds project‑wide — consider targeted mitigations.

Slightly loosening max-branches/locals/returns can mask hot spots. Prefer local refactors or per‑module disables where necessary, and document the rationale in the repo guidelines to avoid further creep.

release_notes_generator/utils/pull_request_utils.py (2)

55-56: Docstring doesn’t describe behavior.

Clarify what the function does (fetches closing issues via GraphQL and returns a set of numbers).

Apply this diff:

-def get_issues_for_pr(pull_number: int) -> set[int]:
-    """Update the placeholder values and format the graphQL query"""
+def get_issues_for_pr(pull_number: int) -> set[int]:
+    """Fetch closing issue numbers for a PR via GitHub GraphQL and return them as a set."""

75-77: Remove ad‑hoc print debug.

Hardcoded print for PR 3645 will leak to stdout in production. Use logging at DEBUG or drop it.

Apply this diff:

-    if pull_number == 3645:
-        print(f"PR #{pull_number} - Extracted issue numbers from GitHub API: {numbers}")
+    # Consider logging at DEBUG level if needed:
+    # logger.debug("PR #%s - Extracted issue numbers from GitHub API: %s", pull_number, numbers)
release_notes_generator/model/record.py (1)

121-126: Docstring return type mismatch.

get_labels() returns list[str], but docstring says set[str].

Apply this diff:

-        Returns:
-            set[str]: A list of labels associated with the record.
+        Returns:
+            list[str]: Labels associated with the record.
release_notes_generator/chapters/service_chapters.py (2)

191-194: Fix param name in docstring.

Parameter documented as “nr” but the argument is “record_id”.

Apply this diff:

-        @param nr: The number of the record.
+        @param record_id: The number of the record.

57-60: Consider using a set for used_record_numbers.

Membership checks are frequent; a set provides O(1) lookups and simplifies de‑dup logic.

release_notes_generator/model/sub_issue_record.py (2)

1-3: Module docstring references the wrong class.

This module defines SubIssueRecord, not IssueRecord.

Apply this diff:

-"""
-A module that defines the IssueRecord class, which represents an issue record in the release notes.
-"""
+"""
+Defines SubIssueRecord, a specialized IssueRecord for sub-issues in hierarchical release notes.
+"""

8-11: Tighten constructor type to SubIssue only (and drop unused Issue import).

Accepting Issue contradicts the runtime type check in property issue and weakens type guarantees.

[duplica te_comment]

Apply this diff:

-from github.Issue import SubIssue, Issue
+from github.Issue import SubIssue
-    def __init__(self, sub_issue: SubIssue | Issue, issue_labels: Optional[list[str]] = None, skip: bool = False):
+    def __init__(self, sub_issue: SubIssue, issue_labels: Optional[list[str]] = None, skip: bool = False):

Also applies to: 21-22

tests/release_notes/model/test_commit_record.py (2)

2-2: Remove imported fixture to fix Ruff F811 redefinition warnings.

Let pytest inject the mock_commit fixture by name; don't import it.

Apply:

-from tests.conftest import mock_commit

52-52: Silence unused lambda param warning (ARG005).

Rename the param to underscore to indicate intentional ignore.

-    monkeypatch.setattr(rec, "get_rls_notes", lambda line_marks=None: "Extra release notes.")
+    monkeypatch.setattr(rec, "get_rls_notes", lambda _line_marks=None: "Extra release notes.")
release_notes_generator/chapters/custom_chapters.py (1)

65-71: Confirm intent: exclude issues with zero PRs from custom chapters.

The pulls_count > 0 gate prevents issues without PRs from appearing in custom chapters. If you intend to show closed issues (with notes but no PRs) under user-defined chapters, relax this check.

release_notes_generator/filter.py (1)

113-158: Hierarchy vs default issue filters are equivalent; consider dedup or clarify difference.

Both _filter_issues_default and _filter_issues_issue_hierarchy include open issues and those closed since. If this is intentional, you can collapse to one implementation; otherwise, document or adjust the hierarchy behavior.

tests/release_notes/builder/test_release_notes_builder.py (1)

1467-1485: Minor nit: setting protected attrs in tests.

Directly assigning to rec._labels is pragmatic in tests but brittle. Prefer public helpers if available (e.g., a setter or constructor arg).

release_notes_generator/record/factory/default_record_factory.py (2)

108-116: Unify PR record creation to pass labels.

Hierarchy factory passes pull_labels into PullRequestRecord; default factory leaves it None. For consistent downstream behavior (e.g., label-based filtering/formatting), pass labels here too.

-            if not linked_from_api and not linked_from_body:
-                self._records[pull.number] = PullRequestRecord(pull, skip=skip_record)
+            if not linked_from_api and not linked_from_body:
+                self._records[pull.number] = PullRequestRecord(pull, pull_labels, skip=skip_record)

49-55: Note: stored _github is unused beyond rate limiter.

Acceptable, but you can drop the field if not needed elsewhere.

action.yml (2)

131-135: Step name mentions PROJECT_ROOT but only sets PYTHONPATH.

Either export PROJECT_ROOT or rename the step.

-    - name: Set PROJECT_ROOT and update PYTHONPATH
+    - name: Update PYTHONPATH
       run: |
         ACTION_ROOT="${{ github.action_path }}"
         echo PYTHONPATH="${PYTHONPATH}:${ACTION_ROOT}/release_notes_generator" >> $GITHUB_ENV
       shell: bash

Or set PROJECT_ROOT explicitly:

       run: |
         ACTION_ROOT="${{ github.action_path }}"
+        echo PROJECT_ROOT="${ACTION_ROOT}" >> $GITHUB_ENV
         echo PYTHONPATH="${PYTHONPATH}:${ACTION_ROOT}/release_notes_generator" >> $GITHUB_ENV

140-161: Use the built-in token instead of env.GITHUB_TOKEN to reduce setup friction.

Prefer github.token (or secrets.GITHUB_TOKEN) for the default.

-        INPUT_GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
+        INPUT_GITHUB_TOKEN: ${{ github.token }}
release_notes_generator/action_inputs.py (2)

172-178: Make boolean parsing tolerant for hierarchy input

Accept common truthy values to reduce user error friction.

-        val = get_action_input("hierarchy", "false")
-        return str(val).strip().lower() == "true"
+        val = get_action_input("hierarchy", "false")
+        return str(val).strip().lower() in ("true", "1", "yes", "y", "on")

488-503: Avoid Python 3.10+ match-case if action must run on older Pythons

If the action targets runners with Python <3.10, replace match-case with a dict/if-elif to avoid SyntaxError. If 3.10+ is guaranteed, ignore.

-        supported_row_format_keys = []
-        match row_type:
-            case ActionInputs.ROW_TYPE_ISSUE:
-                supported_row_format_keys = SUPPORTED_ROW_FORMAT_KEYS_ISSUE
-            case ActionInputs.ROW_TYPE_PR:
-                supported_row_format_keys = SUPPORTED_ROW_FORMAT_KEYS_PULL_REQUEST
-            case ActionInputs.ROW_TYPE_HIERARCHY_ISSUE:
-                supported_row_format_keys = SUPPORTED_ROW_FORMAT_KEYS_HIERARCHY_ISSUE
-            case _:
-                logger.warning(
-                    "Unknown row_type '%s' in _detect_row_format_invalid_keywords; defaulting to Issue keys.",
-                    row_type,
-                )
-                supported_row_format_keys = SUPPORTED_ROW_FORMAT_KEYS_ISSUE
+        mapping = {
+            ActionInputs.ROW_TYPE_ISSUE: SUPPORTED_ROW_FORMAT_KEYS_ISSUE,
+            ActionInputs.ROW_TYPE_PR: SUPPORTED_ROW_FORMAT_KEYS_PULL_REQUEST,
+            ActionInputs.ROW_TYPE_HIERARCHY_ISSUE: SUPPORTED_ROW_FORMAT_KEYS_HIERARCHY_ISSUE,
+        }
+        supported_row_format_keys = mapping.get(row_type)
+        if supported_row_format_keys is None:
+            logger.warning(
+                "Unknown row_type '%s' in _detect_row_format_invalid_keywords; defaulting to Issue keys.",
+                row_type,
+            )
+            supported_row_format_keys = SUPPORTED_ROW_FORMAT_KEYS_ISSUE
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1)

105-150: PR→Issue attachment logic: solid; minor cleanup possible

Reassigning record_keys after dict mutation is unnecessary (dict_keys is a live view). Optional micro‑cleanup below.

-        if len(pull_issues) > 0:
-            record_keys = self._records.keys()
+        if len(pull_issues) > 0:
+            record_keys = self._records.keys()  # live view; no need to reassign later
@@
-                    if parent_issue is not None:
-                        self._create_issue_record_using_sub_issues_existence(parent_issue)
-                        record_keys = self._records.keys()
+                    if parent_issue is not None:
+                        self._create_issue_record_using_sub_issues_existence(parent_issue)
tests/conftest.py (1)

48-59: Silence ARG001 (unused param) in mock_get_issues_for_pr

Rename param to underscore to satisfy linters; also drop commented code.

-def mock_safe_call_decorator(_rate_limiter):
+def mock_safe_call_decorator(_rate_limiter):
     def wrapper(fn):
         if fn.__name__ == "get_issues_for_pr":
             return mock_get_issues_for_pr
         return fn
     return wrapper
 
-def mock_get_issues_for_pr(pull_number: int) -> set[int]:
-    # if pull_number == 150:
-    #     return [451]
-    return set()
+def mock_get_issues_for_pr(_pull_number: int) -> set[int]:
+    return set()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0bd99a and bdd10ed.

📒 Files selected for processing (29)
  • .pylintrc (2 hunks)
  • README.md (3 hunks)
  • action.yml (6 hunks)
  • release_notes_generator/action_inputs.py (9 hunks)
  • release_notes_generator/builder/builder.py (2 hunks)
  • release_notes_generator/chapters/base_chapters.py (4 hunks)
  • release_notes_generator/chapters/custom_chapters.py (2 hunks)
  • release_notes_generator/chapters/service_chapters.py (5 hunks)
  • release_notes_generator/filter.py (4 hunks)
  • release_notes_generator/generator.py (3 hunks)
  • release_notes_generator/miner.py (1 hunks)
  • release_notes_generator/model/hierarchy_issue_record.py (1 hunks)
  • release_notes_generator/model/issue_record.py (3 hunks)
  • release_notes_generator/model/record.py (3 hunks)
  • release_notes_generator/model/sub_issue_record.py (1 hunks)
  • release_notes_generator/record/factory/default_record_factory.py (6 hunks)
  • release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1 hunks)
  • release_notes_generator/record/factory/record_factory.py (1 hunks)
  • release_notes_generator/utils/constants.py (1 hunks)
  • release_notes_generator/utils/pull_request_utils.py (4 hunks)
  • tests/conftest.py (9 hunks)
  • tests/release_notes/builder/test_release_notes_builder.py (1 hunks)
  • tests/release_notes/chapters/test_base_chapters.py (2 hunks)
  • tests/release_notes/chapters/test_custom_chapters.py (2 hunks)
  • tests/release_notes/model/test_commit_record.py (1 hunks)
  • tests/release_notes/record/factory/test_issue_hierarchy_record_factory.py (1 hunks)
  • tests/test_action_inputs.py (3 hunks)
  • tests/test_release_notes_generator.py (6 hunks)
  • tests/utils/test_pull_request_utils.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • tests/release_notes/record/factory/test_issue_hierarchy_record_factory.py
  • release_notes_generator/miner.py
  • release_notes_generator/chapters/base_chapters.py
  • release_notes_generator/model/hierarchy_issue_record.py
  • release_notes_generator/record/factory/record_factory.py
  • README.md
  • release_notes_generator/model/issue_record.py
  • tests/release_notes/chapters/test_custom_chapters.py
  • tests/test_action_inputs.py
  • tests/release_notes/chapters/test_base_chapters.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-24T07:24:03.693Z
Learnt from: miroslavpojer
PR: AbsaOSS/generate-release-notes#168
File: release_notes_generator/utils/pull_request_utils.py:69-70
Timestamp: 2025-09-24T07:24:03.693Z
Learning: In release_notes_generator/utils/pull_request_utils.py, the verify=False parameter in the requests.post call for GitHub API access should be kept as per the repository maintainer's explicit requirement, despite security best practices suggesting otherwise.

Applied to files:

  • release_notes_generator/utils/pull_request_utils.py
🧬 Code graph analysis (15)
release_notes_generator/generator.py (6)
release_notes_generator/builder/builder.py (2)
  • ReleaseNotesBuilder (32-86)
  • build (52-86)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
release_notes_generator/record/factory/default_record_factory.py (2)
  • DefaultRecordFactory (44-171)
  • generate (56-127)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (2)
  • IssueHierarchyRecordFactory (43-258)
  • generate (55-103)
release_notes_generator/chapters/base_chapters.py (2)
  • since (52-61)
  • since (64-65)
release_notes_generator/action_inputs.py (2)
  • ActionInputs (64-515)
  • get_hierarchy (172-177)
tests/test_release_notes_generator.py (2)
tests/conftest.py (6)
  • custom_chapters (63-69)
  • mock_repo (92-95)
  • mock_issue_closed (176-190)
  • mock_issue_closed_i1_bug (194-207)
  • mock_pull_closed_with_rls_notes_101 (431-448)
  • mock_pull_closed_with_rls_notes_102 (452-469)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
tests/release_notes/builder/test_release_notes_builder.py (5)
release_notes_generator/builder/builder.py (2)
  • ReleaseNotesBuilder (32-86)
  • build (52-86)
tests/conftest.py (18)
  • custom_chapters (63-69)
  • mock_safe_call_decorator (48-53)
  • MockLabel (44-46)
  • custom_chapters_not_print_empty_chapters (73-81)
  • record_with_issue_closed_two_pulls (877-881)
  • record_with_issue_closed_no_pull (844-845)
  • pull_request_record_merged (956-959)
  • pull_request_record_closed (981-984)
  • record_with_issue_open_two_pulls_closed (926-930)
  • record_with_two_issue_open_two_pulls_closed (934-944)
  • record_with_issue_closed_one_pull (856-859)
  • record_with_issue_closed_one_pull_merged (863-866)
  • record_with_issue_closed_one_pull_merged_skip (870-873)
  • pull_request_record_closed_with_skip_label (988-994)
  • mined_data_isolated_record_types_no_labels_no_type_defined (582-707)
  • mined_data_isolated_record_types_with_labels_no_type_defined (711-746)
  • mined_data_isolated_record_types_no_labels_with_type_defined (750-801)
  • mined_data_isolated_record_types_with_labels_with_type_defined (805-835)
release_notes_generator/chapters/custom_chapters.py (2)
  • CustomChapters (37-90)
  • from_yaml_array (75-90)
release_notes_generator/record/factory/default_record_factory.py (2)
  • DefaultRecordFactory (44-171)
  • generate (56-127)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (2)
  • IssueHierarchyRecordFactory (43-258)
  • generate (55-103)
release_notes_generator/record/factory/default_record_factory.py (6)
release_notes_generator/record/factory/record_factory.py (2)
  • RecordFactory (29-45)
  • generate (35-42)
release_notes_generator/generator.py (2)
  • rate_limiter (65-67)
  • generate (69-103)
release_notes_generator/utils/github_rate_limiter.py (1)
  • GithubRateLimiter (30-76)
release_notes_generator/utils/decorators.py (1)
  • safe_call_decorator (51-82)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (2)
  • generate (55-103)
  • _create_record_for_issue (198-203)
release_notes_generator/utils/pull_request_utils.py (1)
  • get_issues_for_pr (54-78)
release_notes_generator/chapters/service_chapters.py (1)
release_notes_generator/chapters/base_chapters.py (2)
  • BaseChapters (28-112)
  • add_row (67-78)
tests/conftest.py (6)
release_notes_generator/model/commit_record.py (3)
  • CommitRecord (13-75)
  • get_labels (72-73)
  • commit (45-50)
release_notes_generator/model/hierarchy_issue_record.py (6)
  • HierarchyIssueRecord (16-154)
  • get_labels (71-84)
  • level (30-34)
  • level (37-44)
  • sub_hierarchy_issues (54-58)
  • sub_issues (47-51)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/chapters/service_chapters.py (1)
  • ServiceChapters (43-273)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
release_notes_generator/model/sub_issue_record.py (2)
  • SubIssueRecord (15-36)
  • issue (27-34)
tests/utils/test_pull_request_utils.py (1)
release_notes_generator/utils/pull_request_utils.py (1)
  • get_issues_for_pr (54-78)
release_notes_generator/builder/builder.py (4)
release_notes_generator/action_inputs.py (3)
  • ActionInputs (64-515)
  • get_warnings (274-279)
  • get_print_empty_chapters (282-287)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
release_notes_generator/chapters/service_chapters.py (1)
  • ServiceChapters (43-273)
release_notes_generator/chapters/base_chapters.py (1)
  • populated_record_numbers_list (43-49)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (9)
release_notes_generator/model/hierarchy_issue_record.py (7)
  • HierarchyIssueRecord (16-154)
  • order_hierarchy_levels (142-154)
  • get_labels (71-84)
  • sub_issues (47-51)
  • sub_hierarchy_issues (54-58)
  • level (30-34)
  • level (37-44)
release_notes_generator/model/issue_record.py (6)
  • IssueRecord (16-328)
  • issue (63-68)
  • get_labels (81-83)
  • register_pull_request (192-199)
  • register_commit (201-215)
  • issue_type (71-77)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/action_inputs.py (2)
  • ActionInputs (64-515)
  • get_skip_release_notes_labels (209-217)
release_notes_generator/model/pull_request_record.py (3)
  • PullRequestRecord (15-289)
  • get_labels (107-108)
  • register_commit (167-174)
release_notes_generator/model/record.py (4)
  • Record (29-196)
  • get_labels (121-126)
  • skip (54-56)
  • labels (59-68)
release_notes_generator/model/sub_issue_record.py (2)
  • SubIssueRecord (15-36)
  • issue (27-34)
release_notes_generator/record/factory/default_record_factory.py (4)
  • DefaultRecordFactory (44-171)
  • generate (56-127)
  • register_pull_request (65-97)
  • _create_record_for_issue (155-171)
release_notes_generator/utils/pull_request_utils.py (2)
  • get_issues_for_pr (54-78)
  • extract_issue_numbers_from_body (31-50)
release_notes_generator/action_inputs.py (1)
release_notes_generator/utils/gh_action.py (1)
  • get_action_input (26-38)
release_notes_generator/chapters/custom_chapters.py (4)
release_notes_generator/model/hierarchy_issue_record.py (2)
  • pull_requests_count (60-69)
  • to_chapter_row (87-140)
release_notes_generator/model/issue_record.py (3)
  • IssueRecord (16-328)
  • pull_requests_count (217-223)
  • to_chapter_row (100-123)
release_notes_generator/model/record.py (3)
  • Record (29-196)
  • labels (59-68)
  • to_chapter_row (109-118)
release_notes_generator/model/sub_issue_record.py (1)
  • SubIssueRecord (15-36)
release_notes_generator/model/record.py (5)
tests/release_notes/model/test_record.py (3)
  • labels (42-43)
  • get_labels (55-56)
  • to_chapter_row (49-50)
release_notes_generator/model/issue_record.py (2)
  • get_labels (81-83)
  • to_chapter_row (100-123)
release_notes_generator/model/hierarchy_issue_record.py (2)
  • get_labels (71-84)
  • to_chapter_row (87-140)
release_notes_generator/model/commit_record.py (2)
  • get_labels (72-73)
  • to_chapter_row (54-66)
release_notes_generator/model/pull_request_record.py (2)
  • get_labels (107-108)
  • to_chapter_row (110-128)
release_notes_generator/filter.py (2)
release_notes_generator/action_inputs.py (1)
  • get_hierarchy (172-177)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/model/sub_issue_record.py (2)
release_notes_generator/model/issue_record.py (2)
  • IssueRecord (16-328)
  • issue (63-68)
release_notes_generator/model/record.py (1)
  • skip (54-56)
tests/release_notes/model/test_commit_record.py (2)
tests/conftest.py (1)
  • mock_commit (573-578)
release_notes_generator/model/commit_record.py (6)
  • CommitRecord (13-75)
  • record_id (27-28)
  • is_closed (31-32)
  • is_open (35-36)
  • authors (39-40)
  • to_chapter_row (54-66)
🪛 Ruff (0.13.1)
tests/conftest.py

55-55: Unused function argument: pull_number

(ARG001)

release_notes_generator/model/sub_issue_record.py

33-33: Avoid specifying long messages outside the exception class

(TRY003)

tests/release_notes/model/test_commit_record.py

5-5: Redefinition of unused mock_commit from line 2

(F811)


12-12: Redefinition of unused mock_commit from line 2

(F811)


16-16: Redefinition of unused mock_commit from line 2

(F811)


21-21: Redefinition of unused mock_commit from line 2

(F811)


32-32: Redefinition of unused mock_commit from line 2

(F811)


44-44: Redefinition of unused mock_commit from line 2

(F811)


52-52: Unused lambda argument: line_marks

(ARG005)


56-56: Redefinition of unused mock_commit from line 2

(F811)

🔇 Additional comments (31)
release_notes_generator/utils/pull_request_utils.py (1)

53-56: Good call on bounded caching.

lru_cache with maxsize=1024 is a sensible default for PR lookups.

tests/utils/test_pull_request_utils.py (1)

127-136: Tests correctly updated for set semantics and cache behavior.

Assertions are order‑agnostic, fixture clears lru_cache, and network call count is verified. Looks good.

Also applies to: 155-156, 213-215, 245-246

release_notes_generator/utils/constants.py (1)

37-44: Hierarchy row‑format keys look correct.

PR row keys no longer expose unsupported "pull-requests". Issue/hierarchy rows include it as expected.

release_notes_generator/generator.py (1)

95-118: Factory selection + builder wiring LGTM.

Dynamic switch to IssueHierarchyRecordFactory respects the new “hierarchy” input, and builder invocation is clean.

tests/test_release_notes_generator.py (3)

23-23: Import path update looks correct.

Switching to release_notes_generator.chapters.custom_chapters aligns with the new module layout.


61-63: Good: explicit “no releases” repo mocks.

Setting get_release=None and get_releases=[] removes ambiguity in tests and better reflects “no prior release” scenarios.

Also applies to: 105-107, 159-161


74-74: None-safety in get_issues_for_pr mocks: OK. Verify default factory’s guard.

Patching get_issues_for_pr to return None is fine given the factory path now guards with “or set()”. Please confirm all call sites (default and hierarchy factories) consistently handle None.

Also applies to: 128-129, 178-179

release_notes_generator/chapters/custom_chapters.py (1)

65-73: LGTM: corrected isinstance union and to_chapter_row usage.

  • Using a tuple in isinstance is correct.
  • Passing True into to_chapter_row matches the new signature and membership tracking.
release_notes_generator/builder/builder.py (2)

32-48: Builder refactor reads well.

Dependency injection of CustomChapters and ActionInputs wiring (warnings/print_empty_chapters) is clear and keeps the build flow cohesive.


71-71: No change required — populated_record_numbers_list is a @Property.

release_notes_generator/chapters/base_chapters.py declares populated_record_numbers_list with @Property, so using self.custom_chapters.populated_record_numbers_list (no parentheses) is correct.

Likely an incorrect or invalid review comment.

tests/release_notes/builder/test_release_notes_builder.py (1)

909-925: Solid coverage for both hierarchy and non-hierarchy paths.

Parametrizing hierarchy_value ensures both code paths remain exercised without duplicating tests.

release_notes_generator/record/factory/default_record_factory.py (1)

65-71: Good: safe-call None guard added.

Capturing linked issues and guarding before update avoids TypeError when the API returns None.

action.yml (2)

17-17: Nice rename.

“Release notes scraper” reads better.


35-39: Inputs for hierarchy mode and row format look good.

The new inputs align with the hierarchy feature and are wired to env vars.

Also applies to: 79-83, 157-160

release_notes_generator/action_inputs.py (7)

51-55: Verify constants wiring for hierarchy row formats/keys

Confirm these constants exist in utils.constants and that docs/tests align with their intended placeholders.


69-74: Row-type constants and cache: LGTM

Clear surface for per-type validation and caching.


306-320: Hierarchy issue row format: LGTM

Default template + keyed validation with cleaning is appropriate.


343-345: PR row format type-hinting: LGTM

Explicit row_type improves validation accuracy.


400-402: Hierarchy validation: LGTM

Type validation aligns with new boolean semantics.


440-447: Row-format keyword validation (PR/Hierarchy): LGTM

Covers per-type placeholders; good to run during validation.


466-466: Debug log surface: LGTM

Helpful visibility without noise.

release_notes_generator/record/factory/issue_hierarchy_record_factory.py (8)

63-77: Two‑phase issue registration: LGTM

First pass (with sub-issues) then remaining issues keeps state consistent for later re-binding.


81-89: Direct-commit registration: LGTM

Dedup via __registered_commits prevents duplicate commit records.


151-161: Hierarchy parent discovery: LGTM

Uses get_sub_issues presence as heuristic and records parent links for later re-binding.


168-187: Create HierarchyIssueRecord: LGTM

Skip-label honoring and registration are correct.


188-197: Labels + type mixing: LGTM

Appends normalized issue type when label missing; good for chapter mapping.


198-204: Create IssueRecord: LGTM

Delegates to base and tracks registration to avoid duplicates.


205-213: Create SubIssueRecord: LGTM

Skip-label honoring and proper record placement.


246-259: Hierarchy level ordering: LGTM

Top-down level propagation aligns with rendering requirements.

tests/conftest.py (2)

519-521: Fixture values adjusted: LGTM

Distinct URL/number for merged PR avoids collisions with closed PR fixture.


575-578: Commit mock shape: LGTM

Sets commit.author.login and commit.commit.message to match production access.

Comment on lines +139 to 142
if record_id not in self.used_record_numbers:
self.chapters[OTHERS_NO_TOPIC].add_row(record_id, record.to_chapter_row())
self.used_record_numbers.append(record_id)

Copy link

@coderabbitai coderabbitai bot Sep 24, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Unconditional de‑dup checks override duplicity scope.

The new if record_id in self.used_record_numbers: return bypasses duplicity_allowed() and will prevent duplicates even when duplicity is enabled, changing behavior.

Apply this diff to rely on the existing __is_row_present + duplicity_allowed() guard and remove unconditional checks:

-                    if record_id not in self.used_record_numbers:
-                        self.chapters[OTHERS_NO_TOPIC].add_row(record_id, record.to_chapter_row())
-                        self.used_record_numbers.append(record_id)
+                    self.chapters[OTHERS_NO_TOPIC].add_row(record_id, record.to_chapter_row())
+                    self.used_record_numbers.append(record_id)
-            if record_id in self.used_record_numbers:
-                return
-
             self.chapters[OTHERS_NO_TOPIC].add_row(record_id, record.to_chapter_row())
             self.used_record_numbers.append(record_id)
-                if record_id in self.used_record_numbers:
-                    return
-
                 self.chapters[OTHERS_NO_TOPIC].add_row(record_id, record.to_chapter_row())
                 self.used_record_numbers.append(record_id)
-            if record_id in self.used_record_numbers:
-                return
-
             # not record.is_present_in_chapters:
             self.chapters[OTHERS_NO_TOPIC].add_row(record_id, record.to_chapter_row())
             self.used_record_numbers.append(record_id)

Also applies to: 181-183, 218-220, 240-242

🤖 Prompt for AI Agents
In release_notes_generator/chapters/service_chapters.py around lines 139-142
(and similarly at 181-183, 218-220, 240-242), remove the unconditional check
that prevents adding a row when record_id is already in
self.used_record_numbers; instead rely on the existing __is_row_present(...) +
duplicity_allowed(...) guard to decide whether to add rows. Ensure you do not
short-circuit with an early return or skip based solely on
self.used_record_numbers — keep the add_row(...) and appending to
self.used_record_numbers only when __is_row_present(...) indicates absence or
duplicity_allowed(...) permits duplicates, preserving previous behavior when
duplicity is enabled.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This logic is correct.

Copy link

Choose a reason for hiding this comment

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

@miroslavpojer Thank you for the clarification. I understand that the stricter deduplication logic is intentional and the correct behavior for this implementation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (14)
release_notes_generator/model/record.py (1)

65-69: Avoid property reentry from get_labels implementations.

Subclasses currently do:

  • set self._labels = [...]
  • return self.labels

Prefer returning self._labels directly to avoid unnecessary property reentry and future recursion risks if order changes.

Apply in subclass methods, e.g.:

- self._labels = [label.name for label in list(self._issue.get_labels())]
- return self.labels
+ self._labels = [label.name for label in list(self._issue.get_labels())]
+ return self._labels
release_notes_generator/utils/pull_request_utils.py (2)

68-76: Use dict.get for GraphQL errors; keep message concise.

This addresses RUF019 and TRY003.

Apply:

-    data = response.json()
-    if "errors" in data and data["errors"]:
-        raise RuntimeError(f"GitHub GraphQL errors: {data['errors']}")
+    data = response.json()
+    if data.get("errors"):
+        raise RuntimeError("GitHub GraphQL errors present in response")

Optionally include status/code details in an exception subclass if you need richer context later.


56-66: Minor: add Accept header.

Not required, but a harmless clarity improvement for JSON GraphQL calls.

 headers = {
     "Authorization": f"Bearer {ActionInputs.get_github_token()}",
     "Content-Type": "application/json",
+    "Accept": "application/json",
 }
release_notes_generator/action_inputs.py (1)

488-501: Consolidate invalid keyword logging to reduce noise.

The loop logs the full list for each invalid keyword. Log once, or log per keyword without the full list.

- invalid_keywords = [keyword for keyword in keywords_in_braces if keyword not in supported_row_format_keys]
+ invalid_keywords = [k for k in keywords_in_braces if k not in supported_row_format_keys]
  cleaned_row_format = row_format
- for invalid_keyword in invalid_keywords:
-     logger.error(
-         "Invalid `%s` detected in `%s` row format keyword(s) found: %s. Will be removed from string.",
-         invalid_keyword,
-         row_type,
-         ", ".join(invalid_keywords),
-     )
+ if invalid_keywords:
+     logger.error(
+         "Invalid keyword(s) in %s row format: %s. They will be removed.",
+         row_type,
+         ", ".join(invalid_keywords),
+     )
+ for invalid_keyword in invalid_keywords:
      if clean:
          cleaned_row_format = cleaned_row_format.replace(f"{{{invalid_keyword}}}", "")
release_notes_generator/record/factory/default_record_factory.py (2)

64-71: Drop unused parameter in local helper.

skip_rec isn’t used in register_pull_request; remove it and the argument at callsite.

-        def register_pull_request(pull: PullRequest, skip_rec: bool) -> None:
+        def register_pull_request(pull: PullRequest) -> None:
             detected_issues = extract_issue_numbers_from_body(pull)
             logger.debug("Detected issues - from body: %s", detected_issues)
             linked = self._safe_call(get_issues_for_pr)(pull_number=pull.number)
             if linked:
                 detected_issues.update(linked)
             logger.debug("Detected issues - merged: %s", detected_issues)
-            register_pull_request(pull, skip_record)
+            register_pull_request(pull)

154-171: Pass fetched labels to IssueRecord to avoid extra API roundtrips.

Since you already read labels for skip detection, seed the record with them.

-        if issue_labels is None:
-            issue_labels = [label.name for label in issue.get_labels()]
+        if issue_labels is None:
+            issue_labels = [label.name for label in issue.get_labels()]
         skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels())
-        self._records[issue.number] = IssueRecord(issue=issue, skip=skip_record)
+        self._records[issue.number] = IssueRecord(issue=issue, issue_labels=issue_labels, skip=skip_record)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1)

247-248: Recursion condition: simplify truthiness check.

Use the dict’s truthiness directly for readability.

-        if self.__sub_issue_parents.items() and made_progress:
+        if self.__sub_issue_parents and made_progress:
             self._re_register_hierarchy_issues()
tests/release_notes/model/test_record.py (1)

49-56: Minor: unused args in test stub.

Leaving add_into_chapters/line_marks unused is fine for tests; optionally underscore them to silence linters.

-    def to_chapter_row(self, add_into_chapters: bool = True):
+    def to_chapter_row(self, add_into_chapters: bool = True):
         return f"Row for {self._record_id}"
@@
-    def get_rls_notes(self, line_marks=None):
+    def get_rls_notes(self, line_marks=None):
         return self._rls_notes
release_notes_generator/model/pull_request_record.py (2)

107-110: Return the computed labels directly.

Avoid property reentry.

 def get_labels(self) -> list[str]:
-        self._labels = [label.name for label in list(self._pull_request.get_labels())]
-        return self.labels
+        self._labels = [label.name for label in list(self._pull_request.get_labels())]
+        return self._labels

87-104: Remove unreachable contributors code.

The early return makes the rest dead. Keep the stub simple until implemented.

     @property
     def contributors(self) -> list[str]:
         """
         Gets the GitHub usernames of all contributors to the pull request.
         Returns:
             list[str]: A sorted list of GitHub usernames of contributors, excluding the main author.
         """
         if not self._pull_request:
             return []
-
-        return []
-
-        # TODO - contributors have to be managed in future, make a research if get_commit can return some
-        #  new information in compare to mined ones
-        # Get unique contributors from commits
-        contributors = set()
-        # for commit in self._pull_request.get_commits():
-        #     if commit.author and commit.author.login:
-        #         contributors.add(f"@{commit.author.login}")
-        #
-        # # Remove the main author if they're also in contributors
-        # if self._pull_request.user and self._pull_request.user.login:
-        #     author_name = f"@{self._pull_request.user.login}"
-        #     if author_name in contributors:
-        #         contributors.remove(author_name)
-        #
-        # return sorted(list(contributors))
+        return []
tests/release_notes/builder/test_release_notes_builder.py (2)

1646-1649: Remove stray debug prints from tests

These prints add noise to CI logs.

Apply this diff:

-    print("XXX")
-    print(actual_release_notes)
-    print("YYY")

909-975: Deduplicate repeated ActionInputs patching with a fixture

Many tests patch ActionInputs.get_hierarchy and get_print_empty_chapters. Create a small autouse or parametrized fixture to apply these patches once per test, improving readability and reducing copy/paste.

Example fixture to place in this module:

import pytest

@pytest.fixture(autouse=True)
def patch_action_inputs(mocker, request):
    # default to False if param not present
    hierarchy_value = getattr(request, "param", None)
    if hierarchy_value is None:
        hierarchy_value = next((m.kwargs["args"][0] for m in getattr(request.node, "callspec", {}).params.items() if m[0]=="hierarchy_value"), False) if hasattr(request.node, "callspec") else False

    mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_hierarchy", return_value=hierarchy_value)
    # override per test when needed
    if "no_warnings" in request.node.name:
        mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_warnings", return_value=False)
tests/conftest.py (2)

57-61: Silence unused-argument warning for mock_get_issues_for_pr

pull_number is required by the call site (named arg), but unused in the stub. Mark it used to satisfy Ruff ARG001.

Apply this diff:

 def mock_get_issues_for_pr(pull_number: int) -> set[int]:
-    # if pull_number == 150:
-    #     return [451]
+    # required by the call site; unused in this stub
+    _ = pull_number
     return set()

228-243: Minor: drop superfluous parentheses on return

Style nit; return the object directly.

Apply this diff:

-    return (issue)
+    return issue
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bdd10ed and 3923e17.

📒 Files selected for processing (13)
  • action.yml (6 hunks)
  • release_notes_generator/action_inputs.py (9 hunks)
  • release_notes_generator/chapters/service_chapters.py (5 hunks)
  • release_notes_generator/model/pull_request_record.py (2 hunks)
  • release_notes_generator/model/record.py (4 hunks)
  • release_notes_generator/model/sub_issue_record.py (1 hunks)
  • release_notes_generator/record/factory/default_record_factory.py (6 hunks)
  • release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1 hunks)
  • release_notes_generator/utils/pull_request_utils.py (4 hunks)
  • tests/conftest.py (9 hunks)
  • tests/release_notes/builder/test_release_notes_builder.py (1 hunks)
  • tests/release_notes/model/test_commit_record.py (1 hunks)
  • tests/release_notes/model/test_record.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • action.yml
  • tests/release_notes/model/test_commit_record.py
  • release_notes_generator/chapters/service_chapters.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-24T07:24:03.693Z
Learnt from: miroslavpojer
PR: AbsaOSS/generate-release-notes#168
File: release_notes_generator/utils/pull_request_utils.py:69-70
Timestamp: 2025-09-24T07:24:03.693Z
Learning: In release_notes_generator/utils/pull_request_utils.py, the verify=False parameter in the requests.post call for GitHub API access should be kept as per the repository maintainer's explicit requirement, despite security best practices suggesting otherwise.

Applied to files:

  • release_notes_generator/utils/pull_request_utils.py
🧬 Code graph analysis (9)
release_notes_generator/action_inputs.py (1)
release_notes_generator/utils/gh_action.py (1)
  • get_action_input (26-38)
release_notes_generator/model/sub_issue_record.py (2)
release_notes_generator/model/issue_record.py (2)
  • IssueRecord (16-328)
  • issue (63-68)
release_notes_generator/model/record.py (1)
  • skip (54-56)
release_notes_generator/model/record.py (5)
tests/release_notes/model/test_record.py (3)
  • labels (42-43)
  • get_labels (55-56)
  • to_chapter_row (49-50)
release_notes_generator/model/pull_request_record.py (2)
  • get_labels (107-109)
  • to_chapter_row (111-129)
release_notes_generator/model/issue_record.py (2)
  • get_labels (81-83)
  • to_chapter_row (100-123)
release_notes_generator/model/hierarchy_issue_record.py (2)
  • get_labels (71-84)
  • to_chapter_row (87-140)
release_notes_generator/model/commit_record.py (2)
  • get_labels (72-73)
  • to_chapter_row (54-66)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (8)
release_notes_generator/model/commit_record.py (3)
  • CommitRecord (13-75)
  • commit (45-50)
  • get_labels (72-73)
release_notes_generator/model/hierarchy_issue_record.py (7)
  • HierarchyIssueRecord (16-154)
  • order_hierarchy_levels (142-154)
  • get_labels (71-84)
  • sub_issues (47-51)
  • sub_hierarchy_issues (54-58)
  • level (30-34)
  • level (37-44)
release_notes_generator/model/issue_record.py (6)
  • IssueRecord (16-328)
  • issue (63-68)
  • get_labels (81-83)
  • register_pull_request (192-199)
  • register_commit (201-215)
  • issue_type (71-77)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/action_inputs.py (2)
  • ActionInputs (64-513)
  • get_skip_release_notes_labels (209-217)
release_notes_generator/model/pull_request_record.py (3)
  • PullRequestRecord (15-290)
  • get_labels (107-109)
  • register_commit (168-175)
release_notes_generator/record/factory/default_record_factory.py (3)
  • generate (55-126)
  • register_pull_request (64-96)
  • _create_record_for_issue (154-170)
release_notes_generator/utils/pull_request_utils.py (2)
  • get_issues_for_pr (54-78)
  • extract_issue_numbers_from_body (31-50)
release_notes_generator/model/pull_request_record.py (3)
release_notes_generator/model/record.py (4)
  • labels (59-68)
  • get_labels (121-126)
  • to_chapter_row (109-118)
  • added_into_chapters (138-144)
release_notes_generator/model/issue_record.py (2)
  • get_labels (81-83)
  • to_chapter_row (100-123)
release_notes_generator/model/commit_record.py (2)
  • get_labels (72-73)
  • to_chapter_row (54-66)
tests/conftest.py (10)
release_notes_generator/model/commit_record.py (3)
  • CommitRecord (13-75)
  • get_labels (72-73)
  • commit (45-50)
release_notes_generator/model/hierarchy_issue_record.py (6)
  • HierarchyIssueRecord (16-154)
  • get_labels (71-84)
  • level (30-34)
  • level (37-44)
  • sub_hierarchy_issues (54-58)
  • sub_issues (47-51)
release_notes_generator/model/issue_record.py (5)
  • IssueRecord (16-328)
  • issue (63-68)
  • get_labels (81-83)
  • issue_type (71-77)
  • register_pull_request (192-199)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/model/pull_request_record.py (2)
  • PullRequestRecord (15-290)
  • get_labels (107-109)
release_notes_generator/chapters/service_chapters.py (1)
  • ServiceChapters (43-276)
release_notes_generator/chapters/custom_chapters.py (1)
  • CustomChapters (37-90)
release_notes_generator/model/sub_issue_record.py (2)
  • SubIssueRecord (15-36)
  • issue (27-34)
tests/release_notes/record/factory/test_default_record_factory.py (5)
  • mock_safe_call_decorator (159-164)
  • wrapper (160-163)
  • wrapper (334-337)
  • wrapper (439-442)
  • mock_get_issues_for_pr (166-171)
release_notes_generator/record/factory/default_record_factory.py (1)
  • register_pull_request (64-96)
tests/release_notes/builder/test_release_notes_builder.py (5)
release_notes_generator/builder/builder.py (2)
  • ReleaseNotesBuilder (32-86)
  • build (52-86)
tests/conftest.py (13)
  • custom_chapters (65-71)
  • mock_safe_call_decorator (49-54)
  • MockLabel (44-46)
  • custom_chapters_not_print_empty_chapters (75-83)
  • record_with_issue_closed_two_pulls (879-883)
  • record_with_issue_closed_no_pull (846-847)
  • pull_request_record_merged (958-961)
  • pull_request_record_closed (983-986)
  • pull_request_record_open (965-968)
  • record_with_two_issue_open_two_pulls_closed (936-946)
  • record_with_issue_closed_one_pull (858-861)
  • record_with_issue_closed_one_pull_merged (865-868)
  • record_with_issue_closed_one_pull_merged_skip (872-875)
release_notes_generator/chapters/custom_chapters.py (2)
  • CustomChapters (37-90)
  • from_yaml_array (75-90)
release_notes_generator/record/factory/default_record_factory.py (2)
  • DefaultRecordFactory (44-170)
  • generate (55-126)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (2)
  • IssueHierarchyRecordFactory (43-262)
  • generate (55-103)
release_notes_generator/record/factory/default_record_factory.py (7)
release_notes_generator/record/factory/record_factory.py (2)
  • RecordFactory (29-45)
  • generate (35-42)
release_notes_generator/generator.py (2)
  • rate_limiter (65-67)
  • generate (69-103)
release_notes_generator/utils/github_rate_limiter.py (1)
  • GithubRateLimiter (30-76)
release_notes_generator/utils/decorators.py (1)
  • safe_call_decorator (51-82)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (2)
  • generate (55-103)
  • _create_record_for_issue (197-202)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/model/pull_request_record.py (2)
  • get_labels (107-109)
  • PullRequestRecord (15-290)
tests/release_notes/model/test_record.py (1)
release_notes_generator/model/record.py (5)
  • Record (29-190)
  • skip (54-56)
  • labels (59-68)
  • to_chapter_row (109-118)
  • get_labels (121-126)
🪛 Ruff (0.13.1)
release_notes_generator/utils/pull_request_utils.py

71-71: Unnecessary key check before dictionary access

Replace with dict.get

(RUF019)


72-72: Avoid specifying long messages outside the exception class

(TRY003)

release_notes_generator/model/sub_issue_record.py

33-33: Avoid specifying long messages outside the exception class

(TRY003)

tests/conftest.py

57-57: Unused function argument: pull_number

(ARG001)

tests/release_notes/model/test_record.py

49-49: Unused method argument: add_into_chapters

(ARG002)


52-52: Unused method argument: line_marks

(ARG002)

🪛 GitHub Actions: Build and Test
release_notes_generator/utils/pull_request_utils.py

[error] 1-1: Black formatting check failed. 2 files would be reformatted by Black.

release_notes_generator/record/factory/issue_hierarchy_record_factory.py

[error] 1-1: Black formatting check failed. 2 files would be reformatted by Black.

🔇 Additional comments (13)
release_notes_generator/model/record.py (2)

36-41: Constructor: labels opt-in + skip preserved — good.

The extended signature and internal state look sound and backward-compatible.


108-118: Abstract to_chapter_row(add_into_chapters) — API clarity.

Making this abstract with a clear contract will keep implementations consistent.

release_notes_generator/utils/pull_request_utils.py (3)

31-50: Body parser to set[int] — clear and efficient.

Regex and return type are appropriate.


68-68: TLS verify=False kept per maintainer decision.

Acknowledging prior discussion and the project’s requirement to keep verify=False here.


1-1: Black formatting failure — run Black locally and push reformatted files

CI reports two files need formatting. Verification failed in the sandbox: 'black' not found. Run python -m pip install --upgrade black and black . (or python -m black .) locally, commit and push the reformatted files.

release_notes_generator/action_inputs.py (2)

169-178: New ‘hierarchy’ input — simple and robust parser.

Boolean coercion covers common truthy strings.


306-320: Hierarchy row-format retrieval/validation — good.

Caching + per-type keyword validation looks right.

release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1)

81-90: Direct commit registration doesn’t mark them as registered.

This is fine since registration happens after PR-commit collection, but note __registered_commits won’t include direct commits. If you later rely on that list, consider appending here too.

Do you depend on __registered_commits beyond filtering PR-merge commits? If yes, append direct commit SHAs here.

release_notes_generator/model/sub_issue_record.py (2)

21-24: Constructor narrowed to SubIssue — good.

Matches the intent of a dedicated sub-issue record.


32-34: Runtime guard message OK; ensure factory passes SubIssue.

Given this defensive check, the factory must pass SubIssue instances (see suggested fixes in IssueHierarchyRecordFactory).

tests/release_notes/model/test_record.py (1)

21-27: Test harness matches new Record API.

Good coverage of contains_* and release-notes caching paths.

release_notes_generator/model/pull_request_record.py (1)

111-129: to_chapter_row(add_into_chapters) implementation — solid.

Handles duplicity icon and optional release notes well.

tests/conftest.py (1)

178-191: Ignore — Mock(spec=Issue) won't raise when adding attributes; use spec_set for strictness

tests/conftest.py imports Issue from github.Issue and fixtures use mocker.Mock(spec=Issue); a quick check shows Mock(spec=...) allows assigning new attributes, so setting issue.get_sub_issues.return_value = [] is safe. Use spec_set if you want strict attribute enforcement.

Likely an incorrect or invalid review comment.

Comment on lines 204 to 212
def _create_record_for_sub_issue(self, issue: Issue, issue_labels: Optional[list[str]] = None) -> None:
if issue_labels is None:
issue_labels = self._get_issue_labels_mix_with_type(issue)

skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels())
logger.debug("Created record for sub issue %d: %s", issue.number, issue.title)
self.__registered_issues.append(issue.number)
self._records[issue.number] = SubIssueRecord(issue, issue_labels, skip_record)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Tighten _create_record_for_sub_issue signature to SubIssue.

Align with SubIssueRecord(sub_issue: SubIssue, ...). Current signature accepts Issue and can cause runtime TypeError when .issue property is accessed.

-    def _create_record_for_sub_issue(self, issue: Issue, issue_labels: Optional[list[str]] = None) -> None:
+    def _create_record_for_sub_issue(self, issue: SubIssue, issue_labels: Optional[list[str]] = None) -> None:
         if issue_labels is None:
             issue_labels = self._get_issue_labels_mix_with_type(issue)

         skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels())
         logger.debug("Created record for sub issue %d: %s", issue.number, issue.title)
         self.__registered_issues.append(issue.number)
         self._records[issue.number] = SubIssueRecord(issue, issue_labels, skip_record)

Also ensure the import at the top includes:

from github.Issue import Issue, SubIssue
🤖 Prompt for AI Agents
In release_notes_generator/record/factory/issue_hierarchy_record_factory.py
around lines 204 to 212, tighten the _create_record_for_sub_issue signature to
accept a SubIssue (not Issue) to match SubIssueRecord(sub_issue: SubIssue, ...)
and avoid runtime TypeError when SubIssue-specific properties are accessed;
update the method definition and any internal type hints from Issue to SubIssue,
update all call sites to pass a SubIssue (or coerce/validate before calling),
and add or update the top-level import to include SubIssue (from github.Issue
import Issue, SubIssue).

Comment on lines +1166 to +1171
expected_release_notes = RELEASE_NOTES_DATA_CUSTOM_CHAPTERS_MORE_LABELS_DUPLICITY_REDUCTION_ON
rec = record_with_issue_closed_two_pulls
rec.issue.labels.append(MockLabel("enhancement"))
rec.issue.title = "I1+bug-enhancement"
mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_print_empty_chapters", return_value=False)
mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_hierarchy", return_value=hierarchy_value)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don't assign to Issue.labels on a spec'd mock; patch get_labels() return value instead

Spec'd PyGithub Issue likely doesn't expose a mutable .labels. Updating it may raise or be ignored. Patch get_labels.return_value to include the label you need.

Apply this diff:

-    rec.issue.labels.append(MockLabel("enhancement"))
+    rec.issue.get_labels.return_value = rec.issue.get_labels.return_value + [MockLabel("enhancement")]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expected_release_notes = RELEASE_NOTES_DATA_CUSTOM_CHAPTERS_MORE_LABELS_DUPLICITY_REDUCTION_ON
rec = record_with_issue_closed_two_pulls
rec.issue.labels.append(MockLabel("enhancement"))
rec.issue.title = "I1+bug-enhancement"
mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_print_empty_chapters", return_value=False)
mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_hierarchy", return_value=hierarchy_value)
expected_release_notes = RELEASE_NOTES_DATA_CUSTOM_CHAPTERS_MORE_LABELS_DUPLICITY_REDUCTION_ON
rec = record_with_issue_closed_two_pulls
rec.issue.get_labels.return_value = rec.issue.get_labels.return_value + [MockLabel("enhancement")]
rec.issue.title = "I1+bug-enhancement"
mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_print_empty_chapters", return_value=False)
mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_hierarchy", return_value=hierarchy_value)
🤖 Prompt for AI Agents
In tests/release_notes/builder/test_release_notes_builder.py around lines
1166-1171, the test mutates a spec'd PyGithub Issue by appending to
rec.issue.labels which may be unsupported; instead patch the Issue.get_labels()
return value (or the ActionInputs/get_labels wrapper used in code) to include
MockLabel("enhancement") so the code under test sees the label without assigning
to the spec'd attribute; update the test to set
rec.issue.get_labels.return_value (or use mocker.patch on the method) to return
the list containing the existing labels plus MockLabel("enhancement") and keep
the other mocks (get_print_empty_chapters, get_hierarchy) unchanged.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (12)
release_notes_generator/utils/pull_request_utils.py (3)

17-19: Update docstring to reflect new functionality.

The module docstring still mentions only extracting from pull request bodies, but the module now also fetches issue numbers via GitHub GraphQL API.

Apply this diff to update the docstring:

-This module contains utility functions for extracting issue numbers from pull request bodies.
+This module contains utility functions for extracting and fetching issue numbers from pull requests.

31-50: Remove unused lru_cache import.

The extract_issue_numbers_from_body function no longer uses the @lru_cache decorator, making the import on Line 22 unused.

Apply this diff to remove the unused import:

 import re
-from functools import lru_cache
 
 import requests

71-72: Consider simplifying the error check.

The static analysis tool suggests using dict.get instead of explicit key checking, which would be more concise.

Apply this diff to simplify the error checking:

-    if "errors" in data and data["errors"]:
-        raise RuntimeError(f"GitHub GraphQL errors: {data['errors']}")
+    if data.get("errors"):
+        raise RuntimeError(f"GitHub GraphQL errors: {data['errors']}")
release_notes_generator/model/sub_issue_record.py (3)

8-11: Drop unused Issue import after tightening type.

-from github.Issue import SubIssue, Issue
+from github.Issue import SubIssue

32-34: Shorten error message to satisfy Ruff TRY003.

-        if not isinstance(self._issue, SubIssue):
-            raise TypeError("SubIssueRecord.issue is expected to be a SubIssue")
+        if not isinstance(self._issue, SubIssue):
+            raise TypeError("Expected SubIssue")

16-19: Fix docstring: correct parent class and purpose.

-    A class used to represent an issue record in the release notes.
-    Inherits from Record and provides additional functionality specific to issues.
+    Represents a sub-issue record in the release notes.
+    Inherits from IssueRecord and specializes behavior for sub-issues.
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (6)

248-249: Use dict truthiness instead of .items() for recursion guard.

-        if self.__sub_issue_parents.items() and made_progress:
+        if self.__sub_issue_parents and made_progress:
             self._re_register_hierarchy_issues()

51-54: Use sets for membership hot paths (issues/commits).

Membership checks are frequent; sets give O(1) lookups.

-        self.__registered_issues: list[int] = []
+        self.__registered_issues: set[int] = set()
         self.__sub_issue_parents: dict[int, int] = {}
-        self.__registered_commits: list[str] = []
+        self.__registered_commits: set[str] = set()

Follow-ups:

  • Line 109: replace extend(...) with update(...).
  • Lines 184, 202, 210: replace append(...) with add(...).

109-110: Adjust for set: use update() instead of extend().

-        self.__registered_commits.extend(c.sha for c in related_commits)
+        self.__registered_commits.update(c.sha for c in related_commits)

183-186: Adjust for set: add() when marking hierarchy issue registered.

-        self.__registered_issues.append(i.number)
+        self.__registered_issues.add(i.number)

201-203: Adjust for set: add() when marking issue registered.

-        self.__registered_issues.append(issue.number)
+        self.__registered_issues.add(issue.number)

208-211: Adjust for set: add() when marking sub-issue registered.

-        self.__registered_issues.append(issue.number)
+        self.__registered_issues.add(issue.number)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3923e17 and 91fdaa1.

📒 Files selected for processing (3)
  • release_notes_generator/model/sub_issue_record.py (1 hunks)
  • release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1 hunks)
  • release_notes_generator/utils/pull_request_utils.py (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-24T07:24:03.693Z
Learnt from: miroslavpojer
PR: AbsaOSS/generate-release-notes#168
File: release_notes_generator/utils/pull_request_utils.py:69-70
Timestamp: 2025-09-24T07:24:03.693Z
Learning: In release_notes_generator/utils/pull_request_utils.py, the verify=False parameter in the requests.post call for GitHub API access should be kept as per the repository maintainer's explicit requirement, despite security best practices suggesting otherwise.

Applied to files:

  • release_notes_generator/utils/pull_request_utils.py
🧬 Code graph analysis (2)
release_notes_generator/model/sub_issue_record.py (1)
release_notes_generator/model/issue_record.py (2)
  • IssueRecord (16-328)
  • issue (63-68)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (9)
release_notes_generator/model/commit_record.py (3)
  • CommitRecord (13-75)
  • commit (45-50)
  • get_labels (72-73)
release_notes_generator/model/hierarchy_issue_record.py (4)
  • order_hierarchy_levels (142-154)
  • get_labels (71-84)
  • level (30-34)
  • level (37-44)
release_notes_generator/model/issue_record.py (5)
  • IssueRecord (16-328)
  • issue (63-68)
  • get_labels (81-83)
  • register_pull_request (192-199)
  • register_commit (201-215)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/action_inputs.py (2)
  • ActionInputs (64-513)
  • get_skip_release_notes_labels (209-217)
release_notes_generator/model/pull_request_record.py (3)
  • PullRequestRecord (15-290)
  • get_labels (107-109)
  • register_commit (168-175)
release_notes_generator/model/sub_issue_record.py (1)
  • issue (27-34)
release_notes_generator/record/factory/default_record_factory.py (4)
  • DefaultRecordFactory (44-170)
  • generate (55-126)
  • register_pull_request (64-96)
  • _create_record_for_issue (154-170)
release_notes_generator/utils/pull_request_utils.py (2)
  • get_issues_for_pr (54-75)
  • extract_issue_numbers_from_body (31-50)
🪛 Ruff (0.13.1)
release_notes_generator/model/sub_issue_record.py

33-33: Avoid specifying long messages outside the exception class

(TRY003)

release_notes_generator/utils/pull_request_utils.py

71-71: Unnecessary key check before dictionary access

Replace with dict.get

(RUF019)


72-72: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (6)
release_notes_generator/utils/pull_request_utils.py (2)

31-50: LGTM! Clean extraction logic.

The function correctly extracts issue numbers from PR bodies using an appropriate regex pattern and returns them as a set for deduplication.


53-75: LGTM! Well-implemented GraphQL integration.

The function correctly:

  • Uses GraphQL to fetch closing issue references
  • Includes proper error handling for both HTTP and GraphQL errors
  • Returns a set for consistent deduplication
  • Maintains the lru_cache decorator for performance

The verify=False parameter is kept as per your explicit requirement from the retrieved learnings.

release_notes_generator/model/sub_issue_record.py (1)

21-24: Tighten ctor to SubIssue only (prevents runtime TypeError in .issue).

Accepting Issue allows constructing an invalid SubIssueRecord that will later raise in the property.

Apply:

-    def __init__(self, sub_issue: SubIssue | Issue, issue_labels: Optional[list[str]] = None, skip: bool = False):
+    def __init__(self, sub_issue: SubIssue, issue_labels: Optional[list[str]] = None, skip: bool = False):
         super().__init__(sub_issue, issue_labels, skip)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (3)

150-160: Create SubIssueRecord at discovery time to preserve SubIssue typing.

Materialize sub-issue records when discovered; avoids later constructing SubIssueRecord from a plain Issue.

         if len(sub_issues) > 0:
             self._create_record_for_hierarchy_issue(issue)
             for si in sub_issues:
                 # register sub-issue and its parent for later hierarchy building
                 self.__sub_issue_parents[si.number] = issue.number  # Note: GitHub now allows only 1 parent
+                # materialize SubIssueRecord now to keep SubIssue typing
+                self._create_record_for_sub_issue(si)

24-27: Import SubIssue for type-correct helper signature below.

-from github.Issue import Issue
+from github.Issue import Issue, SubIssue

204-212: Tighten _create_record_for_sub_issue signature to SubIssue.

Aligns with SubIssueRecord(sub_issue: SubIssue, ...); prevents runtime TypeError paths.

-    def _create_record_for_sub_issue(self, issue: Issue, issue_labels: Optional[list[str]] = None) -> None:
+    def _create_record_for_sub_issue(self, issue: SubIssue, issue_labels: Optional[list[str]] = None) -> None:
         if issue_labels is None:
             issue_labels = self._get_issue_labels_mix_with_type(issue)

         skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels())
         logger.debug("Created record for sub issue %d: %s", issue.number, issue.title)
-        self.__registered_issues.append(issue.number)
+        self.__registered_issues.append(issue.number)
         self._records[issue.number] = SubIssueRecord(issue, issue_labels, skip_record)

Note: If you convert __registered_issues to a set (see next comment), replace append(...) with add(...).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
release_notes_generator/utils/pull_request_utils.py (1)

53-62: Factor cache key to include repo owner and name

  • Default‐arg approach won’t include resolved owner/name in the lru_cache key. Extract a private, owner-aware function:
    @lru_cache(maxsize=1024)
    def _get_issues_for_pr(owner: str, name: str, pull_number: int) -> set[int]:
        …
    
    def get_issues_for_pr(pull_number: int) -> set[int]:
        owner = ActionInputs.get_github_owner()
        name = ActionInputs.get_github_repo_name()
        return _get_issues_for_pr(owner, name, pull_number)
  • CI workflows use Python 3.11, so set[int] is supported.
🧹 Nitpick comments (4)
release_notes_generator/model/sub_issue_record.py (3)

26-35: Silence Ruff TRY003 and keep a precise error by using a custom exception

Ruff flags the string message on the TypeError. Define a small custom exception and raise it.

Add near the logger:

 logger = logging.getLogger(__name__)
 
+class InvalidSubIssueError(TypeError):
+    """Raised when SubIssueRecord holds a non-SubIssue issue instance."""

Then change the raise:

-        if not isinstance(self._issue, SubIssue):
-            raise TypeError("Expected SubIssue")
+        if not isinstance(self._issue, SubIssue):
+            raise InvalidSubIssueError

12-12: Optional: remove unused logger if not intended

logger is currently unused; either remove it or plan to log in this module.


36-37: Nit: remove empty section marker

The trailing comment “# properties - specific to IssueRecord” has no content; consider removing to reduce noise.

-    # properties - specific to IssueRecord
-
+    # properties - specific to IssueRecord
release_notes_generator/utils/pull_request_utils.py (1)

68-68: Keep verify=False (as required), but silence the linter and document intent.

Add an inline suppression and rationale to avoid recurring S501 alerts.

Apply this diff:

-    response = requests.post(github_api_url, json={"query": query}, headers=headers, verify=False, timeout=10)
+    response = requests.post(
+        github_api_url,
+        json={"query": query},
+        headers=headers,
+        verify=False,  # noqa: S501 — TLS verification intentionally disabled per repo requirement
+        timeout=10,
+    )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91fdaa1 and 7cc7b71.

📒 Files selected for processing (3)
  • release_notes_generator/model/sub_issue_record.py (1 hunks)
  • release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1 hunks)
  • release_notes_generator/utils/pull_request_utils.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • release_notes_generator/record/factory/issue_hierarchy_record_factory.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-24T07:24:03.693Z
Learnt from: miroslavpojer
PR: AbsaOSS/generate-release-notes#168
File: release_notes_generator/utils/pull_request_utils.py:69-70
Timestamp: 2025-09-24T07:24:03.693Z
Learning: In release_notes_generator/utils/pull_request_utils.py, the verify=False parameter in the requests.post call for GitHub API access should be kept as per the repository maintainer's explicit requirement, despite security best practices suggesting otherwise.

Applied to files:

  • release_notes_generator/utils/pull_request_utils.py
🧬 Code graph analysis (1)
release_notes_generator/model/sub_issue_record.py (2)
release_notes_generator/model/issue_record.py (2)
  • IssueRecord (16-328)
  • issue (63-68)
release_notes_generator/model/record.py (1)
  • skip (54-56)
🪛 Ruff (0.13.1)
release_notes_generator/model/sub_issue_record.py

33-33: Avoid specifying long messages outside the exception class

(TRY003)

release_notes_generator/utils/pull_request_utils.py

72-72: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (4)
release_notes_generator/model/sub_issue_record.py (2)

21-22: Tighten constructor to accept only SubIssue (avoid downstream type breakage)

Accepting Issue allows constructing invalid state that later triggers a TypeError in issue property. Restrict to SubIssue as previously requested.

Apply this diff:

-    def __init__(self, sub_issue: SubIssue | Issue, issue_labels: Optional[list[str]] = None, skip: bool = False):
+    def __init__(self, sub_issue: SubIssue, issue_labels: Optional[list[str]] = None, skip: bool = False):

8-9: Define or remove SubIssue
SubIssue is neither part of PyGithub nor defined in the local codebase; this import will fail. Either implement a SubIssue class (e.g. in release_notes_generator/model/sub_issue.py) or remove all references to SubIssue in sub_issue_record.py.

Likely an incorrect or invalid review comment.

release_notes_generator/utils/pull_request_utils.py (2)

18-19: Clearer module docstring — LGTM.


35-40: Docstring parameter/return clarifications — LGTM.

Comment on lines +70 to +74
data = response.json()
if data.get("errors"):
raise RuntimeError(f"GitHub GraphQL errors: {data['errors']}")

return {node["number"] for node in data["data"]["repository"]["pullRequest"]["closingIssuesReferences"]["nodes"]}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Harden JSON parsing for nulls and improve GraphQL error messaging (avoid KeyError/TypeError).

If GraphQL returns data with nulls (e.g., PR not found) but no "errors", the current indexing can raise. Also, format errors to show messages and satisfy TRY003.

Apply this diff:

-    data = response.json()
-    if data.get("errors"):
-        raise RuntimeError(f"GitHub GraphQL errors: {data['errors']}")
-
-    return {node["number"] for node in data["data"]["repository"]["pullRequest"]["closingIssuesReferences"]["nodes"]}
+    data = response.json()
+    if data.get("errors"):
+        messages = ", ".join(
+            e.get("message", "<no message>") if isinstance(e, dict) else str(e)
+            for e in data["errors"]
+        )
+        raise RuntimeError(f"GitHub GraphQL errors: {messages}")  # noqa: TRY003
+
+    repo = (data.get("data") or {}).get("repository") or {}
+    pr = (repo.get("pullRequest") or {})
+    closing_refs = (pr.get("closingIssuesReferences") or {})
+    nodes = closing_refs.get("nodes") or []
+    return {n.get("number") for n in nodes if isinstance(n, dict) and "number" in n}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data = response.json()
if data.get("errors"):
raise RuntimeError(f"GitHub GraphQL errors: {data['errors']}")
return {node["number"] for node in data["data"]["repository"]["pullRequest"]["closingIssuesReferences"]["nodes"]}
data = response.json()
if data.get("errors"):
messages = ", ".join(
e.get("message", "<no message>") if isinstance(e, dict) else str(e)
for e in data["errors"]
)
raise RuntimeError(f"GitHub GraphQL errors: {messages}") # noqa: TRY003
repo = (data.get("data") or {}).get("repository") or {}
pr = (repo.get("pullRequest") or {})
closing_refs = (pr.get("closingIssuesReferences") or {})
nodes = closing_refs.get("nodes") or []
return {n.get("number") for n in nodes if isinstance(n, dict) and "number" in n}
🧰 Tools
🪛 Ruff (0.13.1)

72-72: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In release_notes_generator/utils/pull_request_utils.py around lines 70 to 74,
the current code directly indexes into response.json() and into deep keys which
can raise KeyError/TypeError when GraphQL returns nulls (e.g., PR not found) and
also logs raw error objects; change the logic to: parse response.json() into
data, if data.get("errors") is truthy build a clear RuntimeError message by
joining each error's "message" (falling back to str(error) if message missing)
and raise it, otherwise safely traverse nested keys using successive .get calls
(checking for None at each level) and if
repository/pullRequest/closingIssuesReferences/nodes is missing or null raise a
descriptive RuntimeError indicating the missing field, finally return an empty
set if nodes is empty or otherwise return a set of node["number"] while
validating node is a mapping and has "number".

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1)

118-136: Simplify membership checks; avoid caching keys view

Using self._records directly improves readability; the keys view is dynamic but the indirection is unnecessary.

-            record_keys = self._records.keys()
-
             for issue_number in pull_issues:
-                if issue_number not in record_keys:
+                if issue_number not in self._records:
@@
-                if issue_number in record_keys and isinstance(
+                if issue_number in self._records and isinstance(
                     self._records[issue_number], (SubIssueRecord, HierarchyIssueRecord, IssueRecord)
                 ):
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cc7b71 and 1d294f3.

📒 Files selected for processing (1)
  • release_notes_generator/record/factory/issue_hierarchy_record_factory.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (9)
release_notes_generator/model/commit_record.py (3)
  • CommitRecord (13-75)
  • commit (45-50)
  • get_labels (72-73)
release_notes_generator/model/hierarchy_issue_record.py (7)
  • HierarchyIssueRecord (16-154)
  • order_hierarchy_levels (142-154)
  • get_labels (71-84)
  • sub_issues (47-51)
  • sub_hierarchy_issues (54-58)
  • level (30-34)
  • level (37-44)
release_notes_generator/model/issue_record.py (6)
  • IssueRecord (16-328)
  • issue (63-68)
  • get_labels (81-83)
  • register_pull_request (192-199)
  • register_commit (201-215)
  • issue_type (71-77)
release_notes_generator/model/mined_data.py (1)
  • MinedData (38-56)
release_notes_generator/action_inputs.py (2)
  • ActionInputs (64-513)
  • get_skip_release_notes_labels (209-217)
release_notes_generator/model/pull_request_record.py (3)
  • PullRequestRecord (15-290)
  • get_labels (107-109)
  • register_commit (168-175)
release_notes_generator/model/sub_issue_record.py (2)
  • SubIssueRecord (15-36)
  • issue (27-34)
release_notes_generator/record/factory/default_record_factory.py (4)
  • DefaultRecordFactory (44-170)
  • generate (55-126)
  • register_pull_request (64-96)
  • _create_record_for_issue (154-170)
release_notes_generator/utils/pull_request_utils.py (2)
  • get_issues_for_pr (54-74)
  • extract_issue_numbers_from_body (31-50)
🔇 Additional comments (6)
release_notes_generator/record/factory/issue_hierarchy_record_factory.py (6)

108-114: Verify: only merge commit is associated to PR

You attach only the merge commit to the PR. Rebase merges or unmerged PRs may leave commits classified as “direct”. Confirm this is intended for hierarchy mode.

Would you like me to propose logic to associate all PR commits (when available via API) and keep dedupe for direct commits?


248-249: Good termination guard on re-registration recursion

Recursing only when progress is made avoids infinite loops; dropping unresolved mappings is correct.


24-27: Import SubIssue to match intended typing

SubIssueRecord expects a SubIssue. Import it so the factory can construct typed sub-issue records.

-from github.Issue import Issue
+from github.Issue import Issue, SubIssue

154-159: Materialize SubIssueRecord at discovery time (preserve SubIssue typing)

Without creating SubIssueRecord here, later code constructs it from Issue, which can trigger TypeError when accessing SubIssueRecord.issue. Instantiate it now.

         if len(sub_issues) > 0:
             self._create_record_for_hierarchy_issue(issue)
             for si in sub_issues:
                 # register sub-issue and its parent for later hierarchy building
                 self.__sub_issue_parents[si.number] = issue.number  # Note: GitHub now allows only 1 parent
+                # preserve SubIssue typing by creating the record immediately
+                self._create_record_for_sub_issue(si)

160-166: Do not construct SubIssueRecord from Issue

When the issue is known to be a sub-issue (present in the mapping), skip creating it from Issue; it was already created from SubIssue above.

     def _create_issue_record_using_sub_issues_not_existence(self, issue: Issue) -> None:
         # Expected to run after all issue with sub-issues are registered
-        if issue.number in self.__sub_issue_parents.keys():  # pylint: disable=consider-iterating-dictionary
-            self._create_record_for_sub_issue(issue)
-        else:
-            self._create_record_for_issue(issue)
+        if issue.number in self.__sub_issue_parents:
+            return  # already created as SubIssueRecord from a SubIssue
+        self._create_record_for_issue(issue)

204-212: Tighten _create_record_for_sub_issue signature to SubIssue

Aligns with SubIssueRecord expectations and prevents runtime TypeError when the .issue property is accessed.

-    def _create_record_for_sub_issue(self, issue: Issue, issue_labels: Optional[list[str]] = None) -> None:
+    def _create_record_for_sub_issue(self, issue: SubIssue, issue_labels: Optional[list[str]] = None) -> None:
         if issue_labels is None:
             issue_labels = self._get_issue_labels_mix_with_type(issue)

         skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels())
         logger.debug("Created record for sub issue %d: %s", issue.number, issue.title)
         self.__registered_issues.add(issue.number)
         self._records[issue.number] = SubIssueRecord(issue, issue_labels, skip_record)

@miroslavpojer miroslavpojer merged commit 2edc159 into master Sep 24, 2025
7 checks passed
@miroslavpojer miroslavpojer deleted the feature/167-Introduce-regime-for-hierarchical-release-notes branch September 24, 2025 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce regime for hierarchical release notes Add support for Epics

1 participant