@redocly/cli 1.6.0 → 1.8.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.
Files changed (126) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +5 -5
  3. package/lib/__tests__/commands/build-docs.test.js +3 -3
  4. package/lib/__tests__/commands/bundle.test.js +5 -5
  5. package/lib/__tests__/commands/join.test.js +11 -11
  6. package/lib/__tests__/commands/lint.test.js +14 -14
  7. package/lib/__tests__/commands/push-region.test.js +1 -1
  8. package/lib/__tests__/commands/push.test.js +11 -11
  9. package/lib/__tests__/fetch-with-timeout.test.js +4 -13
  10. package/lib/__tests__/spinner.test.js +43 -0
  11. package/lib/__tests__/utils.test.js +54 -36
  12. package/lib/__tests__/wrapper.test.js +8 -8
  13. package/lib/cms/api/__tests__/api-keys.test.d.ts +1 -0
  14. package/lib/cms/api/__tests__/api-keys.test.js +26 -0
  15. package/lib/cms/api/__tests__/api.client.test.d.ts +1 -0
  16. package/lib/cms/api/__tests__/api.client.test.js +217 -0
  17. package/lib/cms/api/__tests__/domains.test.d.ts +1 -0
  18. package/lib/cms/api/__tests__/domains.test.js +13 -0
  19. package/lib/cms/api/api-client.d.ts +50 -0
  20. package/lib/cms/api/api-client.js +148 -0
  21. package/lib/cms/api/api-keys.d.ts +1 -0
  22. package/lib/cms/api/api-keys.js +24 -0
  23. package/lib/cms/api/domains.d.ts +1 -0
  24. package/lib/cms/api/domains.js +12 -0
  25. package/lib/cms/api/index.d.ts +3 -0
  26. package/lib/cms/api/index.js +19 -0
  27. package/lib/cms/api/types.d.ts +91 -0
  28. package/lib/cms/api/types.js +2 -0
  29. package/lib/cms/commands/__tests__/push-status.test.d.ts +1 -0
  30. package/lib/cms/commands/__tests__/push-status.test.js +164 -0
  31. package/lib/cms/commands/__tests__/push.test.d.ts +1 -0
  32. package/lib/cms/commands/__tests__/push.test.js +226 -0
  33. package/lib/cms/commands/push-status.d.ts +12 -0
  34. package/lib/cms/commands/push-status.js +150 -0
  35. package/lib/cms/commands/push.d.ts +23 -0
  36. package/lib/cms/commands/push.js +142 -0
  37. package/lib/cms/utils.d.ts +2 -0
  38. package/lib/cms/utils.js +6 -0
  39. package/lib/commands/build-docs/index.js +4 -4
  40. package/lib/commands/build-docs/utils.js +2 -2
  41. package/lib/commands/bundle.js +13 -13
  42. package/lib/commands/join.js +25 -25
  43. package/lib/commands/lint.js +10 -10
  44. package/lib/commands/login.js +2 -2
  45. package/lib/commands/preview-docs/index.js +4 -4
  46. package/lib/commands/preview-docs/preview-server/preview-server.js +2 -2
  47. package/lib/commands/preview-project/constants.d.ts +14 -0
  48. package/lib/commands/preview-project/constants.js +22 -0
  49. package/lib/commands/preview-project/index.d.ts +2 -0
  50. package/lib/commands/preview-project/index.js +58 -0
  51. package/lib/commands/preview-project/types.d.ts +10 -0
  52. package/lib/commands/preview-project/types.js +2 -0
  53. package/lib/commands/push.d.ts +5 -0
  54. package/lib/commands/push.js +25 -17
  55. package/lib/commands/split/__tests__/index.test.js +2 -2
  56. package/lib/commands/split/index.js +20 -20
  57. package/lib/commands/stats.js +4 -4
  58. package/lib/index.d.ts +1 -1
  59. package/lib/index.js +169 -25
  60. package/lib/types.d.ts +9 -1
  61. package/lib/{__mocks__/utils.js → utils/__mocks__/miscellaneous.js} +1 -1
  62. package/lib/utils/assert-node-version.d.ts +1 -0
  63. package/lib/{fetch-with-timeout.js → utils/fetch-with-timeout.js} +2 -7
  64. package/lib/{utils.d.ts → utils/miscellaneous.d.ts} +1 -1
  65. package/lib/{utils.js → utils/miscellaneous.js} +20 -2
  66. package/lib/utils/spinner.d.ts +10 -0
  67. package/lib/utils/spinner.js +42 -0
  68. package/lib/{update-version-notifier.js → utils/update-version-notifier.js} +4 -4
  69. package/lib/wrapper.js +5 -5
  70. package/package.json +5 -3
  71. package/src/__tests__/commands/build-docs.test.ts +2 -2
  72. package/src/__tests__/commands/bundle.test.ts +2 -2
  73. package/src/__tests__/commands/join.test.ts +2 -2
  74. package/src/__tests__/commands/lint.test.ts +3 -3
  75. package/src/__tests__/commands/push-region.test.ts +1 -1
  76. package/src/__tests__/commands/push.test.ts +2 -2
  77. package/src/__tests__/fetch-with-timeout.test.ts +4 -16
  78. package/src/__tests__/spinner.test.ts +51 -0
  79. package/src/__tests__/utils.test.ts +20 -5
  80. package/src/__tests__/wrapper.test.ts +2 -2
  81. package/src/cms/api/__tests__/api-keys.test.ts +37 -0
  82. package/src/cms/api/__tests__/api.client.test.ts +275 -0
  83. package/src/cms/api/__tests__/domains.test.ts +15 -0
  84. package/src/cms/api/api-client.ts +199 -0
  85. package/src/cms/api/api-keys.ts +26 -0
  86. package/src/cms/api/domains.ts +11 -0
  87. package/src/cms/api/index.ts +3 -0
  88. package/src/cms/api/types.ts +101 -0
  89. package/src/cms/commands/__tests__/push-status.test.ts +212 -0
  90. package/src/cms/commands/__tests__/push.test.ts +293 -0
  91. package/src/cms/commands/push-status.ts +203 -0
  92. package/src/cms/commands/push.ts +215 -0
  93. package/src/cms/utils.ts +1 -0
  94. package/src/commands/build-docs/index.ts +1 -1
  95. package/src/commands/build-docs/utils.ts +1 -1
  96. package/src/commands/bundle.ts +2 -2
  97. package/src/commands/join.ts +2 -2
  98. package/src/commands/lint.ts +1 -1
  99. package/src/commands/login.ts +1 -1
  100. package/src/commands/preview-docs/index.ts +5 -1
  101. package/src/commands/preview-docs/preview-server/preview-server.ts +1 -1
  102. package/src/commands/preview-project/constants.ts +23 -0
  103. package/src/commands/preview-project/index.ts +58 -0
  104. package/src/commands/preview-project/types.ts +12 -0
  105. package/src/commands/push.ts +15 -1
  106. package/src/commands/split/__tests__/index.test.ts +3 -4
  107. package/src/commands/split/index.ts +5 -5
  108. package/src/commands/stats.ts +2 -2
  109. package/src/index.ts +184 -28
  110. package/src/types.ts +12 -1
  111. package/src/{__mocks__/utils.ts → utils/__mocks__/miscellaneous.ts} +1 -1
  112. package/src/{fetch-with-timeout.ts → utils/fetch-with-timeout.ts} +1 -6
  113. package/src/{utils.ts → utils/miscellaneous.ts} +20 -2
  114. package/src/utils/spinner.ts +50 -0
  115. package/src/{update-version-notifier.ts → utils/update-version-notifier.ts} +2 -2
  116. package/src/wrapper.ts +7 -2
  117. package/tsconfig.tsbuildinfo +1 -1
  118. /package/lib/{assert-node-version.d.ts → __tests__/spinner.test.d.ts} +0 -0
  119. /package/lib/{__mocks__/utils.d.ts → utils/__mocks__/miscellaneous.d.ts} +0 -0
  120. /package/lib/{assert-node-version.js → utils/assert-node-version.js} +0 -0
  121. /package/lib/{fetch-with-timeout.d.ts → utils/fetch-with-timeout.d.ts} +0 -0
  122. /package/lib/{js-utils.d.ts → utils/js-utils.d.ts} +0 -0
  123. /package/lib/{js-utils.js → utils/js-utils.js} +0 -0
  124. /package/lib/{update-version-notifier.d.ts → utils/update-version-notifier.d.ts} +0 -0
  125. /package/src/{assert-node-version.ts → utils/assert-node-version.ts} +0 -0
  126. /package/src/{js-utils.ts → utils/js-utils.ts} +0 -0
