11#!/usr/bin/env node
22
33import { execFileSync } from 'node:child_process' ;
4- import {
5- existsSync ,
6- mkdtempSync ,
7- readFileSync ,
8- readdirSync ,
9- rmSync ,
10- writeFileSync ,
11- } from 'node:fs' ;
12- import { tmpdir } from 'node:os' ;
4+ import { existsSync , readFileSync , readdirSync , writeFileSync } from 'node:fs' ;
135import path from 'node:path' ;
146import process from 'node:process' ;
157import { ReleaseClient , release } from 'nx/release' ;
@@ -41,16 +33,6 @@ function isReleaseBranch(ref) {
4133 return / ^ r e l e a s e \/ v \d + \. \d + (?: \. \d + ) ? $ / . test ( ref ) ;
4234}
4335
44- function parseReleaseBranchVersion ( ref ) {
45- const match = ref . match ( / ^ r e l e a s e \/ v ( \d + ) \. ( \d + ) (?: \. ( \d + ) ) ? $ / ) ;
46-
47- if ( ! match ) {
48- return null ;
49- }
50-
51- return `${ match [ 1 ] } .${ match [ 2 ] } .${ match [ 3 ] ?? '0' } ` ;
52- }
53-
5436function run ( command , args , options = { } ) {
5537 execFileSync ( command , args , {
5638 stdio : 'inherit' ,
@@ -138,81 +120,25 @@ function getVersionPlans() {
138120 . map ( ( fileName ) => path . join ( versionPlansDir , fileName ) ) ;
139121}
140122
141- function deleteVersionPlans ( versionPlans ) {
142- for ( const versionPlan of versionPlans ) {
143- rmSync ( versionPlan , { force : true } ) ;
144- }
145- }
146-
147- function stageReleaseFiles ( ) {
148- run ( 'git' , [
149- 'add' ,
150- '-A' ,
151- '.nx/version-plans' ,
152- 'packages' ,
153- 'CHANGELOG.md' ,
154- 'pnpm-lock.yaml' ,
155- ] ) ;
156-
157- const staged = runOutput ( 'git' , [ 'diff' , '--cached' , '--name-only' ] ) ;
158-
159- if ( staged . length === 0 ) {
160- fail ( 'no release files were staged for commit' ) ;
161- }
162- }
163-
164- function commitVersionChanges ( version ) {
165- stageReleaseFiles ( ) ;
166- run ( 'git' , [ 'commit' , '-m' , `chore: release v${ version } ` ] ) ;
167- }
168-
169- function createTag ( version ) {
170- const tag = `v${ version } ` ;
171-
172- if ( commandSucceeds ( 'git' , [ 'rev-parse' , '--verify' , `refs/tags/${ tag } ` ] ) ) {
173- fail ( `tag ${ tag } already exists locally` ) ;
174- }
175-
176- if (
177- commandSucceeds ( 'git' , [
178- 'ls-remote' ,
179- '--exit-code' ,
180- '--tags' ,
181- remote ,
182- `refs/tags/${ tag } ` ,
183- ] )
184- ) {
185- fail ( `tag ${ tag } already exists on ${ remote } ` ) ;
186- }
187-
188- run ( 'git' , [ 'tag' , tag ] ) ;
189- }
190-
191- function pushBranchAndTag ( version ) {
192- run ( 'git' , [ 'push' , remote , `HEAD:${ branch } ` ] ) ;
193- run ( 'git' , [ 'push' , remote , `refs/tags/v${ version } ` ] ) ;
194- }
195-
196- function createGitHubRelease ( version , notes , prerelease = false ) {
197- ensureGithubToken ( ) ;
198-
199- const tempDir = mkdtempSync ( path . join ( tmpdir ( ) , 'release-notes-' ) ) ;
200- const notesPath = path . join ( tempDir , 'notes.md' ) ;
201-
202- writeFileSync ( notesPath , notes ) ;
203-
204- const args = [ 'release' , 'create' , `v${ version } ` , '--title' , `v${ version } ` ] ;
205-
206- if ( prerelease ) {
207- args . push ( '--prerelease' ) ;
208- }
209-
210- args . push ( '--notes-file' , notesPath , '--target' , branch ) ;
211-
212- try {
213- run ( 'gh' , args ) ;
214- } finally {
215- rmSync ( tempDir , { recursive : true , force : true } ) ;
123+ function convertVersionPlanReleaseType ( releaseType ) {
124+ switch ( releaseType ) {
125+ case 'major' :
126+ case 'feat!' :
127+ case 'fix!' :
128+ return 'premajor' ;
129+ case 'minor' :
130+ case 'feat' :
131+ return 'preminor' ;
132+ case 'patch' :
133+ case 'fix' :
134+ return 'prepatch' ;
135+ case 'premajor' :
136+ case 'preminor' :
137+ case 'prepatch' :
138+ case 'prerelease' :
139+ return releaseType ;
140+ default :
141+ return releaseType ;
216142 }
217143}
218144
@@ -226,31 +152,48 @@ function assertPublishSucceeded(results) {
226152 }
227153}
228154
229- function getNextRcVersion ( targetVersion , currentVersion ) {
230- const parsedCurrent = semver . parse ( currentVersion ) ;
155+ function rewriteVersionPlanForRc ( content ) {
156+ return content . replace ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - / m, ( match , frontMatter ) => {
157+ const rewrittenFrontMatter = frontMatter . replace (
158+ / ^ ( \s * [ ^ : \n ] + : \s * ) ( \S + ) \s * $ / gm,
159+ ( _ , prefix , releaseType ) =>
160+ `${ prefix } ${ convertVersionPlanReleaseType ( releaseType ) } `
161+ ) ;
231162
232- if ( ! parsedCurrent ) {
233- fail ( `current version ${ currentVersion } is not valid semver` ) ;
234- }
163+ return `---\n ${ rewrittenFrontMatter } \n---` ;
164+ } ) ;
165+ }
235166
236- if ( parsedCurrent . version === targetVersion ) {
237- return `${ targetVersion } -rc.0` ;
238- }
167+ async function runRcReleaseWithVersionPlans ( ) {
168+ const versionPlans = getVersionPlans ( ) ;
239169
240- const prerelease = parsedCurrent . prerelease ;
241- const stableCurrent = `${ parsedCurrent . major } .${ parsedCurrent . minor } .${ parsedCurrent . patch } ` ;
170+ if ( versionPlans . length === 0 ) {
171+ fail ( 'rc releases require at least one version plan in .nx/version-plans' ) ;
172+ }
242173
243- if ( stableCurrent === targetVersion && prerelease [ 0 ] === 'rc' ) {
244- const nextVersion = semver . inc ( currentVersion , 'prerelease' , 'rc' ) ;
174+ const originalContents = new Map (
175+ versionPlans . map ( ( filePath ) => [ filePath , readFileSync ( filePath , 'utf8' ) ] )
176+ ) ;
177+ let releaseSucceeded = false ;
245178
246- if ( ! nextVersion ) {
247- fail ( `could not determine next rc version from ${ currentVersion } ` ) ;
179+ try {
180+ for ( const [ filePath , content ] of originalContents ) {
181+ writeFileSync ( filePath , rewriteVersionPlanForRc ( content ) ) ;
248182 }
249183
250- return nextVersion ;
184+ await release ( {
185+ yes : true ,
186+ skipPublish : false ,
187+ preid : 'rc' ,
188+ } ) ;
189+ releaseSucceeded = true ;
190+ } finally {
191+ if ( ! releaseSucceeded ) {
192+ for ( const [ filePath , content ] of originalContents ) {
193+ writeFileSync ( filePath , content ) ;
194+ }
195+ }
251196 }
252-
253- return `${ targetVersion } -rc.0` ;
254197}
255198
256199function getCanaryVersion ( currentVersion ) {
@@ -300,62 +243,7 @@ async function runRcRelease() {
300243 ensureGithubToken ( ) ;
301244 ensureNpmToken ( ) ;
302245
303- const versionPlans = getVersionPlans ( ) ;
304-
305- if ( versionPlans . length === 0 ) {
306- fail ( 'rc releases require at least one version plan in .nx/version-plans' ) ;
307- }
308-
309- const targetVersion = parseReleaseBranchVersion ( branch ) ;
310-
311- if ( ! targetVersion ) {
312- fail ( `could not determine target version from ${ branch } ` ) ;
313- }
314-
315- const nextVersion = getNextRcVersion ( targetVersion , readVersion ( ) ) ;
316- const releaseClient = new ReleaseClient ( { } ) ;
317- const { workspaceVersion, projectsVersionData, releaseGraph } =
318- await releaseClient . releaseVersion ( {
319- specifier : nextVersion ,
320- gitCommit : false ,
321- gitTag : false ,
322- stageChanges : true ,
323- deleteVersionPlans : false ,
324- } ) ;
325- const changelogResult = await releaseClient . releaseChangelog ( {
326- releaseGraph,
327- versionData : projectsVersionData ,
328- version : workspaceVersion ?? nextVersion ,
329- gitCommit : false ,
330- gitTag : false ,
331- gitPush : false ,
332- stageChanges : true ,
333- createRelease : false ,
334- deleteVersionPlans : false ,
335- } ) ;
336-
337- deleteVersionPlans ( versionPlans ) ;
338- commitVersionChanges ( nextVersion ) ;
339- createTag ( nextVersion ) ;
340- pushBranchAndTag ( nextVersion ) ;
341-
342- if ( changelogResult . workspaceChangelog ?. contents ) {
343- createGitHubRelease (
344- nextVersion ,
345- changelogResult . workspaceChangelog . contents ,
346- true
347- ) ;
348- }
349-
350- const publishResults = await releaseClient . releasePublish ( {
351- releaseGraph,
352- versionData : projectsVersionData ,
353- tag : 'rc' ,
354- access : 'public' ,
355- outputStyle : 'static' ,
356- } ) ;
357-
358- assertPublishSucceeded ( publishResults ) ;
246+ await runRcReleaseWithVersionPlans ( ) ;
359247}
360248
361249async function runCanaryRelease ( ) {
0 commit comments