release-it 14.13.1 → 14.14.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.
@@ -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,
@@ -107,6 +107,66 @@ class GitLab extends Release {
107
107
  }
108
108
  }
109
109
 
110
+ async beforeRelease() {
111
+ await super.beforeRelease();
112
+ await this.checkReleaseMilestones();
113
+ }
114
+
115
+ async checkReleaseMilestones() {
116
+ if (this.options.skipChecks) return;
117
+
118
+ const releaseMilestones = this.getReleaseMilestones();
119
+ if (releaseMilestones.length < 1) {
120
+ return;
121
+ }
122
+
123
+ this.log.exec(`gitlab releases#checkReleaseMilestones`);
124
+
125
+ const { id } = this.getContext();
126
+ const endpoint = `projects/${id}/milestones`;
127
+ const requests = releaseMilestones.map(milestone => {
128
+ const options = {
129
+ method: 'GET',
130
+ searchParams: {
131
+ title: milestone,
132
+ include_parent_milestones: true
133
+ }
134
+ };
135
+ return this.request(endpoint, options).then(response => {
136
+ if (!Array.isArray(response)) {
137
+ const { baseUrl } = this.getContext();
138
+ throw new Error(
139
+ `Unexpected response from ${baseUrl}/${endpoint}. Expected an array but got: ${JSON.stringify(response)}`
140
+ );
141
+ }
142
+ if (response.length === 0) {
143
+ const error = new Error(`Milestone '${milestone}' does not exist.`);
144
+ this.log.warn(error.message);
145
+ throw error;
146
+ }
147
+ this.log.verbose(`gitlab releases#checkReleaseMilestones: milestone '${milestone}' exists`);
148
+ });
149
+ });
150
+ try {
151
+ await Promise.allSettled(requests).then(results => {
152
+ for (const result of results) {
153
+ if (result.status === 'rejected') {
154
+ throw e('Missing one or more milestones in GitLab. Creating a GitLab release will fail.', docs);
155
+ }
156
+ }
157
+ });
158
+ } catch (err) {
159
+ this.debug(err);
160
+ throw err;
161
+ }
162
+ this.log.verbose('gitlab releases#checkReleaseMilestones: done');
163
+ }
164
+
165
+ getReleaseMilestones() {
166
+ const { milestones } = this.options;
167
+ return (milestones || []).map(milestone => format(milestone, this.config.getContext()));
168
+ }
169
+
110
170
  async release() {
111
171
  const glRelease = () => this.createRelease();
112
172
  const glUploadAssets = () => this.uploadAssets();
@@ -138,6 +198,7 @@ class GitLab extends Release {
138
198
  const name = format(releaseName, this.config.getContext());
139
199
  const description = releaseNotes || '-';
140
200
  const releaseUrl = `${origin}/${repo.repository}/-/releases`;
201
+ const releaseMilestones = this.getReleaseMilestones();
141
202
 
142
203
  this.log.exec(`gitlab releases#createRelease "${name}" (${tagName})`, { isDryRun });
143
204
 
@@ -161,6 +222,10 @@ class GitLab extends Release {
161
222
  };
162
223
  }
163
224
 
225
+ if (releaseMilestones.length) {
226
+ options.json.milestones = releaseMilestones;
227
+ }
228
+
164
229
  try {
165
230
  await this.request(endpoint, options);
166
231
  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.0",
4
4
  "description": "Generic CLI tool to automate versioning and package publishing related tasks.",
5
5
  "keywords": [
6
6
  "build",
@@ -60,17 +60,17 @@
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
68
  "got": "11.8.3",
69
69
  "import-cwd": "3.0.0",
70
- "inquirer": "8.2.0",
70
+ "inquirer": "8.2.2",
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",
@@ -88,19 +88,19 @@
88
88
  "devDependencies": {
89
89
  "@octokit/request-error": "2.1.0",
90
90
  "ava": "3.15.0",
91
- "eslint": "8.7.0",
92
- "eslint-config-prettier": "8.3.0",
91
+ "eslint": "8.12.0",
92
+ "eslint-config-prettier": "8.5.0",
93
93
  "eslint-plugin-ava": "13.2.0",
94
94
  "eslint-plugin-import": "2.25.4",
95
95
  "eslint-plugin-prettier": "4.0.0",
96
96
  "markdown-toc": "1.2.0",
97
97
  "mock-fs": "5.1.2",
98
98
  "mock-stdio": "1.0.3",
99
- "nock": "13.2.2",
99
+ "nock": "13.2.4",
100
100
  "node-fetch": "2.6.7",
101
- "prettier": "2.5.1",
101
+ "prettier": "2.6.1",
102
102
  "proxyquire": "2.1.3",
103
- "sinon": "12.0.1",
103
+ "sinon": "13.0.1",
104
104
  "strip-ansi": "6.0.0"
105
105
  },
106
106
  "engines": {
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({