release-it 17.6.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.
@@ -65,6 +65,7 @@
65
65
  "certificateAuthorityFile": null,
66
66
  "secure": null,
67
67
  "assets": null,
68
+ "useIdsForUrls": false,
68
69
  "origin": null,
69
70
  "skipChecks": false
70
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';
@@ -30,22 +28,6 @@ class GitLab extends Release {
30
28
  this.certificateAuthorityOption = _.isEmpty(https) ? {} : { https };
31
29
  }
32
30
 
33
- get client() {
34
- if (this._client) return this._client;
35
- const { tokenHeader } = this.options;
36
- const { baseUrl } = this.getContext();
37
- this._client = got.extend({
38
- prefixUrl: baseUrl,
39
- method: 'POST',
40
- headers: {
41
- 'user-agent': 'webpro/release-it',
42
- [tokenHeader]: this.token
43
- },
44
- ...this.certificateAuthorityOption
45
- });
46
- return this._client;
47
- }
48
-
49
31
  async init() {
50
32
  await super.init();
51
33
 
@@ -179,8 +161,28 @@ class GitLab extends Release {
179
161
  const { baseUrl } = this.getContext();
180
162
  this.debug(Object.assign({ url: `${baseUrl}/${endpoint}` }, options));
181
163
  const method = (options.method || 'POST').toLowerCase();
182
- const response = await this.client[method](endpoint, options);
183
- 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();
184
186
  this.debug(body);
185
187
  return body;
186
188
  }
@@ -235,11 +237,13 @@ class GitLab extends Release {
235
237
 
236
238
  async uploadAsset(filePath) {
237
239
  const name = path.basename(filePath);
240
+ const { useIdsForUrls } = this.options;
238
241
  const { id, origin, repo } = this.getContext();
239
242
  const endpoint = `projects/${id}/uploads`;
240
243
 
241
244
  const body = new FormData();
242
- body.set('file', fileFromSync(filePath));
245
+ const rawData = await fs.promises.readFile(filePath);
246
+ body.set('file', new Blob([rawData]), name);
243
247
  const options = { body };
244
248
 
245
249
  try {
@@ -247,7 +251,7 @@ class GitLab extends Release {
247
251
  this.log.verbose(`gitlab releases#uploadAsset: done (${body.url})`);
248
252
  this.assets.push({
249
253
  name,
250
- url: `${origin}/${repo.repository}${body.url}`
254
+ url: useIdsForUrls ? `${origin}${body.full_path}` : `${origin}/${repo.repository}${body.url}`
251
255
  });
252
256
  } catch (err) {
253
257
  this.debug(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-it",
3
- "version": "17.6.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,18 +82,16 @@
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",
@@ -123,7 +121,7 @@
123
121
  "knip": "5.26.0",
124
122
  "memfs": "4.9.3",
125
123
  "mock-stdio": "1.0.3",
126
- "nock": "13.5.4",
124
+ "nock": "14.0.0-beta.8",
127
125
  "prettier": "3.3.3",
128
126
  "remark-cli": "12.0.1",
129
127
  "remark-preset-webpro": "1.1.0",
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"
@@ -52,6 +52,10 @@
52
52
  "assets": {
53
53
  "default": null
54
54
  },
55
+ "useIdsForUrls": {
56
+ "type": "boolean",
57
+ "default": false
58
+ },
55
59
  "origin": {
56
60
  "type": "string",
57
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
@@ -111,6 +111,31 @@ test.serial('should upload assets and release', async t => {
111
111
  t.is(releaseUrl, `${pushRepo}/-/releases`);
112
112
  });
113
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
+
114
139
  test.serial('should throw when release milestone is missing', async t => {
115
140
  const pushRepo = 'https://gitlab.com/user/repo';
116
141
  const options = {
@@ -226,29 +251,24 @@ test('should not make requests in dry run', async t => {
226
251
  const options = { 'dry-run': true, git: { pushRepo }, gitlab: { releaseName: 'R', tokenRef } };
227
252
  const gitlab = factory(GitLab, { options });
228
253
  sinon.stub(gitlab, 'getLatestVersion').resolves('1.0.0');
229
- const spy = sinon.spy(gitlab, 'client', ['get']);
230
254
 
231
255
  await runTasks(gitlab);
232
256
 
233
257
  const { isReleased, releaseUrl } = gitlab.getContext();
234
- t.is(spy.get.callCount, 0);
258
+
235
259
  t.is(gitlab.log.exec.args[2][0], 'gitlab releases#uploadAssets');
236
260
  t.is(gitlab.log.exec.args[3][0], 'gitlab releases#createRelease "R" (1.0.1)');
237
261
  t.true(isReleased);
238
262
  t.is(releaseUrl, `${pushRepo}/-/releases`);
239
- spy.get.restore();
240
263
  });
241
264
 
242
265
  test('should skip checks', async t => {
243
266
  const options = { gitlab: { tokenRef, skipChecks: true, release: true, milestones: ['v1.0.0'] } };
244
267
  const gitlab = factory(GitLab, { options });
245
- const spy = sinon.spy(gitlab, 'client', ['get']);
246
268
 
247
269
  await t.notThrowsAsync(gitlab.init());
248
270
  await t.notThrowsAsync(gitlab.beforeRelease());
249
271
 
250
- t.is(spy.get.callCount, 0);
251
-
252
272
  t.is(gitlab.log.exec.args.filter(entry => /checkReleaseMilestones/.test(entry[0])).length, 0);
253
273
  });
254
274
 
@@ -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
 
@@ -170,13 +177,16 @@ export interface Config {
170
177
 
171
178
  /** @default null */
172
179
  certificateAuthorityFile?: any;
173
-
180
+
174
181
  /** @default null */
175
182
  secure?: boolean;
176
183
 
177
184
  /** @default null */
178
185
  assets?: any;
179
186
 
187
+ /** @default false */
188
+ useIdsForUrls?: boolean;
189
+
180
190
  /** @default null */
181
191
  origin?: any;
182
192