1
- import * as OctokitApi from '@octokit/rest' ;
2
1
import { bold , green , italic , red , yellow } from 'chalk' ;
3
- import { existsSync , readFileSync } from 'fs ' ;
4
- import { prompt } from 'inquirer ' ;
2
+ import { execSync } from 'child_process ' ;
3
+ import { readFileSync } from 'fs ' ;
5
4
import { join } from 'path' ;
6
- import { GitReleaseTask } from './git -release-task' ;
5
+ import { BaseReleaseTask } from './base -release-task' ;
7
6
import { GitClient } from './git/git-client' ;
7
+ import { getGithubReleasesUrl } from './git/github-urls' ;
8
+ import { isNpmAuthenticated , runInteractiveNpmLogin , runNpmPublish } from './npm/npm-client' ;
8
9
import { promptForNpmDistTag } from './prompt/npm-dist-tag-prompt' ;
9
10
import { checkReleasePackage } from './release-output/check-packages' ;
10
11
import { releasePackages } from './release-output/release-packages' ;
11
12
import { parseVersionName , Version } from './version-name/parse-version' ;
12
- import { execSync } from 'child_process' ;
13
+
14
+ /** Maximum allowed tries to authenticate NPM. */
15
+ const MAX_NPM_LOGIN_TRIES = 2 ;
13
16
14
17
/**
15
18
* Class that can be instantiated in order to create a new release. The tasks requires user
16
19
* interaction/input through command line prompts.
17
20
*/
18
- class PublishReleaseTask extends GitReleaseTask {
21
+ class PublishReleaseTask extends BaseReleaseTask {
19
22
20
23
/** Path to the project package JSON. */
21
24
packageJsonPath : string ;
@@ -26,27 +29,20 @@ class PublishReleaseTask extends GitReleaseTask {
26
29
/** Parsed current version of the project. */
27
30
currentVersion : Version ;
28
31
32
+ /** Path to the release output of the project. */
33
+ releaseOutputPath : string ;
34
+
29
35
/** Instance of a wrapper that can execute Git commands. */
30
36
git : GitClient ;
31
37
32
- /** Octokit API instance that can be used to make Github API calls. */
33
- githubApi : OctokitApi ;
34
-
35
38
constructor ( public projectDir : string ,
36
39
public repositoryOwner : string ,
37
40
public repositoryName : string ) {
38
41
super ( new GitClient ( projectDir ,
39
42
`https://github.com/${ repositoryOwner } /${ repositoryName } .git` ) ) ;
40
43
41
44
this . packageJsonPath = join ( projectDir , 'package.json' ) ;
42
-
43
- console . log ( this . projectDir ) ;
44
-
45
- if ( ! existsSync ( this . packageJsonPath ) ) {
46
- console . error ( red ( `The specified directory is not referring to a project directory. ` +
47
- `There must be a ${ italic ( 'package.json' ) } file in the project directory.` ) ) ;
48
- process . exit ( 1 ) ;
49
- }
45
+ this . releaseOutputPath = join ( projectDir , 'dist/releases' ) ;
50
46
51
47
this . packageJson = JSON . parse ( readFileSync ( this . packageJsonPath , 'utf-8' ) ) ;
52
48
this . currentVersion = parseVersionName ( this . packageJson . version ) ;
@@ -56,8 +52,6 @@ class PublishReleaseTask extends GitReleaseTask {
56
52
`make sure "${ this . packageJson . version } " is a valid Semver version.` ) ) ;
57
53
process . exit ( 1 ) ;
58
54
}
59
-
60
- this . githubApi = new OctokitApi ( ) ;
61
55
}
62
56
63
57
async run ( ) {
@@ -67,26 +61,54 @@ class PublishReleaseTask extends GitReleaseTask {
67
61
console . log ( green ( '-----------------------------------------' ) ) ;
68
62
console . log ( ) ;
69
63
64
+ const newVersion = this . currentVersion ;
65
+ const newVersionName = this . currentVersion . format ( ) ;
66
+
70
67
// Ensure there are no uncommitted changes. Checking this before switching to a
71
68
// publish branch is sufficient as unstaged changes are not specific to Git branches.
72
69
this . verifyNoUncommittedChanges ( ) ;
73
70
74
71
// Branch that will be used to build the output for the release of the current version.
75
- const publishBranch = this . switchToPublishBranch ( this . currentVersion ) ;
72
+ const publishBranch = this . switchToPublishBranch ( newVersion ) ;
76
73
77
74
this . verifyLastCommitVersionBump ( ) ;
78
75
this . verifyLocalCommitsMatchUpstream ( publishBranch ) ;
79
76
80
- const npmDistTag = await promptForNpmDistTag ( this . currentVersion ) ;
77
+ const npmDistTag = await promptForNpmDistTag ( newVersion ) ;
81
78
82
79
// In case the user wants to publish a stable version to the "next" npm tag, we want
83
80
// to double-check because usually only pre-release's are pushed to that tag.
84
- if ( npmDistTag === 'next' && ! this . currentVersion . prereleaseLabel ) {
81
+ if ( npmDistTag === 'next' && ! newVersion . prereleaseLabel ) {
85
82
await this . promptStableVersionForNextTag ( ) ;
86
83
}
87
84
88
85
this . buildReleasePackages ( ) ;
86
+ console . info ( green ( ` ✓ Built the release output.` ) ) ;
87
+
89
88
this . checkReleaseOutput ( ) ;
89
+ console . info ( green ( ` ✓ Release output passed validation checks.` ) ) ;
90
+
91
+ // TODO(devversion): find a way to extract the changelog part just for this version.
92
+ this . git . createTag ( 'HEAD' , newVersionName , '' ) ;
93
+ console . info ( green ( ` ✓ Created release tag: "${ italic ( newVersionName ) } "` ) ) ;
94
+
95
+ // Ensure that we are authenticated before running "npm publish" for each package.
96
+ this . checkNpmAuthentication ( ) ;
97
+
98
+ // Just in order to double-check that the user is sure to publish to NPM, we want
99
+ // the user to interactively confirm that the script should continue.
100
+ await this . promptConfirmReleasePublish ( ) ;
101
+
102
+ for ( let packageName of releasePackages ) {
103
+ this . publishPackageToNpm ( packageName , npmDistTag ) ;
104
+ }
105
+
106
+ console . log ( ) ;
107
+ console . info ( green ( bold ( ` ✓ Published all packages successfully` ) ) ) ;
108
+ console . info ( yellow ( ` ⚠ Please push the newly created tag to Github and draft a new ` +
109
+ `release.` ) ) ;
110
+ console . info ( yellow (
111
+ ` ${ getGithubReleasesUrl ( this . repositoryOwner , this . repositoryName ) } ` ) ) ;
90
112
}
91
113
92
114
/**
@@ -116,11 +138,10 @@ class PublishReleaseTask extends GitReleaseTask {
116
138
117
139
/** Checks the release output by running the release-output validations. */
118
140
private checkReleaseOutput ( ) {
119
- const releasesPath = join ( this . projectDir , 'dist/releases' ) ;
120
141
let hasFailed = false ;
121
142
122
143
releasePackages . forEach ( packageName => {
123
- if ( ! checkReleasePackage ( releasesPath , packageName ) ) {
144
+ if ( ! checkReleasePackage ( this . releaseOutputPath , packageName ) ) {
124
145
hasFailed = true ;
125
146
}
126
147
} ) ;
@@ -135,22 +156,80 @@ class PublishReleaseTask extends GitReleaseTask {
135
156
}
136
157
137
158
/**
138
- * Prompts the user whether he is sure that the current stable version should be
159
+ * Prompts the user whether they are sure that the current stable version should be
139
160
* released to the "next" NPM dist-tag.
140
161
*/
141
162
private async promptStableVersionForNextTag ( ) {
142
- const { shouldContinue} = await prompt < { shouldContinue : boolean } > ( {
143
- type : 'confirm' ,
144
- name : 'shouldContinue' ,
145
- message : 'Are you sure that you want to release a stable version to the "next" tag?'
146
- } ) ;
163
+ if ( ! await this . promptConfirm (
164
+ 'Are you sure that you want to release a stable version to the "next" tag?' ) ) {
165
+ console . log ( ) ;
166
+ console . log ( yellow ( 'Aborting publish...' ) ) ;
167
+ process . exit ( 0 ) ;
168
+ }
169
+ }
147
170
148
- if ( ! shouldContinue ) {
171
+
172
+ /**
173
+ * Prompts the user whether he is sure that the script should continue publishing
174
+ * the release to NPM.
175
+ */
176
+ private async promptConfirmReleasePublish ( ) {
177
+ if ( ! await this . promptConfirm ( 'Are you sure that you want to release now?' ) ) {
149
178
console . log ( ) ;
150
179
console . log ( yellow ( 'Aborting publish...' ) ) ;
151
180
process . exit ( 0 ) ;
152
181
}
153
182
}
183
+
184
+ /**
185
+ * Checks whether NPM is currently authenticated. If not, the user will be prompted to enter
186
+ * the NPM credentials that are necessary to publish the release. We achieve this by basically
187
+ * running "npm login" as a child process and piping stdin/stdout/stderr to the current tty.
188
+ */
189
+ private checkNpmAuthentication ( ) {
190
+ if ( isNpmAuthenticated ( ) ) {
191
+ console . info ( green ( ` ✓ NPM is authenticated.` ) ) ;
192
+ return ;
193
+ }
194
+
195
+ let failedAuthentication = false ;
196
+ console . log ( yellow ( ` ⚠ NPM is currently not authenticated. Running "npm login"..` ) ) ;
197
+
198
+ for ( let i = 0 ; i < MAX_NPM_LOGIN_TRIES ; i ++ ) {
199
+ if ( runInteractiveNpmLogin ( ) ) {
200
+ // In case the user was able to login properly, we want to exit the loop as we
201
+ // don't need to ask for authentication again.
202
+ break ;
203
+ }
204
+
205
+ failedAuthentication = true ;
206
+ console . error ( red ( ` ✘ Could not authenticate successfully. Please try again.` ) ) ;
207
+ }
208
+
209
+ if ( failedAuthentication ) {
210
+ console . error ( red ( ` ✘ Could not authenticate after ${ MAX_NPM_LOGIN_TRIES } tries. ` +
211
+ `Exiting..` ) ) ;
212
+ process . exit ( 1 ) ;
213
+ }
214
+
215
+ console . info ( green ( ` ✓ Successfully authenticated NPM.` ) ) ;
216
+ }
217
+
218
+ /** Publishes the specified package within the given NPM dist tag. */
219
+ private publishPackageToNpm ( packageName : string , npmDistTag : string ) {
220
+ console . info ( green ( ` ⭮ Publishing "${ packageName } "..` ) ) ;
221
+
222
+ const errorOutput = runNpmPublish ( join ( this . releaseOutputPath , packageName ) , npmDistTag ) ;
223
+
224
+ if ( errorOutput ) {
225
+ console . error ( red ( ` ✘ An error occurred while publishing "${ packageName } ".` ) ) ;
226
+ console . error ( red ( ` Please check the terminal output and reach out to the team.` ) ) ;
227
+ console . error ( red ( `\n${ errorOutput } ` ) ) ;
228
+ process . exit ( 1 ) ;
229
+ }
230
+
231
+ console . info ( green ( ` ✓ Successfully published "${ packageName } "` ) ) ;
232
+ }
154
233
}
155
234
156
235
/** Entry-point for the create release script. */
0 commit comments