@@ -0,0 +1,199 @@
1
+ import fetchWithTimeout from '../../utils/fetch-with-timeout';
2
+ import fetch from 'node-fetch';
3
+ import * as FormData from 'form-data';
4
+
5
+ import type { Response } from 'node-fetch';
6
+ import type { ReadStream } from 'fs';
7
+ import type {
8
+ ListRemotesResponse,
9
+ ProjectSourceResponse,
10
+ PushResponse,
11
+ UpsertRemoteResponse,
12
+ } from './types';
13
+
14
+ class RemotesApiClient {
15
+ constructor(private readonly domain: string, private readonly apiKey: string) {}
16
+
17
+ private async getParsedResponse<T>(response: Response): Promise<T> {
18
+ const responseBody = await response.json();
19
+
20
+ if (response.ok) {
21
+ return responseBody as T;
22
+ }
23
+
24
+ throw new Error(responseBody.title || response.statusText);
25
+ }
26
+
27
+ async getDefaultBranch(organizationId: string, projectId: string) {
28
+ const response = await fetch(
29
+ `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/source`,
30
+ {
31
+ method: 'GET',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Authorization: `Bearer ${this.apiKey}`,
35
+ },
36
+ }
37
+ );
38
+
39
+ try {
40
+ const source = await this.getParsedResponse<ProjectSourceResponse>(response);
41
+
42
+ return source.branchName;
43
+ } catch (err) {
44
+ throw new Error(`Failed to fetch default branch: ${err.message || 'Unknown error'}`);
45
+ }
46
+ }
47
+
48
+ async upsert(
49
+ organizationId: string,
50
+ projectId: string,
51
+ remote: {
52
+ mountPath: string;
53
+ mountBranchName: string;
54
+ }
55
+ ): Promise<UpsertRemoteResponse> {
56
+ const response = await fetch(
57
+ `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes`,
58
+ {
59
+ method: 'POST',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ Authorization: `Bearer ${this.apiKey}`,
63
+ },
64
+ body: JSON.stringify({
65
+ mountPath: remote.mountPath,
66
+ mountBranchName: remote.mountBranchName,
67
+ type: 'CICD',
68
+ autoMerge: true,
69
+ }),
70
+ }
71
+ );
72
+
73
+ try {
74
+ return await this.getParsedResponse<UpsertRemoteResponse>(response);
75
+ } catch (err) {
76
+ throw new Error(`Failed to upsert remote: ${err.message || 'Unknown error'}`);
77
+ }
78
+ }
79
+
80
+ async push(
81
+ organizationId: string,
82
+ projectId: string,
83
+ payload: PushPayload,
84
+ files: { path: string; stream: ReadStream | Buffer }[]
85
+ ): Promise<PushResponse> {
86
+ const formData = new FormData();
87
+
88
+ formData.append('remoteId', payload.remoteId);
89
+ formData.append('commit[message]', payload.commit.message);
90
+ formData.append('commit[author][name]', payload.commit.author.name);
91
+ formData.append('commit[author][email]', payload.commit.author.email);
92
+ formData.append('commit[branchName]', payload.commit.branchName);
93
+ payload.commit.url && formData.append('commit[url]', payload.commit.url);
94
+ payload.commit.namespace && formData.append('commit[namespaceId]', payload.commit.namespace);
95
+ payload.commit.sha && formData.append('commit[sha]', payload.commit.sha);
96
+ payload.commit.repository && formData.append('commit[repositoryId]', payload.commit.repository);
97
+ payload.commit.createdAt && formData.append('commit[createdAt]', payload.commit.createdAt);
98
+
99
+ for (const file of files) {
100
+ formData.append(`files[${file.path}]`, file.stream);
101
+ }
102
+
103
+ payload.isMainBranch && formData.append('isMainBranch', 'true');
104
+
105
+ const response = await fetch(
106
+ `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/pushes`,
107
+ {
108
+ method: 'POST',
109
+ headers: {
110
+ Authorization: `Bearer ${this.apiKey}`,
111
+ },
112
+ body: formData,
113
+ }
114
+ );
115
+
116
+ try {
117
+ return await this.getParsedResponse<PushResponse>(response);
118
+ } catch (err) {
119
+ throw new Error(`Failed to push: ${err.message || 'Unknown error'}`);
120
+ }
121
+ }
122
+
123
+ async getRemotesList(organizationId: string, projectId: string, mountPath: string) {
124
+ const response = await fetch(
125
+ `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes?filter=mountPath:/${mountPath}/`,
126
+ {
127
+ method: 'GET',
128
+ headers: {
129
+ 'Content-Type': 'application/json',
130
+ Authorization: `Bearer ${this.apiKey}`,
131
+ },
132
+ }
133
+ );
134
+
135
+ try {
136
+ return await this.getParsedResponse<ListRemotesResponse>(response);
137
+ } catch (err) {
138
+ throw new Error(`Failed to get remote list: ${err.message || 'Unknown error'}`);
139
+ }
140
+ }
141
+
142
+ async getPush({
143
+ organizationId,
144
+ projectId,
145
+ pushId,
146
+ }: {
147
+ organizationId: string;
148
+ projectId: string;
149
+ pushId: string;
150
+ }) {
151
+ const response = await fetchWithTimeout(
152
+ `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/pushes/${pushId}`,
153
+ {
154
+ method: 'GET',
155
+ headers: {
156
+ 'Content-Type': 'application/json',
157
+ Authorization: `Bearer ${this.apiKey}`,
158
+ },
159
+ }
160
+ );
161
+
162
+ if (!response) {
163
+ throw new Error(`Failed to get push status: Time is up`);
164
+ }
165
+
166
+ try {
167
+ return await this.getParsedResponse<PushResponse>(response);
168
+ } catch (err) {
169
+ throw new Error(`Failed to get push status: ${err.message || 'Unknown error'}`);
170
+ }
171
+ }
172
+ }
173
+
174
+ export class ReuniteApiClient {
175
+ remotes: RemotesApiClient;
176
+
177
+ constructor(public domain: string, private readonly apiKey: string) {
178
+ this.remotes = new RemotesApiClient(this.domain, this.apiKey);
179
+ }
180
+ }
181
+
182
+ export type PushPayload = {
183
+ remoteId: string;
184
+ commit: {
185
+ message: string;
186
+ branchName: string;
187
+ sha?: string;
188
+ url?: string;
189
+ createdAt?: string;
190
+ namespace?: string;
191
+ repository?: string;
192
+ author: {
193
+ name: string;
194
+ email: string;
195
+ image?: string;
196
+ };
197
+ };
198
+ isMainBranch?: boolean;
199
+ };
@@ -0,0 +1,26 @@
1
+ import { resolve } from 'path';
2
+ import { homedir } from 'os';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { isNotEmptyObject } from '@redocly/openapi-core/lib/utils';
5
+ import { TOKEN_FILENAME } from '@redocly/openapi-core/lib/redocly';
6
+
7
+ function readCredentialsFile(credentialsPath: string) {
8
+ return existsSync(credentialsPath) ? JSON.parse(readFileSync(credentialsPath, 'utf-8')) : {};
9
+ }
10
+
11
+ export function getApiKeys(domain: string) {
12
+ const apiKey = process.env.REDOCLY_AUTHORIZATION;
13
+
14
+ if (apiKey) {
15
+ return apiKey;
16
+ }
17
+
18
+ const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
19
+ const credentials = readCredentialsFile(credentialsPath);
20
+
21
+ if (isNotEmptyObject(credentials) && credentials[domain]) {
22
+ return credentials[domain];
23
+ }
24
+
25
+ throw new Error('No api key provided, please use environment variable REDOCLY_AUTHORIZATION.');
26
+ }
@@ -0,0 +1,11 @@
1
+ const DEFAULT_DOMAIN = 'https://app.cloud.redocly.com';
2
+
3
+ export function getDomain(): string {
4
+ const domain = process.env.REDOCLY_DOMAIN;
5
+
6
+ if (domain) {
7
+ return domain;
8
+ }
9
+
10
+ return DEFAULT_DOMAIN;
11
+ }
@@ -0,0 +1,3 @@
1
+ export * from './api-client';
2
+ export * from './domains';
3
+ export * from './api-keys';
@@ -0,0 +1,101 @@
1
+ export type ProjectSourceResponse = {
2
+ branchName: string;
3
+ contentPath: string;
4
+ isInternal: boolean;
5
+ };
6
+
7
+ export type UpsertRemoteResponse = {
8
+ id: string;
9
+ type: 'CICD';
10
+ mountPath: string;
11
+ mountBranchName: string;
12
+ organizationId: string;
13
+ projectId: string;
14
+ };
15
+
16
+ export type ListRemotesResponse = {
17
+ object: 'list';
18
+ page: {
19
+ endCursor: string;
20
+ startCursor: string;
21
+ haxNextPage: boolean;
22
+ hasPrevPage: boolean;
23
+ limit: number;
24
+ total: number;
25
+ };
26
+ items: Remote[];
27
+ };
28
+
29
+ export type Remote = {
30
+ mountPath: string;
31
+ type: string;
32
+ autoSync: boolean;
33
+ autoMerge: boolean;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ providerType: string;
37
+ namespaceId: string;
38
+ repositoryId: string;
39
+ projectId: string;
40
+ mountBranchName: string;
41
+ contentPath: string;
42
+ credentialId: string;
43
+ branchName: string;
44
+ contentType: string;
45
+ id: string;
46
+ };
47
+
48
+ export type PushResponse = {
49
+ id: string;
50
+ remoteId: string;
51
+ commit: {
52
+ message: string;
53
+ branchName: string;
54
+ sha: string | null;
55
+ url: string | null;
56
+ createdAt: string | null;
57
+ namespace: string | null;
58
+ repository: string | null;
59
+ author: {
60
+ name: string;
61
+ email: string;
62
+ image: string | null;
63
+ };
64
+ };
65
+ remote: {
66
+ commits: {
67
+ branchName: string;
68
+ sha: string;
69
+ }[];
70
+ };
71
+ hasChanges: boolean;
72
+ isOutdated: boolean;
73
+ isMainBranch: boolean;
74
+ status: PushStatusResponse;
75
+ };
76
+
77
+ type DeploymentStatusResponse = {
78
+ deploy: {
79
+ url: string | null;
80
+ status: DeploymentStatus;
81
+ };
82
+ scorecard: ScorecardItem[];
83
+ };
84
+
85
+ export type PushStatusResponse = {
86
+ preview: DeploymentStatusResponse;
87
+ production: DeploymentStatusResponse;
88
+ };
89
+
90
+ export type ScorecardItem = {
91
+ name: string;
92
+ status: PushStatusBase;
93
+ description: string;
94
+ url: string;
95
+ };
96
+
97
+ export type PushStatusBase = 'pending' | 'success' | 'running' | 'failed';
98
+
99
+ // export type BuildStatus = PushStatusBase | 'NOT_STARTED' | 'QUEUED';
100
+
101
+ export type DeploymentStatus = 'skipped' | PushStatusBase;
@@ -0,0 +1,212 @@
1
+ import { handlePushStatus } from '../push-status';
2
+ import { PushResponse } from '../../api/types';
3
+ import { exitWithError } from '../../../utils/miscellaneous';
4
+
5
+ const remotes = {
6
+ getPush: jest.fn(),
7
+ getRemotesList: jest.fn(),
8
+ };
9
+
10
+ jest.mock('../../../utils/miscellaneous');
11
+
12
+ jest.mock('colorette', () => ({
13
+ green: (str: string) => str,
14
+ yellow: (str: string) => str,
15
+ red: (str: string) => str,
16
+ gray: (str: string) => str,
17
+ magenta: (str: string) => str,
18
+ cyan: (str: string) => str,
19
+ }));
20
+
21
+ jest.mock('../../api', () => ({
22
+ ...jest.requireActual('../../api'),
23
+ ReuniteApiClient: jest.fn().mockImplementation(function (this: any, ...args) {
24
+ this.remotes = remotes;
25
+ }),
26
+ }));
27
+
28
+ describe('handlePushStatus()', () => {
29
+ const mockConfig = { apis: {} } as any;
30
+
31
+ const pushResponseStub = {
32
+ hasChanges: true,
33
+ status: {
34
+ preview: {
35
+ scorecard: [],
36
+ deploy: {
37
+ url: 'https://test-url',
38
+ status: 'success',
39
+ },
40
+ },
41
+ production: {
42
+ scorecard: [],
43
+ deploy: {
44
+ url: 'https://test-url',
45
+ status: 'success',
46
+ },
47
+ },
48
+ },
49
+ } as unknown as PushResponse;
50
+
51
+ beforeEach(() => {
52
+ jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
53
+ jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
54
+ });
55
+
56
+ afterEach(() => {
57
+ jest.clearAllMocks();
58
+ });
59
+
60
+ 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
70
+ );
71
+
72
+ expect(exitWithError).toHaveBeenCalledWith(
73
+ "No organization provided, please use --organization option or specify the 'organization' field in the config file."
74
+ );
75
+ });
76
+
77
+ it('should return success push status for preview-build', async () => {
78
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
79
+ remotes.getPush.mockResolvedValueOnce(pushResponseStub);
80
+
81
+ await handlePushStatus(
82
+ {
83
+ domain: 'test-domain',
84
+ organization: 'test-org',
85
+ project: 'test-project',
86
+ pushId: 'test-push-id',
87
+ 'max-execution-time': 1000,
88
+ },
89
+ mockConfig
90
+ );
91
+ expect(process.stdout.write).toHaveBeenCalledTimes(1);
92
+ expect(process.stdout.write).toHaveBeenCalledWith(
93
+ '🚀 PREVIEW deployment succeeded.\nPreview URL: https://test-url\n'
94
+ );
95
+ });
96
+
97
+ it('should return success push status for preview and production builds', async () => {
98
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
99
+ remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true });
100
+
101
+ await handlePushStatus(
102
+ {
103
+ domain: 'test-domain',
104
+ organization: 'test-org',
105
+ project: 'test-project',
106
+ pushId: 'test-push-id',
107
+ 'max-execution-time': 1000,
108
+ },
109
+ mockConfig
110
+ );
111
+ expect(process.stdout.write).toHaveBeenCalledTimes(2);
112
+ expect(process.stdout.write).toHaveBeenCalledWith(
113
+ '🚀 PREVIEW deployment succeeded.\nPreview URL: https://test-url\n'
114
+ );
115
+ expect(process.stdout.write).toHaveBeenCalledWith(
116
+ '🚀 PRODUCTION deployment succeeded.\nPreview URL: https://test-url\n'
117
+ );
118
+ });
119
+
120
+ it('should return failed push status for preview build', async () => {
121
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
122
+
123
+ remotes.getPush.mockResolvedValue({
124
+ isOutdated: false,
125
+ hasChanges: true,
126
+ status: {
127
+ preview: { deploy: { status: 'failed', url: 'https://test-url' }, scorecard: [] },
128
+ },
129
+ });
130
+
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 deployment failed.\nPreview URL: https://test-url'
143
+ );
144
+ });
145
+
146
+ it('should return success push status for preview build and print scorecards', async () => {
147
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
148
+
149
+ remotes.getPush.mockResolvedValue({
150
+ isOutdated: false,
151
+ hasChanges: true,
152
+ status: {
153
+ preview: {
154
+ deploy: { status: 'success', url: 'https://test-url' },
155
+ scorecard: [
156
+ {
157
+ name: 'test-name',
158
+ status: 'success',
159
+ description: 'test-description',
160
+ url: 'test-url',
161
+ },
162
+ ],
163
+ },
164
+ },
165
+ });
166
+
167
+ await handlePushStatus(
168
+ {
169
+ domain: 'test-domain',
170
+ organization: 'test-org',
171
+ project: 'test-project',
172
+ pushId: 'test-push-id',
173
+ 'max-execution-time': 1000,
174
+ },
175
+ mockConfig
176
+ );
177
+ expect(process.stdout.write).toHaveBeenCalledTimes(4);
178
+ expect(process.stdout.write).toHaveBeenCalledWith(
179
+ '🚀 PREVIEW deployment succeeded.\nPreview URL: https://test-url\n'
180
+ );
181
+ expect(process.stdout.write).toHaveBeenCalledWith('\nScorecard:');
182
+ expect(process.stdout.write).toHaveBeenCalledWith(
183
+ '\n Name: test-name\n Status: success\n URL: test-url\n Description: test-description\n'
184
+ );
185
+ expect(process.stdout.write).toHaveBeenCalledWith('\n');
186
+ });
187
+
188
+ it('should display message if there is no changes', async () => {
189
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
190
+
191
+ remotes.getPush.mockResolvedValueOnce({
192
+ isOutdated: false,
193
+ hasChanges: false,
194
+ status: {
195
+ preview: { deploy: { status: 'skipped', url: 'https://test-url' }, scorecard: [] },
196
+ },
197
+ });
198
+
199
+ await handlePushStatus(
200
+ {
201
+ domain: 'test-domain',
202
+ organization: 'test-org',
203
+ project: 'test-project',
204
+ pushId: 'test-push-id',
205
+ wait: true,
206
+ 'max-execution-time': 1000,
207
+ },
208
+ mockConfig
209
+ );
210
+ expect(process.stderr.write).toHaveBeenCalledWith('Files not uploaded. Reason: no changes.\n');
211
+ });
212
+ });