Skip to content

Release Notes Generation

Learn how to customize Versu's release notes generation.

Changelogs vs Release Notes

Alongside changelogs, Versu generates release notes: a self-contained summary of a single release, written to a separate file (default RELEASE.md). While a changelog accumulates the full history of a module, release notes describe only what changed in the current release - making them perfect as the body of a GitHub release.

ChangelogRelease Notes
Default fileCHANGELOG.mdRELEASE.md
Configuration keychangelogrelease
ContentFull history, new entries prependedSummary of the current release
Committed by defaultYes (with version changes)No (opt-in via --commit-release-notes)
Toggle--generate-changelog--generate-release-notes

Both are rendered by the same engine (conventional-changelog-writer), so everything you learned in the Changelog Generation guide - grouping, transforms, handlebars templates - applies here too.

Basic Configuration

Release notes are configured through the top-level release key, with the exact same structure as changelog: a root and a module section, each accepting a context and options passed to conventional-changelog-writer.

javascript
export default {
  // Other configuration options
  release: {
    // Release notes configuration options
    root: {
      // Root release notes configuration (optional)
      context: { /* */ }, // Context<Commit>
      options: { /* */ }, // Options<Commit>
    },
    module: {
      // Module release notes configuration (optional)
      context: { /* */ }, // Context<Commit>
      options: { /* */ }, // Options<Commit>
    },
  },
};

Merged with defaults

Your configuration is deep-merged with Versu's defaults. If you only override options.mainTemplate, the default commit grouping, transform and partials are kept.

Default Output

Versu ships with release notes templates out of the box, so you get useful release notes with zero configuration. Commits are grouped by type with the same emoji sections used in changelogs (✨ Features, 🐛 Bug Fixes, ...).

To illustrate, let's reuse the running example from Multi-Module Projects - common, auth, api, web and mobile - right after common received a new feature and the bump cascaded through all dependents.

Module Release Notes

Each versioned module gets its own release notes file (e.g., packages/common/RELEASE.md) summarizing its release, ending with a compare link between the previous and current tag:

markdown
## What's changed

### ✨ Features

