release-it 19.0.0-next.1 → 19.0.0-next.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/test/github.js CHANGED
@@ -1,6 +1,7 @@
1
- import test from 'ava';
2
- import sinon from 'sinon';
1
+ import test, { describe, before, after, afterEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
3
  import { RequestError } from '@octokit/request-error';
4
+ import { MockServer, FetchMocker } from 'mentoss';
4
5
  import GitHub from '../lib/plugin/github/GitHub.js';
5
6
  import { getSearchQueries } from '../lib/plugin/github/util.js';
6
7
  import { factory, runTasks } from './util/index.js';
@@ -13,535 +14,619 @@ import {
13
14
  interceptAsset
14
15
  } from './stub/github.js';
15
16
 
16
- const tokenRef = 'GITHUB_TOKEN';
17
- const pushRepo = 'git://github.com/user/repo';
18
- const host = 'github.com';
19
- const git = { changelog: '' };
20
- const requestErrorOptions = { request: { url: '', headers: {} }, response: { headers: {} } };
17
+ describe('github', () => {
18
+ const tokenRef = 'GITHUB_TOKEN';
19
+ const pushRepo = 'git://github.com/user/repo';
20
+ const host = 'github.com';
21
+ const git = { changelog: '' };
22
+ const requestErrorOptions = { request: { url: '', headers: {} }, response: { headers: {} } };
21
23
 
22
- test.serial('should check token and perform checks', async t => {
23
- const tokenRef = 'MY_GITHUB_TOKEN';
24
- const options = { github: { release: true, tokenRef, pushRepo } };
25
- const github = factory(GitHub, { options });
24
+ const api = new MockServer('https://api.github.com');
25
+ const assets = new MockServer('https://uploads.github.com');
26
+ const example = new MockServer('https://github.example.org/api/v3');
27
+ const custom = new MockServer('https://custom.example.org/api/v3');
26
28
 
27
- process.env[tokenRef] = '123';
29
+ const mocker = new FetchMocker({
30
+ servers: [api, assets, example, custom]
31
+ });
28
32
 
29
- interceptAuthentication();
30
- interceptCollaborator();
31
- await t.notThrowsAsync(github.init());
32
- });
33
+ before(() => {
34
+ mocker.mockGlobal();
35
+ });
33
36
 
34
- test.serial('should check token and warn', async t => {
35
- const tokenRef = 'MY_GITHUB_TOKEN';
36
- const options = { github: { release: true, tokenRef, pushRepo } };
37
- const github = factory(GitHub, { options });
38
- delete process.env[tokenRef];
37
+ afterEach(() => {
38
+ mocker.clearAll();
39
+ });
39
40
 
40
- await t.notThrowsAsync(github.init());
41
+ after(() => {
42
+ mocker.unmockGlobal();
43
+ });
41
44
 
42
- t.is(github.log.warn.args[0][0], 'Environment variable "MY_GITHUB_TOKEN" is required for automated GitHub Releases.');
43
- t.is(github.log.warn.args[1][0], 'Falling back to web-based GitHub Release.');
44
- });
45
+ test('should check token and perform checks', async () => {
46
+ const tokenRef = 'MY_GITHUB_TOKEN';
47
+ const options = { github: { release: true, tokenRef, pushRepo } };
48
+ const github = factory(GitHub, { options });
45
49
 
46
- test('should release and upload assets', async t => {
47
- const options = {
48
- git,
49
- github: {
50
- pushRepo,
51
- tokenRef,
52
- release: true,
53
- releaseName: 'Release ${tagName}',
54
- releaseNotes: 'echo Custom notes',
55
- assets: 'test/resources/file-v${version}.txt'
56
- }
57
- };
58
- const github = factory(GitHub, { options });
59
- const exec = sinon.stub(github.shell, 'exec').callThrough();
60
- exec.withArgs('git log --pretty=format:"* %s (%h)" ${from}...${to}').resolves('');
61
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('2.0.1');
62
-
63
- interceptAuthentication();
64
- interceptCollaborator();
65
- interceptCreate({ body: { tag_name: '2.0.2', name: 'Release 2.0.2', body: 'Custom notes' } });
66
- interceptAsset({ body: '*' });
67
-
68
- await runTasks(github);
69
-
70
- const { isReleased, releaseUrl } = github.getContext();
71
- t.true(isReleased);
72
- t.is(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
73
- exec.restore();
74
- });
50
+ interceptAuthentication(api);
51
+ interceptCollaborator(api);
52
+ await assert.doesNotReject(github.init());
53
+ });
75
54
 
76
- test('should create a pre-release and draft release notes', async t => {
77
- const options = {
78
- git,
79
- github: {
80
- pushRepo,
81
- tokenRef,
82
- release: true,
83
- releaseName: 'Release ${tagName}',
84
- preRelease: true,
85
- draft: true
86
- }
87
- };
88
- const github = factory(GitHub, { options });
89
- const exec = sinon.stub(github.shell, 'exec').callThrough();
90
- exec.withArgs('git log --pretty=format:"* %s (%h)" ${from}...${to}').resolves('');
91
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('2.0.1');
92
-
93
- interceptAuthentication();
94
- interceptCollaborator();
95
- interceptCreate({ body: { tag_name: '2.0.2', name: 'Release 2.0.2', prerelease: true, draft: true } });
96
-
97
- await runTasks(github);
98
-
99
- const { isReleased, releaseUrl } = github.getContext();
100
- t.true(isReleased);
101
- t.is(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
102
- exec.restore();
103
- });
55
+ test('should check token and warn', async () => {
56
+ const tokenRef = 'MY_GITHUB_TOKEN';
57
+ const options = { github: { release: true, tokenRef, pushRepo } };
58
+ const github = factory(GitHub, { options });
59
+ delete process.env[tokenRef];
104
60
 
105
- test('should create auto-generated release notes', async t => {
106
- const options = {
107
- git,
108
- github: {
109
- pushRepo,
110
- tokenRef,
111
- release: true,
112
- releaseName: 'Release ${tagName}',
113
- autoGenerate: true
114
- }
115
- };
116
- const github = factory(GitHub, { options });
117
- const exec = sinon.stub(github.shell, 'exec').callThrough();
118
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('2.0.1');
61
+ await assert.doesNotReject(github.init());
119
62
 
120
- interceptAuthentication();
121
- interceptCollaborator();
122
- interceptCreate({ body: { tag_name: '2.0.2', name: 'Release 2.0.2', generate_release_notes: true, body: '' } });
63
+ assert.equal(
64
+ github.log.warn.mock.calls[0].arguments[0],
65
+ 'Environment variable "MY_GITHUB_TOKEN" is required for automated GitHub Releases.'
66
+ );
67
+ assert.equal(github.log.warn.mock.calls[1].arguments[0], 'Falling back to web-based GitHub Release.');
68
+ });
123
69
 
124
- await runTasks(github);
70
+ test('should release and upload assets', async t => {
71
+ const options = {
72
+ git,
73
+ github: {
74
+ pushRepo,
75
+ tokenRef,
76
+ release: true,
77
+ releaseName: 'Release ${tagName}',
78
+ releaseNotes: 'echo Custom notes',
79
+ assets: 'test/resources/file-v${version}.txt'
80
+ }
81
+ };
82
+ const github = factory(GitHub, { options });
83
+
84
+ const original = github.shell.exec.bind(github.shell);
85
+ t.mock.method(github.shell, 'exec', (...args) => {
86
+ if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
87
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
88
+ return original(...args);
89
+ });
90
+
91
+ interceptAuthentication(api);
92
+ interceptCollaborator(api);
93
+ interceptCreate(api, { body: { tag_name: '2.0.2', name: 'Release 2.0.2', body: 'Custom notes' } });
94
+ interceptAsset(assets, { body: '*' });
95
+
96
+ await runTasks(github);
97
+
98
+ const { isReleased, releaseUrl } = github.getContext();
99
+ assert(isReleased);
100
+ assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
101
+ });
125
102
 
126
- const { isReleased, releaseUrl } = github.getContext();
127
- t.true(isReleased);
128
- t.is(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
129
- exec.restore();
130
- });
103
+ test('should create a pre-release and draft release notes', async t => {
104
+ const options = {
105
+ git,
106
+ github: {
107
+ pushRepo,
108
+ tokenRef,
109
+ release: true,
110
+ releaseName: 'Release ${tagName}',
111
+ preRelease: true,
112
+ draft: true
113
+ }
114
+ };
115
+ const github = factory(GitHub, { options });
131
116
 
132
- test('should update release and upload assets', async t => {
133
- const asset = 'file1';
134
- const options = {
135
- increment: false,
136
- git,
137
- github: {
138
- update: true,
139
- pushRepo,
140
- tokenRef,
141
- release: true,
142
- releaseName: 'Release ${tagName}',
143
- releaseNotes: 'echo Custom notes',
144
- assets: `test/resources/${asset}`
145
- }
146
- };
147
- const github = factory(GitHub, { options });
148
- const exec = sinon.stub(github.shell, 'exec').callThrough();
149
- exec.withArgs('git log --pretty=format:"* %s (%h)" ${from}...${to}').resolves('');
150
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('2.0.1');
151
- exec.withArgs('git rev-list 2.0.1 --tags --max-count=1').resolves('a123456');
152
- exec.withArgs('git describe --tags --abbrev=0 "a123456^"').resolves('2.0.1');
153
-
154
- interceptAuthentication();
155
- interceptCollaborator();
156
- interceptListReleases({ tag_name: '2.0.1' });
157
- interceptUpdate({ body: { tag_name: '2.0.1', name: 'Release 2.0.1', body: 'Custom notes' } });
158
- interceptAsset({ body: asset });
159
-
160
- await runTasks(github);
161
-
162
- const { isReleased, releaseUrl } = github.getContext();
163
- t.true(isReleased);
164
- t.is(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.1');
165
- exec.restore();
166
- });
117
+ const original = github.shell.exec.bind(github.shell);
118
+ t.mock.method(github.shell, 'exec', (...args) => {
119
+ if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
120
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
121
+ return original(...args);
122
+ });
123
+
124
+ interceptAuthentication(api);
125
+ interceptCollaborator(api);
126
+ interceptCreate(api, { body: { tag_name: '2.0.2', name: 'Release 2.0.2', prerelease: true, draft: true } });
127
+
128
+ await runTasks(github);
129
+
130
+ const { isReleased, releaseUrl } = github.getContext();
131
+ assert(isReleased);
132
+ assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
133
+ });
167
134
 
168
- test('should create custom release notes using releaseNotes function', async t => {
169
- const options = {
170
- git,
171
- github: {
172
- pushRepo,
173
- tokenRef,
174
- release: true,
175
- releaseName: 'Release ${tagName}',
176
- releaseNotes(context) {
177
- return `Custom notes for tag ${context.tagName}`;
135
+ test('should create auto-generated release notes', async t => {
136
+ const options = {
137
+ git,
138
+ github: {
139
+ pushRepo,
140
+ tokenRef,
141
+ release: true,
142
+ releaseName: 'Release ${tagName}',
143
+ autoGenerate: true
178
144
  }
179
- }
180
- };
181
- const github = factory(GitHub, { options });
182
- const exec = sinon.stub(github.shell, 'exec').callThrough();
183
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('2.0.1');
145
+ };
146
+ const github = factory(GitHub, { options });
147
+
148
+ const original = github.shell.exec.bind(github.shell);
149
+ t.mock.method(github.shell, 'exec', (...args) => {
150
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
151
+ return original(...args);
152
+ });
153
+
154
+ interceptAuthentication(api);
155
+ interceptCollaborator(api);
156
+ interceptCreate(api, {
157
+ body: { tag_name: '2.0.2', name: 'Release 2.0.2', generate_release_notes: true, body: '' }
158
+ });
159
+
160
+ await runTasks(github);
161
+
162
+ const { isReleased, releaseUrl } = github.getContext();
163
+ assert(isReleased);
164
+ assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
165
+ });
184
166
 
185
- interceptAuthentication();
186
- interceptCollaborator();
187
- interceptCreate({ body: { tag_name: '2.0.2', name: 'Release 2.0.2', body: 'Custom notes for tag 2.0.2' } });
167
+ test('should update release and upload assets', async t => {
168
+ const asset = 'file1';
169
+ const options = {
170
+ increment: false,
171
+ git,
172
+ github: {
173
+ update: true,
174
+ pushRepo,
175
+ tokenRef,
176
+ release: true,
177
+ releaseName: 'Release ${tagName}',
178
+ releaseNotes: 'echo Custom notes',
179
+ assets: `test/resources/${asset}`
180
+ }
181
+ };
182
+ const github = factory(GitHub, { options });
183
+
184
+ const original = github.shell.exec.bind(github.shell);
185
+ t.mock.method(github.shell, 'exec', (...args) => {
186
+ if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
187
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
188
+ if (args[0] === 'git rev-list 2.0.1 --tags --max-count=1') return Promise.resolve('a123456');
189
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
190
+ return original(...args);
191
+ });
192
+
193
+ interceptAuthentication(api);
194
+ interceptCollaborator(api);
195
+ interceptListReleases(api, { tag_name: '2.0.1' });
196
+ interceptUpdate(api, { body: { tag_name: '2.0.1', name: 'Release 2.0.1', body: 'Custom notes' } });
197
+ interceptAsset(assets, { body: asset });
198
+
199
+ await runTasks(github);
200
+
201
+ const { isReleased, releaseUrl } = github.getContext();
202
+ assert(isReleased);
203
+ assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.1');
204
+ });
188
205
 
189
- await runTasks(github);
206
+ test('should create custom release notes using releaseNotes function', async t => {
207
+ const options = {
208
+ git,
209
+ github: {
210
+ pushRepo,
211
+ tokenRef,
212
+ release: true,
213
+ releaseName: 'Release ${tagName}',
214
+ releaseNotes(context) {
215
+ return `Custom notes for tag ${context.tagName}`;
216
+ }
217
+ }
218
+ };
219
+ const github = factory(GitHub, { options });
220
+
221
+ const original = github.shell.exec.bind(github.shell);
222
+ t.mock.method(github.shell, 'exec', (...args) => {
223
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
224
+ return original(...args);
225
+ });
226
+
227
+ interceptAuthentication(api);
228
+ interceptCollaborator(api);
229
+ interceptCreate(api, {
230
+ body: { tag_name: '2.0.2', name: 'Release 2.0.2', body: 'Custom notes for tag 2.0.2' }
231
+ });
232
+
233
+ await runTasks(github);
234
+
235
+ const { isReleased, releaseUrl } = github.getContext();
236
+ assert(isReleased);
237
+ assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
238
+ });
190
239
 
191
- const { isReleased, releaseUrl } = github.getContext();
192
- t.true(isReleased);
193
- t.is(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
194
- exec.restore();
195
- });
240
+ test('should create new release for unreleased tag', async t => {
241
+ const options = {
242
+ increment: false,
243
+ git,
244
+ github: {
245
+ update: true,
246
+ pushRepo,
247
+ tokenRef,
248
+ release: true,
249
+ releaseName: 'Release ${tagName}',
250
+ releaseNotes: 'echo Custom notes'
251
+ }
252
+ };
253
+ const github = factory(GitHub, { options });
254
+
255
+ const original = github.shell.exec.bind(github.shell);
256
+ t.mock.method(github.shell, 'exec', (...args) => {
257
+ if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
258
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
259
+ if (args[0] === 'git rev-list 2.0.1 --tags --max-count=1') return Promise.resolve('b123456');
260
+ if (args[0] === 'git describe --tags --abbrev=0 "b123456^"') return Promise.resolve('2.0.1');
261
+ return original(...args);
262
+ });
263
+
264
+ interceptAuthentication(api);
265
+ interceptCollaborator(api);
266
+ interceptListReleases(api, { tag_name: '2.0.0' });
267
+ interceptCreate(api, { body: { tag_name: '2.0.1', name: 'Release 2.0.1', body: 'Custom notes' } });
268
+
269
+ await runTasks(github);
270
+
271
+ const { isReleased, releaseUrl } = github.getContext();
272
+ assert(isReleased);
273
+ assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.1');
274
+ });
196
275
 
197
- test('should create new release for unreleased tag', async t => {
198
- const options = {
199
- increment: false,
200
- git,
201
- github: {
202
- update: true,
203
- pushRepo,
204
- tokenRef,
205
- release: true,
206
- releaseName: 'Release ${tagName}',
207
- releaseNotes: 'echo Custom notes'
208
- }
209
- };
210
- const github = factory(GitHub, { options });
211
- const exec = sinon.stub(github.shell, 'exec').callThrough();
212
- exec.withArgs('git log --pretty=format:"* %s (%h)" ${from}...${to}').resolves('');
213
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('2.0.1');
214
- exec.withArgs('git rev-list 2.0.1 --tags --max-count=1').resolves('b123456');
215
- exec.withArgs('git describe --tags --abbrev=0 "b123456^"').resolves('2.0.1');
216
-
217
- interceptAuthentication();
218
- interceptCollaborator();
219
- interceptListReleases({ tag_name: '2.0.0' });
220
- interceptCreate({ body: { tag_name: '2.0.1', name: 'Release 2.0.1', body: 'Custom notes' } });
221
-
222
- await runTasks(github);
223
-
224
- const { isReleased, releaseUrl } = github.getContext();
225
- t.true(isReleased);
226
- t.is(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.1');
227
- exec.restore();
228
- });
276
+ test('should release to enterprise host', async t => {
277
+ const options = { git, github: { tokenRef, pushRepo: 'git://github.example.org/user/repo' } };
278
+ const github = factory(GitHub, { options });
279
+
280
+ const original = github.shell.exec.bind(github.shell);
281
+ t.mock.method(github.shell, 'exec', (...args) => {
282
+ if (args[0] === 'git remote get-url origin') return Promise.resolve(`https://github.example.org/user/repo`);
283
+ if (args[0] === 'git config --get remote.origin.url')
284
+ return Promise.resolve('https://github.example.org/user/repo');
285
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('1.0.0');
286
+ return original(...args);
287
+ });
288
+
289
+ interceptAuthentication(example);
290
+ interceptCollaborator(example);
291
+ interceptCreate(example, {
292
+ api: 'https://github.example.org/api/v3',
293
+ host: 'github.example.org',
294
+ body: { tag_name: '1.0.1' }
295
+ });
296
+
297
+ await runTasks(github);
298
+
299
+ const { isReleased, releaseUrl } = github.getContext();
300
+ assert(isReleased);
301
+ assert.equal(releaseUrl, 'https://github.example.org/user/repo/releases/tag/1.0.1');
302
+ });
229
303
 
230
- test('should release to enterprise host', async t => {
231
- const options = { git, github: { tokenRef, pushRepo: 'git://github.example.org/user/repo' } };
232
- const github = factory(GitHub, { options });
233
- const exec = sinon.stub(github.shell, 'exec').callThrough();
234
- exec.withArgs('git remote get-url origin').resolves(`https://github.example.org/user/repo`);
235
- exec.withArgs('git config --get remote.origin.url').resolves(`https://github.example.org/user/repo`);
236
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves(`1.0.0`);
237
-
238
- const remote = { api: 'https://github.example.org/api/v3', host: 'github.example.org' };
239
- interceptAuthentication(remote);
240
- interceptCollaborator(remote);
241
- interceptCreate(Object.assign({ body: { tag_name: '1.0.1' } }, remote));
242
-
243
- await runTasks(github);
244
-
245
- const { isReleased, releaseUrl } = github.getContext();
246
- t.true(isReleased);
247
- t.is(releaseUrl, `https://github.example.org/user/repo/releases/tag/1.0.1`);
248
- exec.restore();
249
- });
304
+ test('should release to alternative host and proxy', async t => {
305
+ const options = {
306
+ git,
307
+ github: {
308
+ tokenRef,
309
+ pushRepo: `git://custom.example.org/user/repo`,
310
+ host: 'custom.example.org',
311
+ proxy: 'http://proxy:8080'
312
+ }
313
+ };
314
+ const github = factory(GitHub, { options });
315
+
316
+ const original = github.shell.exec.bind(github.shell);
317
+ t.mock.method(github.shell, 'exec', (...args) => {
318
+ if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
319
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('1.0.0');
320
+ return original(...args);
321
+ });
322
+
323
+ interceptAuthentication(custom);
324
+ interceptCollaborator(custom);
325
+ interceptCreate(custom, {
326
+ api: 'https://custom.example.org/api/v3',
327
+ host: 'custom.example.org',
328
+ body: { tag_name: '1.0.1' }
329
+ });
330
+
331
+ await runTasks(github);
250
332
 
251
- test('should release to alternative host and proxy', async t => {
252
- const remote = { api: 'https://custom.example.org/api/v3', host: 'custom.example.org' };
253
- interceptAuthentication(remote);
254
- interceptCollaborator(remote);
255
- interceptCreate(Object.assign({ body: { tag_name: '1.0.1' } }, remote));
256
- const options = {
257
- git,
258
- github: {
259
- tokenRef,
260
- pushRepo: `git://custom.example.org/user/repo`,
333
+ const { isReleased, releaseUrl } = github.getContext();
334
+ assert(isReleased);
335
+ assert.equal(releaseUrl, 'https://custom.example.org/user/repo/releases/tag/1.0.1');
336
+ assert.equal(github.options.proxy, 'http://proxy:8080');
337
+ });
338
+
339
+ test('should release to git.pushRepo', async t => {
340
+ const options = { git: { pushRepo: 'upstream', changelog: '' }, github: { tokenRef, skipChecks: true } };
341
+ const github = factory(GitHub, { options });
342
+
343
+ const original = github.shell.exec.bind(github.shell);
344
+ t.mock.method(github.shell, 'exec', (...args) => {
345
+ if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
346
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('1.0.0');
347
+ if (args[0] === 'git remote get-url upstream') return Promise.resolve('https://custom.example.org/user/repo');
348
+ return original(...args);
349
+ });
350
+
351
+ interceptCreate(custom, {
352
+ api: 'https://custom.example.org/api/v3',
261
353
  host: 'custom.example.org',
262
- proxy: 'http://proxy:8080'
263
- }
264
- };
265
- const github = factory(GitHub, { options });
266
- const exec = sinon.stub(github.shell, 'exec').callThrough();
267
- exec.withArgs('git log --pretty=format:"* %s (%h)" ${from}...${to}').resolves('');
268
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('1.0.0');
269
-
270
- await runTasks(github);
271
-
272
- const { isReleased, releaseUrl } = github.getContext();
273
- t.true(isReleased);
274
- t.is(releaseUrl, `https://custom.example.org/user/repo/releases/tag/1.0.1`);
275
- t.is(github.options.proxy, 'http://proxy:8080');
276
- exec.restore();
277
- });
354
+ body: { tag_name: '1.0.1' }
355
+ });
278
356
 
279
- test('should release to git.pushRepo', async t => {
280
- const remote = { api: 'https://custom.example.org/api/v3', host: 'custom.example.org' };
281
- interceptCreate(Object.assign({ body: { tag_name: '1.0.1' } }, remote));
282
- const options = { git: { pushRepo: 'upstream', changelog: '' }, github: { tokenRef, skipChecks: true } };
283
- const github = factory(GitHub, { options });
284
- const exec = sinon.stub(github.shell, 'exec').callThrough();
285
- exec.withArgs('git log --pretty=format:"* %s (%h)" ${from}...${to}').resolves('');
286
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('1.0.0');
287
- exec.withArgs('git remote get-url upstream').resolves('https://custom.example.org/user/repo');
288
-
289
- await runTasks(github);
290
-
291
- const { isReleased, releaseUrl } = github.getContext();
292
- t.true(isReleased);
293
- t.is(releaseUrl, 'https://custom.example.org/user/repo/releases/tag/1.0.1');
294
- exec.restore();
295
- });
357
+ await runTasks(github);
358
+
359
+ const { isReleased, releaseUrl } = github.getContext();
360
+ assert(isReleased);
361
+ assert.equal(releaseUrl, 'https://custom.example.org/user/repo/releases/tag/1.0.1');
362
+ });
363
+
364
+ const testSkipOnActions = process.env.GITHUB_ACTIONS ? test.skip : test;
365
+
366
+ testSkipOnActions('should throw for unauthenticated user', async t => {
367
+ const options = { github: { tokenRef, pushRepo, host } };
368
+ const github = factory(GitHub, { options });
296
369
 
297
- const testSkipOnActions = process.env.GITHUB_ACTIONS ? test.skip : test;
370
+ const getAuthenticated = t.mock.method(github.client.users, 'getAuthenticated', () => {
371
+ throw new RequestError('Bad credentials', 401, requestErrorOptions);
372
+ });
298
373
 
299
- testSkipOnActions('should throw for unauthenticated user', async t => {
300
- const options = { github: { tokenRef, pushRepo, host } };
301
- const github = factory(GitHub, { options });
302
- const stub = sinon.stub(github.client.users, 'getAuthenticated');
303
- stub.throws(new RequestError('Bad credentials', 401, requestErrorOptions));
374
+ await assert.rejects(runTasks(github), {
375
+ message: /Could not authenticate with GitHub using environment variable "GITHUB_TOKEN"/
376
+ });
304
377
 
305
- await t.throwsAsync(runTasks(github), {
306
- message: /^Could not authenticate with GitHub using environment variable "GITHUB_TOKEN"/
378
+ assert.equal(getAuthenticated.mock.callCount(), 1);
307
379
  });
308
380
 
309
- t.is(stub.callCount, 1);
310
- stub.restore();
311
- });
381
+ testSkipOnActions('should throw for non-collaborator', async t => {
382
+ const options = { github: { tokenRef, pushRepo, host } };
383
+ const github = factory(GitHub, { options });
312
384
 
313
- testSkipOnActions('should throw for non-collaborator', async t => {
314
- interceptAuthentication({ username: 'john' });
315
- const options = { github: { tokenRef, pushRepo, host } };
316
- const github = factory(GitHub, { options });
317
- const stub = sinon.stub(github.client.repos, 'checkCollaborator');
318
- stub.throws(new RequestError('HttpError', 401, requestErrorOptions));
385
+ t.mock.method(github.client.repos, 'checkCollaborator', () => {
386
+ throw new RequestError('HttpError', 401, requestErrorOptions);
387
+ });
319
388
 
320
- await t.throwsAsync(runTasks(github), { message: /^User john is not a collaborator for user\/repo/ });
389
+ interceptAuthentication(api, { username: 'john' });
321
390
 
322
- stub.restore();
323
- });
391
+ await assert.rejects(runTasks(github), /User john is not a collaborator for user\/repo/);
392
+ });
324
393
 
325
- test.serial('should skip authentication and collaborator checks when running on GitHub Actions', async t => {
326
- const { GITHUB_ACTIONS, GITHUB_ACTOR } = process.env;
327
- if (!GITHUB_ACTIONS) {
328
- process.env.GITHUB_ACTIONS = 1;
329
- process.env.GITHUB_ACTOR = 'webpro';
330
- }
394
+ test('should skip authentication and collaborator checks when running on GitHub Actions', async t => {
395
+ const { GITHUB_ACTIONS, GITHUB_ACTOR } = process.env;
396
+ if (!GITHUB_ACTIONS) {
397
+ process.env.GITHUB_ACTIONS = 1;
398
+ process.env.GITHUB_ACTOR = 'webpro';
399
+ }
331
400
 
332
- const options = { github: { tokenRef } };
333
- const github = factory(GitHub, { options });
334
- const authStub = sinon.stub(github, 'isAuthenticated');
335
- const collaboratorStub = sinon.stub(github, 'isCollaborator');
401
+ const options = { github: { tokenRef } };
402
+ const github = factory(GitHub, { options });
403
+ const authStub = t.mock.method(github, 'isAuthenticated');
404
+ const collaboratorStub = t.mock.method(github, 'isCollaborator');
336
405
 
337
- await t.notThrowsAsync(github.init());
406
+ await assert.doesNotReject(github.init());
338
407
 
339
- t.is(authStub.callCount, 0);
340
- t.is(collaboratorStub.callCount, 0);
341
- t.is(github.getContext('username'), process.env.GITHUB_ACTOR);
408
+ assert.equal(authStub.mock.callCount(), 0);
409
+ assert.equal(collaboratorStub.mock.callCount(), 0);
410
+ assert.equal(github.getContext('username'), process.env.GITHUB_ACTOR);
342
411
 
343
- authStub.restore();
344
- collaboratorStub.restore();
412
+ if (!GITHUB_ACTIONS) {
413
+ process.env.GITHUB_ACTIONS = GITHUB_ACTIONS || '';
414
+ process.env.GITHUB_ACTOR = GITHUB_ACTOR || '';
415
+ }
416
+ });
345
417
 
346
- if (!GITHUB_ACTIONS) {
347
- process.env.GITHUB_ACTIONS = GITHUB_ACTIONS || '';
348
- process.env.GITHUB_ACTOR = GITHUB_ACTOR || '';
349
- }
350
- });
418
+ test('should handle octokit client error (without retries)', async t => {
419
+ const github = factory(GitHub, { options: { github: { tokenRef, pushRepo, host } } });
420
+ const createRelease = t.mock.method(github.client.repos, 'createRelease', () => {
421
+ throw new RequestError('Not found', 404, requestErrorOptions);
422
+ });
351
423
 
352
- test('should handle octokit client error (without retries)', async t => {
353
- const github = factory(GitHub, { options: { github: { tokenRef, pushRepo, host } } });
354
- const stub = sinon.stub(github.client.repos, 'createRelease');
355
- stub.throws(new RequestError('Not found', 404, requestErrorOptions));
356
- interceptAuthentication();
357
- interceptCollaborator();
424
+ interceptAuthentication(api);
425
+ interceptCollaborator(api);
358
426
 
359
- await t.throwsAsync(runTasks(github), { message: /^404 \(Not found\)/ });
427
+ await assert.rejects(runTasks(github), /404 \(Not found\)/);
360
428
 
361
- t.is(stub.callCount, 1);
362
- stub.restore();
363
- });
429
+ assert.equal(createRelease.mock.callCount(), 1);
430
+ });
364
431
 
365
- test('should handle octokit client error (with retries)', async t => {
366
- const options = { github: { tokenRef, pushRepo, host, retryMinTimeout: 0 } };
367
- const github = factory(GitHub, { options });
368
- const stub = sinon.stub(github.client.repos, 'createRelease');
369
- stub.throws(new RequestError('Request failed', 500, requestErrorOptions));
370
- interceptAuthentication();
371
- interceptCollaborator();
432
+ test('should handle octokit client error (with retries)', async t => {
433
+ const options = { github: { tokenRef, pushRepo, host, retryMinTimeout: 0 } };
434
+ const github = factory(GitHub, { options });
372
435
 
373
- await t.throwsAsync(runTasks(github), { message: /^500 \(Request failed\)/ });
436
+ const createRelease = t.mock.method(github.client.repos, 'createRelease', () => {
437
+ throw new RequestError('Request failed', 500, requestErrorOptions);
438
+ });
374
439
 
375
- t.is(stub.callCount, 3);
376
- stub.restore();
377
- });
440
+ interceptAuthentication(api);
441
+ interceptCollaborator(api);
378
442
 
379
- test('should not call octokit client in dry run', async t => {
380
- const options = { 'dry-run': true, git, github: { tokenRef, pushRepo, releaseName: 'R ${version}', assets: ['*'] } };
381
- const github = factory(GitHub, { options });
382
- const spy = sinon.spy(github, 'client', ['get']);
383
- const exec = sinon.stub(github.shell, 'exec').callThrough();
384
- exec.withArgs('git log --pretty=format:"* %s (%h)" ${from}...${to}').resolves('');
385
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('v1.0.0');
386
-
387
- await runTasks(github);
388
-
389
- t.is(spy.get.callCount, 0);
390
- t.is(github.log.exec.args[1][0], 'octokit repos.createRelease "R 1.0.1" (v1.0.1)');
391
- t.is(github.log.exec.lastCall.args[0], 'octokit repos.uploadReleaseAssets');
392
- const { isReleased, releaseUrl } = github.getContext();
393
- t.true(isReleased);
394
- t.is(releaseUrl, 'https://github.com/user/repo/releases/tag/v1.0.1');
395
- spy.get.restore();
396
- exec.restore();
397
- });
443
+ await assert.rejects(runTasks(github), /500 \(Request failed\)/);
398
444
 
399
- test('should skip checks', async t => {
400
- const options = { github: { tokenRef, skipChecks: true } };
401
- const github = factory(GitHub, { options });
402
- await t.notThrowsAsync(github.init());
403
- });
445
+ assert.equal(createRelease.mock.callCount(), 3);
446
+ });
404
447
 
405
- test('should generate GitHub web release url', async t => {
406
- const options = {
407
- github: {
408
- pushRepo,
409
- release: true,
410
- web: true,
411
- releaseName: 'Release ${tagName}',
412
- releaseNotes: 'echo Custom notes'
413
- }
414
- };
415
- const github = factory(GitHub, { options });
416
- const exec = sinon.stub(github.shell, 'exec').callThrough();
417
- exec.withArgs('git log --pretty=format:"* %s (%h)" ${from}...${to}').resolves('');
418
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('2.0.1');
419
-
420
- await runTasks(github);
421
-
422
- const { isReleased, releaseUrl } = github.getContext();
423
- t.true(isReleased);
424
- t.is(
425
- releaseUrl,
426
- 'https://github.com/user/repo/releases/new?tag=2.0.2&title=Release+2.0.2&body=Custom+notes&prerelease=false'
427
- );
428
- exec.restore();
429
- });
448
+ test('should not call octokit client in dry run', async t => {
449
+ const options = {
450
+ 'dry-run': true,
451
+ git,
452
+ github: { tokenRef, pushRepo, releaseName: 'R ${version}', assets: ['*'] }
453
+ };
454
+ const github = factory(GitHub, { options });
455
+
456
+ const get = t.mock.getter(github, 'client');
457
+ const original = github.shell.exec.bind(github.shell);
458
+ t.mock.method(github.shell, 'exec', (...args) => {
459
+ if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
460
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('v1.0.0');
461
+ return original(...args);
462
+ });
463
+
464
+ await runTasks(github);
465
+
466
+ assert.equal(get.mock.callCount(), 0);
467
+ assert.equal(github.log.exec.mock.calls[1].arguments[0], 'octokit repos.createRelease "R 1.0.1" (v1.0.1)');
468
+ assert.equal(github.log.exec.mock.calls.at(-1).arguments[0], 'octokit repos.uploadReleaseAssets');
469
+ const { isReleased, releaseUrl } = github.getContext();
470
+ assert(isReleased);
471
+ assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/v1.0.1');
472
+ });
430
473
 
431
- test('should generate GitHub web release url for enterprise host', async t => {
432
- const options = {
433
- git,
434
- github: {
435
- pushRepo: 'git@custom.example.org:user/repo',
436
- release: true,
437
- web: true,
438
- host: 'custom.example.org',
439
- releaseName: 'The Launch',
440
- releaseNotes: 'echo It happened'
441
- }
442
- };
443
- const github = factory(GitHub, { options });
444
- const exec = sinon.stub(github.shell, 'exec').callThrough();
445
- exec.withArgs('git log --pretty=format:"* %s (%h)" ${from}...${to}').resolves('');
446
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('2.0.1');
447
-
448
- await runTasks(github);
449
-
450
- const { isReleased, releaseUrl } = github.getContext();
451
- t.true(isReleased);
452
- t.is(
453
- releaseUrl,
454
- 'https://custom.example.org/user/repo/releases/new?tag=2.0.2&title=The+Launch&body=It+happened&prerelease=false'
455
- );
456
- exec.restore();
457
- });
474
+ test('should generate GitHub web release url', async t => {
475
+ const options = {
476
+ github: {
477
+ pushRepo,
478
+ release: true,
479
+ web: true,
480
+ releaseName: 'Release ${tagName}',
481
+ releaseNotes: 'echo Custom notes'
482
+ }
483
+ };
484
+ const github = factory(GitHub, { options });
485
+
486
+ const original = github.shell.exec.bind(github.shell);
487
+ t.mock.method(github.shell, 'exec', (...args) => {
488
+ if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
489
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
490
+ return original(...args);
491
+ });
492
+
493
+ await runTasks(github);
494
+
495
+ const { isReleased, releaseUrl } = github.getContext();
496
+ assert(isReleased);
497
+ assert.equal(
498
+ releaseUrl,
499
+ 'https://github.com/user/repo/releases/new?tag=2.0.2&title=Release+2.0.2&body=Custom+notes&prerelease=false'
500
+ );
501
+ });
458
502
 
459
- // eslint-disable-next-line ava/no-skip-test
460
- test.skip('should truncate long body', async t => {
461
- const releaseNotes = 'a'.repeat(125001);
462
- const body = 'a'.repeat(124000) + '...';
463
- const options = {
464
- git,
465
- github: {
466
- pushRepo,
467
- tokenRef,
468
- release: true,
469
- releaseName: 'Release ${tagName}',
470
- releaseNotes: 'echo ' + releaseNotes
471
- }
472
- };
473
- const github = factory(GitHub, { options });
474
- const exec = sinon.stub(github.shell, 'exec').callThrough();
475
- exec.withArgs('git log --pretty=format:"* %s (%h)" ${from}...${to}').resolves('');
476
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('2.0.1');
477
-
478
- interceptAuthentication();
479
- interceptCollaborator();
480
- interceptCreate({ body: { tag_name: '2.0.2', name: 'Release 2.0.2', body } });
481
-
482
- await runTasks(github);
483
-
484
- const { isReleased, releaseUrl } = github.getContext();
485
- t.true(isReleased);
486
- t.is(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
487
- exec.restore();
488
- });
503
+ test('should generate GitHub web release url for enterprise host', async t => {
504
+ const options = {
505
+ git,
506
+ github: {
507
+ pushRepo: 'git@custom.example.org:user/repo',
508
+ release: true,
509
+ web: true,
510
+ host: 'custom.example.org',
511
+ releaseName: 'The Launch',
512
+ releaseNotes: 'echo It happened'
513
+ }
514
+ };
515
+ const github = factory(GitHub, { options });
516
+
517
+ const original = github.shell.exec.bind(github.shell);
518
+ t.mock.method(github.shell, 'exec', (...args) => {
519
+ if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
520
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
521
+ return original(...args);
522
+ });
523
+
524
+ await runTasks(github);
525
+
526
+ const { isReleased, releaseUrl } = github.getContext();
527
+ assert(isReleased);
528
+ assert.equal(
529
+ releaseUrl,
530
+ 'https://custom.example.org/user/repo/releases/new?tag=2.0.2&title=The+Launch&body=It+happened&prerelease=false'
531
+ );
532
+ });
489
533
 
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
- });
534
+ test.skip('should truncate long body', async t => {
535
+ const releaseNotes = 'a'.repeat(125001);
536
+ const body = 'a'.repeat(124000) + '...';
537
+ const options = {
538
+ git,
539
+ github: {
540
+ pushRepo,
541
+ tokenRef,
542
+ release: true,
543
+ releaseName: 'Release ${tagName}',
544
+ releaseNotes: 'echo ' + releaseNotes
545
+ }
546
+ };
547
+ const github = factory(GitHub, { options });
511
548
 
512
- test('should create auto-generated discussion', async t => {
513
- const options = {
514
- git,
515
- github: {
516
- pushRepo,
517
- tokenRef,
518
- release: true,
519
- releaseName: 'Release ${tagName}',
520
- autoGenerate: false,
521
- discussionCategoryName: 'Announcement'
522
- }
523
- };
524
- const github = factory(GitHub, { options });
525
- const exec = sinon.stub(github.shell, 'exec').callThrough();
526
- exec.withArgs('git describe --tags --match=* --abbrev=0').resolves('2.0.1');
527
-
528
- interceptAuthentication();
529
- interceptCollaborator();
530
- interceptCreate({
531
- body: {
532
- tag_name: '2.0.2',
533
- name: 'Release 2.0.2',
534
- generate_release_notes: false,
535
- body: null,
536
- discussion_category_name: 'Announcement'
537
- }
549
+ const original = github.shell.exec.bind(github.shell);
550
+ t.mock.method(github.shell, 'exec', (...args) => {
551
+ if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
552
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
553
+ return original(...args);
554
+ });
555
+
556
+ interceptAuthentication(api);
557
+ interceptCollaborator(api);
558
+ interceptCreate(api, { body: { tag_name: '2.0.2', name: 'Release 2.0.2', body } });
559
+
560
+ await runTasks(github);
561
+
562
+ const { isReleased, releaseUrl } = github.getContext();
563
+ assert(isReleased);
564
+ assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
538
565
  });
539
566
 
540
- await runTasks(github);
567
+ test('should generate search queries correctly', () => {
568
+ const generateCommit = () => Math.random().toString(36).substring(2, 9);
569
+ const base = 'repo:owner/repo+type:pr+is:merged';
570
+ const commits = Array.from({ length: 5 }, generateCommit);
571
+ const separator = '+';
572
+
573
+ const result = getSearchQueries(base, commits, separator);
574
+
575
+ // Test case 1: Check if all commits are included in the search queries
576
+ const allCommitsIncluded = commits.every(commit => result.some(query => query.includes(commit)));
577
+ assert(allCommitsIncluded, 'All commits should be included in the search queries');
578
+
579
+ assert.equal(
580
+ commits.every(commit => result.some(query => query.includes(commit))),
581
+ true
582
+ );
583
+
584
+ // Test case 2: Check if the function respects the 256 character limit
585
+ const manyCommits = Array.from({ length: 100 }, generateCommit);
586
+ const longResult = getSearchQueries(base, manyCommits, separator);
587
+ assert(longResult.length > 1, 'Many commits should be split into multiple queries');
588
+ assert(
589
+ longResult.every(query => encodeURIComponent(query).length <= 256),
590
+ 'Each query should not exceed 256 characters after encoding'
591
+ );
592
+ });
541
593
 
542
- const { isReleased, releaseUrl, discussionUrl } = github.getContext();
543
- t.true(isReleased);
544
- t.is(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
545
- t.is(discussionUrl, 'https://github.com/user/repo/discussions/1');
546
- exec.restore();
594
+ test('should create auto-generated discussion', async t => {
595
+ const options = {
596
+ git,
597
+ github: {
598
+ pushRepo,
599
+ tokenRef,
600
+ release: true,
601
+ releaseName: 'Release ${tagName}',
602
+ autoGenerate: false,
603
+ discussionCategoryName: 'Announcement'
604
+ }
605
+ };
606
+ const github = factory(GitHub, { options });
607
+ const original = github.shell.exec.bind(github.shell);
608
+ t.mock.method(github.shell, 'exec', (...args) => {
609
+ if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
610
+ return original(...args);
611
+ });
612
+
613
+ interceptAuthentication(api);
614
+ interceptCollaborator(api);
615
+ interceptCreate(api, {
616
+ body: {
617
+ tag_name: '2.0.2',
618
+ name: 'Release 2.0.2',
619
+ generate_release_notes: false,
620
+ body: null,
621
+ discussion_category_name: 'Announcement'
622
+ }
623
+ });
624
+
625
+ await runTasks(github);
626
+
627
+ const { isReleased, releaseUrl, discussionUrl } = github.getContext();
628
+ assert(isReleased);
629
+ assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
630
+ assert.equal(discussionUrl, 'https://github.com/user/repo/discussions/1');
631
+ });
547
632
  });