@@ -4,7 +4,6 @@ import { promises as fs } from 'node:fs';
4
4
import semver from 'semver' ;
5
5
import { replaceInFile } from 'replace-in-file' ;
6
6
7
- import { getMergedConfig } from './config.js' ;
8
7
import { runAsync , runSync } from './run.js' ;
9
8
import { writeJson , readJson } from './file.js' ;
10
9
import Request from './request.js' ;
@@ -15,58 +14,25 @@ import {
15
14
updateTestProcessRelease
16
15
} from './release/utils.js' ;
17
16
import CherryPick from './cherry_pick.js' ;
17
+ import Session from './session.js' ;
18
18
19
19
const isWindows = process . platform === 'win32' ;
20
20
21
- export default class ReleasePreparation {
21
+ export default class ReleasePreparation extends Session {
22
22
constructor ( argv , cli , dir ) {
23
- this . cli = cli ;
24
- this . dir = dir ;
23
+ super ( cli , dir ) ;
25
24
this . isSecurityRelease = argv . security ;
26
25
this . isLTS = false ;
27
26
this . isLTSTransition = argv . startLTS ;
28
27
this . runBranchDiff = ! argv . skipBranchDiff ;
29
28
this . ltsCodename = '' ;
30
29
this . date = '' ;
31
- this . config = getMergedConfig ( this . dir ) ;
32
30
this . filterLabels = argv . filterLabel && argv . filterLabel . split ( ',' ) ;
31
+ this . newVersion = argv . newVersion ;
32
+ }
33
33
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 ;
70
36
}
71
37
72
38
warnForNonMergeablePR ( pr ) {
@@ -369,24 +335,29 @@ export default class ReleasePreparation {
369
335
return missing ;
370
336
}
371
337
372
- calculateNewVersion ( ) {
373
- let newVersion ;
338
+ async calculateNewVersion ( major ) {
339
+ const { cli } = this ;
374
340
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 ) ;
381
347
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 } ` ) ;
383
350
351
+ const newVersion = { major, minor, patch } ;
384
352
if ( changelog . includes ( 'SEMVER-MAJOR' ) ) {
385
- newVersion = `${ lastTag . major + 1 } .0.0` ;
353
+ newVersion . major ++ ;
354
+ newVersion . minor = 0 ;
355
+ newVersion . patch = 0 ;
386
356
} else if ( changelog . includes ( 'SEMVER-MINOR' ) || this . isLTSTransition ) {
387
- newVersion = `${ lastTag . major } .${ lastTag . minor + 1 } .0` ;
357
+ newVersion . minor ++ ;
358
+ newVersion . patch = 0 ;
388
359
} else {
389
- newVersion = ` ${ lastTag . major } . ${ lastTag . minor } . ${ lastTag . patch + 1 } ` ;
360
+ newVersion . patch ++ ;
390
361
}
391
362
392
363
return newVersion ;
@@ -396,11 +367,22 @@ export default class ReleasePreparation {
396
367
return runSync ( 'git' , [ 'rev-parse' , '--abbrev-ref' , 'HEAD' ] ) . trim ( ) ;
397
368
}
398
369
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 ;
401
383
}
402
384
403
- getChangelog ( ) {
385
+ getChangelog ( tagName ) {
404
386
const changelogMaker = new URL (
405
387
'../node_modules/.bin/changelog-maker' + ( isWindows ? '.cmd' : '' ) ,
406
388
import . meta. url
@@ -411,7 +393,7 @@ export default class ReleasePreparation {
411
393
'--markdown' ,
412
394
'--filter-release' ,
413
395
'--start-ref' ,
414
- this . getLastRef ( )
396
+ this . getLastRef ( tagName )
415
397
] ) . trim ( ) ;
416
398
}
417
399
@@ -736,6 +718,45 @@ export default class ReleasePreparation {
736
718
return runSync ( branchDiff , branchDiffOptions ) ;
737
719
}
738
720
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
+
739
760
warnForWrongBranch ( ) {
740
761
const {
741
762
cli,
0 commit comments