From e3e79992c6b6c82ab067946c923f2219027137ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Za=C5=84?= Date: Mon, 18 May 2026 15:36:33 +0200 Subject: [PATCH 1/3] Updated pre-release version suggestions. --- .../src/utils/determinenextversion.ts | 14 +++-- .../src/utils/providenewversion.ts | 18 +++--- .../tests/utils/determinenextversion.ts | 60 +++++++++++++++++-- .../tests/utils/providenewversion.ts | 42 ++++++++++++- 4 files changed, 114 insertions(+), 20 deletions(-) diff --git a/packages/ckeditor5-dev-changelog/src/utils/determinenextversion.ts b/packages/ckeditor5-dev-changelog/src/utils/determinenextversion.ts index 2fc76a79d..c5512462b 100644 --- a/packages/ckeditor5-dev-changelog/src/utils/determinenextversion.ts +++ b/packages/ckeditor5-dev-changelog/src/utils/determinenextversion.ts @@ -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'; @@ -60,14 +60,20 @@ 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 ) diff --git a/packages/ckeditor5-dev-changelog/src/utils/providenewversion.ts b/packages/ckeditor5-dev-changelog/src/utils/providenewversion.ts index 0f14ff479..9e4560dcf 100644 --- a/packages/ckeditor5-dev-changelog/src/utils/providenewversion.ts +++ b/packages/ckeditor5-dev-changelog/src/utils/providenewversion.ts @@ -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( { @@ -134,10 +133,11 @@ function getChoices( { } ) { const proposedVersions: Array = []; const preReleaseChannels = [ 'alpha', 'beta', 'rc' ]; + const preInitialBumpTypes: Array = [ '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' )!, @@ -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' )!, @@ -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' )! ); diff --git a/packages/ckeditor5-dev-changelog/tests/utils/determinenextversion.ts b/packages/ckeditor5-dev-changelog/tests/utils/determinenextversion.ts index 1e34efb2c..943dc82d4 100644 --- a/packages/ckeditor5-dev-changelog/tests/utils/determinenextversion.ts +++ b/packages/ckeditor5-dev-changelog/tests/utils/determinenextversion.ts @@ -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' }, @@ -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' } @@ -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 () => { diff --git a/packages/ckeditor5-dev-changelog/tests/utils/providenewversion.ts b/packages/ckeditor5-dev-changelog/tests/utils/providenewversion.ts index 5431d2ee9..6a1e5bcf3 100644 --- a/packages/ckeditor5-dev-changelog/tests/utils/providenewversion.ts +++ b/packages/ckeditor5-dev-changelog/tests/utils/providenewversion.ts @@ -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' @@ -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, From 2e52a2b52aaf25cc45834c8052dda8a384da8148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Za=C5=84?= Date: Mon, 18 May 2026 16:20:16 +0200 Subject: [PATCH 2/3] Changeset. --- .changelog/20260518161120_ci_4458.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changelog/20260518161120_ci_4458.md diff --git a/.changelog/20260518161120_ci_4458.md b/.changelog/20260518161120_ci_4458.md new file mode 100644 index 000000000..b9b184efb --- /dev/null +++ b/.changelog/20260518161120_ci_4458.md @@ -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. From 37b7a3f458d62429ba83342fc67a63cc90add144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Za=C5=84?= Date: Tue, 19 May 2026 10:38:30 +0200 Subject: [PATCH 3/3] Style adjustment. --- .../ckeditor5-dev-changelog/src/utils/determinenextversion.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ckeditor5-dev-changelog/src/utils/determinenextversion.ts b/packages/ckeditor5-dev-changelog/src/utils/determinenextversion.ts index c5512462b..20540eef4 100644 --- a/packages/ckeditor5-dev-changelog/src/utils/determinenextversion.ts +++ b/packages/ckeditor5-dev-changelog/src/utils/determinenextversion.ts @@ -69,9 +69,7 @@ export async function determineNextVersion( options: DetermineNextVersionOptions 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; + bumpType = semver.prerelease( currentVersion ) ? 'prerelease' : `pre${ bumpType }` as ReleaseType; } const areErrorsPresent = !!sections.invalid.entries.length;