Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ commit-and-tag-version --first-release
npx commit-and-tag-version --first-release
```

This will tag a release **without bumping the version `bumpFiles`[1]()**.
This will tag a release **without bumping the version `bumpFiles`[1](<>)**.

When you are ready, push the git tag and `npm publish` your first release. \o/

Expand Down Expand Up @@ -299,6 +299,29 @@ commit-and-tag-version --prerelease xyz # Creates v1.4.3-xyz.1 (auto-incremente

This behavior applies to both named prereleases (e.g., `-alpha.0`, `-beta.1`) and unnamed prereleases (e.g., `-0`, `-1`), ensuring that you can safely cut multiple prerelease versions without encountering git tag conflicts.

#### What Each Changelog Section Covers

Each release's changelog section starts where the previous release ended, and which earlier release counts as "previous" depends on what you are releasing:

- A stable release covers everything since the last stable release, including changes that already appeared in prereleases.
- A prerelease covers everything since the most recent tag that is either a stable release or a prerelease with the same name. Prereleases with other names are ignored.

For example, starting from `v1.0.0`:

```bash
# feat: add a
commit-and-tag-version --prerelease alpha # v1.1.0-alpha.0 lists "add a"
# feat: add b
commit-and-tag-version --prerelease alpha # v1.1.0-alpha.1 lists "add b" only
# fix: correct c
commit-and-tag-version --prerelease beta # v1.1.0-beta.0 lists a, b and c: it is the first beta
commit-and-tag-version # v1.1.0 lists a, b and c
```

The compare link in each heading starts from the same tag, so `v1.1.0-alpha.1` links to `v1.1.0-alpha.0...v1.1.0-alpha.1` and `v1.1.0` links to `v1.0.0...v1.1.0`.

Unnamed prereleases (e.g., `-0`, `-1`) are treated as sharing one name.

#### Keeping Prereleases out of a Final Changelog

`commit-and-tag-version` does not currently provide a general-purpose option to
Expand Down Expand Up @@ -663,7 +686,7 @@ module.exports.writeVersion = function (contents, version) {
### Why do breaking changes before 1.0.0 not trigger a 1.0.0 release?

Below 1.0.0, the semver specification doesn't give any guarantees about the
meaning of version numbers.
meaning of version numbers.

However, with npm there is a community convention,
and implementation-defined behaviour: If your version is between 0.1.0 and
Expand Down
67 changes: 55 additions & 12 deletions lib/lifecycles/changelog.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ConventionalGitClient } from '@conventional-changelog/git-client';
import chalk from 'chalk';
import { ConventionalChangelog } from 'conventional-changelog';
import fs from 'fs';
Expand Down Expand Up @@ -42,6 +43,52 @@ function extractChangelogBody(oldContent) {
: oldContent;
}

/**
* The prerelease identifier of a version: 'alpha' for 1.0.0-alpha.1, '' for an
* unnamed prerelease such as 1.0.0-0, and null for a stable version.
*/
function prereleaseChannel(version) {
const prerelease = semver.prerelease(version);
if (!prerelease) return null;
return typeof prerelease[0] === 'string' ? prerelease[0] : '';
}

/**
* Whether an earlier release ends the changelog section of the version being
* released. Stable releases always do. Prereleases only do for a later
* prerelease with the same identifier, so that:
* - a stable release, or the first prerelease with a new identifier, covers
* everything since the last stable release, including changes that only
* appeared in other prereleases (#310)
* - a continued prerelease only covers changes since the previous prerelease
* with that identifier, rather than repeating them every time (#349)
*/
function isReleaseBoundary(version, newVersion) {
if (!semver.valid(version)) return false;
const channel = prereleaseChannel(version);
return channel === null || channel === prereleaseChannel(newVersion);
}

/**
* Git client that only reports the tags which are release boundaries for the
* version being released, so the changelog's commit range, section splits
* and compare links all agree on where the previous release was.
*/
class ReleaseBoundaryGitClient extends ConventionalGitClient {
constructor(cwd, newVersion) {
super(cwd);
this.newVersion = newVersion;
}

async *getSemverTags(params = {}) {
for await (const tag of super.getSemverTags(params)) {
if (isReleaseBoundary(tag.replace(params.prefix, ''), this.newVersion)) {
yield tag;
}
}
}
}

async function outputChangelog(args, newVersion) {
createIfMissing(args);
const header = args.header;
Expand All @@ -55,13 +102,12 @@ async function outputChangelog(args, newVersion) {

const changelogFrontMatter = extractFrontMatter(oldContent);

const generator = new ConventionalChangelog()
const generator = new ConventionalChangelog(
new ReleaseBoundaryGitClient(process.cwd(), newVersion),
)
.readPackage()
.loadPreset(presetLoader(args))
// skipUnstable keeps the changelog window aligned with the bump logic:
// both cover all commits since the last stable release, so changes that
// only appeared in prereleases are not dropped from the release (#310).
.tags({ prefix: args.tagPrefix, skipUnstable: true })
.tags({ prefix: args.tagPrefix })
.options({
releaseCount: args.releaseCount,
...(args.verbose
Expand All @@ -73,13 +119,10 @@ async function outputChangelog(args, newVersion) {
.context({ version: newVersion })
.commits({ merges: null, path: args.path }, args.parserOpts)
.writer({
// Split sections at stable release tags only, so commits already
// released in intermediate prereleases stay in this release's section
// rather than being regenerated under the prerelease's heading (#310).
generateOn: (commit) =>
Boolean(
semver.valid(commit.version) && !semver.prerelease(commit.version),
),
// Split sections at the same tags the commit range is built from, so
// commits released in other prereleases stay in this release's section
// rather than being regenerated under that prerelease's heading.
generateOn: (commit) => isReleaseBoundary(commit.version, newVersion),
...(args.writerOpts || {}),
});

Expand Down
45 changes: 42 additions & 3 deletions test/prerelease-window.integration-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,50 @@ describe('prerelease bump and changelog windows (#310)', function () {
runCli('--prerelease', 'dev');
expect(tags()).toContain('v1.1.0-dev.1');

// Prerelease sections are cumulative: each one covers everything since
// the last stable release.
// Only the changes since the previous prerelease with this identifier
// are listed; the feat is already in the dev.0 section (#349).
const devSection = changelogSection('1.1.0-dev.1');
expect(devSection).toContain('add feat');
expect(devSection).not.toContain('add feat');
expect(devSection).toContain('add fix');
expect(devSection).toContain('v1.1.0-dev.0...v1.1.0-dev.1');
expect(changelogSection('1.1.0-dev.0')).toContain('add feat');
});

it('lists each change once across continued prereleases and again in the stable release', function () {
commit('feat: add a');
runCli('--prerelease', 'alpha');
commit('feat: add b');
runCli('--prerelease', 'alpha');
commit('fix: correct c');
runCli('--prerelease', 'alpha');

const prereleases = fs
.readFileSync('CHANGELOG.md', 'utf-8')
.split(/^#+ \[?1\.0\.0[\] ]/m)[0];
expect(prereleases.match(/add a/g)).toHaveLength(1);
expect(prereleases.match(/add b/g)).toHaveLength(1);
expect(prereleases.match(/correct c/g)).toHaveLength(1);

runCli();
const stableSection = changelogSection('1.1.0');
expect(stableSection).toContain('add a');
expect(stableSection).toContain('add b');
expect(stableSection).toContain('correct c');
expect(stableSection).toContain('v1.0.0...v1.1.0');
});

it('covers other prereleases when returning to an earlier identifier', function () {
commit('feat: add feat');
runCli('--prerelease', 'dev');
commit('fix: add fix');
runCli('--prerelease', 'rc');
commit('fix: second fix');
runCli('--prerelease', 'dev');

const devSection = changelogSection('1.1.0-dev.1');
expect(devSection).not.toContain('add feat');
expect(devSection).toContain('add fix');
expect(devSection).toContain('second fix');
});

it('escalates a continued prerelease when a bigger change lands', function () {
Expand Down
Loading