- Add shared validation helper ([abc1234](https://github.com/my-org/my-monorepo/commit/abc1234))

**Full Changelog:** [`2.3.1...2.4.0`](https://github.com/my-org/my-monorepo/compare/common@2.3.1...common@2.4.0)

---
*Generated by versu*

Root Release Notes

In multi-module projects, the root RELEASE.md starts with a Module Releases overview - one line per released module, linking its changelog and a version compare - followed by any changes not belonging to a specific module:

markdown
## What's changed

### 🚀 Module Releases

- [common](packages/common/CHANGELOG.md) - [2.4.0](https://github.com/my-org/my-monorepo/compare/common@2.3.1...common@2.4.0)
- [auth](packages/auth/CHANGELOG.md) - [1.6.0](https://github.com/my-org/my-monorepo/compare/auth@1.5.2...auth@1.6.0)
- [api](packages/api/CHANGELOG.md) - [1.3.0](https://github.com/my-org/my-monorepo/compare/api@1.2.0...api@1.3.0)
- [web](packages/web/CHANGELOG.md) - [3.2.0](https://github.com/my-org/my-monorepo/compare/web@3.1.0...web@3.2.0)
- [mobile](packages/mobile/CHANGELOG.md) - [0.10.0](https://github.com/my-org/my-monorepo/compare/mobile@0.9.0...mobile@0.10.0)

## 📝 Other Changes

#### ✨ Features

- Add shared validation helper ([abc1234](https://github.com/my-org/my-monorepo/commit/abc1234))

Modules without a declared version (like the root in the running example) are excluded from the Module Releases list.

Fresh files, not history

Release notes share the prepend mechanism with changelogs: if the target file already exists, new content is inserted at the prependPlaceholder. However, the default release templates intentionally don't emit the placeholder - release notes are meant to be regenerated per release, not accumulated. Since they aren't committed by default, each CI run starts from a clean checkout and renders fresh files.

Customizing Release Notes

All the customization options from the Changelog Generation guide work here: groupBy, transform, commitGroupsSort and the handlebars templates (mainTemplate, headerPartial, commitPartial, footerPartial).

As an example, let's extend the module release notes with install instructions:

handlebars
## What's changed
{{#each commitGroups}}

### {{title}}

{{#each commits}}
{{> commit}}
{{/each}}
{{/each}}

## Installation

```sh
npm install my-lib@{{version}}
```

**Full Changelog:** {{#if linkCompare~}}
[`{{previousTagVersion}}...{{version}}`]({{repoUrl}}/compare/{{previousTag}}...{{currentTag}})
{{else~}}
[`{{version}}`]({{repoUrl}}/releases/tag/{{currentTag}})
{{/if}}
javascript
import * as fs from "fs";
import * as path from "path";

const cwd = import.meta.dirname;

const releaseMainTemplate = fs.readFileSync(
  path.join(cwd, "templates", "release-main.hbs"),
  "utf-8",
);

export default {
  // Other configuration options
  release: {
    module: {
      options: {
        mainTemplate: releaseMainTemplate,
      },
    },
  },
};

Since the configuration is merged with the defaults, the default grouping, transform and commit partial keep working inside your custom template.

Template Variables

Versu provides these context variables to both changelog and release notes templates:

VariableDescription
versionThe new version being released (only set for final releases)
currentTagTag created for this release (e.g., common@2.4.0)
previousTagThe module's previous release tag
previousTagVersionVersion extracted from the previous tag
linkCompareWhether both tags exist, enabling a compare link
repoUrl, host, owner, repositoryRepository information detected from Git
prependWhether an existing file is being updated
providerDetected provider (e.g., github)

Root templates additionally receive moduleResults: an array with one entry per processed module (id, name, path, type, from, to, bumpType, declaredVersion, isRelease, tagName) - this is what powers the Module Releases section.

The following handlebars helpers are also registered: eq, ne, and, or, not, and getModuleTagName (builds a module's tag name from a name and a version, as used by the default root template for the compare links).

Controlling Generation

Release notes generation is enabled by default and can be controlled through the CLI or the GitHub Action:

CLI flagAction inputDescription
--no-generate-release-notesgenerate-release-notes: falseDisable release notes generation
--release-notes-filenamerelease-notes-filenameChange the output filename (default RELEASE.md)
--commit-release-notescommit-release-notesInclude release notes in the release commit (default off)

The paths of the generated files are reported in the runner result (releaseNotesPaths) and in the action's release-notes-paths output as a JSON array of { moduleId, path }.

Publishing GitHub Releases

The typical use for release notes is feeding them into GitHub releases. Combining the created-tags and release-notes-paths outputs:

yaml
- name: Run Versu
  id: versu
  uses: versuhq/versu@v3
  # ... inputs

- name: Create GitHub releases
  if: steps.versu.outputs.bumped == 'true'
  uses: actions/github-script@v9
  env:
    CREATED_TAGS: ${{ steps.versu.outputs.created-tags }}
    NOTES_PATHS: ${{ steps.versu.outputs.release-notes-paths }}
  with:
    script: |
      const fs = require("fs");

      const createdTags = JSON.parse(process.env.CREATED_TAGS);
      const notesPaths = JSON.parse(process.env.NOTES_PATHS);

      for (const { moduleId, tag } of createdTags) {
        const notes = notesPaths.find((entry) => entry.moduleId === moduleId);

        await github.rest.repos.createRelease({
          owner: context.repo.owner,
          repo: context.repo.repo,
          tag_name: tag,
          name: tag,
          body: notes ? fs.readFileSync(notes.path, "utf8") : undefined,
          // Fall back to GitHub's auto-generated notes if none were rendered
          generate_release_notes: !notes,
        });
      }

Each released module gets its own GitHub release, with its tag as the title and its generated release notes as the body. The workflow's GITHUB_TOKEN needs the contents: write permission to create releases.

Next Steps


Ready to set up pre-releases? Check out the Pre-release Versions guide!

Released under the MIT License.