@redocly/cli 0.0.0-snapshot.1737554067
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/README.md +114 -0
- package/bin/cli.js +3 -0
- package/lib/__mocks__/@redocly/openapi-core.d.ts +99 -0
- package/lib/__mocks__/@redocly/openapi-core.js +84 -0
- package/lib/__mocks__/documents.d.ts +150 -0
- package/lib/__mocks__/documents.js +123 -0
- package/lib/__mocks__/fs.d.ts +8 -0
- package/lib/__mocks__/fs.js +9 -0
- package/lib/__mocks__/perf_hooks.d.ts +3 -0
- package/lib/__mocks__/perf_hooks.js +6 -0
- package/lib/__mocks__/redoc.d.ts +6 -0
- package/lib/__mocks__/redoc.js +5 -0
- package/lib/__tests__/commands/build-docs.test.d.ts +1 -0
- package/lib/__tests__/commands/build-docs.test.js +54 -0
- package/lib/__tests__/commands/bundle.test.d.ts +1 -0
- package/lib/__tests__/commands/bundle.test.js +235 -0
- package/lib/__tests__/commands/join.test.d.ts +1 -0
- package/lib/__tests__/commands/join.test.js +274 -0
- package/lib/__tests__/commands/lint.test.d.ts +1 -0
- package/lib/__tests__/commands/lint.test.js +149 -0
- package/lib/__tests__/commands/push-region.test.d.ts +1 -0
- package/lib/__tests__/commands/push-region.test.js +55 -0
- package/lib/__tests__/commands/push.test.d.ts +1 -0
- package/lib/__tests__/commands/push.test.js +463 -0
- package/lib/__tests__/fetch-with-timeout.test.d.ts +1 -0
- package/lib/__tests__/fetch-with-timeout.test.js +44 -0
- package/lib/__tests__/fixtures/config.d.ts +21 -0
- package/lib/__tests__/fixtures/config.js +24 -0
- package/lib/__tests__/spinner.test.d.ts +1 -0
- package/lib/__tests__/spinner.test.js +43 -0
- package/lib/__tests__/utils.test.d.ts +1 -0
- package/lib/__tests__/utils.test.js +593 -0
- package/lib/__tests__/wrapper.test.d.ts +1 -0
- package/lib/__tests__/wrapper.test.js +68 -0
- package/lib/cms/api/__tests__/api-keys.test.d.ts +1 -0
- package/lib/cms/api/__tests__/api-keys.test.js +26 -0
- package/lib/cms/api/__tests__/api.client.test.d.ts +1 -0
- package/lib/cms/api/__tests__/api.client.test.js +333 -0
- package/lib/cms/api/__tests__/domains.test.d.ts +1 -0
- package/lib/cms/api/__tests__/domains.test.js +13 -0
- package/lib/cms/api/api-client.d.ts +75 -0
- package/lib/cms/api/api-client.js +225 -0
- package/lib/cms/api/api-keys.d.ts +1 -0
- package/lib/cms/api/api-keys.js +23 -0
- package/lib/cms/api/domains.d.ts +1 -0
- package/lib/cms/api/domains.js +11 -0
- package/lib/cms/api/index.d.ts +3 -0
- package/lib/cms/api/index.js +19 -0
- package/lib/cms/api/types.d.ts +102 -0
- package/lib/cms/api/types.js +2 -0
- package/lib/cms/commands/__tests__/push-status.test.d.ts +1 -0
- package/lib/cms/commands/__tests__/push-status.test.js +563 -0
- package/lib/cms/commands/__tests__/push.test.d.ts +1 -0
- package/lib/cms/commands/__tests__/push.test.js +315 -0
- package/lib/cms/commands/__tests__/utils.test.d.ts +1 -0
- package/lib/cms/commands/__tests__/utils.test.js +51 -0
- package/lib/cms/commands/push-status.d.ts +23 -0
- package/lib/cms/commands/push-status.js +206 -0
- package/lib/cms/commands/push.d.ts +28 -0
- package/lib/cms/commands/push.js +142 -0
- package/lib/cms/commands/utils.d.ts +25 -0
- package/lib/cms/commands/utils.js +46 -0
- package/lib/cms/utils.d.ts +2 -0
- package/lib/cms/utils.js +6 -0
- package/lib/commands/build-docs/index.d.ts +3 -0
- package/lib/commands/build-docs/index.js +39 -0
- package/lib/commands/build-docs/template.hbs +23 -0
- package/lib/commands/build-docs/types.d.ts +23 -0
- package/lib/commands/build-docs/types.js +2 -0
- package/lib/commands/build-docs/utils.d.ts +7 -0
- package/lib/commands/build-docs/utils.js +87 -0
- package/lib/commands/bundle.d.ts +14 -0
- package/lib/commands/bundle.js +91 -0
- package/lib/commands/eject.d.ts +9 -0
- package/lib/commands/eject.js +28 -0
- package/lib/commands/join.d.ts +11 -0
- package/lib/commands/join.js +565 -0
- package/lib/commands/lint.d.ts +13 -0
- package/lib/commands/lint.js +108 -0
- package/lib/commands/login.d.ts +9 -0
- package/lib/commands/login.js +23 -0
- package/lib/commands/preview-docs/index.d.ts +12 -0
- package/lib/commands/preview-docs/index.js +127 -0
- package/lib/commands/preview-docs/preview-server/default.hbs +24 -0
- package/lib/commands/preview-docs/preview-server/hot.js +59 -0
- package/lib/commands/preview-docs/preview-server/oauth2-redirect.html +21 -0
- package/lib/commands/preview-docs/preview-server/preview-server.d.ts +5 -0
- package/lib/commands/preview-docs/preview-server/preview-server.js +113 -0
- package/lib/commands/preview-docs/preview-server/server.d.ts +22 -0
- package/lib/commands/preview-docs/preview-server/server.js +85 -0
- package/lib/commands/preview-project/constants.d.ts +14 -0
- package/lib/commands/preview-project/constants.js +22 -0
- package/lib/commands/preview-project/index.d.ts +3 -0
- package/lib/commands/preview-project/index.js +56 -0
- package/lib/commands/preview-project/types.d.ts +10 -0
- package/lib/commands/preview-project/types.js +2 -0
- package/lib/commands/push.d.ts +44 -0
- package/lib/commands/push.js +295 -0
- package/lib/commands/split/__tests__/index.test.d.ts +1 -0
- package/lib/commands/split/__tests__/index.test.js +91 -0
- package/lib/commands/split/index.d.ts +13 -0
- package/lib/commands/split/index.js +259 -0
- package/lib/commands/split/types.d.ts +36 -0
- package/lib/commands/split/types.js +51 -0
- package/lib/commands/stats.d.ts +8 -0
- package/lib/commands/stats.js +96 -0
- package/lib/commands/translations.d.ts +7 -0
- package/lib/commands/translations.js +20 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +733 -0
- package/lib/types.d.ts +43 -0
- package/lib/types.js +5 -0
- package/lib/utils/__mocks__/miscellaneous.d.ts +43 -0
- package/lib/utils/__mocks__/miscellaneous.js +24 -0
- package/lib/utils/assert-node-version.d.ts +1 -0
- package/lib/utils/assert-node-version.js +16 -0
- package/lib/utils/fetch-with-timeout.d.ts +7 -0
- package/lib/utils/fetch-with-timeout.js +26 -0
- package/lib/utils/getCommandNameFromArgs.d.ts +2 -0
- package/lib/utils/getCommandNameFromArgs.js +6 -0
- package/lib/utils/js-utils.d.ts +5 -0
- package/lib/utils/js-utils.js +28 -0
- package/lib/utils/miscellaneous.d.ts +81 -0
- package/lib/utils/miscellaneous.js +551 -0
- package/lib/utils/platform.d.ts +16 -0
- package/lib/utils/platform.js +34 -0
- package/lib/utils/spinner.d.ts +10 -0
- package/lib/utils/spinner.js +42 -0
- package/lib/utils/update-version-notifier.d.ts +3 -0
- package/lib/utils/update-version-notifier.js +102 -0
- package/lib/wrapper.d.ts +11 -0
- package/lib/wrapper.js +65 -0
- package/package.json +69 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const api_keys_1 = require("../api-keys");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
describe('getApiKeys()', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
jest.resetAllMocks();
|
|
8
|
+
});
|
|
9
|
+
it('should return api key from environment variable', () => {
|
|
10
|
+
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
|
11
|
+
expect((0, api_keys_1.getApiKeys)('test-domain')).toEqual('test-api-key');
|
|
12
|
+
});
|
|
13
|
+
it('should return api key from credentials file', () => {
|
|
14
|
+
process.env.REDOCLY_AUTHORIZATION = '';
|
|
15
|
+
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
16
|
+
jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({
|
|
17
|
+
['test-domain']: 'test-api-key-from-credentials-file',
|
|
18
|
+
}));
|
|
19
|
+
expect((0, api_keys_1.getApiKeys)('test-domain')).toEqual('test-api-key-from-credentials-file');
|
|
20
|
+
});
|
|
21
|
+
it('should throw an error if no api key provided', () => {
|
|
22
|
+
process.env.REDOCLY_AUTHORIZATION = '';
|
|
23
|
+
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
|
|
24
|
+
expect(() => (0, api_keys_1.getApiKeys)('test-domain')).toThrowError('No api key provided, please use environment variable REDOCLY_AUTHORIZATION.');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_fetch_1 = require("node-fetch");
|
|
4
|
+
const FormData = require("form-data");
|
|
5
|
+
const colorette_1 = require("colorette");
|
|
6
|
+
const api_client_1 = require("../api-client");
|
|
7
|
+
jest.mock('node-fetch', () => ({
|
|
8
|
+
default: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
function mockFetchResponse(response) {
|
|
11
|
+
node_fetch_1.default.mockResolvedValue(response);
|
|
12
|
+
}
|
|
13
|
+
describe('ApiClient', () => {
|
|
14
|
+
const testToken = 'test-token';
|
|
15
|
+
const testDomain = 'test-domain.com';
|
|
16
|
+
const testOrg = 'test-org';
|
|
17
|
+
const testProject = 'test-project';
|
|
18
|
+
const version = '1.0.0';
|
|
19
|
+
const command = 'push';
|
|
20
|
+
const expectedUserAgent = `redocly-cli/${version} ${command}`;
|
|
21
|
+
describe('getDefaultBranch()', () => {
|
|
22
|
+
let apiClient;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
apiClient = new api_client_1.ReuniteApi({ domain: testDomain, apiKey: testToken, version, command });
|
|
25
|
+
});
|
|
26
|
+
it('should get default project branch', async () => {
|
|
27
|
+
mockFetchResponse({
|
|
28
|
+
ok: true,
|
|
29
|
+
json: jest.fn().mockResolvedValue({
|
|
30
|
+
branchName: 'test-branch',
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
const result = await apiClient.remotes.getDefaultBranch(testOrg, testProject);
|
|
34
|
+
expect(node_fetch_1.default).toHaveBeenCalledWith(`${testDomain}/api/orgs/${testOrg}/projects/${testProject}/source`, {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
Authorization: `Bearer ${testToken}`,
|
|
39
|
+
'user-agent': expectedUserAgent,
|
|
40
|
+
},
|
|
41
|
+
signal: expect.any(Object),
|
|
42
|
+
});
|
|
43
|
+
expect(result).toEqual('test-branch');
|
|
44
|
+
});
|
|
45
|
+
it('should throw parsed error if response is not ok', async () => {
|
|
46
|
+
mockFetchResponse({
|
|
47
|
+
ok: false,
|
|
48
|
+
json: jest.fn().mockResolvedValue({
|
|
49
|
+
type: 'about:blank',
|
|
50
|
+
title: 'Project source not found',
|
|
51
|
+
status: 404,
|
|
52
|
+
detail: 'Not Found',
|
|
53
|
+
object: 'problem',
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
await expect(apiClient.remotes.getDefaultBranch(testOrg, testProject)).rejects.toThrow(new api_client_1.ReuniteApiError('Failed to fetch default branch. Project source not found.', 404));
|
|
57
|
+
});
|
|
58
|
+
it('should throw statusText error if response is not ok', async () => {
|
|
59
|
+
mockFetchResponse({
|
|
60
|
+
ok: false,
|
|
61
|
+
statusText: 'Not found',
|
|
62
|
+
json: jest.fn().mockResolvedValue({
|
|
63
|
+
unknownField: 'unknown-error',
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
await expect(apiClient.remotes.getDefaultBranch(testOrg, testProject)).rejects.toThrow(new api_client_1.ReuniteApiError('Failed to fetch default branch. Not found.', 404));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('upsert()', () => {
|
|
70
|
+
const remotePayload = {
|
|
71
|
+
mountBranchName: 'remote-mount-branch-name',
|
|
72
|
+
mountPath: 'remote-mount-path',
|
|
73
|
+
};
|
|
74
|
+
const responseMock = {
|
|
75
|
+
id: 'remote-id',
|
|
76
|
+
type: 'CICD',
|
|
77
|
+
mountPath: 'remote-mount-path',
|
|
78
|
+
mountBranchName: 'remote-mount-branch-name',
|
|
79
|
+
organizationId: testOrg,
|
|
80
|
+
projectId: testProject,
|
|
81
|
+
};
|
|
82
|
+
let apiClient;
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
apiClient = new api_client_1.ReuniteApi({ domain: testDomain, apiKey: testToken, version, command });
|
|
85
|
+
});
|
|
86
|
+
it('should upsert remote', async () => {
|
|
87
|
+
mockFetchResponse({
|
|
88
|
+
ok: true,
|
|
89
|
+
json: jest.fn().mockResolvedValue(responseMock),
|
|
90
|
+
});
|
|
91
|
+
const result = await apiClient.remotes.upsert(testOrg, testProject, remotePayload);
|
|
92
|
+
expect(node_fetch_1.default).toHaveBeenCalledWith(`${testDomain}/api/orgs/${testOrg}/projects/${testProject}/remotes`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
Authorization: `Bearer ${testToken}`,
|
|
97
|
+
'user-agent': expectedUserAgent,
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
mountPath: remotePayload.mountPath,
|
|
101
|
+
mountBranchName: remotePayload.mountBranchName,
|
|
102
|
+
type: 'CICD',
|
|
103
|
+
autoMerge: true,
|
|
104
|
+
}),
|
|
105
|
+
signal: expect.any(Object),
|
|
106
|
+
agent: undefined,
|
|
107
|
+
});
|
|
108
|
+
expect(result).toEqual(responseMock);
|
|
109
|
+
});
|
|
110
|
+
it('should throw parsed error if response is not ok', async () => {
|
|
111
|
+
mockFetchResponse({
|
|
112
|
+
ok: false,
|
|
113
|
+
json: jest.fn().mockResolvedValue({
|
|
114
|
+
type: 'about:blank',
|
|
115
|
+
title: 'Not allowed to mount remote outside of project content path: /docs',
|
|
116
|
+
status: 403,
|
|
117
|
+
detail: 'Forbidden',
|
|
118
|
+
object: 'problem',
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
await expect(apiClient.remotes.upsert(testOrg, testProject, remotePayload)).rejects.toThrow(new api_client_1.ReuniteApiError('Failed to upsert remote. Not allowed to mount remote outside of project content path: /docs.', 403));
|
|
122
|
+
});
|
|
123
|
+
it('should throw statusText error if response is not ok', async () => {
|
|
124
|
+
mockFetchResponse({
|
|
125
|
+
ok: false,
|
|
126
|
+
status: 404,
|
|
127
|
+
statusText: 'Not found',
|
|
128
|
+
json: jest.fn().mockResolvedValue({
|
|
129
|
+
unknownField: 'unknown-error',
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
await expect(apiClient.remotes.upsert(testOrg, testProject, remotePayload)).rejects.toThrow(new api_client_1.ReuniteApiError('Failed to upsert remote. Not found.', 404));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('push()', () => {
|
|
136
|
+
const testRemoteId = 'test-remote-id';
|
|
137
|
+
const pushPayload = {
|
|
138
|
+
remoteId: testRemoteId,
|
|
139
|
+
commit: {
|
|
140
|
+
message: 'test-message',
|
|
141
|
+
author: {
|
|
142
|
+
name: 'test-name',
|
|
143
|
+
email: 'test-email',
|
|
144
|
+
},
|
|
145
|
+
branchName: 'test-branch-name',
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const filesMock = [{ path: 'some-file.yaml', stream: Buffer.from('fefef') }];
|
|
149
|
+
const responseMock = {
|
|
150
|
+
branchName: 'rem/cicd/rem_01he7sr6ys2agb7w0g9t7978fn-main',
|
|
151
|
+
hasChanges: true,
|
|
152
|
+
files: [
|
|
153
|
+
{
|
|
154
|
+
type: 'file',
|
|
155
|
+
name: 'some-file.yaml',
|
|
156
|
+
path: 'docs/remotes/some-file.yaml',
|
|
157
|
+
lastModified: 1698925132394.2993,
|
|
158
|
+
mimeType: 'text/yaml',
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
commitSha: 'bb23a2f8e012ac0b7b9961b57fb40d8686b21b43',
|
|
162
|
+
outdated: false,
|
|
163
|
+
};
|
|
164
|
+
let apiClient;
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
apiClient = new api_client_1.ReuniteApi({ domain: testDomain, apiKey: testToken, version, command });
|
|
167
|
+
});
|
|
168
|
+
it('should push to remote', async () => {
|
|
169
|
+
let passedFormData = new FormData();
|
|
170
|
+
node_fetch_1.default.mockImplementationOnce(async (_, options) => {
|
|
171
|
+
passedFormData = options.body;
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
json: jest.fn().mockResolvedValue(responseMock),
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
const formData = new FormData();
|
|
178
|
+
formData.append('remoteId', testRemoteId);
|
|
179
|
+
formData.append('commit[message]', pushPayload.commit.message);
|
|
180
|
+
formData.append('commit[author][name]', pushPayload.commit.author.name);
|
|
181
|
+
formData.append('commit[author][email]', pushPayload.commit.author.email);
|
|
182
|
+
formData.append('commit[branchName]', pushPayload.commit.branchName);
|
|
183
|
+
formData.append('files[some-file.yaml]', filesMock[0].stream);
|
|
184
|
+
const result = await apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock);
|
|
185
|
+
expect(node_fetch_1.default).toHaveBeenCalledWith(`${testDomain}/api/orgs/${testOrg}/projects/${testProject}/pushes`, expect.objectContaining({
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: {
|
|
188
|
+
Authorization: `Bearer ${testToken}`,
|
|
189
|
+
'user-agent': expectedUserAgent,
|
|
190
|
+
},
|
|
191
|
+
}));
|
|
192
|
+
expect(JSON.stringify(passedFormData).replace(new RegExp(passedFormData.getBoundary(), 'g'), '')).toEqual(JSON.stringify(formData).replace(new RegExp(formData.getBoundary(), 'g'), ''));
|
|
193
|
+
expect(result).toEqual(responseMock);
|
|
194
|
+
});
|
|
195
|
+
it('should throw parsed error if response is not ok', async () => {
|
|
196
|
+
mockFetchResponse({
|
|
197
|
+
ok: false,
|
|
198
|
+
json: jest.fn().mockResolvedValue({
|
|
199
|
+
type: 'about:blank',
|
|
200
|
+
title: 'Cannot push to remote',
|
|
201
|
+
status: 403,
|
|
202
|
+
detail: 'Forbidden',
|
|
203
|
+
object: 'problem',
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
await expect(apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock)).rejects.toThrow(new api_client_1.ReuniteApiError('Failed to push. Cannot push to remote.', 403));
|
|
207
|
+
});
|
|
208
|
+
it('should throw statusText error if response is not ok', async () => {
|
|
209
|
+
mockFetchResponse({
|
|
210
|
+
ok: false,
|
|
211
|
+
status: 404,
|
|
212
|
+
statusText: 'Not found',
|
|
213
|
+
json: jest.fn().mockResolvedValue({
|
|
214
|
+
unknownField: 'unknown-error',
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
await expect(apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock)).rejects.toThrow(new api_client_1.ReuniteApiError('Failed to push. Not found.', 404));
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
describe('Sunset header', () => {
|
|
221
|
+
const upsertRemoteMock = {
|
|
222
|
+
requestFn: () => apiClient.remotes.upsert(testOrg, testProject, {
|
|
223
|
+
mountBranchName: 'remote-mount-branch-name',
|
|
224
|
+
mountPath: 'remote-mount-path',
|
|
225
|
+
}),
|
|
226
|
+
responseBody: {
|
|
227
|
+
id: 'remote-id',
|
|
228
|
+
type: 'CICD',
|
|
229
|
+
mountPath: 'remote-mount-path',
|
|
230
|
+
mountBranchName: 'remote-mount-branch-name',
|
|
231
|
+
organizationId: testOrg,
|
|
232
|
+
projectId: testProject,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
const getDefaultBranchMock = {
|
|
236
|
+
requestFn: () => apiClient.remotes.getDefaultBranch(testOrg, testProject),
|
|
237
|
+
responseBody: {
|
|
238
|
+
branchName: 'test-branch',
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
const pushMock = {
|
|
242
|
+
requestFn: () => apiClient.remotes.push(testOrg, testProject, {
|
|
243
|
+
remoteId: 'test-remote-id',
|
|
244
|
+
commit: {
|
|
245
|
+
message: 'test-message',
|
|
246
|
+
author: {
|
|
247
|
+
name: 'test-name',
|
|
248
|
+
email: 'test-email',
|
|
249
|
+
},
|
|
250
|
+
branchName: 'test-branch-name',
|
|
251
|
+
},
|
|
252
|
+
}, [{ path: 'some-file.yaml', stream: Buffer.from('text content') }]),
|
|
253
|
+
responseBody: {
|
|
254
|
+
branchName: 'rem/cicd/rem_01he7sr6ys2agb7w0g9t7978fn-main',
|
|
255
|
+
hasChanges: true,
|
|
256
|
+
files: [
|
|
257
|
+
{
|
|
258
|
+
type: 'file',
|
|
259
|
+
name: 'some-file.yaml',
|
|
260
|
+
path: 'docs/remotes/some-file.yaml',
|
|
261
|
+
lastModified: 1698925132394.2993,
|
|
262
|
+
mimeType: 'text/yaml',
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
commitSha: 'bb23a2f8e012ac0b7b9961b57fb40d8686b21b43',
|
|
266
|
+
outdated: false,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
const endpointMocks = [upsertRemoteMock, getDefaultBranchMock, pushMock];
|
|
270
|
+
let apiClient;
|
|
271
|
+
beforeEach(() => {
|
|
272
|
+
apiClient = new api_client_1.ReuniteApi({ domain: testDomain, apiKey: testToken, version, command });
|
|
273
|
+
});
|
|
274
|
+
it.each(endpointMocks)('should report endpoint sunset in the past', async ({ responseBody, requestFn }) => {
|
|
275
|
+
jest.spyOn(process.stdout, 'write').mockImplementationOnce(() => true);
|
|
276
|
+
const sunsetDate = new Date('2024-09-06T12:30:32.456Z');
|
|
277
|
+
mockFetchResponse({
|
|
278
|
+
ok: true,
|
|
279
|
+
json: jest.fn().mockResolvedValue(responseBody),
|
|
280
|
+
headers: new Headers({
|
|
281
|
+
Sunset: sunsetDate.toISOString(),
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
await requestFn();
|
|
285
|
+
apiClient.reportSunsetWarnings();
|
|
286
|
+
expect(process.stdout.write).toHaveBeenCalledWith((0, colorette_1.red)(`The "push" command is not compatible with your version of Redocly CLI. Update to the latest version by running "npm install @redocly/cli@latest".\n\n`));
|
|
287
|
+
});
|
|
288
|
+
it.each(endpointMocks)('should report endpoint sunset in the future', async ({ responseBody, requestFn }) => {
|
|
289
|
+
jest.spyOn(process.stdout, 'write').mockImplementationOnce(() => true);
|
|
290
|
+
const sunsetDate = new Date(Date.now() + 1000 * 60 * 60 * 24);
|
|
291
|
+
mockFetchResponse({
|
|
292
|
+
ok: true,
|
|
293
|
+
json: jest.fn().mockResolvedValue(responseBody),
|
|
294
|
+
headers: new Headers({
|
|
295
|
+
Sunset: sunsetDate.toISOString(),
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
await requestFn();
|
|
299
|
+
apiClient.reportSunsetWarnings();
|
|
300
|
+
expect(process.stdout.write).toHaveBeenCalledWith((0, colorette_1.yellow)(`The "push" command will be incompatible with your version of Redocly CLI after ${sunsetDate.toLocaleString()}. Update to the latest version by running "npm install @redocly/cli@latest".\n\n`));
|
|
301
|
+
});
|
|
302
|
+
it('should report only expired resource', async () => {
|
|
303
|
+
jest.spyOn(process.stdout, 'write').mockImplementationOnce(() => true);
|
|
304
|
+
mockFetchResponse({
|
|
305
|
+
ok: true,
|
|
306
|
+
json: jest.fn().mockResolvedValue(upsertRemoteMock.responseBody),
|
|
307
|
+
headers: new Headers({
|
|
308
|
+
Sunset: new Date('2024-08-06T12:30:32.456Z').toISOString(),
|
|
309
|
+
}),
|
|
310
|
+
});
|
|
311
|
+
await upsertRemoteMock.requestFn();
|
|
312
|
+
mockFetchResponse({
|
|
313
|
+
ok: true,
|
|
314
|
+
json: jest.fn().mockResolvedValue(getDefaultBranchMock.responseBody),
|
|
315
|
+
headers: new Headers({
|
|
316
|
+
Sunset: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(),
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
319
|
+
await getDefaultBranchMock.requestFn();
|
|
320
|
+
mockFetchResponse({
|
|
321
|
+
ok: true,
|
|
322
|
+
json: jest.fn().mockResolvedValue(pushMock.responseBody),
|
|
323
|
+
headers: new Headers({
|
|
324
|
+
Sunset: new Date('2024-08-06T12:30:32.456Z').toISOString(),
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
await pushMock.requestFn();
|
|
328
|
+
apiClient.reportSunsetWarnings();
|
|
329
|
+
expect(process.stdout.write).toHaveBeenCalledTimes(1);
|
|
330
|
+
expect(process.stdout.write).toHaveBeenCalledWith((0, colorette_1.red)(`The "push" command is not compatible with your version of Redocly CLI. Update to the latest version by running "npm install @redocly/cli@latest".\n\n`));
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const domains_1 = require("../domains");
|
|
4
|
+
describe('getDomain()', () => {
|
|
5
|
+
it('should return the domain from environment variable', () => {
|
|
6
|
+
process.env.REDOCLY_DOMAIN = 'test-domain';
|
|
7
|
+
expect((0, domains_1.getDomain)()).toBe('test-domain');
|
|
8
|
+
});
|
|
9
|
+
it('should return the default domain if no domain provided', () => {
|
|
10
|
+
process.env.REDOCLY_DOMAIN = '';
|
|
11
|
+
expect((0, domains_1.getDomain)()).toBe('https://app.cloud.redocly.com');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type FetchWithTimeoutOptions } from '../../utils/fetch-with-timeout';
|
|
2
|
+
import type { Response } from 'node-fetch';
|
|
3
|
+
import type { ReadStream } from 'fs';
|
|
4
|
+
import type { ListRemotesResponse, PushResponse, UpsertRemoteResponse } from './types';
|
|
5
|
+
interface BaseApiClient {
|
|
6
|
+
request(url: string, options: FetchWithTimeoutOptions): Promise<Response>;
|
|
7
|
+
}
|
|
8
|
+
type CommandOption = 'push' | 'push-status';
|
|
9
|
+
export type SunsetWarning = {
|
|
10
|
+
sunsetDate: Date;
|
|
11
|
+
isSunsetExpired: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type SunsetWarningsBuffer = SunsetWarning[];
|
|
14
|
+
export declare class ReuniteApiError extends Error {
|
|
15
|
+
status: number;
|
|
16
|
+
constructor(message: string, status: number);
|
|
17
|
+
}
|
|
18
|
+
declare class RemotesApi {
|
|
19
|
+
private client;
|
|
20
|
+
private readonly domain;
|
|
21
|
+
private readonly apiKey;
|
|
22
|
+
constructor(client: BaseApiClient, domain: string, apiKey: string);
|
|
23
|
+
protected getParsedResponse<T>(response: Response): Promise<T>;
|
|
24
|
+
getDefaultBranch(organizationId: string, projectId: string): Promise<string>;
|
|
25
|
+
upsert(organizationId: string, projectId: string, remote: {
|
|
26
|
+
mountPath: string;
|
|
27
|
+
mountBranchName: string;
|
|
28
|
+
}): Promise<UpsertRemoteResponse>;
|
|
29
|
+
push(organizationId: string, projectId: string, payload: PushPayload, files: {
|
|
30
|
+
path: string;
|
|
31
|
+
stream: ReadStream | Buffer;
|
|
32
|
+
}[]): Promise<PushResponse>;
|
|
33
|
+
getRemotesList({ organizationId, projectId, mountPath, }: {
|
|
34
|
+
organizationId: string;
|
|
35
|
+
projectId: string;
|
|
36
|
+
mountPath: string;
|
|
37
|
+
}): Promise<ListRemotesResponse>;
|
|
38
|
+
getPush({ organizationId, projectId, pushId, }: {
|
|
39
|
+
organizationId: string;
|
|
40
|
+
projectId: string;
|
|
41
|
+
pushId: string;
|
|
42
|
+
}): Promise<PushResponse>;
|
|
43
|
+
}
|
|
44
|
+
export declare class ReuniteApi {
|
|
45
|
+
private apiClient;
|
|
46
|
+
private version;
|
|
47
|
+
private command;
|
|
48
|
+
remotes: RemotesApi;
|
|
49
|
+
constructor({ domain, apiKey, version, command, }: {
|
|
50
|
+
domain: string;
|
|
51
|
+
apiKey: string;
|
|
52
|
+
version: string;
|
|
53
|
+
command: CommandOption;
|
|
54
|
+
});
|
|
55
|
+
reportSunsetWarnings(): void;
|
|
56
|
+
}
|
|
57
|
+
export type PushPayload = {
|
|
58
|
+
remoteId: string;
|
|
59
|
+
commit: {
|
|
60
|
+
message: string;
|
|
61
|
+
branchName: string;
|
|
62
|
+
sha?: string;
|
|
63
|
+
url?: string;
|
|
64
|
+
createdAt?: string;
|
|
65
|
+
namespace?: string;
|
|
66
|
+
repository?: string;
|
|
67
|
+
author: {
|
|
68
|
+
name: string;
|
|
69
|
+
email: string;
|
|
70
|
+
image?: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
isMainBranch?: boolean;
|
|
74
|
+
};
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReuniteApi = exports.ReuniteApiError = void 0;
|
|
4
|
+
const colorette_1 = require("colorette");
|
|
5
|
+
const FormData = require("form-data");
|
|
6
|
+
const fetch_with_timeout_1 = require("../../utils/fetch-with-timeout");
|
|
7
|
+
class ReuniteApiError extends Error {
|
|
8
|
+
constructor(message, status) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.ReuniteApiError = ReuniteApiError;
|
|
14
|
+
class ReuniteApiClient {
|
|
15
|
+
constructor(version, command) {
|
|
16
|
+
this.version = version;
|
|
17
|
+
this.command = command;
|
|
18
|
+
this.sunsetWarnings = [];
|
|
19
|
+
}
|
|
20
|
+
async request(url, options) {
|
|
21
|
+
const headers = {
|
|
22
|
+
...options.headers,
|
|
23
|
+
'user-agent': `redocly-cli/${this.version.trim()} ${this.command}`,
|
|
24
|
+
};
|
|
25
|
+
const response = await (0, fetch_with_timeout_1.default)(url, {
|
|
26
|
+
...options,
|
|
27
|
+
headers,
|
|
28
|
+
});
|
|
29
|
+
this.collectSunsetWarning(response);
|
|
30
|
+
return response;
|
|
31
|
+
}
|
|
32
|
+
collectSunsetWarning(response) {
|
|
33
|
+
const sunsetTime = this.getSunsetDate(response);
|
|
34
|
+
if (!sunsetTime)
|
|
35
|
+
return;
|
|
36
|
+
const sunsetDate = new Date(sunsetTime);
|
|
37
|
+
if (sunsetTime > Date.now()) {
|
|
38
|
+
this.sunsetWarnings.push({
|
|
39
|
+
sunsetDate,
|
|
40
|
+
isSunsetExpired: false,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.sunsetWarnings.push({
|
|
45
|
+
sunsetDate,
|
|
46
|
+
isSunsetExpired: true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
getSunsetDate(response) {
|
|
51
|
+
const { headers } = response;
|
|
52
|
+
if (!headers) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const sunsetDate = headers.get('sunset') || headers.get('Sunset');
|
|
56
|
+
if (!sunsetDate) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
return Date.parse(sunsetDate);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
class RemotesApi {
|
|
63
|
+
constructor(client, domain, apiKey) {
|
|
64
|
+
this.client = client;
|
|
65
|
+
this.domain = domain;
|
|
66
|
+
this.apiKey = apiKey;
|
|
67
|
+
}
|
|
68
|
+
async getParsedResponse(response) {
|
|
69
|
+
const responseBody = await response.json();
|
|
70
|
+
if (response.ok) {
|
|
71
|
+
return responseBody;
|
|
72
|
+
}
|
|
73
|
+
throw new ReuniteApiError(`${responseBody.title || response.statusText || 'Unknown error'}.`, response.status);
|
|
74
|
+
}
|
|
75
|
+
async getDefaultBranch(organizationId, projectId) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await this.client.request(`${this.domain}/api/orgs/${organizationId}/projects/${projectId}/source`, {
|
|
78
|
+
timeout: fetch_with_timeout_1.DEFAULT_FETCH_TIMEOUT,
|
|
79
|
+
method: 'GET',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const source = await this.getParsedResponse(response);
|
|
86
|
+
return source.branchName;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const message = `Failed to fetch default branch. ${err.message}`;
|
|
90
|
+
if (err instanceof ReuniteApiError) {
|
|
91
|
+
throw new ReuniteApiError(message, err.status);
|
|
92
|
+
}
|
|
93
|
+
throw new Error(message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async upsert(organizationId, projectId, remote) {
|
|
97
|
+
try {
|
|
98
|
+
const response = await this.client.request(`${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes`, {
|
|
99
|
+
timeout: fetch_with_timeout_1.DEFAULT_FETCH_TIMEOUT,
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: {
|
|
102
|
+
'Content-Type': 'application/json',
|
|
103
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
mountPath: remote.mountPath,
|
|
107
|
+
mountBranchName: remote.mountBranchName,
|
|
108
|
+
type: 'CICD',
|
|
109
|
+
autoMerge: true,
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
return await this.getParsedResponse(response);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
const message = `Failed to upsert remote. ${err.message}`;
|
|
116
|
+
if (err instanceof ReuniteApiError) {
|
|
117
|
+
throw new ReuniteApiError(message, err.status);
|
|
118
|
+
}
|
|
119
|
+
throw new Error(message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async push(organizationId, projectId, payload, files) {
|
|
123
|
+
const formData = new FormData();
|
|
124
|
+
formData.append('remoteId', payload.remoteId);
|
|
125
|
+
formData.append('commit[message]', payload.commit.message);
|
|
126
|
+
formData.append('commit[author][name]', payload.commit.author.name);
|
|
127
|
+
formData.append('commit[author][email]', payload.commit.author.email);
|
|
128
|
+
formData.append('commit[branchName]', payload.commit.branchName);
|
|
129
|
+
payload.commit.url && formData.append('commit[url]', payload.commit.url);
|
|
130
|
+
payload.commit.namespace && formData.append('commit[namespaceId]', payload.commit.namespace);
|
|
131
|
+
payload.commit.sha && formData.append('commit[sha]', payload.commit.sha);
|
|
132
|
+
payload.commit.repository && formData.append('commit[repositoryId]', payload.commit.repository);
|
|
133
|
+
payload.commit.createdAt && formData.append('commit[createdAt]', payload.commit.createdAt);
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
formData.append(`files[${file.path}]`, file.stream);
|
|
136
|
+
}
|
|
137
|
+
payload.isMainBranch && formData.append('isMainBranch', 'true');
|
|
138
|
+
try {
|
|
139
|
+
const response = await this.client.request(`${this.domain}/api/orgs/${organizationId}/projects/${projectId}/pushes`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
143
|
+
},
|
|
144
|
+
body: formData,
|
|
145
|
+
});
|
|
146
|
+
return await this.getParsedResponse(response);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
const message = `Failed to push. ${err.message}`;
|
|
150
|
+
if (err instanceof ReuniteApiError) {
|
|
151
|
+
throw new ReuniteApiError(message, err.status);
|
|
152
|
+
}
|
|
153
|
+
throw new Error(message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async getRemotesList({ organizationId, projectId, mountPath, }) {
|
|
157
|
+
try {
|
|
158
|
+
const response = await this.client.request(`${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes?filter=mountPath:/${mountPath}/`, {
|
|
159
|
+
timeout: fetch_with_timeout_1.DEFAULT_FETCH_TIMEOUT,
|
|
160
|
+
method: 'GET',
|
|
161
|
+
headers: {
|
|
162
|
+
'Content-Type': 'application/json',
|
|
163
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
return await this.getParsedResponse(response);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
const message = `Failed to get remote list. ${err.message}`;
|
|
170
|
+
if (err instanceof ReuniteApiError) {
|
|
171
|
+
throw new ReuniteApiError(message, err.status);
|
|
172
|
+
}
|
|
173
|
+
throw new Error(message);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async getPush({ organizationId, projectId, pushId, }) {
|
|
177
|
+
try {
|
|
178
|
+
const response = await this.client.request(`${this.domain}/api/orgs/${organizationId}/projects/${projectId}/pushes/${pushId}`, {
|
|
179
|
+
timeout: fetch_with_timeout_1.DEFAULT_FETCH_TIMEOUT,
|
|
180
|
+
method: 'GET',
|
|
181
|
+
headers: {
|
|
182
|
+
'Content-Type': 'application/json',
|
|
183
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
return await this.getParsedResponse(response);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
const message = `Failed to get push status. ${err.message}`;
|
|
190
|
+
if (err instanceof ReuniteApiError) {
|
|
191
|
+
throw new ReuniteApiError(message, err.status);
|
|
192
|
+
}
|
|
193
|
+
throw new Error(message);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
class ReuniteApi {
|
|
198
|
+
constructor({ domain, apiKey, version, command, }) {
|
|
199
|
+
this.command = command;
|
|
200
|
+
this.version = version;
|
|
201
|
+
this.apiClient = new ReuniteApiClient(this.version, this.command);
|
|
202
|
+
this.remotes = new RemotesApi(this.apiClient, domain, apiKey);
|
|
203
|
+
}
|
|
204
|
+
reportSunsetWarnings() {
|
|
205
|
+
const sunsetWarnings = this.apiClient.sunsetWarnings;
|
|
206
|
+
if (sunsetWarnings.length) {
|
|
207
|
+
const [{ isSunsetExpired, sunsetDate }] = sunsetWarnings.sort((a, b) => {
|
|
208
|
+
// First, prioritize by expiration status
|
|
209
|
+
if (a.isSunsetExpired !== b.isSunsetExpired) {
|
|
210
|
+
return a.isSunsetExpired ? -1 : 1;
|
|
211
|
+
}
|
|
212
|
+
// If both are either expired or not, sort by sunset date
|
|
213
|
+
return a.sunsetDate > b.sunsetDate ? 1 : -1;
|
|
214
|
+
});
|
|
215
|
+
const updateVersionMessage = `Update to the latest version by running "npm install @redocly/cli@latest".`;
|
|
216
|
+
if (isSunsetExpired) {
|
|
217
|
+
process.stdout.write((0, colorette_1.red)(`The "${this.command}" command is not compatible with your version of Redocly CLI. ${updateVersionMessage}\n\n`));
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
process.stdout.write((0, colorette_1.yellow)(`The "${this.command}" command will be incompatible with your version of Redocly CLI after ${sunsetDate.toLocaleString()}. ${updateVersionMessage}\n\n`));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
exports.ReuniteApi = ReuniteApi;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getApiKeys(domain: string): any;
|