Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changelog/20260518161120_ci_4458.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: Fix
scope:
- ckeditor5-dev-changelog
---

Fixed the suggested pre-release version when starting a pre-release cycle from a stable version. The default now reflects the underlying change type β€” a major bump only when breaking changes are present, a minor bump for features, and a patch bump otherwise β€” instead of always defaulting to a major bump.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { styleText } from 'node:util';
import { type ReleaseType } from 'semver';
import semver, { type ReleaseType } from 'semver';
import { provideNewVersion } from './providenewversion.js';
import { logInfo } from './loginfo.js';
import { detectReleaseChannel } from './detectreleasechannel.js';
Expand Down Expand Up @@ -60,14 +60,18 @@ export async function determineNextVersion( options: DetermineNextVersionOptions

let bumpType: ReleaseType = 'patch';

if ( releaseType === 'prerelease' || releaseType === 'prerelease-promote' ) {
bumpType = 'prerelease';
} else if ( sections.major.entries.length || sections.breaking.entries.length ) {
if ( sections.major.entries.length || sections.breaking.entries.length ) {
bumpType = 'major';
} else if ( sections.minor.entries.length || sections.feature.entries.length ) {
bumpType = 'minor';
}

if ( releaseType === 'prerelease' || releaseType === 'prerelease-promote' ) {
// When already on a prerelease channel, continuing or promoting uses the plain `prerelease` bump.
// Initiating a prerelease from a stable version uses `premajor` / `preminor` / `prepatch` based on commits.
bumpType = semver.prerelease( currentVersion ) ? 'prerelease' : `pre${ bumpType }` as ReleaseType;
}

const areErrorsPresent = !!sections.invalid.entries.length;
const areWarningsPresent = Object.values( sections ).some( section =>
section.entries.some( entry => entry.data.validations && entry.data.validations.length > 0 )
Expand Down
18 changes: 9 additions & 9 deletions packages/ckeditor5-dev-changelog/src/utils/providenewversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,13 @@ function createVersionQuestion( options: Options ) {
function getSuggestedVersion( bumpType: ReleaseType, version: string, releaseChannel: ReleaseChannel ) {
if ( bumpType === 'prerelease' && releaseChannel !== 'latest' ) {
return semver.inc( version, bumpType, releaseChannel );
} else if ( bumpType === 'prerelease' && releaseChannel === 'latest' ) {
// Using 'premajor` and `alpha` channel for a case, when introducing a prerelease for the next major.
// E.g. 1.0.0 -> 2.0.0-alpha.0.
}

return semver.inc( version, 'premajor', 'alpha' );
} else {
return semver.inc( version, bumpType );
if ( bumpType === 'premajor' || bumpType === 'preminor' || bumpType === 'prepatch' ) {
return semver.inc( version, bumpType, 'alpha' );
}

return semver.inc( version, bumpType );
}

function getChoices( {
Expand All @@ -134,10 +133,11 @@ function getChoices( {
} ) {
const proposedVersions: Array<string> = [];
const preReleaseChannels = [ 'alpha', 'beta', 'rc' ];
const preInitialBumpTypes: Array<ReleaseType> = [ 'premajor', 'preminor', 'prepatch' ];
const validPromotionChannels = preReleaseChannels.filter( ( value, index, array ) => index >= array.indexOf( releaseChannel ) );

// 6.0.0 => Latest (stable) release (7.0.0 | 6.1.0 | 6.0.1)
if ( bumpType !== 'prerelease' && releaseType === 'latest' && releaseChannel === 'latest' ) {
if ( releaseType === 'latest' && releaseChannel === 'latest' ) {
proposedVersions.push(
semver.inc( version, 'major' )!,
semver.inc( version, 'minor' )!,
Expand All @@ -146,7 +146,7 @@ function getChoices( {
}

// 6.0.0 => Pre-release (7.0.0-alpha.0 | 6.1.0-alpha.0 | 6.0.1-alpha.0)
if ( bumpType === 'prerelease' && releaseType === 'prerelease' && releaseChannel === 'latest' ) {
if ( preInitialBumpTypes.includes( bumpType ) && releaseType === 'prerelease' && releaseChannel === 'latest' ) {
proposedVersions.push(
semver.inc( version, 'premajor', 'alpha' )!,
semver.inc( version, 'preminor', 'alpha' )!,
Expand All @@ -155,7 +155,7 @@ function getChoices( {
}

// 6.0.0-alpha.0 => Latest (stable) release (6.0.0)
if ( bumpType !== 'prerelease' && releaseType === 'latest' && preReleaseChannels.includes( releaseChannel ) ) {
if ( releaseType === 'latest' && preReleaseChannels.includes( releaseChannel ) ) {
proposedVersions.push(
semver.inc( version, 'release' )!
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ describe( 'determineNextVersion()', () => {
expect( mockedDetectReleaseChannel ).toHaveBeenCalledWith( '1.0.0', false );
} );

it( 'should return a prerelease bump version when releaseType is prerelease', async () => {
mockedProvideNewVersion.mockResolvedValueOnce( '1.0.0-alpha.1' );
it( 'should return a premajor bump when initiating a prerelease from stable with breaking changes', async () => {
mockedProvideNewVersion.mockResolvedValueOnce( '2.0.0-alpha.0' );
options.releaseType = 'prerelease';
options.sections = createSectionsWithEntries( {
major: { entries: [ createEntry( 'Major breaking change' ) ], title: 'Major Breaking Changes' },
Expand All @@ -230,16 +230,66 @@ describe( 'determineNextVersion()', () => {

const result = await determineNextVersion( options );

expect( result ).toBe( '1.0.0-alpha.1' );
expect( result ).toBe( '2.0.0-alpha.0' );

expect( mockedProvideNewVersion ).toHaveBeenCalledWith( expect.objectContaining( {
bumpType: 'prerelease'
bumpType: 'premajor'
} ) );
expect( mockedDetectReleaseChannel ).toHaveBeenCalledWith( '1.0.0', false );
} );

it( 'should return a preminor bump when initiating a prerelease from stable with only feature entries', async () => {
mockedProvideNewVersion.mockResolvedValueOnce( '1.1.0-alpha.0' );
options.releaseType = 'prerelease';
options.sections = createSectionsWithEntries( {
feature: { entries: [ createEntry( 'New feature' ) ], title: 'Features' }
} );

const result = await determineNextVersion( options );

expect( result ).toBe( '1.1.0-alpha.0' );
expect( mockedProvideNewVersion ).toHaveBeenCalledWith( expect.objectContaining( {
bumpType: 'preminor'
} ) );
} );

it( 'should return a prepatch bump when initiating a prerelease from stable with no breaking/feature entries', async () => {
mockedProvideNewVersion.mockResolvedValueOnce( '1.0.1-alpha.0' );
options.releaseType = 'prerelease';
options.sections = createSectionsWithEntries( {
fix: { entries: [ createEntry( 'A fix' ) ], title: 'Bug fixes' }
} );

const result = await determineNextVersion( options );

expect( result ).toBe( '1.0.1-alpha.0' );
expect( mockedProvideNewVersion ).toHaveBeenCalledWith( expect.objectContaining( {
bumpType: 'prepatch'
} ) );
} );

it( 'should return a prerelease bump when continuing a prerelease channel', async () => {
mockedDetectReleaseChannel.mockReturnValue( 'alpha' );
mockedProvideNewVersion.mockResolvedValueOnce( '1.0.0-alpha.1' );
options.currentVersion = '1.0.0-alpha.0';
options.releaseType = 'prerelease';
options.sections = createSectionsWithEntries( {
major: { entries: [ createEntry( 'Major breaking change' ) ], title: 'Major Breaking Changes' }
} );

const result = await determineNextVersion( options );

expect( result ).toBe( '1.0.0-alpha.1' );
expect( mockedProvideNewVersion ).toHaveBeenCalledWith( expect.objectContaining( {
bumpType: 'prerelease'
} ) );
expect( mockedDetectReleaseChannel ).toHaveBeenCalledWith( '1.0.0-alpha.0', false );
} );

it( 'should call detectReleaseChannel with promotePrerelease=true when releaseType is prerelease-promote', async () => {
mockedDetectReleaseChannel.mockReturnValue( 'beta' );
mockedProvideNewVersion.mockResolvedValueOnce( '1.0.0-beta.0' );
options.currentVersion = '1.0.0-alpha.0';
options.releaseType = 'prerelease-promote';
options.sections = createSectionsWithEntries( {
major: { entries: [ createEntry( 'Major breaking change' ) ], title: 'Major Breaking Changes' }
Expand All @@ -251,7 +301,7 @@ describe( 'determineNextVersion()', () => {
expect( mockedProvideNewVersion ).toHaveBeenCalledWith( expect.objectContaining( {
bumpType: 'prerelease'
} ) );
expect( mockedDetectReleaseChannel ).toHaveBeenCalledWith( '1.0.0', true );
expect( mockedDetectReleaseChannel ).toHaveBeenCalledWith( '1.0.0-alpha.0', true );
} );

it( 'should set displayValidationWarning to true when invalid entries are present', async () => {
Expand Down
42 changes: 40 additions & 2 deletions packages/ckeditor5-dev-changelog/tests/utils/providenewversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,10 @@ describe( 'provideNewVersion()', () => {
] );
} );

it( 'should suggest premajor alpha for prerelease bump type on latest channel', async () => {
it( 'should suggest premajor alpha for premajor bump type on latest channel', async () => {
await provideNewVersion( {
...defaultOptions,
bumpType: 'prerelease',
bumpType: 'premajor',
releaseType: 'prerelease',
releaseChannel: 'latest',
version: '1.0.0'
Expand All @@ -165,6 +165,44 @@ describe( 'provideNewVersion()', () => {
] );
} );

it( 'should suggest preminor alpha for preminor bump type on latest channel', async () => {
await provideNewVersion( {
...defaultOptions,
bumpType: 'preminor',
releaseType: 'prerelease',
releaseChannel: 'latest',
version: '1.0.0'
} );

const promptCall = vi.mocked( inquirer.prompt ).mock.calls[ 0 ]?.[ 0 ] as any;
expect( promptCall[ 0 ].default ).toBe( '1.1.0-alpha.0' );
expect( promptCall[ 0 ].choices ).toEqual( [
{ name: '2.0.0-alpha.0', value: '2.0.0-alpha.0' },
{ name: '1.1.0-alpha.0', value: '1.1.0-alpha.0' },
{ name: '1.0.1-alpha.0', value: '1.0.1-alpha.0' },
{ name: 'Custom...', value: 'custom' }
] );
} );

it( 'should suggest prepatch alpha for prepatch bump type on latest channel', async () => {
await provideNewVersion( {
...defaultOptions,
bumpType: 'prepatch',
releaseType: 'prerelease',
releaseChannel: 'latest',
version: '1.0.0'
} );

const promptCall = vi.mocked( inquirer.prompt ).mock.calls[ 0 ]?.[ 0 ] as any;
expect( promptCall[ 0 ].default ).toBe( '1.0.1-alpha.0' );
expect( promptCall[ 0 ].choices ).toEqual( [
{ name: '2.0.0-alpha.0', value: '2.0.0-alpha.0' },
{ name: '1.1.0-alpha.0', value: '1.1.0-alpha.0' },
{ name: '1.0.1-alpha.0', value: '1.0.1-alpha.0' },
{ name: 'Custom...', value: 'custom' }
] );
} );

it( 'should suggest stable version when bumping patch from alpha to latest', async () => {
await provideNewVersion( {
...defaultOptions,
Expand Down