Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 16, 2026

Overview

Release notes rendered dangling text fragments when issue metadata was missing, producing output like:

- N/A: #231 _Dependency Dashboard_ author is @renovate[bot] assigned to  developed by  in

This PR implements intelligent suppression of empty field fragments so missing metadata results in clean output:

- #231 _Dependency Dashboard_ author is @renovate[bot]

Release Notes

  • Fixed release note row formatting to omit text fragments when placeholders are empty (no more "N/A:", "assigned to ", "developed by in ")
  • Added intelligent empty-field suppression for issue and hierarchy issue row templates

Changes

Core Implementation

  • Added format_row_with_suppression() in release_notes_generator/utils/record_utils.py:

    • Removes {type}: prefix when type is missing (no "N/A" substitution)
    • Removes assigned to {assignees} phrase when assignees list is empty
    • Removes developed by {developers} in {pull-requests} phrase when both are empty
    • Removes in {pull-requests} suffix when only PRs are missing
    • Reuses field name constants from constants.py for maintainability
    • Uses documented regex patterns for suppression rules
  • Updated IssueRecord.to_chapter_row(): Changed type fallback from 'N/A' to '', delegates formatting to suppression helper

  • Updated HierarchyIssueRecord.to_chapter_row(): Changed type fallback from 'None' to '', delegates formatting to suppression helper

Testing & Documentation

  • Added 5 unit tests covering missing field combinations in test_issue_record_row_formatting.py, following project test style conventions and reusing existing fixtures from conftest.py
  • Updated 306 existing test expectations to reflect corrected behavior
  • Enhanced docs/features/custom_row_formats.md documenting empty field suppression behavior with examples, clarifying that suppression only applies to specific placeholders ({type}, {assignees}, {developers}, {pull-requests})

Template Coupling Note: Suppression rules target default row format patterns. Custom templates with different phrase structures may not benefit from all suppression rules.

Original prompt

This section details on the original issue you should resolve

<issue_title>Fix release note row rendering to skip missing fields (type/assignee/developers/PRs)</issue_title>
<issue_description>### Feature Description

Update release-note row rendering so that when issue metadata is missing, the generator omits the entire related text fragment (prefix/label phrase), instead of printing placeholders as empty strings.

Concrete example:

  • Actual (today):
    - N/A: AbsaOSS/generate-release-notes#231 _Dependency Dashboard_ author is @renovate[bot] assigned to developed by in

  • Expected (after fix):
    - AbsaOSS/generate-release-notes#231 _Dependency Dashboard_ author is @renovate[bot]

This should apply to issue rows (and, if applicable, hierarchy issue rows) generated by the Action’s row-format templates.

Problem / Opportunity

The generated release notes can contain redundant, confusing, or broken text segments when certain GitHub fields are missing:

  • If an issue has no Task type, the output prepends N/A: (noise; looks like a bug).
  • If an issue has no assignee, the output can contain assigned to followed by nothing (dangling phrase).
  • If an issue has no related PR/commit/developer attribution, the output can contain developed by in (dangling phrase).

This reduces readability and professionalism of release notes and forces manual cleanup for otherwise valid issues (e.g., Renovate bot dashboard issues).

Who benefits:

  • Maintainers producing release notes (less manual editing)
  • Consumers of release notes (cleaner, more readable changelogs)
  • Contributors (more predictable formatting and fewer “why is this blank?” questions)

Acceptance Criteria

  • For issues with no type:
    • Output must not contain N/A and must not start with N/A: when issue.type is absent.
    • The {type}: prefix (or equivalent prefix fragment) must be omitted entirely.
  • For issues with no assignees:
    • Output must not contain the phrase assigned to at all.
  • For issues with no PR/commit linkage and no derived developers:
    • Output must not contain developed by nor in fragments (no developed by in).
  • Existing cases where values are present must remain unchanged (no regressions).
  • Unit tests must cover the new behavior for missing-field combinations (see “Additional Context”).

Proposed Solution

