@redocly/cli 1.11.0 → 1.12.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.
- package/CHANGELOG.md +11 -0
- package/lib/__tests__/utils.test.js +3 -3
- package/lib/cms/api/types.d.ts +22 -11
- package/lib/cms/commands/__tests__/push-status.test.js +338 -29
- package/lib/cms/commands/__tests__/push.test.js +32 -2
- package/lib/cms/commands/__tests__/utils.test.d.ts +1 -0
- package/lib/cms/commands/__tests__/utils.test.js +60 -0
- package/lib/cms/commands/push-status.d.ts +14 -4
- package/lib/cms/commands/push-status.js +160 -90
- package/lib/cms/commands/push.d.ts +6 -2
- package/lib/cms/commands/push.js +8 -2
- package/lib/cms/commands/utils.d.ts +22 -0
- package/lib/cms/commands/utils.js +53 -0
- package/lib/index.js +12 -1
- package/lib/utils/miscellaneous.js +5 -4
- package/lib/wrapper.d.ts +1 -1
- package/package.json +2 -2
- package/src/__tests__/utils.test.ts +3 -3
- package/src/cms/api/types.ts +19 -12
- package/src/cms/commands/__tests__/push-status.test.ts +473 -47
- package/src/cms/commands/__tests__/push.test.ts +40 -2
- package/src/cms/commands/__tests__/utils.test.ts +62 -0
- package/src/cms/commands/push-status.ts +242 -120
- package/src/cms/commands/push.ts +21 -5
- package/src/cms/commands/utils.ts +52 -0
- package/src/index.ts +13 -2
- package/src/utils/miscellaneous.ts +5 -4
- package/src/wrapper.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -29,8 +29,8 @@ describe('handlePush()', () => {
|
|
|
29
29
|
|
|
30
30
|
beforeEach(() => {
|
|
31
31
|
remotes.getDefaultBranch.mockResolvedValueOnce('test-default-branch');
|
|
32
|
-
remotes.upsert.mockResolvedValueOnce({ id: 'test-remote-id' });
|
|
33
|
-
remotes.push.mockResolvedValueOnce({ branchName: 'uploaded-to-branch' });
|
|
32
|
+
remotes.upsert.mockResolvedValueOnce({ id: 'test-remote-id', mountPath: 'test-mount-path' });
|
|
33
|
+
remotes.push.mockResolvedValueOnce({ branchName: 'uploaded-to-branch', id: 'test-id' });
|
|
34
34
|
|
|
35
35
|
jest.spyOn(fs, 'createReadStream').mockReturnValue('stream' as any);
|
|
36
36
|
|
|
@@ -118,6 +118,44 @@ describe('handlePush()', () => {
|
|
|
118
118
|
);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
it('should return push id', async () => {
|
|
122
|
+
const mockConfig = { apis: {} } as any;
|
|
123
|
+
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
|
124
|
+
|
|
125
|
+
fsStatSyncSpy.mockReturnValueOnce({
|
|
126
|
+
isDirectory() {
|
|
127
|
+
return false;
|
|
128
|
+
},
|
|
129
|
+
} as any);
|
|
130
|
+
|
|
131
|
+
pathResolveSpy.mockImplementationOnce((p) => p);
|
|
132
|
+
pathRelativeSpy.mockImplementationOnce((_, p) => p);
|
|
133
|
+
pathDirnameSpy.mockImplementation((_: string) => '.');
|
|
134
|
+
|
|
135
|
+
const result = await handlePush(
|
|
136
|
+
{
|
|
137
|
+
domain: 'test-domain',
|
|
138
|
+
'mount-path': 'test-mount-path',
|
|
139
|
+
organization: 'test-org',
|
|
140
|
+
project: 'test-project',
|
|
141
|
+
branch: 'test-branch',
|
|
142
|
+
namespace: 'test-namespace',
|
|
143
|
+
repository: 'test-repository',
|
|
144
|
+
'commit-sha': 'test-commit-sha',
|
|
145
|
+
'commit-url': 'test-commit-url',
|
|
146
|
+
'default-branch': 'test-branch',
|
|
147
|
+
'created-at': 'test-created-at',
|
|
148
|
+
author: 'TestAuthor <test-author@mail.com>',
|
|
149
|
+
message: 'Test message',
|
|
150
|
+
files: ['test-file'],
|
|
151
|
+
'max-execution-time': 10,
|
|
152
|
+
},
|
|
153
|
+
mockConfig
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(result).toEqual({ pushId: 'test-id' });
|
|
157
|
+
});
|
|
158
|
+
|
|
121
159
|
it('should collect files from directory and preserve file structure', async () => {
|
|
122
160
|
const mockConfig = { apis: {} } as any;
|
|
123
161
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { retryUntilConditionMet } from '../utils';
|
|
2
|
+
|
|
3
|
+
jest.mock('@redocly/openapi-core', () => ({
|
|
4
|
+
pause: jest.requireActual('@redocly/openapi-core').pause,
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
describe('retryUntilConditionMet()', () => {
|
|
8
|
+
it('should retry until condition meet and return result', async () => {
|
|
9
|
+
const operation = jest
|
|
10
|
+
.fn()
|
|
11
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
|
12
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
|
13
|
+
.mockResolvedValueOnce({ status: 'done' });
|
|
14
|
+
|
|
15
|
+
const data = await retryUntilConditionMet({
|
|
16
|
+
operation,
|
|
17
|
+
condition: (result: any) => result?.status === 'done',
|
|
18
|
+
retryIntervalMs: 100,
|
|
19
|
+
retryTimeoutMs: 1000,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(data).toEqual({ status: 'done' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should throw error if condition not meet for desired timeout', async () => {
|
|
26
|
+
const operation = jest.fn().mockResolvedValue({ status: 'pending' });
|
|
27
|
+
|
|
28
|
+
await expect(
|
|
29
|
+
retryUntilConditionMet({
|
|
30
|
+
operation,
|
|
31
|
+
condition: (result: any) => result?.status === 'done',
|
|
32
|
+
retryIntervalMs: 100,
|
|
33
|
+
retryTimeoutMs: 1000,
|
|
34
|
+
})
|
|
35
|
+
).rejects.toThrow('Timeout exceeded');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should call "onConditionNotMet" and "onRetry" callbacks', async () => {
|
|
39
|
+
const operation = jest
|
|
40
|
+
.fn()
|
|
41
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
|
42
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
|
43
|
+
.mockResolvedValueOnce({ status: 'done' });
|
|
44
|
+
|
|
45
|
+
const onConditionNotMet = jest.fn();
|
|
46
|
+
const onRetry = jest.fn();
|
|
47
|
+
|
|
48
|
+
const data = await retryUntilConditionMet({
|
|
49
|
+
operation,
|
|
50
|
+
condition: (result: any) => result?.status === 'done',
|
|
51
|
+
retryIntervalMs: 100,
|
|
52
|
+
retryTimeoutMs: 1000,
|
|
53
|
+
onConditionNotMet,
|
|
54
|
+
onRetry,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(data).toEqual({ status: 'done' });
|
|
58
|
+
|
|
59
|
+
expect(onConditionNotMet).toHaveBeenCalledTimes(2);
|
|
60
|
+
expect(onRetry).toHaveBeenCalledTimes(2);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import * as colors from 'colorette';
|
|
2
|
-
import { Config } from '@redocly/openapi-core';
|
|
2
|
+
import type { Config, OutputFormat } from '@redocly/openapi-core';
|
|
3
|
+
|
|
3
4
|
import { exitWithError, printExecutionTime } from '../../utils/miscellaneous';
|
|
4
5
|
import { Spinner } from '../../utils/spinner';
|
|
5
6
|
import { DeploymentError } from '../utils';
|
|
6
|
-
import { yellow } from 'colorette';
|
|
7
7
|
import { ReuniteApiClient, getApiKeys, getDomain } from '../api';
|
|
8
8
|
import { capitalize } from '../../utils/js-utils';
|
|
9
|
+
import type {
|
|
10
|
+
DeploymentStatus,
|
|
11
|
+
DeploymentStatusResponse,
|
|
12
|
+
PushResponse,
|
|
13
|
+
ScorecardItem,
|
|
14
|
+
} from '../api/types';
|
|
15
|
+
import { retryUntilConditionMet } from './utils';
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const INTERVAL = 5000;
|
|
17
|
+
const RETRY_INTERVAL_MS = 5000; // 5 sec
|
|
13
18
|
|
|
14
19
|
export type PushStatusOptions = {
|
|
15
20
|
organization: string;
|
|
@@ -17,12 +22,25 @@ export type PushStatusOptions = {
|
|
|
17
22
|
pushId: string;
|
|
18
23
|
domain?: string;
|
|
19
24
|
config?: string;
|
|
20
|
-
format?: 'stylish'
|
|
25
|
+
format?: Extract<OutputFormat, 'stylish'>;
|
|
21
26
|
wait?: boolean;
|
|
22
|
-
'max-execution-time'
|
|
27
|
+
'max-execution-time'?: number; // in seconds
|
|
28
|
+
'retry-interval'?: number; // in seconds
|
|
29
|
+
'start-time'?: number; // in milliseconds
|
|
30
|
+
'continue-on-deploy-failures'?: boolean;
|
|
31
|
+
onRetry?: (lasSummary: PushStatusSummary) => void;
|
|
23
32
|
};
|
|
24
33
|
|
|
25
|
-
export
|
|
34
|
+
export interface PushStatusSummary {
|
|
35
|
+
preview: DeploymentStatusResponse;
|
|
36
|
+
production: DeploymentStatusResponse | null;
|
|
37
|
+
commit: PushResponse['commit'];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function handlePushStatus(
|
|
41
|
+
argv: PushStatusOptions,
|
|
42
|
+
config: Config
|
|
43
|
+
): Promise<PushStatusSummary | undefined> {
|
|
26
44
|
const startedAt = performance.now();
|
|
27
45
|
const spinner = new Spinner();
|
|
28
46
|
|
|
@@ -31,123 +49,198 @@ export async function handlePushStatus(argv: PushStatusOptions, config: Config)
|
|
|
31
49
|
const orgId = organization || config.organization;
|
|
32
50
|
|
|
33
51
|
if (!orgId) {
|
|
34
|
-
|
|
52
|
+
exitWithError(
|
|
35
53
|
`No organization provided, please use --organization option or specify the 'organization' field in the config file.`
|
|
36
54
|
);
|
|
55
|
+
return;
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
const domain = argv.domain || getDomain();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const maxExecutionTime = argv['max-execution-time'] || 600;
|
|
59
|
+
const maxExecutionTime = argv['max-execution-time'] || 1200; // 20 min
|
|
60
|
+
const retryIntervalMs = argv['retry-interval']
|
|
61
|
+
? argv['retry-interval'] * 1000
|
|
62
|
+
: RETRY_INTERVAL_MS;
|
|
63
|
+
const startTime = argv['start-time'] || Date.now();
|
|
64
|
+
const retryTimeoutMs = maxExecutionTime * 1000;
|
|
65
|
+
const continueOnDeployFailures = argv['continue-on-deploy-failures'] || false;
|
|
48
66
|
|
|
49
67
|
try {
|
|
50
68
|
const apiKey = getApiKeys(domain);
|
|
51
69
|
const client = new ReuniteApiClient(domain, apiKey);
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
const push = await waitForDeployment(client, 'preview');
|
|
71
|
+
let pushResponse: PushResponse;
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
pushResponse = await retryUntilConditionMet({
|
|
74
|
+
operation: () =>
|
|
75
|
+
client.remotes.getPush({
|
|
76
|
+
organizationId: orgId,
|
|
77
|
+
projectId,
|
|
78
|
+
pushId,
|
|
79
|
+
}),
|
|
80
|
+
condition: wait
|
|
81
|
+
? // Keep retrying if status is "pending" or "running" (returning false, so the operation will be retried)
|
|
82
|
+
(result) => !['pending', 'running'].includes(result.status['preview'].deploy.status)
|
|
83
|
+
: null,
|
|
84
|
+
onConditionNotMet: (lastResult) => {
|
|
85
|
+
displayDeploymentAndBuildStatus({
|
|
86
|
+
status: lastResult.status['preview'].deploy.status,
|
|
87
|
+
url: lastResult.status['preview'].deploy.url,
|
|
88
|
+
spinner,
|
|
89
|
+
buildType: 'preview',
|
|
90
|
+
continueOnDeployFailures,
|
|
91
|
+
wait,
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
onRetry: (lastResult) => {
|
|
95
|
+
if (argv.onRetry) {
|
|
96
|
+
argv.onRetry({
|
|
97
|
+
preview: lastResult.status.preview,
|
|
98
|
+
production: lastResult.isMainBranch ? lastResult.status.production : null,
|
|
99
|
+
commit: lastResult.commit,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
startTime,
|
|
104
|
+
retryTimeoutMs,
|
|
105
|
+
retryIntervalMs,
|
|
106
|
+
});
|
|
59
107
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
108
|
+
printPushStatus({
|
|
109
|
+
buildType: 'preview',
|
|
110
|
+
spinner,
|
|
111
|
+
wait,
|
|
112
|
+
push: pushResponse,
|
|
113
|
+
continueOnDeployFailures,
|
|
114
|
+
});
|
|
115
|
+
printScorecard(pushResponse.status.preview.scorecard);
|
|
63
116
|
|
|
64
|
-
const
|
|
65
|
-
|
|
117
|
+
const shouldWaitForProdDeployment =
|
|
118
|
+
pushResponse.isMainBranch &&
|
|
119
|
+
(wait ? pushResponse.status.preview.deploy.status === 'success' : true);
|
|
66
120
|
|
|
67
|
-
if (
|
|
68
|
-
await
|
|
69
|
-
|
|
121
|
+
if (shouldWaitForProdDeployment) {
|
|
122
|
+
pushResponse = await retryUntilConditionMet({
|
|
123
|
+
operation: () =>
|
|
124
|
+
client.remotes.getPush({
|
|
125
|
+
organizationId: orgId,
|
|
126
|
+
projectId,
|
|
127
|
+
pushId,
|
|
128
|
+
}),
|
|
129
|
+
condition: wait
|
|
130
|
+
? // Keep retrying if status is "pending" or "running" (returning false, so the operation will be retried)
|
|
131
|
+
(result) => !['pending', 'running'].includes(result.status['production'].deploy.status)
|
|
132
|
+
: null,
|
|
133
|
+
onConditionNotMet: (lastResult) => {
|
|
134
|
+
displayDeploymentAndBuildStatus({
|
|
135
|
+
status: lastResult.status['production'].deploy.status,
|
|
136
|
+
url: lastResult.status['production'].deploy.url,
|
|
137
|
+
spinner,
|
|
138
|
+
buildType: 'production',
|
|
139
|
+
continueOnDeployFailures,
|
|
140
|
+
wait,
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
onRetry: (lastResult) => {
|
|
144
|
+
if (argv.onRetry) {
|
|
145
|
+
argv.onRetry({
|
|
146
|
+
preview: lastResult.status.preview,
|
|
147
|
+
production: lastResult.isMainBranch ? lastResult.status.production : null,
|
|
148
|
+
commit: lastResult.commit,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
startTime,
|
|
153
|
+
retryTimeoutMs,
|
|
154
|
+
retryIntervalMs,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (pushResponse.isMainBranch) {
|
|
159
|
+
printPushStatus({
|
|
160
|
+
buildType: 'production',
|
|
161
|
+
spinner,
|
|
162
|
+
wait,
|
|
163
|
+
push: pushResponse,
|
|
164
|
+
continueOnDeployFailures,
|
|
165
|
+
});
|
|
166
|
+
printScorecard(pushResponse.status.production.scorecard);
|
|
70
167
|
}
|
|
168
|
+
printPushStatusInfo({ orgId, projectId, pushId, startedAt });
|
|
71
169
|
|
|
72
|
-
|
|
170
|
+
const summary: PushStatusSummary = {
|
|
171
|
+
preview: pushResponse.status.preview,
|
|
172
|
+
production: pushResponse.isMainBranch ? pushResponse.status.production : null,
|
|
173
|
+
commit: pushResponse.commit,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return summary;
|
|
73
177
|
} catch (err) {
|
|
178
|
+
spinner.stop(); // Spinner can block process exit, so we need to stop it explicitly.
|
|
179
|
+
|
|
74
180
|
const message =
|
|
75
181
|
err instanceof DeploymentError
|
|
76
182
|
? err.message
|
|
77
183
|
: `✗ Failed to get push status. Reason: ${err.message}\n`;
|
|
78
184
|
exitWithError(message);
|
|
185
|
+
return;
|
|
186
|
+
} finally {
|
|
187
|
+
spinner.stop(); // Spinner can block process exit, so we need to stop it explicitly.
|
|
79
188
|
}
|
|
189
|
+
}
|
|
80
190
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
getAndPrintPushStatus(client, buildType)
|
|
101
|
-
.then((push) => {
|
|
102
|
-
if (!['pending', 'running'].includes(push.status[buildType].deploy.status)) {
|
|
103
|
-
printScorecard(push.status[buildType].scorecard);
|
|
104
|
-
resolve(push);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
191
|
+
function printPushStatusInfo({
|
|
192
|
+
orgId,
|
|
193
|
+
projectId,
|
|
194
|
+
pushId,
|
|
195
|
+
startedAt,
|
|
196
|
+
}: {
|
|
197
|
+
orgId: string;
|
|
198
|
+
projectId: string;
|
|
199
|
+
pushId: string;
|
|
200
|
+
startedAt: number;
|
|
201
|
+
}) {
|
|
202
|
+
process.stderr.write(
|
|
203
|
+
`\nProcessed push-status for ${colors.yellow(orgId!)}, ${colors.yellow(
|
|
204
|
+
projectId
|
|
205
|
+
)} and pushID ${colors.yellow(pushId)}.\n`
|
|
206
|
+
);
|
|
207
|
+
printExecutionTime('push-status', startedAt, 'Finished');
|
|
208
|
+
}
|
|
107
209
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
210
|
+
function printPushStatus({
|
|
211
|
+
buildType,
|
|
212
|
+
spinner,
|
|
213
|
+
push,
|
|
214
|
+
continueOnDeployFailures,
|
|
215
|
+
}: {
|
|
216
|
+
buildType: 'preview' | 'production';
|
|
217
|
+
spinner: Spinner;
|
|
218
|
+
wait?: boolean;
|
|
219
|
+
push?: PushResponse | null;
|
|
220
|
+
continueOnDeployFailures: boolean;
|
|
221
|
+
}) {
|
|
222
|
+
if (!push) {
|
|
223
|
+
return;
|
|
119
224
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
225
|
+
if (push.isOutdated || !push.hasChanges) {
|
|
226
|
+
process.stderr.write(
|
|
227
|
+
colors.yellow(
|
|
228
|
+
`Files not added to your project. Reason: ${push.isOutdated ? 'outdated' : 'no changes'}.\n`
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
} else {
|
|
232
|
+
displayDeploymentAndBuildStatus({
|
|
233
|
+
status: push.status[buildType].deploy.status,
|
|
234
|
+
url: push.status[buildType].deploy.url,
|
|
235
|
+
buildType,
|
|
236
|
+
spinner,
|
|
237
|
+
continueOnDeployFailures,
|
|
129
238
|
});
|
|
130
|
-
|
|
131
|
-
if (push.isOutdated || !push.hasChanges) {
|
|
132
|
-
process.stderr.write(
|
|
133
|
-
yellow(`Files not uploaded. Reason: ${push.isOutdated ? 'outdated' : 'no changes'}.\n`)
|
|
134
|
-
);
|
|
135
|
-
} else {
|
|
136
|
-
displayDeploymentAndBuildStatus({
|
|
137
|
-
status: push.status[buildType].deploy.status,
|
|
138
|
-
previewUrl: push.status[buildType].deploy.url,
|
|
139
|
-
buildType,
|
|
140
|
-
spinner,
|
|
141
|
-
wait,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return push;
|
|
146
239
|
}
|
|
147
240
|
}
|
|
148
241
|
|
|
149
|
-
function printScorecard(scorecard
|
|
150
|
-
if (!scorecard.length) {
|
|
242
|
+
function printScorecard(scorecard?: ScorecardItem[]) {
|
|
243
|
+
if (!scorecard || scorecard.length === 0) {
|
|
151
244
|
return;
|
|
152
245
|
}
|
|
153
246
|
process.stdout.write(`\n${colors.magenta('Scorecard')}:`);
|
|
@@ -163,42 +256,71 @@ function printScorecard(scorecard: ScorecardItem[]) {
|
|
|
163
256
|
|
|
164
257
|
function displayDeploymentAndBuildStatus({
|
|
165
258
|
status,
|
|
166
|
-
|
|
259
|
+
url,
|
|
167
260
|
spinner,
|
|
168
261
|
buildType,
|
|
262
|
+
continueOnDeployFailures,
|
|
169
263
|
wait,
|
|
170
264
|
}: {
|
|
171
265
|
status: DeploymentStatus;
|
|
172
|
-
|
|
266
|
+
url: string | null;
|
|
173
267
|
spinner: Spinner;
|
|
174
268
|
buildType: 'preview' | 'production';
|
|
269
|
+
continueOnDeployFailures: boolean;
|
|
175
270
|
wait?: boolean;
|
|
176
271
|
}) {
|
|
272
|
+
const message = getMessage({ status, url, buildType, wait });
|
|
273
|
+
|
|
274
|
+
if (status === 'failed' && !continueOnDeployFailures) {
|
|
275
|
+
spinner.stop();
|
|
276
|
+
throw new DeploymentError(message);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (wait && (status === 'pending' || status === 'running')) {
|
|
280
|
+
return spinner.start(message);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
spinner.stop();
|
|
284
|
+
return process.stdout.write(message);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getMessage({
|
|
288
|
+
status,
|
|
289
|
+
url,
|
|
290
|
+
buildType,
|
|
291
|
+
wait,
|
|
292
|
+
}: {
|
|
293
|
+
status: DeploymentStatus;
|
|
294
|
+
url: string | null;
|
|
295
|
+
buildType: 'preview' | 'production';
|
|
296
|
+
wait?: boolean;
|
|
297
|
+
}): string {
|
|
177
298
|
switch (status) {
|
|
299
|
+
case 'skipped':
|
|
300
|
+
return `${colors.yellow(`Skipped ${buildType}`)}\n`;
|
|
301
|
+
|
|
302
|
+
case 'pending': {
|
|
303
|
+
const message = `${colors.yellow(`Pending ${buildType}`)}`;
|
|
304
|
+
return wait ? message : `Status: ${message}\n`;
|
|
305
|
+
}
|
|
306
|
+
case 'running': {
|
|
307
|
+
const message = `${colors.yellow(`Running ${buildType}`)}`;
|
|
308
|
+
return wait ? message : `Status: ${message}\n`;
|
|
309
|
+
}
|
|
178
310
|
case 'success':
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
)}: ${colors.cyan(previewUrl!)}\n`
|
|
184
|
-
);
|
|
311
|
+
return `${colors.green(`🚀 ${capitalize(buildType)} deploy success.`)}\n${colors.magenta(
|
|
312
|
+
`${capitalize(buildType)} URL`
|
|
313
|
+
)}: ${colors.cyan(url || 'No URL yet.')}\n`;
|
|
314
|
+
|
|
185
315
|
case 'failed':
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
return wait
|
|
194
|
-
|
|
195
|
-
: process.stdout.write(`Status: ${colors.yellow(`Pending ${buildType}`)}\n`);
|
|
196
|
-
case 'skipped':
|
|
197
|
-
spinner.stop();
|
|
198
|
-
return process.stdout.write(`${colors.yellow(`Skipped ${buildType}`)}\n`);
|
|
199
|
-
case 'running':
|
|
200
|
-
return wait
|
|
201
|
-
? spinner.start(`${colors.yellow(`Running ${buildType}`)}`)
|
|
202
|
-
: process.stdout.write(`Status: ${colors.yellow(`Running ${buildType}`)}\n`);
|
|
316
|
+
return `${colors.red(`❌ ${capitalize(buildType)} deploy fail.`)}\n${colors.magenta(
|
|
317
|
+
`${capitalize(buildType)} URL`
|
|
318
|
+
)}: ${colors.cyan(url || 'No URL yet.')}`;
|
|
319
|
+
|
|
320
|
+
default: {
|
|
321
|
+
const message = `${colors.yellow(`No status yet for ${buildType} deploy`)}`;
|
|
322
|
+
|
|
323
|
+
return wait ? message : `Status: ${message}\n`;
|
|
324
|
+
}
|
|
203
325
|
}
|
|
204
326
|
}
|
package/src/cms/commands/push.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import {
|
|
4
|
-
import { exitWithError, HandledError, printExecutionTime } from '../../utils/miscellaneous';
|
|
3
|
+
import { slash } from '@redocly/openapi-core';
|
|
5
4
|
import { green, yellow } from 'colorette';
|
|
6
5
|
import pluralize = require('pluralize');
|
|
6
|
+
|
|
7
|
+
import type { OutputFormat, Config } from '@redocly/openapi-core';
|
|
8
|
+
|
|
9
|
+
import { exitWithError, HandledError, printExecutionTime } from '../../utils/miscellaneous';
|
|
7
10
|
import { handlePushStatus } from './push-status';
|
|
8
11
|
import { ReuniteApiClient, getDomain, getApiKeys } from '../api';
|
|
9
12
|
|
|
@@ -29,13 +32,20 @@ export type PushOptions = {
|
|
|
29
32
|
config?: string;
|
|
30
33
|
'wait-for-deployment'?: boolean;
|
|
31
34
|
'max-execution-time': number;
|
|
35
|
+
'continue-on-deploy-failures'?: boolean;
|
|
32
36
|
verbose?: boolean;
|
|
37
|
+
format?: Extract<OutputFormat, 'stylish'>;
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
type FileToUpload = { name: string; path: string };
|
|
36
41
|
|
|
37
|
-
export async function handlePush(
|
|
38
|
-
|
|
42
|
+
export async function handlePush(
|
|
43
|
+
argv: PushOptions,
|
|
44
|
+
config: Config
|
|
45
|
+
): Promise<{ pushId: string } | void> {
|
|
46
|
+
const startedAt = performance.now(); // for printing execution time
|
|
47
|
+
const startTime = Date.now(); // for push-status command
|
|
48
|
+
|
|
39
49
|
const { organization, project: projectId, 'mount-path': mountPath, verbose } = argv;
|
|
40
50
|
|
|
41
51
|
const orgId = organization || config.organization;
|
|
@@ -111,8 +121,8 @@ export async function handlePush(argv: PushOptions, config: Config) {
|
|
|
111
121
|
filesToUpload.forEach((f) => {
|
|
112
122
|
process.stderr.write(green(`✓ ${f.name}\n`));
|
|
113
123
|
});
|
|
114
|
-
process.stdout.write('\n');
|
|
115
124
|
|
|
125
|
+
process.stdout.write('\n');
|
|
116
126
|
process.stdout.write(`Push ID: ${id}\n`);
|
|
117
127
|
|
|
118
128
|
if (waitForDeployment) {
|
|
@@ -126,6 +136,8 @@ export async function handlePush(argv: PushOptions, config: Config) {
|
|
|
126
136
|
wait: true,
|
|
127
137
|
domain,
|
|
128
138
|
'max-execution-time': maxExecutionTime,
|
|
139
|
+
'start-time': startTime,
|
|
140
|
+
'continue-on-deploy-failures': argv['continue-on-deploy-failures'],
|
|
129
141
|
},
|
|
130
142
|
config
|
|
131
143
|
);
|
|
@@ -139,6 +151,10 @@ export async function handlePush(argv: PushOptions, config: Config) {
|
|
|
139
151
|
filesToUpload.length
|
|
140
152
|
)} uploaded to organization ${orgId}, project ${projectId}. Push ID: ${id}.`
|
|
141
153
|
);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
pushId: id,
|
|
157
|
+
};
|
|
142
158
|
} catch (err) {
|
|
143
159
|
const message =
|
|
144
160
|
err instanceof HandledError ? '' : `✗ File upload failed. Reason: ${err.message}`;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { pause } from '@redocly/openapi-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This function retries an operation until a condition is met or a timeout is exceeded.
|
|
5
|
+
* If the condition is not met within the timeout, an error is thrown.
|
|
6
|
+
* @operation The operation to retry.
|
|
7
|
+
* @condition The condition to check after each operation result. Return false to continue retrying. Return true to stop retrying.
|
|
8
|
+
* If not provided, the first result will be returned.
|
|
9
|
+
* @param onConditionNotMet Will be called with the last result right after checking condition and before timeout and retrying.
|
|
10
|
+
* @param onRetry Will be called right before retrying operation with the last result before retrying.
|
|
11
|
+
* @param startTime The start time of the operation. Default is the current time.
|
|
12
|
+
* @param retryTimeoutMs The maximum time to retry the operation. Default is 10 minutes.
|
|
13
|
+
* @param retryIntervalMs The interval between retries. Default is 5 seconds.
|
|
14
|
+
*/
|
|
15
|
+
export async function retryUntilConditionMet<T>({
|
|
16
|
+
operation,
|
|
17
|
+
condition,
|
|
18
|
+
onConditionNotMet,
|
|
19
|
+
onRetry,
|
|
20
|
+
startTime = Date.now(),
|
|
21
|
+
retryTimeoutMs = 600000, // 10 min
|
|
22
|
+
retryIntervalMs = 5000, // 5 sec
|
|
23
|
+
}: {
|
|
24
|
+
operation: () => Promise<T>;
|
|
25
|
+
condition?: ((result: T) => boolean) | null;
|
|
26
|
+
onConditionNotMet?: (lastResult: T) => void;
|
|
27
|
+
onRetry?: (lastResult: T) => void | Promise<void>;
|
|
28
|
+
startTime?: number;
|
|
29
|
+
retryTimeoutMs?: number;
|
|
30
|
+
retryIntervalMs?: number;
|
|
31
|
+
}): Promise<T> {
|
|
32
|
+
async function attempt(): Promise<T> {
|
|
33
|
+
const result = await operation();
|
|
34
|
+
|
|
35
|
+
if (!condition) {
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (condition(result)) {
|
|
40
|
+
return result;
|
|
41
|
+
} else if (Date.now() - startTime > retryTimeoutMs) {
|
|
42
|
+
throw new Error('Timeout exceeded');
|
|
43
|
+
} else {
|
|
44
|
+
onConditionNotMet?.(result);
|
|
45
|
+
await pause(retryIntervalMs);
|
|
46
|
+
await onRetry?.(result);
|
|
47
|
+
return attempt();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return attempt();
|
|
52
|
+
}
|