release-it 17.5.0 → 17.7.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.
@@ -63,7 +63,9 @@
63
63
  "tokenRef": "GITLAB_TOKEN",
64
64
  "tokenHeader": "Private-Token",
65
65
  "certificateAuthorityFile": null,
66
+ "secure": null,
66
67
  "assets": null,
68
+ "useIdsForUrls": false,
67
69
  "origin": null,
68
70
  "skipChecks": false
69
71
  }
package/lib/config.js CHANGED
@@ -2,7 +2,7 @@ import util from 'node:util';
2
2
  import { cosmiconfigSync } from 'cosmiconfig';
3
3
  import parseToml from '@iarna/toml/parse-string.js';
4
4
  import _ from 'lodash';
5
- import isCI from 'is-ci';
5
+ import { isCI } from 'ci-info';
6
6
  import { readJSON, getSystemInfo } from './util.js';
7
7
 
8
8
  const debug = util.debug('release-it:config');
@@ -2,7 +2,6 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import open from 'open';
4
4
  import { Octokit } from '@octokit/rest';
5
- import fetch from 'node-fetch';
6
5
  import { globby } from 'globby';
7
6
  import mime from 'mime-types';
8
7
  import _ from 'lodash';
@@ -14,6 +13,10 @@ import Release from '../GitRelease.js';
14
13
  import prompts from './prompts.js';
15
14
  import { getCommitsFromChangelog, getResolvedIssuesFromChangelog, searchQueries } from './util.js';
16
15
 
16
+ /**
17
+ * @typedef {import('@octokit/rest').RestEndpointMethodTypes['repos']['createRelease']['parameters']} CreateReleaseOptions
18
+ */
19
+
17
20
  const pkg = readJSON(new URL('../../../package.json', import.meta.url));
18
21
 
19
22
  const docs = 'https://git.io/release-it-github';
@@ -177,8 +180,7 @@ class GitHub extends Release {
177
180
  userAgent: `release-it/${pkg.version}`,
178
181
  log: this.config.isDebug ? console : null,
179
182
  request: {
180
- timeout,
181
- fetch
183
+ timeout
182
184
  }
183
185
  };
184
186
 