High-level approach:

  1. Adjust row rendering so templates don’t blindly .format() into strings that include constant phrases around empty placeholders.
  2. Implement “empty-field suppression” in the formatting layer:
    • If {type} is empty/missing → omit the entire “type prefix” fragment (not substitute N/A).
    • If {assignees} is empty → omit the entire “assigned to …” fragment.
    • If {developers} or {pull-requests} is empty → omit the entire “developed by … in …” fragment.
  3. Apply consistently for:
    • Issue rows (row-format-issue)
    • Hierarchy issue rows (row-format-hierarchy-issue) where the same placeholders/fragments exist.

Expected code touchpoints (permalinks):

  • Issue formatting currently injects N/A and always emits potentially-empty fields:
    • IssueRecord.to_chapter_row()
      issue_number (int): The number of the issue.
      Returns:
      IssueRecord: The issue record with that number.
      """
      if self._issue.number == issue_number:
      return self
      return None
      def to_chapter_row(self, add_into_chapters: bool = True) -> str:
      row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.chapter_presence_count() > 1 else ""
      format_values: dict[str, Any] = {}
      # collect format values
      format_values["type"] = f"{self._issue.type.name if self._issue.type else 'N/A'}"
      format_values["number"] = f"#{self._issue.number}"
      format_values["title"] = self._issue.title
      format_values["author"] = self.author
      format_values["assignees"] = ", ".join(self.assignees)
      format_values["developers"] = ", ".join(self.developers)
      list_pr_links = self.get_pr_links()
      if len(list_pr_links) > 0:
      format_values["pull-requests"] = ", ".join(list_pr_links)
      else:
      format_values["pull-requests"] = ""
      # contributors are not used in IssueRecord, so commented out for now
      # format_values["contributors"] = self.contributors if self.contributors is not None else ""
      row = f"{row_prefix}" + ActionInputs.get_row_format_issue().format(**format_values)
      if self.contains_release_notes():
      row = f"{row}\n{self.get_rls_notes()}"
      return row
  • Hierarchy formatting has similar fallback behavior (type = "None") and empty-string risks:
    • HierarchyIssueRecord.to_chapter_row()
      for sub_hierarchy_issue in self._sub_hierarchy_issues.values():
      labels.update(sub_hierarchy_issue.labels)
      for pull in self._pull_requests.values():
      labels.update(label.name for label in pull.get_labels())
      return list(labels)
      # methods - override ancestor methods
      def to_chapter_row(self, add_into_chapters: bool = True) -> str:
      logger.debug("Rendering hierarchy issue row for issue #%s", self.issue.number)
      row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.chapter_presence_count() > 1 else ""
      format_values: dict[str, Any] = {}
      # collect format values
      format_values["number"] = f"#{self.issue.number}"
      format_values["title"] = self.issue.title
      format_values["author"] = self.author
      format_values["assignees"] = ", ".join(self.assignees)
      format_values["developers"] = ", ".join(self.developers)
      if self.issue_type is not None:
      format_values["type"] = self.issue_type
      else:
      format_values["type"] = "None"
      list_pr_links = self.get_pr_links()
      if len(list_pr_links) > 0:
      format_values["pull-requests"] = ", ".join(list_pr_links)
      else:
      format_values["pull-requests"] = ""
      indent: str = " " * self._level
  • Default templates that currently produce dangling phrases:
    • action.yml row-format defaults
      description: 'List of "group names" to be ignored by release notes detection logic.'
      required: false
      default: ''
      row-format-hierarchy-issue:
      description: 'Format of the hierarchy issue in the release notes. Available placeholders: {type}, {number}, {title}, {author}, {assignees}, {developers}. Placeholders are case-insensitive.'
      required: false
      default: '{type}: _{title}_ {number}'
      row-format-issue:
      description: 'Format of the issue row in the release notes. Available placeholders: {type}, {number}, {title}, {author}, {assignees}, {developers}, {pull-requests}. Placeholders are case-insensitive.'
      required: false
      default: '{type}: {number} _{title}_ developed by {developers} in {pull-requests}'
      row-format-pr:
      description: 'Format of the pr row in the release notes. Available placeholders: {number}, {title}, {developers}. Placeholders are case-insensitive.'
      required: false
      default: '{number} _{title}_ developed by {developers}'
      row-format-link-pr:
      description: 'Add prefix "PR:" before link to PR when not linked an Issue.'
      required: false
      default: 'true'

Expected docs touchpoints:

  • Document placeholders + clarify omission behavior when placeholders are empty:
    • docs/features/custom_row_formats.md
      # Feature: Custom Row Formats
      ## Purpose
      Customize how individual issue, PR, and hierarchy issue lines are rendered in the release notes. Ensures output matches team conventions without post-processing.
      ## How It Works
      - Controlled by inputs:
      - `row-format-hierarchy-issue`
      - `row-format-issue`
      - `row-format-pr`
      - `row-format-link-pr` (boolean controlling prefix `PR:` presence for standalone PR links)
      - Placeholders are case-insensitive; unknown placeholders are removed.
      - Available placeholders:
      - Hierarchy issue rows: `{type}`, `{number}`, `{title}`, `{author}`, `{assignees}`, `{developers}`
      - Issue rows: `{type}`, `{number}`, `{title}`, `{author}`, `{assignees}`, `{developers}`, `{pull-requests}`
      - PR rows: `{number}`, `{title}`, `{author}`, `{assignees}`, `{developers}`
      - Duplicity icon (if triggered) is prefixed before the formatted row.
      ## Configuration
      ```yaml
      - name: Generate Release Notes
      id: release_notes_scrapper
      uses: AbsaOSS/generate-release-notes@v1
      env:

Expected tests:

  • Extend existing builder/unit tests (or add parameterized tests) to assert no dangling fragments:
    • tests/unit/release_notes_generator/builder/test_release_notes_builder.py
      custom_chapters=custom_chapters_not_print_empty_chapters,
      )
      actual_release_notes = builder.build()
      assert expected_release_notes == actual_release_notes
      def test_build_hierarchy_rls_notes_no_labels_no_type(
      mocker, mock_repo,
      custom_chapters_not_print_empty_chapters,
      mined_data_isolated_record_types_no_labels_no_type_defined
      ):
      expected_release_notes = RELEASE_NOTES_DATA_HIERARCHY_NO_LABELS_NO_TYPE
      mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator)
      mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_print_empty_chapters", return_value=True)
      mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_hierarchy", return_value=True)
      mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_row_format_hierarchy_issue", return_value="{type}: _{title}_ {number}")
      mock_github_client = mocker.Mock(spec=Github)
      mock_rate_limit = mocker.Mock()
      mock_rate_limit.rate.remaining = 10
      mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600
      mock_github_client.get_rate_limit.return_value = mock_rate_limit
      factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo)
      records = factory.generate(mined_data_isolated_record_types_no_labels_no_type_defined)
      builder = ReleaseNotesBuilder(
      records=records,
      changelog_url=DEFAULT_CHANGELOG_URL,
      custom_chapters=custom_chapters_not_print_empty_chapters,
      )
      actual_release_notes = builder.build()
      assert expected_release_notes == actual_release_notes
      def test_build_hierarchy_rls_notes_with_labels_no_type(
      mocker, mock_repo,
      custom_chapters_not_print_empty_chapters, mined_data_isolated_record_types_with_labels_no_type_defined
      ):
      expected_release_notes = RELEASE_NOTES_DATA_HIERARCHY_WITH_LABELS_NO_TYPE
      mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator)
      mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_print_empty_chapters", return_value=True)
      mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_hierarchy", return_value=True)
      mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_row_format_hierarchy_issue", return_value="{type}: _{title}_ {number}")
      mock_github_client = mocker.Mock(spec=Github)
      mock_rate_limit = mocker.Mock()

Dependencies / Related

No response

Additional Context

Relevant reference docs / constants:

  • Row-format placeholder documentation:
    • # Feature: Custom Row Formats
      ## Purpose
      Customize how individual issue, PR, and hierarchy issue lines are rendered in the release notes. Ensures output matches team conventions without post-processing.
      ## How It Works
      - Controlled by inputs:
      - `row-format-hierarchy-issue`
      - `row-format-issue`
      - `row-format-pr`
      - `row-format-link-pr` (boolean controlling prefix `PR:` presence for standalone PR links)
      - Placeholders are case-insensitive; unknown placeholders are removed.
      - Available placeholders:
      - Hierarchy issue rows: `{type}`, `{number}`, `{title}`, `{author}`, `{assignees}`, `{developers}`
      - Issue rows: `{type}`, `{number}`, `{title}`, `{author}`, `{assignees}`, `{developers}`, `{pull-requests}`
      - PR rows: `{number}`, `{title}`, `{author}`, `{assignees}`, `{developers}`
      - Duplicity icon (if triggered) is prefixed before the formatted row.
      ## Configuration
      ```yaml
      - name: Generate Release Notes
      id: release_notes_scrapper
      uses: AbsaOSS/generate-release-notes@v1
      env:
  • Supported placeholder keys:

Custom agent used: Senior Developer
Implements features and fixes with high quality, meeting specs and tests.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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

Copilot AI changed the title [WIP] Fix release note row rendering for missing fields Fix release note row rendering to skip missing fields (type/assignee/developers/PRs) Jan 16, 2026
Copilot AI requested a review from miroslavpojer January 16, 2026 11:28
Copy link
Collaborator

@miroslavpojer miroslavpojer left a comment

Choose a reason for hiding this comment

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

  • pulled
  • code review - comments created
  • local test run is passing

```
(Formatting reflects the provided custom templates.)
(Formatting reflects the provided custom templates. Note how issues without a type omit the type prefix, and issues without PRs omit the "in {pull-requests}" suffix.)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
(Formatting reflects the provided custom templates. Note how issues without a type omit the type prefix, and issues without PRs omit the "in {pull-requests}" suffix.)
> Formatting reflects the provided custom templates. Note how issues without a type omit the type prefix, and issues without PRs omit the "in {pull-requests}" suffix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Applied the suggested formatting change. Updated in commit a9df629.

# require escaping in regex patterns (hyphens are only special inside character classes).
# The triple-brace syntax {{{var}}} in f-strings produces {{var}} which becomes
# \{var\} in the final regex pattern, correctly matching literal braces.
FIELD_TYPE = "type"
Copy link
Collaborator

Choose a reason for hiding this comment

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

@copilot here are defined supported placeholders, but their string form is already defined in constants.py

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to reuse field name constants from SUPPORTED_ROW_FORMAT_KEYS_ISSUE in constants.py. Updated in commit a9df629.

- `row-format-pr`
- `row-format-link-pr` (boolean controlling prefix `PR:` presence for standalone PR links)
- Placeholders are case-insensitive; unknown placeholders are removed.
- **Empty field suppression**: When a placeholder value is empty, the generator intelligently omits the surrounding text fragment:
Copy link
Collaborator

Choose a reason for hiding this comment

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

@copilot here you should mention that suppression is implemented only for a sub group of all supported placeholders.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added clarification that suppression is only implemented for specific placeholders (type, assignees, developers, pull-requests) with a note that other placeholders do not trigger suppression. Updated in commit a9df629.

return user


def make_mock_issue(mocker, number: int, title: str, type_name=None, assignees=None, author=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

@copilot try to reuse already existing mocks in conftest.py.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored tests to reuse existing mocks from conftest.py (mock_issue_closed, mock_user, mock_pull_closed) instead of creating custom helper functions. Updated in commit a9df629.

return pr


class TestIssueRecordRowFormattingMissingFields:
Copy link
Collaborator

Choose a reason for hiding this comment

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

@copilot follow style in other test files. There are no Test classes. There are just test methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the Test class wrapper and converted to plain test functions following the project's existing test style. Updated in commit a9df629.

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.

Fix release note row rendering to skip missing fields (type/assignee/developers/PRs)

2 participants