Skip to content

Changelog generator splits long entries into multiple bullets, each with duplicated [#NNNN] link #69454

@dwoz

Description

@dwoz

Description

The towncrier-based changelog generator splits any changelog fragment that contains hard line breaks into one bullet per source line, and appends the [#NNNN] issue link to every single bullet. The result for any multi-paragraph fragment is N broken bullets all tagged with the same issue number, with sentences sheared in half across bullets.

This regressed in 808241d ("update towncrier template for multi-line entry"), which replaced the single-line render with a per-line loop in changelog/.template.jinja.

Reproduction

Source fragment (one entry, intentionally wrapped at ~72 columns for readability), at changelog/69031.fixed.md in the v3008.1 tag:

Fixed two distinct bugs in the `salt.engines.redis_sentinel` engine that
together prevented it from being usable. `start()` no longer raises
`AttributeError: 'dict_values' object has no attribute 'pop'` on Python 3
(the dict.values() result is now wrapped in `list(...)`). `Listener` and
`start()` now accept an optional `password` argument and forward it to
the redis client, allowing the engine to authenticate against a Sentinel
that requires AUTH; the default of `None` keeps existing configurations
working unchanged.

Rendered output in CHANGELOG.md at v3008.1 (the ### Fixed block):

- Fixed two distinct bugs in the `salt.engines.redis_sentinel` engine that [#69031](https://github.com/saltstack/salt/issues/69031)
- together prevented it from being usable. `start()` no longer raises [#69031](https://github.com/saltstack/salt/issues/69031)
- `AttributeError: 'dict_values' object has no attribute 'pop'` on Python 3 [#69031](https://github.com/saltstack/salt/issues/69031)
- (the dict.values() result is now wrapped in `list(...)`). `Listener` and [#69031](https://github.com/saltstack/salt/issues/69031)
- `start()` now accept an optional `password` argument and forward it to [#69031](https://github.com/saltstack/salt/issues/69031)
- the redis client, allowing the engine to authenticate against a Sentinel [#69031](https://github.com/saltstack/salt/issues/69031)
- that requires AUTH; the default of `None` keeps existing configurations [#69031](https://github.com/saltstack/salt/issues/69031)
- working unchanged. [#69031](https://github.com/saltstack/salt/issues/69031)

The same shape repeats in the v3008.1 changelog for #69032, #69033, #69035, #69037, #69038, #69039, #69129, #43718, and every other multi-line fragment in that release. It also pollutes the RPM pkg/rpm/salt.spec changelog block, which is regenerated from the same source.

Expected

A single bullet per fragment, rendered as a multi-line markdown list item, with the [#NNNN] link appended exactly once at the end:

- Fixed two distinct bugs in the `salt.engines.redis_sentinel` engine that
  together prevented it from being usable. `start()` no longer raises
  `AttributeError: 'dict_values' object has no attribute 'pop'` on Python 3
  (the dict.values() result is now wrapped in `list(...)`). `Listener` and
  `start()` now accept an optional `password` argument and forward it to
  the redis client, allowing the engine to authenticate against a Sentinel
  that requires AUTH; the default of `None` keeps existing configurations
  working unchanged. [#69031](https://github.com/saltstack/salt/issues/69031)

Actual

N bullets per fragment, each ending in the same [#NNNN] link, with sentences cut in half at the source line breaks. Severity is cosmetic but the rendered CHANGELOG.md is genuinely hard to read and the broken bullets look unprofessional on the GitHub releases page.

Root cause

changelog/.template.jinja (introduced in 808241d, "update towncrier template for multi-line entry"):

{% for text, values in sections[""][category].items() %}
{% set lines = text.split('\n') %}
{% for line in lines %}
{% if line.startswith('- ') %}
{{ line | trim }} {{ values|join(', ') }}
{% else %}
- {{ line | trim }} {{ values|join(', ') }}
{% endif %}
{% endfor %}
{% endfor %}

The template splits the fragment text on \n, emits a - bullet for each line, and appends {{ values|join(', ') }} (the issue link list) to every line. The intent of that commit was to support entries that already include their own - sub-bullets, but the implementation made every soft line break in a fragment into a top-level bullet.

[tool.towncrier] in pyproject.toml does not set wrap, so towncrier itself is not re-wrapping the text -- the multiplication is purely a template bug.

Suggested fix

Render text once as a single bullet and only append the issue link to the last line. Something like:

{% for text, values in sections[""][category].items() %}
- {{ text | indent(2) }} {{ values|join(', ') }}

{% endfor %}

(plus whatever escape hatch is needed for the rare fragment that genuinely wants its own nested sub-bullets -- e.g. require the fragment author to write a properly-indented markdown list and treat the whole fragment as one bullet regardless).

Versions

Affects every release built with the post-808241d3154d template (3008.0, 3008.1, and any nightly built off master / 3008.x since 2025-05-27).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugbroken, incorrect, or confusing behavior

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions