@@ -60,12 +60,64 @@ async function getCurrentVersion(): Promise<string> {
6060 return packageJson . version ;
6161}
6262
63+ async function getLatestGitVersion ( ) : Promise < string | null > {
64+ try {
65+ // Fetch tags to ensure we have the latest
66+ // Use --force to overwrite local tags that conflict with remote
67+ await $ `git fetch --tags --force --quiet` ;
68+
69+ // Get all version tags
70+ const result = await $ `git tag -l "v*"` ;
71+ const tags = result . stdout . trim ( ) . split ( "\n" ) . filter ( Boolean ) ;
72+
73+ if ( tags . length === 0 ) {
74+ return null ;
75+ }
76+
77+ // Parse and find the latest version (excluding prereleases)
78+ const versions = tags
79+ . map ( tag => tag . replace ( / ^ v / , "" ) )
80+ . filter ( v => semver . valid ( v ) )
81+ . sort ( ( a , b ) => semver . rcompare ( a , b ) ) ;
82+
83+ return versions [ 0 ] || null ;
84+ } catch ( error ) {
85+ console . warn ( "Warning: Could not fetch git tags:" , error ) ;
86+ return null ;
87+ }
88+ }
89+
90+ async function shouldTagAsLatest ( newVersion : string ) : Promise < boolean > {
91+ // Check if version has prerelease identifier (e.g., 1.0.0-rc.1)
92+ const parsedVersion = semver . parse ( newVersion ) ;
93+ if ( ! parsedVersion ) {
94+ throw new Error ( `Invalid semantic version: ${ newVersion } ` ) ;
95+ }
96+
97+ // If it has a prerelease identifier, it's not latest
98+ if ( parsedVersion . prerelease . length > 0 ) {
99+ return false ;
100+ }
101+
102+ // Get the latest version from git tags
103+ const latestGitVersion = await getLatestGitVersion ( ) ;
104+
105+ // If no previous versions exist, this is the latest
106+ if ( ! latestGitVersion ) {
107+ return true ;
108+ }
109+
110+ // Check if new version is greater than the latest git version
111+ return semver . gt ( newVersion , latestGitVersion ) ;
112+ }
113+
63114async function validateReuseVersion ( version : string ) : Promise < void > {
64115 console . log ( `Validating that version ${ version } exists...` ) ;
65116
66117 // Fetch tags to ensure we have the latest
118+ // Use --force to overwrite local tags that conflict with remote
67119 console . log ( `Fetching tags...` ) ;
68- await $ ( { stdio : "inherit" } ) `git fetch --tags` ;
120+ await $ ( { stdio : "inherit" } ) `git fetch --tags --force ` ;
69121
70122 // Get short commit from version tag
71123 let shortCommit : string ;
@@ -195,6 +247,7 @@ async function getVersionFromArgs(opts: {
195247
196248// Available steps
197249const STEPS = [
250+ "confirm-release" ,
198251 "update-version" ,
199252 "generate-fern" ,
200253 "git-commit" ,
@@ -224,6 +277,7 @@ const PHASE_MAP: Record<Phase, Step[]> = {
224277 // These steps modify the source code, so they need to be ran & committed
225278 // locally. CI cannot push commits.
226279 "setup-local" : [
280+ "confirm-release" ,
227281 "update-version" ,
228282 "generate-fern" ,
229283 "git-commit" ,
@@ -331,6 +385,18 @@ async function main() {
331385 "version must be a valid semantic version" ,
332386 ) ;
333387
388+ // Automatically determine if this should be tagged as latest
389+ // Can be overridden by --latest or --no-latest flags
390+ let isLatest : boolean ;
391+ if ( opts . latest !== undefined ) {
392+ // User explicitly set the flag
393+ isLatest = opts . latest ;
394+ } else {
395+ // Auto-determine based on version
396+ isLatest = await shouldTagAsLatest ( version ) ;
397+ console . log ( `Auto-determined latest flag: ${ isLatest } (version: ${ version } )` ) ;
398+ }
399+
334400 // Setup opts
335401 let commit : string ;
336402 if ( opts . overrideCommit ) {
@@ -345,7 +411,7 @@ async function main() {
345411 const releaseOpts : ReleaseOpts = {
346412 root : ROOT_DIR ,
347413 version : version ,
348- latest : opts . latest ,
414+ latest : isLatest ,
349415 commit,
350416 reuseEngineVersion : opts . reuseEngineVersion ,
351417 } ;
@@ -361,6 +427,47 @@ async function main() {
361427 await validateGit ( releaseOpts ) ;
362428 }
363429
430+ if ( shouldRunStep ( "confirm-release" ) ) {
431+ console . log ( "==> Release Confirmation" ) ;
432+ console . log ( `\nRelease Details:` ) ;
433+ console . log ( ` Version: ${ releaseOpts . version } ` ) ;
434+ console . log ( ` Latest: ${ releaseOpts . latest } ` ) ;
435+ console . log ( ` Commit: ${ releaseOpts . commit } ` ) ;
436+ if ( releaseOpts . reuseEngineVersion ) {
437+ console . log ( ` Reusing engine version: ${ releaseOpts . reuseEngineVersion } ` ) ;
438+ }
439+
440+ // Get current branch
441+ const branchResult = await $ `git rev-parse --abbrev-ref HEAD` ;
442+ const branch = branchResult . stdout . trim ( ) ;
443+ console . log ( ` Branch: ${ branch } ` ) ;
444+
445+ // Get latest git version for context
446+ const latestGitVersion = await getLatestGitVersion ( ) ;
447+ if ( latestGitVersion ) {
448+ console . log ( ` Current latest version: ${ latestGitVersion } ` ) ;
449+ }
450+
451+ // Prompt for confirmation
452+ const readline = await import ( "node:readline" ) ;
453+ const rl = readline . createInterface ( {
454+ input : process . stdin ,
455+ output : process . stdout ,
456+ } ) ;
457+
458+ const answer = await new Promise < string > ( ( resolve ) => {
459+ rl . question ( "\nProceed with release? (yes/no): " , resolve ) ;
460+ } ) ;
461+ rl . close ( ) ;
462+
463+ if ( answer . toLowerCase ( ) !== "yes" && answer . toLowerCase ( ) !== "y" ) {
464+ console . log ( "Release cancelled" ) ;
465+ process . exit ( 0 ) ;
466+ }
467+
468+ console . log ( "✅ Release confirmed" ) ;
469+ }
470+
364471 if ( shouldRunStep ( "update-version" ) ) {
365472 console . log ( "==> Updating Version" ) ;
366473 await updateVersion ( releaseOpts ) ;
0 commit comments