release-it 20.0.1 → 20.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/release-it.json +2 -0
- package/lib/args.js +1 -0
- package/lib/cli.js +1 -0
- package/lib/config.js +4 -0
- package/lib/index.js +3 -2
- package/lib/log.js +3 -1
- package/lib/plugin/github/GitHub.js +54 -12
- package/lib/plugin/gitlab/GitLab.js +1 -0
- package/lib/plugin/npm/npm.js +22 -9
- package/package.json +1 -1
- package/schema/npm.json +4 -0
- package/schema/release-it.json +5 -0
- package/test/github.js +78 -3
- package/test/log.js +18 -0
- package/test/npm.js +54 -0
- package/test/stub/github.js +23 -2
- package/test/tasks.js +49 -1
- package/types/config.d.ts +6 -0
package/config/release-it.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"quiet": false,
|
|
2
3
|
"hooks": {},
|
|
3
4
|
"git": {
|
|
4
5
|
"changelog": "git log --pretty=format:\"* %s (%h)\" ${from}...${to}",
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
"publishArgs": [],
|
|
30
31
|
"publishPackageManager": "npm",
|
|
31
32
|
"tag": null,
|
|
33
|
+
"stage": false,
|
|
32
34
|
"otp": null,
|
|
33
35
|
"ignoreVersion": false,
|
|
34
36
|
"allowSameVersion": false,
|
package/lib/args.js
CHANGED
package/lib/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const helpText = `Release It! v${pkg.version}
|
|
|
18
18
|
-v --version Print release-it version number
|
|
19
19
|
--release-version Print version number to be released
|
|
20
20
|
--changelog Print changelog for the version to be released
|
|
21
|
+
--quiet Suppress preview output during release
|
|
21
22
|
-V --verbose Verbose output (user hooks output)
|
|
22
23
|
-VV Extra verbose output (also internal commands output)
|
|
23
24
|
|
package/lib/config.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -15,9 +15,9 @@ const runTasks = async (opts, di) => {
|
|
|
15
15
|
await container.config.init();
|
|
16
16
|
|
|
17
17
|
const { config } = container;
|
|
18
|
-
const { isCI, isVerbose, verbosityLevel, isDryRun, isChangelog, isReleaseVersion } = config;
|
|
18
|
+
const { isCI, isVerbose, verbosityLevel, isDryRun, isChangelog, isReleaseVersion, isQuiet } = config;
|
|
19
19
|
|
|
20
|
-
container.log = container.log || new Logger({ isCI, isVerbose, verbosityLevel, isDryRun });
|
|
20
|
+
container.log = container.log || new Logger({ isCI, isVerbose, verbosityLevel, isDryRun, isQuiet });
|
|
21
21
|
container.spinner = container.spinner || new Spinner({ container, config });
|
|
22
22
|
container.prompt = container.prompt || new Prompt({ container: { config } });
|
|
23
23
|
container.shell = container.shell || new Shell({ container });
|
|
@@ -94,6 +94,7 @@ const runTasks = async (opts, di) => {
|
|
|
94
94
|
const action = config.isIncrement ? 'release' : 'update';
|
|
95
95
|
const suffix = version && config.isIncrement ? `${latestVersion}...${version}` : `currently at ${latestVersion}`;
|
|
96
96
|
log.obtrusive(`🚀 Let's ${action} ${name} (${suffix})`);
|
|
97
|
+
if (isQuiet) log.info('Preview output hidden (--quiet).');
|
|
97
98
|
log.preview({ title: 'changelog', text: changelog });
|
|
98
99
|
}
|
|
99
100
|
|
package/lib/log.js
CHANGED
|
@@ -4,11 +4,12 @@ import { isObjectLoose } from '@phun-ky/typeof';
|
|
|
4
4
|
import { upperFirst } from './util.js';
|
|
5
5
|
|
|
6
6
|
class Logger {
|
|
7
|
-
constructor({ isCI = true, isVerbose = false, verbosityLevel = 0, isDryRun = false } = {}) {
|
|
7
|
+
constructor({ isCI = true, isVerbose = false, verbosityLevel = 0, isDryRun = false, isQuiet = false } = {}) {
|
|
8
8
|
this.isCI = isCI;
|
|
9
9
|
this.isVerbose = isVerbose;
|
|
10
10
|
this.verbosityLevel = verbosityLevel;
|
|
11
11
|
this.isDryRun = isDryRun;
|
|
12
|
+
this.isQuiet = isQuiet;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
shouldLog(isExternal) {
|
|
@@ -57,6 +58,7 @@ class Logger {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
preview({ title, text }) {
|
|
61
|
+
if (this.isQuiet) return;
|
|
60
62
|
if (text) {
|
|
61
63
|
const header = styleText('bold', upperFirst(title));
|
|
62
64
|
const body = text.replace(new RegExp(EOL + EOL, 'g'), EOL);
|
|
@@ -129,27 +129,41 @@ class GitHub extends Release {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
async release() {
|
|
132
|
-
const { assets } = this.options;
|
|
132
|
+
const { assets, draft } = this.options;
|
|
133
133
|
const { isWeb, isUpdate } = this.getContext();
|
|
134
134
|
const { isCI } = this.config;
|
|
135
135
|
|
|
136
136
|
const type = isUpdate ? 'update' : 'create';
|
|
137
137
|
const publishMethod = `${type}Release`;
|
|
138
138
|
|
|
139
|
+
const useDraftFlow = !isUpdate && assets;
|
|
140
|
+
|
|
139
141
|
if (isWeb) {
|
|
140
142
|
const task = () => this.createWebRelease();
|
|
141
143
|
return this.step({ task, label: 'Generating link to GitHub Release web interface', prompt: 'release' });
|
|
142
144
|
} else if (isCI) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
if (useDraftFlow) {
|
|
146
|
+
await this.step({ task: () => this.createRelease({ draft: true }), label: 'GitHub create release' });
|
|
147
|
+
await this.step({ task: () => this.uploadAssets(), label: 'GitHub upload assets' });
|
|
148
|
+
if (!draft) await this.step({ task: () => this.publishRelease(), label: 'GitHub publish release' });
|
|
149
|
+
} else {
|
|
150
|
+
await this.step({ task: () => this[publishMethod](), label: `GitHub ${type} release` });
|
|
151
|
+
await this.step({ enabled: assets, task: () => this.uploadAssets(), label: 'GitHub upload assets' });
|
|
152
|
+
}
|
|
145
153
|
return this.step({
|
|
146
154
|
task: () => (isUpdate ? Promise.resolve() : this.commentOnResolvedItems()),
|
|
147
155
|
label: 'GitHub comment on resolved items'
|
|
148
156
|
});
|
|
149
157
|
} else {
|
|
150
158
|
const release = async () => {
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
if (useDraftFlow) {
|
|
160
|
+
await this.createRelease({ draft: true });
|
|
161
|
+
await this.uploadAssets();
|
|
162
|
+
if (!draft) await this.publishRelease();
|
|
163
|
+
} else {
|
|
164
|
+
await this[publishMethod]();
|
|
165
|
+
await this.uploadAssets();
|
|
166
|
+
}
|
|
153
167
|
return isUpdate ? Promise.resolve(true) : this.commentOnResolvedItems();
|
|
154
168
|
};
|
|
155
169
|
return this.step({ task: release, label: `GitHub ${type} release`, prompt: 'release' });
|
|
@@ -218,16 +232,14 @@ class GitHub extends Release {
|
|
|
218
232
|
discussionCategoryName = undefined
|
|
219
233
|
} = this.options;
|
|
220
234
|
const { tagName } = this.config.getContext();
|
|
221
|
-
const { version, releaseNotes
|
|
235
|
+
const { version, releaseNotes } = this.getContext();
|
|
222
236
|
const { isPreRelease } = parseVersion(version);
|
|
223
237
|
const name = format(releaseName, this.config.getContext());
|
|
224
238
|
const releaseNotesObject = this.options.releaseNotes;
|
|
225
239
|
|
|
226
|
-
const
|
|
227
|
-
?
|
|
228
|
-
|
|
229
|
-
: ''
|
|
230
|
-
: truncateBody(releaseNotesObject?.commit ? await this.renderReleaseNotes(releaseNotesObject) : releaseNotes);
|
|
240
|
+
const resolvedReleaseNotes =
|
|
241
|
+
(releaseNotesObject?.commit ? await this.renderReleaseNotes(releaseNotesObject) : releaseNotes) ?? '';
|
|
242
|
+
const body = !autoGenerate ? truncateBody(resolvedReleaseNotes) : '';
|
|
231
243
|
|
|
232
244
|
/**
|
|
233
245
|
* @type {CreateReleaseOptions}
|
|
@@ -255,10 +267,12 @@ class GitHub extends Release {
|
|
|
255
267
|
});
|
|
256
268
|
}
|
|
257
269
|
|
|
258
|
-
async createRelease() {
|
|
270
|
+
async createRelease({ draft } = {}) {
|
|
259
271
|
const options = await this.getOctokitReleaseOptions();
|
|
260
272
|
const { isDryRun } = this.config;
|
|
261
273
|
|
|
274
|
+
if (draft === true) options.draft = true;
|
|
275
|
+
|
|
262
276
|
this.log.exec(`octokit repos.createRelease "${options.name}" (${options.tag_name})`, { isDryRun });
|
|
263
277
|
|
|
264
278
|
if (isDryRun) {
|
|
@@ -405,6 +419,34 @@ class GitHub extends Release {
|
|
|
405
419
|
});
|
|
406
420
|
}
|
|
407
421
|
|
|
422
|
+
async publishRelease() {
|
|
423
|
+
const { isDryRun } = this.config;
|
|
424
|
+
const { owner, project: repo } = this.getContext('repo');
|
|
425
|
+
const release_id = this.getContext('releaseId');
|
|
426
|
+
const { tagName } = this.config.getContext();
|
|
427
|
+
|
|
428
|
+
const options = { owner, repo, release_id, draft: false };
|
|
429
|
+
|
|
430
|
+
this.log.exec(`octokit repos.updateRelease (publish ${tagName})`, { isDryRun });
|
|
431
|
+
|
|
432
|
+
if (isDryRun) return true;
|
|
433
|
+
|
|
434
|
+
return this.retry(async bail => {
|
|
435
|
+
try {
|
|
436
|
+
this.debug(options);
|
|
437
|
+
const response = await this.client.repos.updateRelease(options);
|
|
438
|
+
const { html_url, discussion_url } = response.data;
|
|
439
|
+
this.setContext({ releaseUrl: html_url, discussionUrl: discussion_url });
|
|
440
|
+
this.config.setContext({ releaseUrl: html_url, discussionUrl: discussion_url });
|
|
441
|
+
this.debug(response.data);
|
|
442
|
+
this.log.verbose(`octokit repos.updateRelease: done (${response.headers.location})`);
|
|
443
|
+
return true;
|
|
444
|
+
} catch (err) {
|
|
445
|
+
return this.handleError(err, bail);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
408
450
|
async commentOnResolvedItems() {
|
|
409
451
|
const { isDryRun } = this.config;
|
|
410
452
|
const { host, owner, project: repo } = this.getContext('repo');
|
|
@@ -171,6 +171,7 @@ class GitLab extends Release {
|
|
|
171
171
|
const url = `${baseUrl}/${endpoint}${options.searchParams ? `?${new URLSearchParams(options.searchParams)}` : ''}`;
|
|
172
172
|
const headers = {
|
|
173
173
|
'user-agent': 'webpro/release-it',
|
|
174
|
+
'Accept-Encoding': 'identity',
|
|
174
175
|
[tokenHeader]: this.token
|
|
175
176
|
};
|
|
176
177
|
// When using fetch() with FormData bodies, we should not set the Content-Type header.
|
package/lib/plugin/npm/npm.js
CHANGED
|
@@ -98,10 +98,13 @@ class npm extends Plugin {
|
|
|
98
98
|
release() {
|
|
99
99
|
if (this.options.publish === false) return false;
|
|
100
100
|
if (this.getContext('private')) return false;
|
|
101
|
+
const { stage } = this.options;
|
|
101
102
|
const publish = () => this.publish({ otpCallback });
|
|
102
103
|
const otpCallback =
|
|
103
|
-
this.config.isCI && !this.config.isPromptOnlyVersion
|
|
104
|
-
|
|
104
|
+
stage || (this.config.isCI && !this.config.isPromptOnlyVersion)
|
|
105
|
+
? null
|
|
106
|
+
: task => this.step({ prompt: 'otp', task });
|
|
107
|
+
return this.step({ task: publish, label: stage ? 'npm stage publish' : 'npm publish', prompt: 'publish' });
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
isRegistryUp() {
|
|
@@ -254,9 +257,9 @@ class npm extends Plugin {
|
|
|
254
257
|
|
|
255
258
|
async publish({ otp = this.options.otp, otpCallback } = {}) {
|
|
256
259
|
const publishPackageManager = this.options.publishPackageManager || 'npm';
|
|
257
|
-
const { publishPath = '.', publishArgs } = this.options;
|
|
260
|
+
const { publishPath = '.', publishArgs, stage } = this.options;
|
|
258
261
|
const { private: isPrivate, tag = DEFAULT_TAG } = this.getContext();
|
|
259
|
-
const otpArgs = otp ? ['--otp', otp] : [];
|
|
262
|
+
const otpArgs = otp && !stage ? ['--otp', otp] : [];
|
|
260
263
|
const dryRunArg = this.config.isDryRun ? '--dry-run' : '';
|
|
261
264
|
const registry = this.getRegistry();
|
|
262
265
|
const registryArg = registry ? `--registry ${registry}` : '';
|
|
@@ -274,13 +277,18 @@ class npm extends Plugin {
|
|
|
274
277
|
registryArg,
|
|
275
278
|
...fixArgs(publishArgs)
|
|
276
279
|
].filter(Boolean);
|
|
277
|
-
const
|
|
278
|
-
|
|
280
|
+
const publishCommand = stage ? ['stage', 'publish'] : ['publish'];
|
|
281
|
+
const isInteractive = !this.config.isCI || Boolean(this.config.isPromptOnlyVersion);
|
|
282
|
+
return this.exec([publishPackageManager, ...publishCommand, ...args], {
|
|
279
283
|
options: { ...getOptions(), interactive: isInteractive }
|
|
280
284
|
})
|
|
281
285
|
.then(() => {
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
if (stage) {
|
|
287
|
+
this.setContext({ isStaged: true });
|
|
288
|
+
} else {
|
|
289
|
+
this.setContext({ isReleased: true });
|
|
290
|
+
this.config.setContext({ isReleased: true });
|
|
291
|
+
}
|
|
284
292
|
})
|
|
285
293
|
.catch(err => {
|
|
286
294
|
this.debug(err);
|
|
@@ -301,9 +309,14 @@ class npm extends Plugin {
|
|
|
301
309
|
}
|
|
302
310
|
|
|
303
311
|
afterRelease() {
|
|
304
|
-
const { isReleased } = this.getContext();
|
|
312
|
+
const { isReleased, isStaged } = this.getContext();
|
|
305
313
|
if (isReleased) {
|
|
306
314
|
this.log.log(`🔗 ${this.getPackageUrl()}`);
|
|
315
|
+
} else if (isStaged) {
|
|
316
|
+
const pm = this.options.publishPackageManager || 'npm';
|
|
317
|
+
this.log.log(
|
|
318
|
+
`📦 Staged for publishing. Approve with \`${pm} stage approve\` (see \`${pm} stage list\`) or on npmjs.com. Requires 2FA.`
|
|
319
|
+
);
|
|
307
320
|
}
|
|
308
321
|
}
|
|
309
322
|
}
|
package/package.json
CHANGED
package/schema/npm.json
CHANGED
package/schema/release-it.json
CHANGED
|
@@ -88,6 +88,11 @@
|
|
|
88
88
|
"type": "boolean",
|
|
89
89
|
"default": false
|
|
90
90
|
}
|
|
91
|
+
},
|
|
92
|
+
"quiet": {
|
|
93
|
+
"type": "boolean",
|
|
94
|
+
"default": false,
|
|
95
|
+
"description": "Suppress preview output (changelog, changeset, release notes) during the release flow. Does not affect the --changelog mode."
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
}
|
package/test/github.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
interceptListReleases,
|
|
11
11
|
interceptCreate,
|
|
12
12
|
interceptUpdate,
|
|
13
|
+
interceptPublish,
|
|
13
14
|
interceptAsset
|
|
14
15
|
} from './stub/github.js';
|
|
15
16
|
import { mockFetch } from './util/mock.js';
|
|
@@ -88,8 +89,9 @@ describe('github', () => {
|
|
|
88
89
|
|
|
89
90
|
interceptAuthentication(api);
|
|
90
91
|
interceptCollaborator(api);
|
|
91
|
-
interceptCreate(api, { body: { tag_name: '2.0.2', name: 'Release 2.0.2', body: 'Custom notes' } });
|
|
92
|
+
interceptCreate(api, { body: { tag_name: '2.0.2', name: 'Release 2.0.2', body: 'Custom notes', draft: true } });
|
|
92
93
|
interceptAsset(assets, { body: '*' });
|
|
94
|
+
interceptPublish(api, { body: { tag_name: '2.0.2' } });
|
|
93
95
|
|
|
94
96
|
await runTasks(github);
|
|
95
97
|
|
|
@@ -98,6 +100,78 @@ describe('github', () => {
|
|
|
98
100
|
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
|
|
99
101
|
});
|
|
100
102
|
|
|
103
|
+
test('should keep release as draft when draft:true with assets', async t => {
|
|
104
|
+
const options = {
|
|
105
|
+
git,
|
|
106
|
+
github: {
|
|
107
|
+
pushRepo,
|
|
108
|
+
tokenRef,
|
|
109
|
+
release: true,
|
|
110
|
+
releaseName: 'Release ${tagName}',
|
|
111
|
+
releaseNotes: 'echo Custom notes',
|
|
112
|
+
draft: true,
|
|
113
|
+
assets: 'test/resources/file-v${version}.txt'
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const github = await factory(GitHub, { options });
|
|
117
|
+
|
|
118
|
+
const original = github.shell.exec.bind(github.shell);
|
|
119
|
+
t.mock.method(github.shell, 'exec', (...args) => {
|
|
120
|
+
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
|
|
121
|
+
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
|
|
122
|
+
return original(...args);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
interceptAuthentication(api);
|
|
126
|
+
interceptCollaborator(api);
|
|
127
|
+
interceptCreate(api, {
|
|
128
|
+
body: { tag_name: '2.0.2', name: 'Release 2.0.2', body: 'Custom notes', draft: true }
|
|
129
|
+
});
|
|
130
|
+
interceptAsset(assets, { body: '*' });
|
|
131
|
+
|
|
132
|
+
await runTasks(github);
|
|
133
|
+
|
|
134
|
+
const execLabels = github.log.exec.mock.calls.map(c => c.arguments[0]);
|
|
135
|
+
assert(!execLabels.some(label => /updateRelease \(publish/.test(label)));
|
|
136
|
+
const { isReleased } = github.getContext();
|
|
137
|
+
assert(isReleased);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('should create release without draft flow when no assets', async t => {
|
|
141
|
+
const options = {
|
|
142
|
+
git,
|
|
143
|
+
github: {
|
|
144
|
+
pushRepo,
|
|
145
|
+
tokenRef,
|
|
146
|
+
release: true,
|
|
147
|
+
releaseName: 'Release ${tagName}',
|
|
148
|
+
releaseNotes: 'echo Custom notes'
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const github = await factory(GitHub, { options });
|
|
152
|
+
|
|
153
|
+
const original = github.shell.exec.bind(github.shell);
|
|
154
|
+
t.mock.method(github.shell, 'exec', (...args) => {
|
|
155
|
+
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
|
|
156
|
+
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
|
|
157
|
+
return original(...args);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
interceptAuthentication(api);
|
|
161
|
+
interceptCollaborator(api);
|
|
162
|
+
interceptCreate(api, {
|
|
163
|
+
body: { tag_name: '2.0.2', name: 'Release 2.0.2', body: 'Custom notes', draft: false }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await runTasks(github);
|
|
167
|
+
|
|
168
|
+
const execLabels = github.log.exec.mock.calls.map(c => c.arguments[0]);
|
|
169
|
+
assert(!execLabels.some(label => /updateRelease \(publish/.test(label)));
|
|
170
|
+
const { isReleased, releaseUrl } = github.getContext();
|
|
171
|
+
assert(isReleased);
|
|
172
|
+
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
|
|
173
|
+
});
|
|
174
|
+
|
|
101
175
|
test('should create a pre-release and draft release notes', async t => {
|
|
102
176
|
const options = {
|
|
103
177
|
git,
|
|
@@ -463,7 +537,8 @@ describe('github', () => {
|
|
|
463
537
|
|
|
464
538
|
assert.equal(get.mock.callCount(), 0);
|
|
465
539
|
assert.equal(github.log.exec.mock.calls[1].arguments[0], 'octokit repos.createRelease "R 1.0.1" (v1.0.1)');
|
|
466
|
-
assert.equal(github.log.exec.mock.calls.at(-
|
|
540
|
+
assert.equal(github.log.exec.mock.calls.at(-2).arguments[0], 'octokit repos.uploadReleaseAssets');
|
|
541
|
+
assert.equal(github.log.exec.mock.calls.at(-1).arguments[0], 'octokit repos.updateRelease (publish v1.0.1)');
|
|
467
542
|
const { isReleased, releaseUrl } = github.getContext();
|
|
468
543
|
assert(isReleased);
|
|
469
544
|
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/v1.0.1');
|
|
@@ -615,7 +690,7 @@ describe('github', () => {
|
|
|
615
690
|
tag_name: '2.0.2',
|
|
616
691
|
name: 'Release 2.0.2',
|
|
617
692
|
generate_release_notes: false,
|
|
618
|
-
body:
|
|
693
|
+
body: '',
|
|
619
694
|
discussion_category_name: 'Announcement'
|
|
620
695
|
}
|
|
621
696
|
});
|
package/test/log.js
CHANGED
|
@@ -151,4 +151,22 @@ describe('log', () => {
|
|
|
151
151
|
const { stdout } = mockStdIo.end();
|
|
152
152
|
assert.equal(stripVTControlCharacters(stdout), `Title:${EOL}changelog\n`);
|
|
153
153
|
});
|
|
154
|
+
|
|
155
|
+
test('should print preview when not quiet', () => {
|
|
156
|
+
const log = new Log({ isQuiet: false });
|
|
157
|
+
mockStdIo.start();
|
|
158
|
+
log.preview({ title: 'changelog', text: 'x' });
|
|
159
|
+
const { stdout } = mockStdIo.end();
|
|
160
|
+
assert.notEqual(stdout, '');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should suppress every preview when quiet', () => {
|
|
164
|
+
const log = new Log({ isQuiet: true });
|
|
165
|
+
mockStdIo.start();
|
|
166
|
+
log.preview({ title: 'changelog', text: 'x' });
|
|
167
|
+
log.preview({ title: 'changeset', text: 'y' });
|
|
168
|
+
log.preview({ title: 'release notes', text: 'z' });
|
|
169
|
+
const { stdout } = mockStdIo.end();
|
|
170
|
+
assert.equal(stdout, '');
|
|
171
|
+
});
|
|
154
172
|
});
|
package/test/npm.js
CHANGED
|
@@ -288,6 +288,20 @@ describe('npm', async () => {
|
|
|
288
288
|
assert.equal(npmClient.log.warn.mock.calls[0].arguments[0], 'The provided OTP is incorrect or has expired.');
|
|
289
289
|
});
|
|
290
290
|
|
|
291
|
+
test('should let npm own the terminal under --only-version so passkey 2FA works (#1234)', async t => {
|
|
292
|
+
const onlyVersion = await factory(npm, { options: { 'only-version': true } });
|
|
293
|
+
onlyVersion.setContext({ name: 'pkg' });
|
|
294
|
+
const exec = t.mock.method(onlyVersion.shell, 'exec', () => Promise.resolve());
|
|
295
|
+
await onlyVersion.publish();
|
|
296
|
+
assert.equal(exec.mock.calls.at(-1).arguments[1].interactive, true);
|
|
297
|
+
|
|
298
|
+
const ci = await factory(npm);
|
|
299
|
+
ci.setContext({ name: 'pkg' });
|
|
300
|
+
const exec2 = t.mock.method(ci.shell, 'exec', () => Promise.resolve());
|
|
301
|
+
await ci.publish();
|
|
302
|
+
assert.equal(exec2.mock.calls.at(-1).arguments[1].interactive, false);
|
|
303
|
+
});
|
|
304
|
+
|
|
291
305
|
test('should publish', async t => {
|
|
292
306
|
const npmClient = await factory(npm);
|
|
293
307
|
const exec = t.mock.method(npmClient.shell, 'exec', command => {
|
|
@@ -323,6 +337,46 @@ describe('npm', async () => {
|
|
|
323
337
|
]);
|
|
324
338
|
});
|
|
325
339
|
|
|
340
|
+
test('should publish to the staging area when `stage` is enabled', async t => {
|
|
341
|
+
const options = { npm: { skipChecks: true, stage: true } };
|
|
342
|
+
const npmClient = await factory(npm, { options });
|
|
343
|
+
const exec = t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
|
|
344
|
+
await runTasks(npmClient);
|
|
345
|
+
assert.deepEqual(exec.mock.calls.at(-1).arguments[0], [
|
|
346
|
+
'npm',
|
|
347
|
+
'stage',
|
|
348
|
+
'publish',
|
|
349
|
+
'.',
|
|
350
|
+
'--tag',
|
|
351
|
+
'latest',
|
|
352
|
+
'--workspaces=false'
|
|
353
|
+
]);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('should not pass --otp when staging (2FA happens at approval)', async t => {
|
|
357
|
+
const npmClient = await factory(npm, { options: { npm: { stage: true } } });
|
|
358
|
+
npmClient.setContext({ name: 'pkg' });
|
|
359
|
+
const exec = t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
|
|
360
|
+
await npmClient.publish({ otp: '123456' });
|
|
361
|
+
assert.deepEqual(exec.mock.calls.at(-1).arguments[0], [
|
|
362
|
+
'npm',
|
|
363
|
+
'stage',
|
|
364
|
+
'publish',
|
|
365
|
+
'.',
|
|
366
|
+
'--tag',
|
|
367
|
+
'latest',
|
|
368
|
+
'--workspaces=false'
|
|
369
|
+
]);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('should stage publish with pnpm', async t => {
|
|
373
|
+
const npmClient = await factory(npm, { options: { npm: { stage: true, publishPackageManager: 'pnpm' } } });
|
|
374
|
+
npmClient.setContext({ name: 'pkg' });
|
|
375
|
+
const exec = t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
|
|
376
|
+
await npmClient.publish();
|
|
377
|
+
assert.deepEqual(exec.mock.calls.at(-1).arguments[0], ['pnpm', 'stage', 'publish', '.', '--tag', 'latest']);
|
|
378
|
+
});
|
|
379
|
+
|
|
326
380
|
test('should skip checks', async () => {
|
|
327
381
|
const options = { npm: { skipChecks: true } };
|
|
328
382
|
const npmClient = await factory(npm, { options });
|
package/test/stub/github.js
CHANGED
|
@@ -38,7 +38,7 @@ export const interceptCreate = (
|
|
|
38
38
|
body: {
|
|
39
39
|
tag_name,
|
|
40
40
|
name = '',
|
|
41
|
-
body =
|
|
41
|
+
body = '',
|
|
42
42
|
prerelease = false,
|
|
43
43
|
draft = false,
|
|
44
44
|
generate_release_notes = false,
|
|
@@ -81,7 +81,7 @@ export const interceptUpdate = (
|
|
|
81
81
|
body: {
|
|
82
82
|
tag_name,
|
|
83
83
|
name = '',
|
|
84
|
-
body =
|
|
84
|
+
body = '',
|
|
85
85
|
prerelease = false,
|
|
86
86
|
draft = false,
|
|
87
87
|
generate_release_notes = false,
|
|
@@ -121,6 +121,27 @@ export const interceptUpdate = (
|
|
|
121
121
|
);
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
+
export const interceptPublish = (
|
|
125
|
+
server,
|
|
126
|
+
{ host = 'github.com', owner = 'user', project = 'repo', body: { tag_name } = {} } = {}
|
|
127
|
+
) => {
|
|
128
|
+
server.patch(
|
|
129
|
+
{
|
|
130
|
+
url: `/repos/${owner}/${project}/releases/1`,
|
|
131
|
+
body: { draft: false }
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
status: 200,
|
|
135
|
+
body: {
|
|
136
|
+
id: 1,
|
|
137
|
+
tag_name,
|
|
138
|
+
draft: false,
|
|
139
|
+
html_url: `https://${host}/${owner}/${project}/releases/tag/${tag_name}`
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
124
145
|
export const interceptAsset = (
|
|
125
146
|
server,
|
|
126
147
|
{ api = 'https://api.github.com', host = 'github.com', owner = 'user', project = 'repo', tagName } = {}
|
package/test/tasks.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
interceptAuthentication as interceptGitHubAuthentication,
|
|
21
21
|
interceptCollaborator as interceptGitHubCollaborator,
|
|
22
22
|
interceptCreate as interceptGitHubCreate,
|
|
23
|
+
interceptPublish as interceptGitHubPublish,
|
|
23
24
|
interceptAsset as interceptGitHubAsset
|
|
24
25
|
} from './stub/github.js';
|
|
25
26
|
import { factory, LogStub, SpinnerStub } from './util/index.js';
|
|
@@ -256,9 +257,11 @@ describe('tasks', () => {
|
|
|
256
257
|
tag_name: 'v1.1.0-alpha.0',
|
|
257
258
|
name: 'Release 1.1.0-alpha.0',
|
|
258
259
|
body: `Notes for ${pkgName} [v1.1.0-alpha.0]: ${sha}`,
|
|
259
|
-
prerelease: true
|
|
260
|
+
prerelease: true,
|
|
261
|
+
draft: true
|
|
260
262
|
}
|
|
261
263
|
});
|
|
264
|
+
interceptGitHubPublish(github, { owner, project, body: { tag_name: 'v1.1.0-alpha.0' } });
|
|
262
265
|
|
|
263
266
|
interceptGitLabUser(gitlab, { owner });
|
|
264
267
|
interceptGitLabCollaborator(gitlab, { owner, project });
|
|
@@ -575,4 +578,49 @@ describe('tasks', () => {
|
|
|
575
578
|
'echo after:afterRelease'
|
|
576
579
|
]);
|
|
577
580
|
});
|
|
581
|
+
|
|
582
|
+
test('should show changelog preview by default', async () => {
|
|
583
|
+
gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
|
|
584
|
+
childProcess.execSync('git tag 1.2.3', execOpts);
|
|
585
|
+
gitAdd('line', 'file', 'Add file');
|
|
586
|
+
await runTasks(
|
|
587
|
+
{},
|
|
588
|
+
getContainer({
|
|
589
|
+
increment: 'patch'
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
const changelogCalls = log.preview.mock.calls.filter(call => call.arguments[0].title === 'changelog');
|
|
593
|
+
assert.equal(changelogCalls.length, 1);
|
|
594
|
+
assert(changelogCalls[0].arguments[0].text.includes('Add file'));
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test('should show changelog preview when quiet is false', async () => {
|
|
598
|
+
gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
|
|
599
|
+
childProcess.execSync('git tag 1.2.3', execOpts);
|
|
600
|
+
gitAdd('line', 'file', 'Add file');
|
|
601
|
+
await runTasks(
|
|
602
|
+
{},
|
|
603
|
+
getContainer({
|
|
604
|
+
increment: 'patch',
|
|
605
|
+
quiet: false
|
|
606
|
+
})
|
|
607
|
+
);
|
|
608
|
+
const changelogCalls = log.preview.mock.calls.filter(call => call.arguments[0].title === 'changelog');
|
|
609
|
+
assert.equal(changelogCalls.length, 1);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test('should announce hidden preview when quiet is true', async () => {
|
|
613
|
+
gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
|
|
614
|
+
childProcess.execSync('git tag 1.2.3', execOpts);
|
|
615
|
+
gitAdd('line', 'file', 'Add file');
|
|
616
|
+
await runTasks(
|
|
617
|
+
{},
|
|
618
|
+
getContainer({
|
|
619
|
+
increment: 'patch',
|
|
620
|
+
quiet: true
|
|
621
|
+
})
|
|
622
|
+
);
|
|
623
|
+
assert(log.obtrusive.mock.calls[0].arguments[0].includes('release my-package'));
|
|
624
|
+
assert(log.info.mock.calls.some(call => call.arguments[0] === 'Preview output hidden (--quiet).'));
|
|
625
|
+
});
|
|
578
626
|
});
|
package/types/config.d.ts
CHANGED
|
@@ -83,6 +83,9 @@ export interface Config {
|
|
|
83
83
|
/** @default null */
|
|
84
84
|
tag?: any;
|
|
85
85
|
|
|
86
|
+
/** @default false */
|
|
87
|
+
stage?: boolean;
|
|
88
|
+
|
|
86
89
|
/** @default null */
|
|
87
90
|
otp?: any;
|
|
88
91
|
|
|
@@ -205,4 +208,7 @@ export interface Config {
|
|
|
205
208
|
/** @default false */
|
|
206
209
|
skipChecks?: boolean;
|
|
207
210
|
};
|
|
211
|
+
|
|
212
|
+
/** @default false */
|
|
213
|
+
quiet?: boolean;
|
|
208
214
|
}
|