Skip to content

Generated CHANGELOG.md skips heading levels (H1 β†’ H3) when no version is setΒ #3392

@glennmichael123

Description

@glennmichael123

Summary

When logsmith generates a changelog for an unreleased state (no version provided), the section headings (### πŸ› Bug Fixes, ### 🧹 Chores, ### Contributors, etc.) are emitted at H3 directly under the file's # Changelog H1 β€” skipping H2 entirely. This violates markdown/heading-increment and breaks lint in any consuming repo.

Repro

In any repo using logsmith, run:

bunx --bun logsmith --output CHANGELOG.md

When there are unreleased commits and no version tag, the generated file looks like:

# Changelog

[Compare changes](https://github.com/owner/repo/compare/v0.13.2...HEAD)

### πŸ› Bug Fixes
- ...

### 🧹 Chores
- ...

### Contributors
- ...

Then run any markdown linter (e.g. pickier with markdown/heading-increment: error):

CHANGELOG.md
  4:1  error  Heading level should not skip from h1 to h3   markdown/heading-increment

Expected

Every section header should be reachable by walking heading levels one step at a time. With no version (unreleased) the sections should sit at H2, not H3:

# Changelog

[Compare changes](...)

## πŸ› Bug Fixes
- ...

## 🧹 Chores
- ...

## Contributors
- ...

When a version is present, the existing structure is correct β€” the version goes at H2 and sections at H3:

# Changelog

## v0.13.3
[Compare changes](...)

### πŸ› Bug Fixes
- ...

Where it's coming from

packages/logsmith/src/utils.ts β†’ generateChangelogContent() around lines 369–470:

if (changelog.version) {
  lines.push(`## ${config.versionPrefix}${changelog.version}`)  // H2
  ...
}

// ...later, unconditionally:
for (const section of changelog.sections) {
  lines.push(`### ${section.title}`)  // H3 β€” but no H2 above when version is empty
  ...
}

if (changelog.contributors.length > 0 && !config.excludeEmail) {
  lines.push(`### ${getLabel('contributors', config.language)}`)  // same issue
  ...
}

When changelog.version is falsy, the H2 line is skipped, but the H3 sections still emit. The outer wrapper in packages/logsmith/src/changelog.ts (line ~148) then prepends # Changelog as H1 β€” producing the H1β†’H3 jump.

Real-world impact

This breaks CI lint across our 95 repos that use logsmith + pickier. We've been demoting H3β†’H2 manually in each affected CHANGELOG.md, but those edits don't survive the next bunx logsmith run.

Suggested fix

In generateChangelogContent, derive the section level from whether a version header was emitted:

const sectionLevel = changelog.version ? '###' : '##'

for (const section of changelog.sections) {
  lines.push(`${sectionLevel} ${section.title}`)
  ...
}

// same for contributors:
lines.push(`${sectionLevel} ${getLabel('contributors', config.language)}`)

Alternatively, always emit an H2 anchor β€” e.g. ## Unreleased when no version β€” but the dynamic-level approach is less intrusive to existing CHANGELOG layouts.

Related observation

Older entries in some long-lived CHANGELOGs (generated by an earlier logsmith) used H2 for sections (## 🧹 Chores). The recent regression is when this changed to H3-without-an-H2-parent for the unreleased block.

Version

logsmith β€” installed via bunx logsmith (latest at time of report)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions