release-it 20.0.1 → 20.2.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 +34 -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 +95 -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() {
|
|
@@ -212,6 +215,16 @@ class npm extends Plugin {
|
|
|
212
215
|
return urlJoin(baseUrl, publicPath, this.getName());
|
|
213
216
|
}
|
|
214
217
|
|
|
218
|
+
getStagedPackagesUrl() {
|
|
219
|
+
const name = this.getName();
|
|
220
|
+
const entity = name.startsWith('@') ? name.slice(1).split('/')[0] : this.getContext('username');
|
|
221
|
+
return entity ? `https://www.npmjs.com/settings/${entity}/staged-packages` : 'https://www.npmjs.com';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
logWouldStage() {
|
|
225
|
+
this.log.log(`📦 Would stage (dry-run). Approve at ${this.getStagedPackagesUrl()} after a real run.`);
|
|
226
|
+
}
|
|
227
|
+
|
|
215
228
|
getRegistry() {
|
|
216
229
|
const { publishConfig } = this.getContext();
|
|
217
230
|
const registries = publishConfig
|
|
@@ -254,9 +267,9 @@ class npm extends Plugin {
|
|
|
254
267
|
|
|
255
268
|
async publish({ otp = this.options.otp, otpCallback } = {}) {
|
|
256
269
|
const publishPackageManager = this.options.publishPackageManager || 'npm';
|
|
257
|
-
const { publishPath = '.', publishArgs } = this.options;
|
|
270
|
+
const { publishPath = '.', publishArgs, stage } = this.options;
|
|
258
271
|
const { private: isPrivate, tag = DEFAULT_TAG } = this.getContext();
|
|
259
|
-
const otpArgs = otp ? ['--otp', otp] : [];
|
|
272
|
+
const otpArgs = otp && !stage ? ['--otp', otp] : [];
|
|
260
273
|
const dryRunArg = this.config.isDryRun ? '--dry-run' : '';
|
|
261
274
|
const registry = this.getRegistry();
|
|
262
275
|
const registryArg = registry ? `--registry ${registry}` : '';
|
|
@@ -274,17 +287,29 @@ class npm extends Plugin {
|
|
|
274
287
|
registryArg,
|
|
275
288
|
...fixArgs(publishArgs)
|
|
276
289
|
].filter(Boolean);
|
|
277
|
-
const
|
|
278
|
-
|
|
290
|
+
const publishCommand = stage ? ['stage', 'publish'] : ['publish'];
|
|
291
|
+
const isInteractive = !stage && (!this.config.isCI || Boolean(this.config.isPromptOnlyVersion));
|
|
292
|
+
return this.exec([publishPackageManager, ...publishCommand, ...args], {
|
|
279
293
|
options: { ...getOptions(), interactive: isInteractive }
|
|
280
294
|
})
|
|
281
|
-
.then(
|
|
282
|
-
|
|
283
|
-
|
|
295
|
+
.then(output => {
|
|
296
|
+
if (stage) {
|
|
297
|
+
if (this.config.isDryRun) {
|
|
298
|
+
this.logWouldStage();
|
|
299
|
+
} else {
|
|
300
|
+
const id = output?.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0];
|
|
301
|
+
const approve = `${publishPackageManager} stage approve${id ? ` ${id}` : ''}`;
|
|
302
|
+
this.log.log(`📦 Staged, not yet published. Approve at ${this.getStagedPackagesUrl()} (or \`${approve}\`).`);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
this.setContext({ isReleased: true });
|
|
306
|
+
this.config.setContext({ isReleased: true });
|
|
307
|
+
}
|
|
284
308
|
})
|
|
285
309
|
.catch(err => {
|
|
286
310
|
this.debug(err);
|
|
287
311
|
if (this.config.isDryRun && /publish over the previously published version/.test(err)) {
|
|
312
|
+
if (stage) this.logWouldStage();
|
|
288
313
|
return Promise.resolve();
|
|
289
314
|
}
|
|
290
315
|
|
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,87 @@ 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
|
+
|
|
380
|
+
test('should print the staged-packages approval URL after stage publish', async t => {
|
|
381
|
+
const unscoped = await factory(npm, { options: { npm: { stage: true } } });
|
|
382
|
+
unscoped.setContext({ name: 'pkg', username: 'webpro' });
|
|
383
|
+
t.mock.method(unscoped.shell, 'exec', () => Promise.resolve());
|
|
384
|
+
await unscoped.publish();
|
|
385
|
+
assert.match(unscoped.log.log.mock.calls.map(c => c.arguments[0]).join('\n'), /settings\/webpro\/staged-packages/);
|
|
386
|
+
|
|
387
|
+
const scoped = await factory(npm, { options: { npm: { stage: true } } });
|
|
388
|
+
scoped.setContext({ name: '@release-it/x', username: 'webpro' });
|
|
389
|
+
t.mock.method(scoped.shell, 'exec', () => Promise.resolve());
|
|
390
|
+
await scoped.publish();
|
|
391
|
+
assert.match(scoped.log.log.mock.calls.map(c => c.arguments[0]).join('\n'), /settings\/release-it\/staged-packages/);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('should surface the stage id from publish output in the approval message', async t => {
|
|
395
|
+
const npmClient = await factory(npm, { options: { npm: { stage: true } } });
|
|
396
|
+
npmClient.setContext({ name: 'pkg', username: 'webpro' });
|
|
397
|
+
const id = '71289309-c232-432b-a2d4-32a14fa08177';
|
|
398
|
+
t.mock.method(npmClient.shell, 'exec', () => Promise.resolve(`+ pkg@1.0.0 (staged with id ${id})`));
|
|
399
|
+
await npmClient.publish();
|
|
400
|
+
const logged = npmClient.log.log.mock.calls.map(c => c.arguments[0]).join('\n');
|
|
401
|
+
assert.match(logged, new RegExp(`npm stage approve ${id}`));
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('should describe a staged dry-run, whether npm resolves or rejects the un-bumped version', async t => {
|
|
405
|
+
const execs = [
|
|
406
|
+
() => Promise.resolve('staged with id 71289309-c232-432b-a2d4-32a14fa08177'),
|
|
407
|
+
() => Promise.reject(new Error('npm error You cannot publish over the previously published versions: 1.0.0.'))
|
|
408
|
+
];
|
|
409
|
+
for (const exec of execs) {
|
|
410
|
+
const npmClient = await factory(npm, { options: { 'dry-run': true, npm: { stage: true } } });
|
|
411
|
+
npmClient.setContext({ name: 'pkg', username: 'webpro' });
|
|
412
|
+
t.mock.method(npmClient.shell, 'exec', exec);
|
|
413
|
+
await npmClient.publish();
|
|
414
|
+
const logged = npmClient.log.log.mock.calls.map(c => c.arguments[0]).join('\n');
|
|
415
|
+
assert.match(logged, /Would stage \(dry-run\)/);
|
|
416
|
+
assert.match(logged, /settings\/webpro\/staged-packages/);
|
|
417
|
+
assert.doesNotMatch(logged, /stage approve 71289309/);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
326
421
|
test('should skip checks', async () => {
|
|
327
422
|
const options = { npm: { skipChecks: true } };
|
|
328
423
|
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
|
}
|