release-it 14.13.1 → 14.14.2

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/README.md CHANGED
@@ -271,6 +271,14 @@ Use `--verbose` to log the output of the commands.
271
271
  For the sake of verbosity, the full list of hooks is actually: `init`, `beforeBump`, `bump`, `beforeRelease`, `release`
272
272
  or `afterRelease`. However, hooks like `before:beforeRelease` look weird and are usually not useful in practice.
273
273
 
274
+ Note that arguments need to be quoted properly when used from the command line:
275
+
276
+ ```bash
277
+ release-it --'hooks.after:release="echo Successfully released ${name} v${version} to ${repo.repository}."'
278
+ ```
279
+
280
+ Using Inquirer.js inside custom hook scripts might cause issues (since release-it also uses this itself).
281
+
274
282
  ## Plugins
275
283
 
276
284
  Since v11, release-it can be extended in many, many ways. Here are some plugins:
@@ -49,6 +49,7 @@
49
49
  "release": false,
50
50
  "releaseName": "Release ${version}",
51
51
  "releaseNotes": null,
52
+ "milestones": [],
52
53
  "tokenRef": "GITLAB_TOKEN",
53
54
  "tokenHeader": "Private-Token",
54
55
  "certificateAuthorityFile": null,
@@ -10,7 +10,7 @@ class GitRelease extends GitBase {
10
10
  getInitialOptions(options) {
11
11
  const baseOptions = super.getInitialOptions(...arguments);
12
12
  const git = options.git || defaultConfig.git;
13
- const gitOptions = _.pick(git, ['tagName', 'pushRepo', 'changelog']);
13
+ const gitOptions = _.pick(git, ['tagName', 'tagMatch', 'pushRepo', 'changelog']);
14
14
  return _.defaults(baseOptions, gitOptions);
15
15
  }
16
16
 
@@ -4,6 +4,7 @@ const got = require('got');
4
4
  const globby = require('globby');
5
5
  const FormData = require('form-data');
6
6
  const _ = require('lodash');
7
+ const allSettled = require('promise.allsettled');
7
8
  const Release = require('../GitRelease');
8
9
  const { format, e } = require('../../util');
9
10
  const prompts = require('./prompts');
@@ -28,7 +29,7 @@ class GitLab extends Release {
28
29
  const { tokenHeader } = this.options;
29
30
  const { baseUrl } = this.getContext();
30
31
  this._client = got.extend({
31
- prefixUrl: baseUrl,
32
+ baseUrl,
32
33
  method: 'POST',
33
34
  headers: {
34
35
  'user-agent': 'webpro/release-it',
@@ -107,6 +108,66 @@ class GitLab extends Release {
107
108
  }
108
109
  }
109
110
 
111
+ async beforeRelease() {
112
+ await super.beforeRelease();
113
+ await this.checkReleaseMilestones();
114
+ }
115
+
116
+ async checkReleaseMilestones() {
117
+ if (this.options.skipChecks) return;
118
+
119
+ const releaseMilestones = this.getReleaseMilestones();
120
+ if (releaseMilestones.length < 1) {
121
+ return;
122
+ }
123
+
124
+ this.log.exec(`gitlab releases#checkReleaseMilestones`);
125
+
126
+ const { id } = this.getContext();
127
+ const endpoint = `projects/${id}/milestones`;
128
+ const requests = releaseMilestones.map(milestone => {
129
+ const options = {
130
+ method: 'GET',
131
+ query: {
132
+ title: milestone,
133
+ include_parent_milestones: true
134
+ }
135
+ };
136
+ return this.request(endpoint, options).then(response => {
137
+ if (!Array.isArray(response)) {
138
+ const { baseUrl } = this.getContext();
139
+ throw new Error(
140
+ `Unexpected response from ${baseUrl}/${endpoint}. Expected an array but got: ${JSON.stringify(response)}`
141
+ );
142
+ }
143
+ if (response.length === 0) {
144
+ const error = new Error(`Milestone '${milestone}' does not exist.`);
145
+ this.log.warn(error.message);
146
+ throw error;
147
+ }
148
+ this.log.verbose(`gitlab releases#checkReleaseMilestones: milestone '${milestone}' exists`);
149
+ });
150
+ });
151
+ try {
152
+ await allSettled(requests).then(results => {
153
+ for (const result of results) {
154
+ if (result.status === 'rejected') {
155
+ throw e('Missing one or more milestones in GitLab. Creating a GitLab release will fail.', docs);
156
+ }
157
+ }
158
+ });
159
+ } catch (err) {
160
+ this.debug(err);
161
+ throw err;
162
+ }
163
+ this.log.verbose('gitlab releases#checkReleaseMilestones: done');
164
+ }
165
+
166
+ getReleaseMilestones() {
167
+ const { milestones } = this.options;
168
+ return (milestones || []).map(milestone => format(milestone, this.config.getContext()));
169
+ }
170
+
110
171
  async release() {
111
172
  const glRelease = () => this.createRelease();
112
173
  const glUploadAssets = () => this.uploadAssets();
@@ -138,6 +199,7 @@ class GitLab extends Release {
138
199
  const name = format(releaseName, this.config.getContext());
139
200
  const description = releaseNotes || '-';
140
201
  const releaseUrl = `${origin}/${repo.repository}/-/releases`;
202
+ const releaseMilestones = this.getReleaseMilestones();
141
203
 
142
204
  this.log.exec(`gitlab releases#createRelease "${name}" (${tagName})`, { isDryRun });
143
205
 
@@ -148,7 +210,8 @@ class GitLab extends Release {
148
210
 
149
211
  const endpoint = `projects/${id}/releases`;
150
212
  const options = {
151
- json: {
213
+ json: true,
214
+ body: {
152
215
  name,
153
216
  tag_name: tagName,
154
217
  description
@@ -156,11 +219,15 @@ class GitLab extends Release {
156
219
  };
157
220
 
158
221
  if (this.assets.length) {
159
- options.json.assets = {
222
+ options.body.assets = {
160
223
  links: this.assets
161
224
  };
162
225
  }
163
226
 
227
+ if (releaseMilestones.length) {
228
+ options.body.milestones = releaseMilestones;
229
+ }
230
+
164
231
  try {
165
232
  await this.request(endpoint, options);
166
233
  this.log.verbose('gitlab releases#createRelease: done');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-it",
3
- "version": "14.13.1",
3
+ "version": "14.14.2",
4
4
  "description": "Generic CLI tool to automate versioning and package publishing related tasks.",
5
5
  "keywords": [
6
6
  "build",
@@ -60,22 +60,23 @@
60
60
  "async-retry": "1.3.3",
61
61
  "chalk": "4.1.2",
62
62
  "cosmiconfig": "7.0.1",
63
- "debug": "4.3.3",
63
+ "debug": "4.3.4",
64
64
  "execa": "5.1.1",
65
65
  "form-data": "4.0.0",
66
66
  "git-url-parse": "11.6.0",
67
67
  "globby": "11.0.4",
68
- "got": "11.8.3",
68
+ "got": "9.6.0",
69
69
  "import-cwd": "3.0.0",
70
70
  "inquirer": "8.2.0",
71
71
  "is-ci": "3.0.1",
72
72
  "lodash": "4.17.21",
73
- "mime-types": "2.1.34",
73
+ "mime-types": "2.1.35",
74
74
  "new-github-release-url": "1.0.0",
75
75
  "open": "7.4.2",
76
76
  "ora": "5.4.1",
77
77
  "os-name": "4.0.1",
78
78
  "parse-json": "5.2.0",
79
+ "promise.allsettled": "1.0.5",
79
80
  "semver": "7.3.5",
80
81
  "shelljs": "0.8.5",
81
82
  "update-notifier": "5.1.0",
@@ -88,22 +89,25 @@
88
89
  "devDependencies": {
89
90
  "@octokit/request-error": "2.1.0",
90
91
  "ava": "3.15.0",
91
- "eslint": "8.7.0",
92
- "eslint-config-prettier": "8.3.0",
93
- "eslint-plugin-ava": "13.2.0",
94
- "eslint-plugin-import": "2.25.4",
92
+ "eslint": "7.32.0",
93
+ "eslint-config-prettier": "8.5.0",
94
+ "eslint-plugin-ava": "12.0.0",
95
+ "eslint-plugin-import": "2.26.0",
95
96
  "eslint-plugin-prettier": "4.0.0",
96
97
  "markdown-toc": "1.2.0",
97
- "mock-fs": "5.1.2",
98
+ "mock-fs": "4.14.0",
98
99
  "mock-stdio": "1.0.3",
99
- "nock": "13.2.2",
100
+ "nock": "13.2.4",
100
101
  "node-fetch": "2.6.7",
101
- "prettier": "2.5.1",
102
+ "prettier": "2.6.2",
102
103
  "proxyquire": "2.1.3",
103
- "sinon": "12.0.1",
104
+ "sinon": "13.0.1",
104
105
  "strip-ansi": "6.0.0"
105
106
  },
106
107
  "engines": {
107
108
  "node": ">=10"
109
+ },
110
+ "resolutions": {
111
+ "lru-cache": "6.0.0"
108
112
  }
109
113
  }
package/test/github.js CHANGED
@@ -214,7 +214,7 @@ test('should release to enterprise host', async t => {
214
214
  });
215
215
 
216
216
  test('should release to alternative host and proxy', async t => {
217
- const remote = { api: 'https://my-custom-host.org/api/v3', host: 'my-custom-host.org' };
217
+ const remote = { api: 'https://custom.example.org/api/v3', host: 'custom.example.org' };
218
218
  interceptAuthentication(remote);
219
219
  interceptCollaborator(remote);
220
220
  interceptCreate(Object.assign({ body: { tag_name: '1.0.1' } }, remote));
@@ -222,8 +222,8 @@ test('should release to alternative host and proxy', async t => {
222
222
  git,
223
223
  github: {
224
224
  tokenRef,
225
- pushRepo: `git://my-custom-host.org:user/repo`,
226
- host: 'my-custom-host.org',
225
+ pushRepo: `git://custom.example.org:user/repo`,
226
+ host: 'custom.example.org',
227
227
  proxy: 'http://proxy:8080'
228
228
  }
229
229
  };
@@ -235,24 +235,24 @@ test('should release to alternative host and proxy', async t => {
235
235
 
236
236
  const { isReleased, releaseUrl } = github.getContext();
237
237
  t.true(isReleased);
238
- t.is(releaseUrl, `https://my-custom-host.org/user/repo/releases/tag/1.0.1`);
238
+ t.is(releaseUrl, `https://custom.example.org/user/repo/releases/tag/1.0.1`);
239
239
  exec.restore();
240
240
  });
241
241
 
242
242
  test('should release to git.pushRepo', async t => {
243
- const remote = { api: 'https://my-custom-host.org/api/v3', host: 'my-custom-host.org' };
243
+ const remote = { api: 'https://custom.example.org/api/v3', host: 'custom.example.org' };
244
244
  interceptCreate(Object.assign({ body: { tag_name: '1.0.1' } }, remote));
245
245
  const options = { git: { pushRepo: 'upstream', changelog: '' }, github: { tokenRef, skipChecks: true } };
246
246
  const github = factory(GitHub, { options });
247
247
  const exec = sinon.stub(github.shell, 'exec').callThrough();
248
248
  exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('1.0.0');
249
- exec.withArgs('git remote get-url upstream').resolves('https://my-custom-host.org/user/repo');
249
+ exec.withArgs('git remote get-url upstream').resolves('https://custom.example.org/user/repo');
250
250
 
251
251
  await runTasks(github);
252
252
 
253
253
  const { isReleased, releaseUrl } = github.getContext();
254
254
  t.true(isReleased);
255
- t.is(releaseUrl, 'https://my-custom-host.org/user/repo/releases/tag/1.0.1');
255
+ t.is(releaseUrl, 'https://custom.example.org/user/repo/releases/tag/1.0.1');
256
256
  exec.restore();
257
257
  });
258
258
 
@@ -385,10 +385,10 @@ test('should generate GitHub web release url for enterprise host', async t => {
385
385
  const options = {
386
386
  git,
387
387
  github: {
388
- pushRepo: 'git://my-custom-host.org:user/repo',
388
+ pushRepo: 'git://custom.example.org:user/repo',
389
389
  release: true,
390
390
  web: true,
391
- host: 'my-custom-host.org',
391
+ host: 'custom.example.org',
392
392
  releaseName: 'The Launch',
393
393
  releaseNotes: 'echo It happened'
394
394
  }
@@ -403,7 +403,7 @@ test('should generate GitHub web release url for enterprise host', async t => {
403
403
  t.true(isReleased);
404
404
  t.is(
405
405
  releaseUrl,
406
- 'https://my-custom-host.org/user/repo/releases/new?tag=2.0.2&title=The+Launch&body=It+happened&prerelease=false'
406
+ 'https://custom.example.org/user/repo/releases/new?tag=2.0.2&title=The+Launch&body=It+happened&prerelease=false'
407
407
  );
408
408
  exec.restore();
409
409
  });
package/test/gitlab.js CHANGED
@@ -7,7 +7,8 @@ const {
7
7
  interceptCollaborator,
8
8
  interceptCollaboratorFallback,
9
9
  interceptPublish,
10
- interceptAsset
10
+ interceptAsset,
11
+ interceptMilestones
11
12
  } = require('./stub/gitlab');
12
13
  const { factory, runTasks } = require('./util');
13
14
 
@@ -55,7 +56,8 @@ test.serial('should upload assets and release', async t => {
55
56
  release: true,
56
57
  releaseName: 'Release ${version}',
57
58
  releaseNotes: 'echo Custom notes',
58
- assets: 'test/resources/file-v${version}.txt'
59
+ assets: 'test/resources/file-v${version}.txt',
60
+ milestones: ['${version}', '${latestVersion} UAT']
59
61
  }
60
62
  };
61
63
  const gitlab = factory(GitLab, { options });
@@ -63,6 +65,26 @@ test.serial('should upload assets and release', async t => {
63
65
 
64
66
  interceptUser();
65
67
  interceptCollaborator();
68
+ interceptMilestones({
69
+ query: { title: '2.0.1' },
70
+ milestones: [
71
+ {
72
+ id: 17,
73
+ iid: 3,
74
+ title: '2.0.1'
75
+ }
76
+ ]
77
+ });
78
+ interceptMilestones({
79
+ query: { title: '2.0.0 UAT' },
80
+ milestones: [
81
+ {
82
+ id: 42,
83
+ iid: 4,
84
+ title: '2.0.0 UAT'
85
+ }
86
+ ]
87
+ });
66
88
  interceptAsset();
67
89
  interceptPublish({
68
90
  body: {
@@ -76,7 +98,8 @@ test.serial('should upload assets and release', async t => {
76
98
  url: `${pushRepo}/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/file-v2.0.1.txt`
77
99
  }
78
100
  ]
79
- }
101
+ },
102
+ milestones: ['2.0.1', '2.0.0 UAT']
80
103
  }
81
104
  });
82
105
 
@@ -88,6 +111,41 @@ test.serial('should upload assets and release', async t => {
88
111
  t.is(releaseUrl, `${pushRepo}/-/releases`);
89
112
  });
90
113
 
114
+ test.serial('should throw when release milestone is missing', async t => {
115
+ const pushRepo = 'https://gitlab.com/user/repo';
116
+ const options = {
117
+ git: { pushRepo },
118
+ gitlab: {
119
+ tokenRef,
120
+ release: true,
121
+ milestones: ['${version}', '${latestVersion} UAT']
122
+ }
123
+ };
124
+ const gitlab = factory(GitLab, { options });
125
+ sinon.stub(gitlab, 'getLatestVersion').resolves('2.0.0');
126
+
127
+ interceptUser();
128
+ interceptCollaborator();
129
+ interceptMilestones({
130
+ query: { title: '2.0.1' },
131
+ milestones: [
132
+ {
133
+ id: 17,
134
+ iid: 3,
135
+ title: '2.0.1'
136
+ }
137
+ ]
138
+ });
139
+ interceptMilestones({
140
+ query: { title: '2.0.0 UAT' },
141
+ milestones: []
142
+ });
143
+
144
+ await t.throwsAsync(runTasks(gitlab), {
145
+ message: /^Missing one or more milestones in GitLab. Creating a GitLab release will fail./
146
+ });
147
+ });
148
+
91
149
  test.serial('should release to self-managed host', async t => {
92
150
  const host = 'https://gitlab.example.org';
93
151
  const scope = nock(host);
@@ -195,7 +253,14 @@ test('should not make requests in dry run', async t => {
195
253
  });
196
254
 
197
255
  test('should skip checks', async t => {
198
- const options = { gitlab: { tokenRef, skipChecks: true } };
256
+ const options = { gitlab: { tokenRef, skipChecks: true, release: true, milestones: ['v1.0.0'] } };
199
257
  const gitlab = factory(GitLab, { options });
258
+ const spy = sinon.spy(gitlab, 'client', ['get']);
259
+
200
260
  await t.notThrowsAsync(gitlab.init());
261
+ await t.notThrowsAsync(gitlab.beforeRelease());
262
+
263
+ t.is(spy.get.callCount, 0);
264
+
265
+ t.is(gitlab.log.exec.args.filter(entry => /checkReleaseMilestones/.test(entry[0])).length, 0);
201
266
  });
package/test/npm.js CHANGED
@@ -13,9 +13,9 @@ test('should return npm package url', t => {
13
13
  });
14
14
 
15
15
  test('should return npm package url (custom registry)', t => {
16
- const options = { npm: { name: 'my-cool-package', publishConfig: { registry: 'https://my-registry.com/' } } };
16
+ const options = { npm: { name: 'my-cool-package', publishConfig: { registry: 'https://registry.example.org/' } } };
17
17
  const npmClient = factory(npm, { options });
18
- t.is(npmClient.getPackageUrl(), 'https://my-registry.com/package/my-cool-package');
18
+ t.is(npmClient.getPackageUrl(), 'https://registry.example.org/package/my-cool-package');
19
19
  });
20
20
 
21
21
  test('should return default tag', async t => {
@@ -19,6 +19,22 @@ module.exports.interceptCollaboratorFallback = (
19
19
  .get(`/api/v4/projects/${group ? `${group}%2F` : ''}${owner}%2F${project}/members/${userId}`)
20
20
  .reply(200, { id: userId, username: owner, access_level: 30 });
21
21
 
22
+ module.exports.interceptMilestones = (
23
+ { host = 'https://gitlab.com', owner = 'user', project = 'repo', query = {}, milestones = [] } = {},
24
+ options
25
+ ) =>
26
+ nock(host, options)
27
+ .get(`/api/v4/projects/${owner}%2F${project}/milestones`)
28
+ .query(
29
+ Object.assign(
30
+ {
31
+ include_parent_milestones: true
32
+ },
33
+ query
34
+ )
35
+ )
36
+ .reply(200, JSON.stringify(milestones));
37
+
22
38
  module.exports.interceptPublish = (
23
39
  { host = 'https://gitlab.com', owner = 'user', project = 'repo', body } = {},
24
40
  options
package/test/tasks.js CHANGED
@@ -358,7 +358,7 @@ test.serial('should initially publish non-private scoped npm package privately',
358
358
  test.serial('should use pkg.publishConfig.registry', async t => {
359
359
  const { target } = t.context;
360
360
  const pkgName = path.basename(target);
361
- const registry = 'https://my-registry.com';
361
+ const registry = 'https://my-registry.example.org';
362
362
 
363
363
  gitAdd(
364
364
  JSON.stringify({