@@ -207,15 +209,19 @@ class GitHub extends Release {
207
209
 
208
210
  getOctokitReleaseOptions(options = {}) {
209
211
  const { owner, project: repo } = this.getContext('repo');
210
- const { releaseName, draft = false, preRelease = false, autoGenerate = false } = this.options;
212
+ const { releaseName, draft = false, preRelease = false, autoGenerate = false, makeLatest = true } = this.options;
211
213
  const { tagName } = this.config.getContext();
212
214
  const { version, releaseNotes, isUpdate } = this.getContext();
213
215
  const { isPreRelease } = parseVersion(version);
214
216
  const name = format(releaseName, this.config.getContext());
215
217
  const body = autoGenerate ? (isUpdate ? null : '') : truncateBody(releaseNotes);
216
218
 
217
- return Object.assign(options, {
219
+ /**
220
+ * @type {CreateReleaseOptions}
221
+ */
222
+ const contextOptions = {
218
223
  owner,
224
+ make_latest: makeLatest.toString(),
219
225
  repo,
220
226
  tag_name: tagName,
221
227
  name,
@@ -223,7 +229,8 @@ class GitHub extends Release {
223
229
  draft,
224
230
  prerelease: isPreRelease || preRelease,
225
231
  generate_release_notes: autoGenerate
226
- });
232
+ };
233
+ return Object.assign(options, contextOptions);
227
234
  }
228
235
 
229
236
  retry(fn) {
@@ -1,10 +1,12 @@
1
1
  // Totally much borrowed from https://github.com/semantic-release/github/blob/master/lib/success.js
2
2
  import issueParser from 'issue-parser';
3
3
 
4
- const getSearchQueries = (base, commits, separator = '+') => {
4
+ export const getSearchQueries = (base, commits, separator = '+') => {
5
+ const encodedSeparator = encodeURIComponent(separator);
6
+
5
7
  return commits.reduce((searches, commit) => {
6
8
  const lastSearch = searches[searches.length - 1];
7
- if (lastSearch && lastSearch.length + commit.length <= 256 - separator.length) {
9
+ if (lastSearch && encodeURIComponent(lastSearch).length + commit.length <= 256 - encodedSeparator.length) {
8
10
  searches[searches.length - 1] = `${lastSearch}${separator}${commit}`;
9
11
  } else {
10
12
  searches.push(`${base}${separator}${commit}`);
@@ -1,8 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import got from 'got';
4
3
  import { globby } from 'globby';
5
- import { FormData, fileFromSync } from 'node-fetch';
6
4
  import _ from 'lodash';
7
5
  import Release from '../GitRelease.js';
8
6
  import { format, e } from '../../util.js';
@@ -17,26 +15,17 @@ class GitLab extends Release {
17
15
  super(...args);
18
16
  this.registerPrompts(prompts);
19
17
  this.assets = [];
20
- const { certificateAuthorityFile } = this.options;
21
- this.certificateAuthorityOption = certificateAuthorityFile
22
- ? { https: { certificateAuthority: fs.readFileSync(certificateAuthorityFile) } }
23
- : {};
24
- }
18
+ const { certificateAuthorityFile, secure } = this.options;
25
19
 
26
- get client() {
27
- if (this._client) return this._client;
28
- const { tokenHeader } = this.options;
29
- const { baseUrl } = this.getContext();
30
- this._client = got.extend({
31
- prefixUrl: baseUrl,
32
- method: 'POST',
33
- headers: {
34
- 'user-agent': 'webpro/release-it',
35
- [tokenHeader]: this.token
36
- },
37
- ...this.certificateAuthorityOption
38
- });
39
- return this._client;
20
+ const httpsOptions = {
21
+ certificateAuthority: certificateAuthorityFile ? fs.readFileSync(certificateAuthorityFile) : undefined,
22
+ rejectUnauthorized: typeof secure === 'boolean' ? secure : undefined
23
+ };
24
+
25
+ // Remove keys with undefined values
26
+ const https = _.pickBy(httpsOptions, value => value !== undefined);
27
+
28
+ this.certificateAuthorityOption = _.isEmpty(https) ? {} : { https };
40
29
  }
41
30
 
42
31
  async init() {
@@ -172,8 +161,28 @@ class GitLab extends Release {
172
161
  const { baseUrl } = this.getContext();
173
162
  this.debug(Object.assign({ url: `${baseUrl}/${endpoint}` }, options));
174
163
  const method = (options.method || 'POST').toLowerCase();
175
- const response = await this.client[method](endpoint, options);
176
- const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body || {};
164
+ const { tokenHeader } = this.options;
165
+ const url = `${baseUrl}/${endpoint}${options.searchParams ? `?${new URLSearchParams(options.searchParams)}` : ''}`;
166
+ const headers = {
167
+ 'user-agent': 'webpro/release-it',
168
+ [tokenHeader]: this.token
169
+ };
170
+ const requestOptions = {
171
+ method,
172
+ headers,
173
+ ...this.certificateAuthorityOption
174
+ };
175
+
176
+ const response = await fetch(
177
+ url,
178
+ options.json || options.body
179
+ ? {
180
+ ...requestOptions,
181
+ body: options.json ? JSON.stringify(options.json) : options.body
182
+ }
183
+ : requestOptions
184
+ );
185
+ const body = await response.json();
177
186
  this.debug(body);
178
187
  return body;
179
188
  }
@@ -228,11 +237,13 @@ class GitLab extends Release {
228
237
 
229
238
  async uploadAsset(filePath) {
230
239
  const name = path.basename(filePath);
240
+ const { useIdsForUrls } = this.options;
231
241
  const { id, origin, repo } = this.getContext();
232
242
  const endpoint = `projects/${id}/uploads`;
233
243
 
234
244
  const body = new FormData();
235
- body.set('file', fileFromSync(filePath));
245
+ const rawData = await fs.promises.readFile(filePath);
246
+ body.set('file', new Blob([rawData]), name);
236
247
  const options = { body };
237
248
 
238
249
  try {
@@ -240,7 +251,7 @@ class GitLab extends Release {
240
251
  this.log.verbose(`gitlab releases#uploadAsset: done (${body.url})`);
241
252
  this.assets.push({
242
253
  name,
243
- url: `${origin}/${repo.repository}${body.url}`
254
+ url: useIdsForUrls ? `${origin}${body.full_path}` : `${origin}/${repo.repository}${body.url}`
244
255
  });
245
256
  } catch (err) {
246
257
  this.debug(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-it",
3
- "version": "17.5.0",
3
+ "version": "17.7.0",
4
4
  "description": "Generic CLI tool to automate versioning and package publishing-related tasks.",
5
5
  "keywords": [
6
6
  "build",
@@ -82,54 +82,52 @@
82
82
  "@octokit/rest": "20.1.1",
83
83
  "async-retry": "1.3.3",
84
84
  "chalk": "5.3.0",
85
+ "ci-info": "^4.0.0",
85
86
  "cosmiconfig": "9.0.0",
86
87
  "execa": "8.0.1",
87
88
  "git-url-parse": "14.0.0",
88
89
  "globby": "14.0.2",
89
- "got": "13.0.0",
90
90
  "inquirer": "9.3.2",
91
- "is-ci": "3.0.1",
92
91
  "issue-parser": "7.0.1",
93
92
  "lodash": "4.17.21",
94
93
  "mime-types": "2.1.35",
95
94
  "new-github-release-url": "2.0.0",
96
- "node-fetch": "3.3.2",
97
95
  "open": "10.1.0",
98
96
  "ora": "8.0.1",
99
97
  "os-name": "5.1.0",
100
98
  "proxy-agent": "6.4.0",
101
99
  "semver": "7.6.2",
102
100
  "shelljs": "0.8.5",
103
- "update-notifier": "7.0.0",
101
+ "update-notifier": "7.1.0",
104
102
  "url-join": "5.0.0",
105
103
  "wildcard-match": "5.1.3",
106
104
  "yargs-parser": "21.1.1"
107
105
  },
108
106
  "devDependencies": {
109
- "@eslint/compat": "1.1.0",
107
+ "@eslint/compat": "1.1.1",
110
108
  "@eslint/eslintrc": "3.1.0",
111
- "@eslint/js": "9.6.0",
109
+ "@eslint/js": "9.7.0",
112
110
  "@octokit/request-error": "5.1.0",
113
- "@types/node": "20.14.9",
111
+ "@types/node": "20.14.10",
114
112
  "ava": "6.1.3",
115
- "eslint": "9.6.0",
113
+ "eslint": "9.7.0",
116
114
  "eslint-config-prettier": "9.1.0",
117
115
  "eslint-plugin-ava": "15.0.1",
118
- "eslint-plugin-import": "2.29.1",
116
+ "eslint-plugin-import-x": "3.0.1",
119
117
  "eslint-plugin-prettier": "5.1.3",
120
118
  "fs-monkey": "1.0.6",
121
- "globals": "15.7.0",
119
+ "globals": "15.8.0",
122
120
  "installed-check": "9.3.0",
123
- "knip": "5.23.2",
121
+ "knip": "5.26.0",
124
122
  "memfs": "4.9.3",
125
123
  "mock-stdio": "1.0.3",
126
- "nock": "13.5.4",
127
- "prettier": "3.3.2",
124
+ "nock": "14.0.0-beta.8",
125
+ "prettier": "3.3.3",
128
126
  "remark-cli": "12.0.1",
129
127
  "remark-preset-webpro": "1.1.0",
130
128
  "sinon": "18.0.0",
131
129
  "strip-ansi": "7.1.0",
132
- "typescript": "5.5.2"
130
+ "typescript": "5.5.3"
133
131
  },
134
132
  "overrides": {
135
133
  "pac-resolver": "7.0.1",
package/schema/git.json CHANGED
@@ -14,7 +14,11 @@
14
14
  "default": true
15
15
  },
16
16
  "requireBranch": {
17
- "type": "boolean",
17
+ "oneOf": [
18
+ { "type": "boolean", "enum": [false] },
19
+ { "type": "string" },
20
+ { "type": "array", "items": { "type": "string" } }
21
+ ],
18
22
  "default": false
19
23
  },
20
24
  "requireUpstream": {
@@ -20,6 +20,10 @@
20
20
  "type": "boolean",
21
21
  "default": false
22
22
  },
23
+ "makeLatest": {
24
+ "anyOf": [{ "type": "boolean" }, { "const": "legacy" }],
25
+ "default": true
26
+ },
23
27
  "preRelease": {
24
28
  "type": "boolean",
25
29
  "default": false
@@ -28,7 +28,7 @@
28
28
  "type": "boolean",
29
29
  "default": false
30
30
  },
31
- "milesstones": {
31
+ "milestones": {
32
32
  "type": "array",
33
33
  "items": {
34
34
  "type": "string"
@@ -46,9 +46,16 @@
46
46
  "certificateAuthorityFile": {
47
47
  "default": null
48
48
  },
49
+ "secure": {
50
+ "default": null
51
+ },
49
52
  "assets": {
50
53
  "default": null
51
54
  },
55
+ "useIdsForUrls": {
56
+ "type": "boolean",
57
+ "default": false
58
+ },
52
59
  "origin": {
53
60
  "type": "string",
54
61
  "default": null
package/test/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import test from 'ava';
2
- import isCI from 'is-ci';
2
+ import { isCI } from 'ci-info';
3
3
  import Config from '../lib/config.js';
4
4
  import { readJSON } from '../lib/util.js';
5
5
 
package/test/github.js CHANGED
@@ -2,6 +2,7 @@ import test from 'ava';
2
2
  import sinon from 'sinon';
3
3
  import { RequestError } from '@octokit/request-error';
4
4
  import GitHub from '../lib/plugin/github/GitHub.js';
5
+ import { getSearchQueries } from '../lib/plugin/github/util.js';
5
6
  import { factory, runTasks } from './util/index.js';
6
7
  import {
7
8
  interceptAuthentication,
@@ -485,3 +486,25 @@ test.skip('should truncate long body', async t => {
485
486
  t.is(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
486
487
  exec.restore();
487
488
  });
489
+
490
+ test('should generate search queries correctly', t => {
491
+ const generateCommit = () => Math.random().toString(36).substring(2, 9);
492
+ const base = 'repo:owner/repo+type:pr+is:merged';
493
+ const commits = Array.from({ length: 5 }, generateCommit);
494
+ const separator = '+';
495
+
496
+ const result = getSearchQueries(base, commits, separator);
497
+
498
+ // Test case 1: Check if all commits are included in the search queries
499
+ const allCommitsIncluded = commits.every(commit => result.some(query => query.includes(commit)));
500
+ t.true(allCommitsIncluded, 'All commits should be included in the search queries');
501
+
502
+ // Test case 2: Check if the function respects the 256 character limit
503
+ const manyCommits = Array.from({ length: 100 }, generateCommit);
504
+ const longResult = getSearchQueries(base, manyCommits, separator);
505
+ t.true(longResult.length > 1, 'Many commits should be split into multiple queries');
506
+ t.true(
507
+ longResult.every(query => encodeURIComponent(query).length <= 256),
508
+ 'Each query should not exceed 256 characters after encoding'
509
+ );
510
+ });
package/test/gitlab.js CHANGED
@@ -1,3 +1,4 @@
1
+ import fs from 'node:fs';
1
2
  import test from 'ava';
2
3
  import sinon from 'sinon';
3
4
  import nock from 'nock';
@@ -110,6 +111,31 @@ test.serial('should upload assets and release', async t => {
110
111
  t.is(releaseUrl, `${pushRepo}/-/releases`);
111
112
  });
112
113
 
114
+ test.serial('should upload assets with ID-based URLs too', async t => {
115
+ const host = 'https://gitlab.com';
116
+ const pushRepo = `${host}/user/repo`;
117
+ const options = {
118
+ git: { pushRepo },
119
+ gitlab: {
120
+ tokenRef,
121
+ release: true,
122
+ assets: 'test/resources/file-v${version}.txt',
123
+ useIdsForUrls: true
124
+ }
125
+ };
126
+ const gitlab = factory(GitLab, { options });
127
+ sinon.stub(gitlab, 'getLatestVersion').resolves('2.0.0');
128
+
129
+ interceptUser();
130
+ interceptCollaborator();
131
+ interceptAsset();
132
+ interceptPublish();
133
+
134
+ await runTasks(gitlab);
135
+
136
+ t.is(gitlab.assets[0].url, `${host}/-/project/1234/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/file-v2.0.1.txt`);
137
+ });
138
+
113
139
  test.serial('should throw when release milestone is missing', async t => {
114
140
  const pushRepo = 'https://gitlab.com/user/repo';
115
141
  const options = {
@@ -225,28 +251,70 @@ test('should not make requests in dry run', async t => {
225
251
  const options = { 'dry-run': true, git: { pushRepo }, gitlab: { releaseName: 'R', tokenRef } };
226
252
  const gitlab = factory(GitLab, { options });
227
253
  sinon.stub(gitlab, 'getLatestVersion').resolves('1.0.0');
228
- const spy = sinon.spy(gitlab, 'client', ['get']);
229
254
 
230
255
  await runTasks(gitlab);
231
256
 
232
257
  const { isReleased, releaseUrl } = gitlab.getContext();
233
- t.is(spy.get.callCount, 0);
258
+
234
259
  t.is(gitlab.log.exec.args[2][0], 'gitlab releases#uploadAssets');
235
260
  t.is(gitlab.log.exec.args[3][0], 'gitlab releases#createRelease "R" (1.0.1)');
236
261
  t.true(isReleased);
237
262
  t.is(releaseUrl, `${pushRepo}/-/releases`);
238
- spy.get.restore();
239
263
  });
240
264
 
241
265
  test('should skip checks', async t => {
242
266
  const options = { gitlab: { tokenRef, skipChecks: true, release: true, milestones: ['v1.0.0'] } };
243
267
  const gitlab = factory(GitLab, { options });
244
- const spy = sinon.spy(gitlab, 'client', ['get']);
245
268
 
246
269
  await t.notThrowsAsync(gitlab.init());
247
270
  await t.notThrowsAsync(gitlab.beforeRelease());
248
271
 
249
- t.is(spy.get.callCount, 0);
250
-
251
272
  t.is(gitlab.log.exec.args.filter(entry => /checkReleaseMilestones/.test(entry[0])).length, 0);
252
273
  });
274
+
275
+ test('should handle certificate authority options', t => {
276
+ const sandbox = sinon.createSandbox();
277
+ sandbox.stub(fs, 'readFileSync').returns('test certificate');
278
+
279
+ {
280
+ const options = { gitlab: {} };
281
+ const gitlab = factory(GitLab, { options });
282
+ t.deepEqual(gitlab.certificateAuthorityOption, {});
283
+ }
284
+
285
+ {
286
+ const options = { gitlab: { certificateAuthorityFile: 'cert.crt' } };
287
+ const gitlab = factory(GitLab, { options });
288
+ t.deepEqual(gitlab.certificateAuthorityOption, { https: { certificateAuthority: 'test certificate' } });
289
+ }
290
+
291
+ {
292
+ const options = { gitlab: { secure: false } };
293
+ const gitlab = factory(GitLab, { options });
294
+ t.deepEqual(gitlab.certificateAuthorityOption, { https: { rejectUnauthorized: false } });
295
+ }
296
+
297
+ {
298
+ const options = { gitlab: { secure: true } };
299
+ const gitlab = factory(GitLab, { options });
300
+ t.deepEqual(gitlab.certificateAuthorityOption, { https: { rejectUnauthorized: true } });
301
+ }
302
+
303
+ {
304
+ const options = { gitlab: { certificateAuthorityFile: 'cert.crt', secure: true } };
305
+ const gitlab = factory(GitLab, { options });
306
+ t.deepEqual(gitlab.certificateAuthorityOption, {
307
+ https: { certificateAuthority: 'test certificate', rejectUnauthorized: true }
308
+ });
309
+ }
310
+
311
+ {
312
+ const options = { gitlab: { certificateAuthorityFile: 'cert.crt', secure: false } };
313
+ const gitlab = factory(GitLab, { options });
314
+ t.deepEqual(gitlab.certificateAuthorityOption, {
315
+ https: { certificateAuthority: 'test certificate', rejectUnauthorized: false }
316
+ });
317
+ }
318
+
319
+ sandbox.restore();
320
+ });
@@ -53,7 +53,8 @@ const interceptCreate = ({
53
53
  body,
54
54
  prerelease,
55
55
  draft,
56
- generate_release_notes
56
+ generate_release_notes,
57
+ make_latest: 'true'
57
58
  })
58
59
  .reply(() => {
59
60
  const id = 1;
@@ -80,7 +81,15 @@ const interceptUpdate = ({
80
81
  body: { tag_name, name = '', body = null, prerelease = false, draft = false, generate_release_notes = false }
81
82
  } = {}) => {
82
83
  nock(api)
83
- .patch(`/repos/${owner}/${project}/releases/1`, { tag_name, name, body, draft, prerelease, generate_release_notes })
84
+ .patch(`/repos/${owner}/${project}/releases/1`, {
85
+ tag_name,
86
+ name,
87
+ body,
88
+ draft,
89
+ prerelease,
90
+ generate_release_notes,
91
+ make_latest: 'true'
92
+ })
84
93
  .reply(200, {
85
94
  id: 1,
86
95
  tag_name,
@@ -39,6 +39,7 @@ export let interceptAsset = ({ host = 'https://gitlab.com', owner = 'user', proj
39
39
  return {
40
40
  alt: name,
41
41
  url: `/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/${name}`,
42
+ full_path: `/-/project/1234/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/${name}`,
42
43
  markdown: `[${name}](/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/${name})`
43
44
  };
44
45
  });
package/types/config.d.ts CHANGED
@@ -132,6 +132,13 @@ export interface Config {
132
132
  /** @default null */
133
133
  proxy?: any;
134
134
 
135
+ /**
136
+ * @default true
137
+ * 'legacy' - Github determines the latest release based on the release creation date and higher semantic version.
138
+ * See https://docs.github.com/en/rest/releases/releases?apiVersion=latest#create-a-release
139
+ */
140
+ makeLatest?: boolean | 'legacy';
141
+
135
142
  /** @default false */
136
143
  skipChecks?: boolean;
137
144
 
@@ -171,9 +178,15 @@ export interface Config {
171
178
  /** @default null */
172
179
  certificateAuthorityFile?: any;
173
180
 
181
+ /** @default null */
182
+ secure?: boolean;
183
+
174
184
  /** @default null */
175
185
  assets?: any;
176
186
 
187
+ /** @default false */
188
+ useIdsForUrls?: boolean;
189
+
177
190
  /** @default null */
178
191
  origin?: any;
179
192