@redocly/cli 1.11.0 → 1.12.1

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.
@@ -48,33 +48,42 @@ export type Remote = {
48
48
  export type PushResponse = {
49
49
  id: string;
50
50
  remoteId: string;
51
+ isMainBranch: boolean;
52
+ isOutdated: boolean;
53
+ hasChanges: boolean;
54
+ replace: boolean;
55
+ scoutJobId: string | null;
56
+ uploadedFiles: Array<{ path: string; mimeType: string }>;
51
57
  commit: {
52
- message: string;
53
58
  branchName: string;
54
- sha: string | null;
55
- url: string | null;
59
+ message: string;
56
60
  createdAt: string | null;
57
- namespace: string | null;
58
- repository: string | null;
61
+ namespaceId: string | null;
62
+ repositoryId: string | null;
63
+ url: string | null;
64
+ sha: string | null;
59
65
  author: {
60
66
  name: string;
61
67
  email: string;
62
68
  image: string | null;
63
69
  };
70
+ statuses: Array<{
71
+ name: string;
72
+ description: string;
73
+ status: 'pending' | 'running' | 'success' | 'failed';
74
+ url: string | null;
75
+ }>;
64
76
  };
65
77
  remote: {
66
78
  commits: {
67
- branchName: string;
68
79
  sha: string;
80
+ branchName: string;
69
81
  }[];
70
82
  };
71
- hasChanges: boolean;
72
- isOutdated: boolean;
73
- isMainBranch: boolean;
74
83
  status: PushStatusResponse;
75
84
  };
76
85
 
77
- type DeploymentStatusResponse = {
86
+ export type DeploymentStatusResponse = {
78
87
  deploy: {
79
88
  url: string | null;
80
89
  status: DeploymentStatus;
@@ -96,6 +105,4 @@ export type ScorecardItem = {
96
105
 
97
106
  export type PushStatusBase = 'pending' | 'success' | 'running' | 'failed';
98
107
 
99
- // export type BuildStatus = PushStatusBase | 'NOT_STARTED' | 'QUEUED';
100
-
101
108
  export type DeploymentStatus = 'skipped' | PushStatusBase;
@@ -1,14 +1,11 @@
1
1
  import { handlePushStatus } from '../push-status';
2
2
  import { PushResponse } from '../../api/types';
3
- import { exitWithError } from '../../../utils/miscellaneous';
4
3
 
5
4
  const remotes = {
6
5
  getPush: jest.fn(),
7
6
  getRemotesList: jest.fn(),
8
7
  };
9
8
 
10
- jest.mock('../../../utils/miscellaneous');
11
-
12
9
  jest.mock('colorette', () => ({
13
10
  green: (str: string) => str,
14
11
  yellow: (str: string) => str,
@@ -25,28 +22,57 @@ jest.mock('../../api', () => ({
25
22
  }),
26
23
  }));
27
24
 
25
+ jest.mock('@redocly/openapi-core', () => ({
26
+ pause: jest.requireActual('@redocly/openapi-core').pause,
27
+ }));
28
+
28
29
  describe('handlePushStatus()', () => {
29
30
  const mockConfig = { apis: {} } as any;
30
31
 
31
- const pushResponseStub = {
32
+ const commitStub: PushResponse['commit'] = {
33
+ message: 'test-commit-message',
34
+ branchName: 'test-branch-name',
35
+ sha: null,
36
+ url: null,
37
+ createdAt: null,
38
+ namespaceId: null,
39
+ repositoryId: null,
40
+ author: {
41
+ name: 'test-author-name',
42
+ email: 'test-author-email',
43
+ image: null,
44
+ },
45
+ statuses: [],
46
+ };
47
+
48
+ const pushResponseStub: PushResponse = {
49
+ id: 'test-push-id',
50
+ remoteId: 'test-remote-id',
51
+ replace: false,
52
+ scoutJobId: null,
53
+ uploadedFiles: [],
54
+ commit: commitStub,
55
+ remote: { commits: [] },
56
+ isOutdated: false,
57
+ isMainBranch: false,
32
58
  hasChanges: true,
33
59
  status: {
34
60
  preview: {
35
61
  scorecard: [],
36
62
  deploy: {
37
- url: 'https://test-url',
63
+ url: 'https://preview-test-url',
38
64
  status: 'success',
39
65
  },
40
66
  },
41
67
  production: {
42
68
  scorecard: [],
43
69
  deploy: {
44
- url: 'https://test-url',
70
+ url: 'https://production-test-url',
45
71
  status: 'success',
46
72
  },
47
73
  },
48
74
  },
49
- } as unknown as PushResponse;
75
+ };
50
76
 
51
77
  beforeEach(() => {
52
78
  jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
@@ -58,23 +84,27 @@ describe('handlePushStatus()', () => {
58
84
  });
59
85
 
60
86
  it('should throw error if organization not provided', async () => {
61
- await handlePushStatus(
62
- {
63
- domain: 'test-domain',
64
- organization: '',
65
- project: 'test-project',
66
- pushId: 'test-push-id',
67
- 'max-execution-time': 1000,
68
- },
69
- mockConfig
87
+ await expect(
88
+ handlePushStatus(
89
+ {
90
+ domain: 'test-domain',
91
+ organization: '',
92
+ project: 'test-project',
93
+ pushId: 'test-push-id',
94
+ },
95
+ mockConfig
96
+ )
97
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
98
+ `"No organization provided, please use --organization option or specify the 'organization' field in the config file."`
70
99
  );
71
100
 
72
- expect(exitWithError).toHaveBeenCalledWith(
73
- "No organization provided, please use --organization option or specify the 'organization' field in the config file."
101
+ expect(process.stderr.write).toHaveBeenCalledWith(
102
+ `No organization provided, please use --organization option or specify the 'organization' field in the config file.` +
103
+ '\n\n'
74
104
  );
75
105
  });
76
106
 
77
- it('should return success push status for preview-build', async () => {
107
+ it('should print success push status for preview-build', async () => {
78
108
  process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
79
109
  remotes.getPush.mockResolvedValueOnce(pushResponseStub);
80
110
 
@@ -84,17 +114,16 @@ describe('handlePushStatus()', () => {
84
114
  organization: 'test-org',
85
115
  project: 'test-project',
86
116
  pushId: 'test-push-id',
87
- 'max-execution-time': 1000,
88
117
  },
89
118
  mockConfig
90
119
  );
91
120
  expect(process.stdout.write).toHaveBeenCalledTimes(1);
92
121
  expect(process.stdout.write).toHaveBeenCalledWith(
93
- '🚀 Preview deploy success.\nPreview URL: https://test-url\n'
122
+ '🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n'
94
123
  );
95
124
  });
96
125
 
97
- it('should return success push status for preview and production builds', async () => {
126
+ it('should print success push status for preview and production builds', async () => {
98
127
  process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
99
128
  remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true });
100
129
 
@@ -104,46 +133,50 @@ describe('handlePushStatus()', () => {
104
133
  organization: 'test-org',
105
134
  project: 'test-project',
106
135
  pushId: 'test-push-id',
107
- 'max-execution-time': 1000,
108
136
  },
109
137
  mockConfig
110
138
  );
111
139
  expect(process.stdout.write).toHaveBeenCalledTimes(2);
112
140
  expect(process.stdout.write).toHaveBeenCalledWith(
113
- '🚀 Preview deploy success.\nPreview URL: https://test-url\n'
141
+ '🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n'
114
142
  );
115
143
  expect(process.stdout.write).toHaveBeenCalledWith(
116
- '🚀 Production deploy success.\nProduction URL: https://test-url\n'
144
+ '🚀 Production deploy success.\nProduction URL: https://production-test-url\n'
117
145
  );
118
146
  });
119
147
 
120
- it('should return failed push status for preview build', async () => {
148
+ it('should print failed push status for preview build', async () => {
121
149
  process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
122
150
 
123
151
  remotes.getPush.mockResolvedValue({
124
152
  isOutdated: false,
125
153
  hasChanges: true,
126
154
  status: {
127
- preview: { deploy: { status: 'failed', url: 'https://test-url' }, scorecard: [] },
155
+ preview: { deploy: { status: 'failed', url: 'https://preview-test-url' }, scorecard: [] },
128
156
  },
129
157
  });
130
158
 
131
- await handlePushStatus(
132
- {
133
- domain: 'test-domain',
134
- organization: 'test-org',
135
- project: 'test-project',
136
- pushId: 'test-push-id',
137
- 'max-execution-time': 1000,
138
- },
139
- mockConfig
140
- );
141
- expect(exitWithError).toHaveBeenCalledWith(
142
- '❌ Preview deploy fail.\nPreview URL: https://test-url'
159
+ await expect(
160
+ handlePushStatus(
161
+ {
162
+ domain: 'test-domain',
163
+ organization: 'test-org',
164
+ project: 'test-project',
165
+ pushId: 'test-push-id',
166
+ },
167
+ mockConfig
168
+ )
169
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
170
+ "❌ Preview deploy fail.
171
+ Preview URL: https://preview-test-url"
172
+ `);
173
+
174
+ expect(process.stderr.write).toHaveBeenCalledWith(
175
+ '❌ Preview deploy fail.\nPreview URL: https://preview-test-url' + '\n\n'
143
176
  );
144
177
  });
145
178
 
146
- it('should return success push status for preview build and print scorecards', async () => {
179
+ it('should print success push status for preview build and print scorecards', async () => {
147
180
  process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
148
181
 
149
182
  remotes.getPush.mockResolvedValue({
@@ -151,7 +184,7 @@ describe('handlePushStatus()', () => {
151
184
  hasChanges: true,
152
185
  status: {
153
186
  preview: {
154
- deploy: { status: 'success', url: 'https://test-url' },
187
+ deploy: { status: 'success', url: 'https://preview-test-url' },
155
188
  scorecard: [
156
189
  {
157
190
  name: 'test-name',
@@ -170,13 +203,12 @@ describe('handlePushStatus()', () => {
170
203
  organization: 'test-org',
171
204
  project: 'test-project',
172
205
  pushId: 'test-push-id',
173
- 'max-execution-time': 1000,
174
206
  },
175
207
  mockConfig
176
208
  );
177
209
  expect(process.stdout.write).toHaveBeenCalledTimes(4);
178
210
  expect(process.stdout.write).toHaveBeenCalledWith(
179
- '🚀 Preview deploy success.\nPreview URL: https://test-url\n'
211
+ '🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n'
180
212
  );
181
213
  expect(process.stdout.write).toHaveBeenCalledWith('\nScorecard:');
182
214
  expect(process.stdout.write).toHaveBeenCalledWith(
@@ -185,14 +217,18 @@ describe('handlePushStatus()', () => {
185
217
  expect(process.stdout.write).toHaveBeenCalledWith('\n');
186
218
  });
187
219
 
188
- it('should display message if there is no changes', async () => {
220
+ it('should print message if there is no changes', async () => {
189
221
  process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
190
222
 
191
223
  remotes.getPush.mockResolvedValueOnce({
192
224
  isOutdated: false,
193
225
  hasChanges: false,
194
226
  status: {
195
- preview: { deploy: { status: 'skipped', url: 'https://test-url' }, scorecard: [] },
227
+ preview: { deploy: { status: 'skipped', url: 'https://preview-test-url' }, scorecard: [] },
228
+ production: {
229
+ deploy: { status: 'skipped', url: null },
230
+ scorecard: [],
231
+ },
196
232
  },
197
233
  });
198
234
 
@@ -203,10 +239,400 @@ describe('handlePushStatus()', () => {
203
239
  project: 'test-project',
204
240
  pushId: 'test-push-id',
205
241
  wait: true,
206
- 'max-execution-time': 1000,
207
242
  },
208
243
  mockConfig
209
244
  );
210
- expect(process.stderr.write).toHaveBeenCalledWith('Files not uploaded. Reason: no changes.\n');
245
+
246
+ expect(process.stderr.write).toHaveBeenCalledWith(
247
+ 'Files not added to your project. Reason: no changes.\n'
248
+ );
249
+ });
250
+
251
+ describe('return value', () => {
252
+ it('should return preview deployment info', async () => {
253
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
254
+ remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: false });
255
+
256
+ const result = await handlePushStatus(
257
+ {
258
+ domain: 'test-domain',
259
+ organization: 'test-org',
260
+ project: 'test-project',
261
+ pushId: 'test-push-id',
262
+ },
263
+ mockConfig
264
+ );
265
+
266
+ expect(result).toEqual({
267
+ preview: {
268
+ deploy: {
269
+ status: 'success',
270
+ url: 'https://preview-test-url',
271
+ },
272
+ scorecard: [],
273
+ },
274
+ production: null,
275
+ commit: commitStub,
276
+ });
277
+ });
278
+
279
+ it('should return preview and production deployment info', async () => {
280
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
281
+ remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true });
282
+
283
+ const result = await handlePushStatus(
284
+ {
285
+ domain: 'test-domain',
286
+ organization: 'test-org',
287
+ project: 'test-project',
288
+ pushId: 'test-push-id',
289
+ },
290
+ mockConfig
291
+ );
292
+
293
+ expect(result).toEqual({
294
+ preview: {
295
+ deploy: {
296
+ status: 'success',
297
+ url: 'https://preview-test-url',
298
+ },
299
+ scorecard: [],
300
+ },
301
+ production: {
302
+ deploy: {
303
+ status: 'success',
304
+ url: 'https://production-test-url',
305
+ },
306
+ scorecard: [],
307
+ },
308
+ commit: commitStub,
309
+ });
310
+ });
311
+ });
312
+
313
+ describe('"wait" option', () => {
314
+ it('should wait for preview "success" deployment status', async () => {
315
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
316
+
317
+ remotes.getPush.mockResolvedValueOnce({
318
+ ...pushResponseStub,
319
+ status: {
320
+ preview: {
321
+ deploy: { status: 'pending', url: 'https://preview-test-url' },
322
+ scorecard: [],
323
+ },
324
+ },
325
+ });
326
+
327
+ remotes.getPush.mockResolvedValueOnce({
328
+ ...pushResponseStub,
329
+ status: {
330
+ preview: {
331
+ deploy: { status: 'running', url: 'https://preview-test-url' },
332
+ scorecard: [],
333
+ },
334
+ },
335
+ });
336
+
337
+ remotes.getPush.mockResolvedValueOnce({
338
+ ...pushResponseStub,
339
+ status: {
340
+ preview: {
341
+ deploy: { status: 'success', url: 'https://preview-test-url' },
342
+ scorecard: [],
343
+ },
344
+ },
345
+ });
346
+
347
+ const result = await handlePushStatus(
348
+ {
349
+ domain: 'test-domain',
350
+ organization: 'test-org',
351
+ project: 'test-project',
352
+ pushId: 'test-push-id',
353
+ 'retry-interval': 0.5, // 500 ms
354
+ wait: true,
355
+ },
356
+ mockConfig
357
+ );
358
+
359
+ expect(result).toEqual({
360
+ preview: {
361
+ deploy: {
362
+ status: 'success',
363
+ url: 'https://preview-test-url',
364
+ },
365
+ scorecard: [],
366
+ },
367
+ production: null,
368
+ commit: commitStub,
369
+ });
370
+ });
371
+
372
+ it('should wait for production "success" status after preview "success" status', async () => {
373
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
374
+
375
+ remotes.getPush.mockResolvedValueOnce({
376
+ ...pushResponseStub,
377
+ isMainBranch: true,
378
+ status: {
379
+ preview: {
380
+ deploy: { status: 'success', url: 'https://preview-test-url' },
381
+ scorecard: [],
382
+ },
383
+ production: {
384
+ deploy: { status: 'pending', url: 'https://production-test-url' },
385
+ scorecard: [],
386
+ },
387
+ },
388
+ });
389
+
390
+ remotes.getPush.mockResolvedValueOnce({
391
+ ...pushResponseStub,
392
+ isMainBranch: true,
393
+ status: {
394
+ preview: {
395
+ deploy: { status: 'success', url: 'https://preview-test-url' },
396
+ scorecard: [],
397
+ },
398
+ production: {
399
+ deploy: { status: 'running', url: 'https://production-test-url' },
400
+ scorecard: [],
401
+ },
402
+ },
403
+ });
404
+
405
+ remotes.getPush.mockResolvedValueOnce({
406
+ ...pushResponseStub,
407
+ isMainBranch: true,
408
+ status: {
409
+ preview: {
410
+ deploy: { status: 'success', url: 'https://preview-test-url' },
411
+ scorecard: [],
412
+ },
413
+ production: {
414
+ deploy: { status: 'success', url: 'https://production-test-url' },
415
+ scorecard: [],
416
+ },
417
+ },
418
+ });
419
+
420
+ const result = await handlePushStatus(
421
+ {
422
+ domain: 'test-domain',
423
+ organization: 'test-org',
424
+ project: 'test-project',
425
+ pushId: 'test-push-id',
426
+ 'retry-interval': 0.5, // 500 ms
427
+ wait: true,
428
+ },
429
+ mockConfig
430
+ );
431
+
432
+ expect(result).toEqual({
433
+ preview: {
434
+ deploy: { status: 'success', url: 'https://preview-test-url' },
435
+ scorecard: [],
436
+ },
437
+ production: {
438
+ deploy: { status: 'success', url: 'https://production-test-url' },
439
+ scorecard: [],
440
+ },
441
+ commit: commitStub,
442
+ });
443
+ });
444
+ });
445
+
446
+ describe('"continue-on-deploy-failures" option', () => {
447
+ it('should throw error if option value is false', async () => {
448
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
449
+
450
+ remotes.getPush.mockResolvedValueOnce({
451
+ ...pushResponseStub,
452
+ status: {
453
+ preview: {
454
+ deploy: { status: 'failed', url: 'https://preview-test-url' },
455
+ scorecard: [],
456
+ },
457
+ },
458
+ });
459
+
460
+ await expect(
461
+ handlePushStatus(
462
+ {
463
+ domain: 'test-domain',
464
+ organization: 'test-org',
465
+ project: 'test-project',
466
+ pushId: 'test-push-id',
467
+ 'continue-on-deploy-failures': false,
468
+ },
469
+ mockConfig
470
+ )
471
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
472
+ "❌ Preview deploy fail.
473
+ Preview URL: https://preview-test-url"
474
+ `);
475
+ });
476
+
477
+ it('should not throw error if option value is true', async () => {
478
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
479
+
480
+ remotes.getPush.mockResolvedValueOnce({
481
+ ...pushResponseStub,
482
+ status: {
483
+ preview: {
484
+ deploy: { status: 'failed', url: 'https://preview-test-url' },
485
+ scorecard: [],
486
+ },
487
+ },
488
+ });
489
+
490
+ await expect(
491
+ handlePushStatus(
492
+ {
493
+ domain: 'test-domain',
494
+ organization: 'test-org',
495
+ project: 'test-project',
496
+ pushId: 'test-push-id',
497
+ 'continue-on-deploy-failures': true,
498
+ },
499
+ mockConfig
500
+ )
501
+ ).resolves.toStrictEqual({
502
+ preview: {
503
+ deploy: { status: 'failed', url: 'https://preview-test-url' },
504
+ scorecard: [],
505
+ },
506
+ production: null,
507
+ commit: commitStub,
508
+ });
509
+ });
510
+ });
511
+
512
+ describe('"onRetry" callback', () => {
513
+ it('should be called when command retries request to API in wait mode for preview deploy', async () => {
514
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
515
+
516
+ remotes.getPush.mockResolvedValueOnce({
517
+ ...pushResponseStub,
518
+ status: {
519
+ preview: {
520
+ deploy: { status: 'pending', url: 'https://preview-test-url' },
521
+ scorecard: [],
522
+ },
523
+ },
524
+ });
525
+
526
+ remotes.getPush.mockResolvedValueOnce({
527
+ ...pushResponseStub,
528
+ status: {
529
+ preview: {
530
+ deploy: { status: 'running', url: 'https://preview-test-url' },
531
+ scorecard: [],
532
+ },
533
+ },
534
+ });
535
+
536
+ remotes.getPush.mockResolvedValueOnce({
537
+ ...pushResponseStub,
538
+ status: {
539
+ preview: {
540
+ deploy: { status: 'success', url: 'https://preview-test-url' },
541
+ scorecard: [],
542
+ },
543
+ },
544
+ });
545
+
546
+ const onRetrySpy = jest.fn();
547
+
548
+ const result = await handlePushStatus(
549
+ {
550
+ domain: 'test-domain',
551
+ organization: 'test-org',
552
+ project: 'test-project',
553
+ pushId: 'test-push-id',
554
+ wait: true,
555
+ 'retry-interval': 0.5, // 500 ms
556
+ onRetry: onRetrySpy,
557
+ },
558
+ mockConfig
559
+ );
560
+
561
+ expect(onRetrySpy).toBeCalledTimes(2);
562
+
563
+ // first retry
564
+ expect(onRetrySpy).toHaveBeenNthCalledWith(1, {
565
+ preview: {
566
+ deploy: {
567
+ status: 'pending',
568
+ url: 'https://preview-test-url',
569
+ },
570
+ scorecard: [],
571
+ },
572
+ production: null,
573
+ commit: commitStub,
574
+ });
575
+
576
+ // second retry
577
+ expect(onRetrySpy).toHaveBeenNthCalledWith(2, {
578
+ preview: {
579
+ deploy: {
580
+ status: 'running',
581
+ url: 'https://preview-test-url',
582
+ },
583
+ scorecard: [],
584
+ },
585
+ production: null,
586
+ commit: commitStub,
587
+ });
588
+
589
+ // final result
590
+ expect(result).toEqual({
591
+ preview: {
592
+ deploy: {
593
+ status: 'success',
594
+ url: 'https://preview-test-url',
595
+ },
596
+ scorecard: [],
597
+ },
598
+ production: null,
599
+ commit: commitStub,
600
+ });
601
+ });
602
+ });
603
+
604
+ describe('"max-execution-time" option', () => {
605
+ it('should throw error in case "max-execution-time" was exceeded', async () => {
606
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
607
+
608
+ // Stuck deployment simulation
609
+ remotes.getPush.mockResolvedValue({
610
+ ...pushResponseStub,
611
+ status: {
612
+ preview: {
613
+ deploy: { status: 'pending', url: 'https://preview-test-url' },
614
+ scorecard: [],
615
+ },
616
+ },
617
+ });
618
+
619
+ await expect(
620
+ handlePushStatus(
621
+ {
622
+ domain: 'test-domain',
623
+ organization: 'test-org',
624
+ project: 'test-project',
625
+ pushId: 'test-push-id',
626
+ 'retry-interval': 2, // seconds
627
+ 'max-execution-time': 1, // seconds
628
+ wait: true,
629
+ },
630
+ mockConfig
631
+ )
632
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
633
+ "✗ Failed to get push status. Reason: Timeout exceeded
634
+ "
635
+ `);
636
+ });
211
637
  });
212
638
  });