Skip to content

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

@miroslavpojer

Description

@miroslavpojer

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: #231 _Dependency Dashboard_ author is @renovate[bot] assigned to developed by in

  • Expected (after fix):
    - #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:
    • CODERABBIT_SUPPORT_ACTIVE = "coderabbit-support-active"
      CODERABBIT_RELEASE_NOTES_TITLE = "coderabbit-release-notes-title"
      CODERABBIT_SUMMARY_IGNORE_GROUPS = "coderabbit-summary-ignore-groups"
      RUNNER_DEBUG = "RUNNER_DEBUG"
      ROW_FORMAT_HIERARCHY_ISSUE = "row-format-hierarchy-issue"
      ROW_FORMAT_ISSUE = "row-format-issue"
      ROW_FORMAT_PR = "row-format-pr"
      ROW_FORMAT_LINK_PR = "row-format-link-pr"
      SUPPORTED_ROW_FORMAT_KEYS_HIERARCHY_ISSUE = ["type", "number", "title", "author", "assignees", "developers"]
      SUPPORTED_ROW_FORMAT_KEYS_ISSUE = ["type", "number", "title", "author", "assignees", "developers", "pull-requests"]
      SUPPORTED_ROW_FORMAT_KEYS_PULL_REQUEST = ["number", "title", "author", "assignees", "developers"]
      # Features
      WARNINGS = "warnings"

New tests to add (proposed):

  • Parameterized cases for combinations:
    • missing type + no assignees + no PRs
    • missing type + has assignees
    • has type + missing assignees + has PRs
    • has type + has developers + has PRs (control case)
  • Assertions should check that the final rendered row does not include any of:
    • N/A:
    • assigned to (when no assignees)
    • developed by / in (when no developers/PRs)

Metadata

Metadata

Labels

enhancementNew feature or request

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions