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.
@@ -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
@@ -12,6 +12,7 @@ const aliases = {
12
12
  const booleanOptions = [
13
13
  'dry-run',
14
14
  'ci',
15
+ 'quiet',
15
16
  'git',
16
17
  'npm',
17
18
  'github',
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
@@ -70,6 +70,10 @@ class Config {
70
70
  return debug.enabled;
71
71
  }
72
72
 
73
+ get isQuiet() {
74
+ return Boolean(this.options.quiet);
75
+ }
76
+
73
77
  get isCI() {
74
78
  return Boolean(this.options.ci) || this.isReleaseVersion || this.isChangelog;
75
79
  }
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
- await this.step({ task: () => this[publishMethod](), label: `GitHub ${type} release` });
144
- await this.step({ enabled: assets, task: () => this.uploadAssets(), label: 'GitHub upload assets' });
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
- await this[publishMethod]();
152
- await this.uploadAssets();
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, isUpdate } = this.getContext();
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 body = autoGenerate
227
- ? isUpdate
228
- ? null
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.
@@ -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 ? null : task => this.step({ prompt: 'otp', task });
104
- return this.step({ task: publish, label: 'npm publish', prompt: 'publish' });
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 isInteractive = !this.config.isCI;
278
- return this.exec([publishPackageManager, 'publish', ...args], {
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
- this.setContext({ isReleased: true });
283
- this.config.setContext({ isReleased: true });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-it",
3
- "version": "20.0.1",
3
+ "version": "20.1.0",
4
4
  "description": "Generic CLI tool to automate versioning and package publishing-related tasks.",
5
5
  "keywords": [
6
6
  "build",
package/schema/npm.json CHANGED
@@ -29,6 +29,10 @@
29
29
  "type": "string",
30
30
  "default": null
31
31
  },
32
+ "stage": {
33
+ "type": "boolean",
34
+ "default": false
35
+ },
32
36
  "otp": {
33
37
  "type": "string",
34
38
  "default": null
@@ -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(-1).arguments[0], 'octokit repos.uploadReleaseAssets');
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: null,
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 });
@@ -38,7 +38,7 @@ export const interceptCreate = (
38
38
  body: {
39
39
  tag_name,
40
40
  name = '',
41
- body = null,
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 = null,
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
  }