@@ -4,7 +4,6 @@ import { promises as fs } from 'node:fs';
44import semver from 'semver' ;
55import { replaceInFile } from 'replace-in-file' ;
66
7- import { getMergedConfig } from './config.js' ;
87import { runAsync , runSync } from './run.js' ;
98import { writeJson , readJson } from './file.js' ;
109import Request from './request.js' ;
@@ -15,58 +14,25 @@ import {
1514 updateTestProcessRelease
1615} from './release/utils.js' ;
1716import CherryPick from './cherry_pick.js' ;
17+ import Session from './session.js' ;
1818
1919const isWindows = process . platform === 'win32' ;
2020
21- export default class ReleasePreparation {
21+ export default class ReleasePreparation extends Session {
2222 constructor ( argv , cli , dir ) {
23- this . cli = cli ;
24- this . dir = dir ;
23+ super ( cli , dir ) ;
2524 this . isSecurityRelease = argv . security ;
2625 this . isLTS = false ;
2726 this . isLTSTransition = argv . startLTS ;
2827 this . runBranchDiff = ! argv . skipBranchDiff ;
2928 this . ltsCodename = '' ;
3029 this . date = '' ;
31- this . config = getMergedConfig ( this . dir ) ;
3230 this . filterLabels = argv . filterLabel && argv . filterLabel . split ( ',' ) ;
31+ this . newVersion = argv . newVersion ;
32+ }
3333
34- // Ensure the preparer has set an upstream and username.
35- if ( this . warnForMissing ( ) ) {
36- cli . error ( 'Failed to begin the release preparation process.' ) ;
37- return ;
38- }
39-
40- // Allow passing optional new version.
41- if ( argv . newVersion ) {
42- const newVersion = semver . clean ( argv . newVersion ) ;
43- if ( ! semver . valid ( newVersion ) ) {
44- cli . warn ( `${ newVersion } is not a valid semantic version.` ) ;
45- return ;
46- }
47- this . newVersion = newVersion ;
48- } else {
49- this . newVersion = this . calculateNewVersion ( ) ;
50- }
51-
52- const { upstream, owner, repo, newVersion } = this ;
53-
54- this . versionComponents = {
55- major : semver . major ( newVersion ) ,
56- minor : semver . minor ( newVersion ) ,
57- patch : semver . patch ( newVersion )
58- } ;
59-
60- this . stagingBranch = `v${ this . versionComponents . major } .x-staging` ;
61- this . releaseBranch = `v${ this . versionComponents . major } .x` ;
62-
63- const upstreamHref = runSync ( 'git' , [
64- 'config' , '--get' ,
65- `remote.${ upstream } .url` ] ) . trim ( ) ;
66- if ( ! new RegExp ( `${ owner } /${ repo } (?:.git)?$` ) . test ( upstreamHref ) ) {
67- cli . warn ( 'Remote repository URL does not point to the expected ' +
68- `repository ${ owner } /${ repo } ` ) ;
69- }
34+ get branch ( ) {
35+ return this . stagingBranch ;
7036 }
7137
7238 warnForNonMergeablePR ( pr ) {
@@ -369,24 +335,29 @@ export default class ReleasePreparation {
369335 return missing ;
370336 }
371337
372- calculateNewVersion ( ) {
373- let newVersion ;
338+ async calculateNewVersion ( major ) {
339+ const { cli } = this ;
374340
375- const lastTagVersion = semver . clean ( this . getLastRef ( ) ) ;
376- const lastTag = {
377- major : semver . major ( lastTagVersion ) ,
378- minor : semver . minor ( lastTagVersion ) ,
379- patch : semver . patch ( lastTagVersion )
380- } ;
341+ cli . startSpinner ( `Parsing CHANGELOG for most recent release of v ${ major } .x` ) ;
342+ const data = await fs . readFile (
343+ path . resolve ( `doc/changelogs/CHANGELOG_V ${ major } .md` ) ,
344+ 'utf8'
345+ ) ;
346+ const [ , , minor , patch ] = / < a h r e f = " # ( \d + ) \. ( \d + ) \. ( \d + ) " > \1 \. \2 \. \3 < \/ a > < b r \/ > / . exec ( data ) ;
381347
382- const changelog = this . getChangelog ( ) ;
348+ cli . stopSpinner ( `Latest release on ${ major } .x line is ${ major } .${ minor } .${ patch } ` ) ;
349+ const changelog = this . getChangelog ( `v${ major } .${ minor } .${ patch } ` ) ;
383350
351+ const newVersion = { major, minor, patch } ;
384352 if ( changelog . includes ( 'SEMVER-MAJOR' ) ) {
385- newVersion = `${ lastTag . major + 1 } .0.0` ;
353+ newVersion . major ++ ;
354+ newVersion . minor = 0 ;
355+ newVersion . patch = 0 ;
386356 } else if ( changelog . includes ( 'SEMVER-MINOR' ) || this . isLTSTransition ) {
387- newVersion = `${ lastTag . major } .${ lastTag . minor + 1 } .0` ;
357+ newVersion . minor ++ ;
358+ newVersion . patch = 0 ;
388359 } else {
389- newVersion = ` ${ lastTag . major } . ${ lastTag . minor } . ${ lastTag . patch + 1 } ` ;
360+ newVersion . patch ++ ;
390361 }
391362
392363 return newVersion ;
@@ -396,11 +367,22 @@ export default class ReleasePreparation {
396367 return runSync ( 'git' , [ 'rev-parse' , '--abbrev-ref' , 'HEAD' ] ) . trim ( ) ;
397368 }
398369
399- getLastRef ( ) {
400- return runSync ( 'git' , [ 'describe' , '--abbrev=0' , '--tags' ] ) . trim ( ) ;
370+ getLastRef ( tagName ) {
371+ if ( ! tagName ) {
372+ return runSync ( 'git' , [ 'describe' , '--abbrev=0' , '--tags' ] ) . trim ( ) ;
373+ }
374+
375+ try {
376+ runSync ( 'git' , [ 'rev-parse' , tagName ] ) ;
377+ } catch {
378+ this . cli . startSpinner ( `Error parsing git ref ${ tagName } , attempting fetching it as a tag` ) ;
379+ runSync ( 'git' , [ 'fetch' , this . upstream , 'tag' , '-n' , tagName ] ) ;
380+ this . cli . stopSpinner ( `Tag fetched: ${ tagName } ` ) ;
381+ }
382+ return tagName ;
401383 }
402384
403- getChangelog ( ) {
385+ getChangelog ( tagName ) {
404386 const changelogMaker = new URL (
405387 '../node_modules/.bin/changelog-maker' + ( isWindows ? '.cmd' : '' ) ,
406388 import . meta. url
@@ -411,7 +393,7 @@ export default class ReleasePreparation {
411393 '--markdown' ,
412394 '--filter-release' ,
413395 '--start-ref' ,
414- this . getLastRef ( )
396+ this . getLastRef ( tagName )
415397 ] ) . trim ( ) ;
416398 }
417399
@@ -736,6 +718,45 @@ export default class ReleasePreparation {
736718 return runSync ( branchDiff , branchDiffOptions ) ;
737719 }
738720
721+ async prepareLocalBranch ( ) {
722+ const { cli } = this ;
723+ if ( this . newVersion ) {
724+ // If the CLI asked for a specific version:
725+ const newVersion = semver . parse ( this . newVersion ) ;
726+ if ( ! newVersion ) {
727+ cli . warn ( `${ this . newVersion } is not a valid semantic version.` ) ;
728+ return ;
729+ }
730+ this . newVersion = newVersion . version ;
731+ this . versionComponents = {
732+ major : newVersion . major ,
733+ minor : newVersion . minor ,
734+ patch : newVersion . patch
735+ } ;
736+ this . stagingBranch = `v${ newVersion . major } .x-staging` ;
737+ this . releaseBranch = `v${ newVersion . major } .x` ;
738+ await this . tryResetBranch ( ) ;
739+ return ;
740+ }
741+
742+ // Otherwise, we need to figure out what's the next version number for the
743+ // release line of the branch that's currently checked out.
744+ const currentBranch = this . getCurrentBranch ( ) ;
745+ const match = / ^ v ( \d + ) \. x - s t a g i n g $ / . exec ( currentBranch ) ;
746+
747+ if ( ! match ) {
748+ cli . warn ( `Cannot prepare a release from ${ currentBranch
749+ } . Switch to a staging branch before proceeding.`) ;
750+ return ;
751+ }
752+ this . stagingBranch = currentBranch ;
753+ await this . tryResetBranch ( ) ;
754+ this . versionComponents = await this . calculateNewVersion ( match [ 1 ] ) ;
755+ const { major, minor, patch } = this . versionComponents ;
756+ this . newVersion = `${ major } .${ minor } .${ patch } ` ;
757+ this . releaseBranch = `v${ major } .x` ;
758+ }
759+
739760 warnForWrongBranch ( ) {
740761 const {
741762 cli,
0 commit comments