@@ -12,6 +12,7 @@ const runLifecycleScript = require('../run-lifecycle-script');
1212const semver = require ( 'semver' ) ;
1313const writeFile = require ( '../write-file' ) ;
1414const { resolveUpdaterObjectFromArgument } = require ( '../updaters' ) ;
15+ const gitSemverTags = require ( 'git-semver-tags' ) ;
1516let configsToUpdate = { } ;
1617const sanitizeQuotesRegex = / [ ' " ] + / g;
1718
@@ -83,6 +84,15 @@ async function Bump(args, version) {
8384
8485 newVersion = semver . inc ( version , releaseType , args . prerelease ) ;
8586 }
87+
88+ // If creating a prerelease, ensure the computed version is unique among existing git tags
89+ if ( isString ( args . prerelease ) && newVersion ) {
90+ newVersion = await resolveUniquePrereleaseVersion (
91+ newVersion ,
92+ args . tagPrefix ,
93+ args . prerelease ,
94+ ) ;
95+ }
8696 updateConfigs ( args , newVersion ) ;
8797 } else {
8898 checkpoint (
@@ -261,3 +271,78 @@ function updateConfigs(args, newVersion) {
261271}
262272
263273module . exports = Bump ;
274+
275+ /**
276+ * Ensure prerelease version uniqueness by checking existing git tags.
277+ * If a tag for the same base version and prerelease identifier exists, bump the numeric suffix.
278+ * @param {string } proposedVersion The version computed by bump logic, may include build metadata.
279+ * @param {string } tagPrefix The tag prefix to respect when reading tags (e.g., 'v').
280+ * @param {string } prereleaseId The prerelease identifier (e.g., 'alpha', 'beta', 'rc').
281+ * @returns {Promise<string> } The adjusted version that does not collide with existing tags.
282+ */
283+ async function resolveUniquePrereleaseVersion (
284+ proposedVersion ,
285+ tagPrefix ,
286+ prereleaseId ,
287+ ) {
288+ try {
289+ const parsed = new semver . SemVer ( proposedVersion ) ;
290+ const base = `${ parsed . major } .${ parsed . minor } .${ parsed . patch } ` ;
291+ const build = parsed . build ; // preserve build metadata if present
292+
293+ // Current numeric index if present, otherwise default to 0
294+ const currentNum =
295+ typeof parsed . prerelease [ 1 ] === 'number' ? parsed . prerelease [ 1 ] : 0 ;
296+
297+ const tags = await new Promise ( ( resolve , reject ) => {
298+ gitSemverTags ( { tagPrefix } , ( err , t ) =>
299+ err ? reject ( err ) : resolve ( t || [ ] ) ,
300+ ) ;
301+ } ) ;
302+
303+ // strip prefix and clean
304+ const cleaned = tags
305+ . map ( ( t ) => t . replace ( new RegExp ( '^' + tagPrefix ) , '' ) )
306+ . map ( ( t ) => ( semver . valid ( t ) ? semver . clean ( t ) : null ) )
307+ . filter ( Boolean ) ;
308+
309+ // collect numeric suffix for same base and prerelease id
310+ const nums = cleaned
311+ . filter ( ( t ) => {
312+ const v = new semver . SemVer ( t ) ;
313+ if ( ! Array . isArray ( v . prerelease ) || v . prerelease . length === 0 )
314+ return false ;
315+ // same base version and same prerelease id
316+ return (
317+ v . major === parsed . major &&
318+ v . minor === parsed . minor &&
319+ v . patch === parsed . patch &&
320+ String ( v . prerelease [ 0 ] ) === String ( prereleaseId )
321+ ) ;
322+ } )
323+ . map ( ( t ) => {
324+ const v = new semver . SemVer ( t ) ;
325+ return typeof v . prerelease [ 1 ] === 'number' ? v . prerelease [ 1 ] : 0 ;
326+ } ) ;
327+
328+ if ( nums . length === 0 ) {
329+ // no collisions possible
330+ return proposedVersion ;
331+ }
332+
333+ const maxExisting = Math . max ( ...nums ) ;
334+ // If our proposed numeric index is already used or below max, bump to max + 1
335+ if ( currentNum <= maxExisting ) {
336+ let candidate = `${ base } -${ prereleaseId } .${ maxExisting + 1 } ` ;
337+ // re-append build metadata if any
338+ if ( build && build . length ) {
339+ candidate = semvarToVersionStr ( candidate , build ) ;
340+ }
341+ return candidate ;
342+ }
343+ return proposedVersion ;
344+ } catch {
345+ // If anything goes wrong, fall back to proposedVersion
346+ return proposedVersion ;
347+ }
348+ }
0 commit comments