mirror of
https://github.com/LukeHagar/redocly-cli.git
synced 2025-12-06 04:21:09 +00:00
feat: update push and push-status commands (#1481)
* feat(bh): make 'project' and 'domain' required for push command (CMS) * chore: add OutputFormat type for 'format' parameter for push-status command * feat: add 'format' parameter for 'push-status' command * feat: add 'markdown' to OutputFormat type * feat: make domain not required for push-status * refactor: simplify `handlePushStatus` function * feat: add json format for push-status command * feat: remove `async` from printPushStatus * feat: improve printing json summary and exit with error * feat: add 'format' option to cli push command * feat: add push metadata to json output * feat: add return data for push and pushStatus commands * feat: update push status command * tests: add new unit tests for push status * refactor: remove json format for "push" and "push-status" commands * feat: add "ignore-deployment-failures" option * chore: replace "satisfies" with "as" * chore: add changeset * feat: add onTimeOutExceeded callback * Apply suggestions from code review Co-authored-by: Lorna Jane Mitchell <github@lornajane.net> * Apply suggestions from code review Co-authored-by: Andrew Tatomyr <andrew.tatomyr@redocly.com> Co-authored-by: Roman Sainchuk <albuman32@gmail.com> * chore: update return type for commandHandler * tests: use expect.rejects instead of catching errors * feat: rename 'ignore-deployment-failures' to 'continue-on-deployment-failures' * feat: add onConditionNotMet callback * tests: add unit tests * chore: add comments * refactor: rename "wait" to "pause" and move to utils * refactor: change import type * refactor: add explicit array length check * fix: add correct wrapper type for argv params * feat: update types for push status API * chore: add "unit:watch" script to package.json * test: fix unit tests * tests: add tests for "onRetry" and "max-execution-time" parameters * chore: restore museum.yaml * feat: update API types for PushResponse * refactor: rename "continue-on-deployment-failures" to "continue-on-deploy-failures" * feat: update interface for `onRetry` function * feat: remove "instanceof Promise" check * refactor: reorder imports * feat: increase maxExecutionTime to 20 min --------- Co-authored-by: Lorna Jane Mitchell <github@lornajane.net> Co-authored-by: Andrew Tatomyr <andrew.tatomyr@redocly.com> Co-authored-by: Roman Sainchuk <albuman32@gmail.com>
This commit is contained in:
committed by
GitHub
parent
89af9c6eb2
commit
4cf08db490
5
.changeset/heavy-swans-sin.md
Normal file
5
.changeset/heavy-swans-sin.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@redocly/cli": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added return values for the `push` and `push-status` commands.
|
||||||
@@ -166,6 +166,8 @@ Run unit tests with this command: `npm run test`.
|
|||||||
Unit tests in the **cli** package are sensitive to top-level configuration file (**redocly.yaml**).
|
Unit tests in the **cli** package are sensitive to top-level configuration file (**redocly.yaml**).
|
||||||
|
|
||||||
To run a specific test, use this command: `npm run unit -- -t 'Test name'`.
|
To run a specific test, use this command: `npm run unit -- -t 'Test name'`.
|
||||||
|
To run tests in watch mode, run: `npm run unit:watch`
|
||||||
|
To run single file in watch mode, run: `npm run unit:watch -- <path/to/your/file.test.ts>`
|
||||||
To update snapshots, run `npm run unit -- -u`.
|
To update snapshots, run `npm run unit -- -u`.
|
||||||
|
|
||||||
To get coverage per package run `npm run coverage:cli` or `npm run coverage:core`.
|
To get coverage per package run `npm run coverage:cli` or `npm run coverage:core`.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"test": "npm run typecheck && npm run unit",
|
"test": "npm run typecheck && npm run unit",
|
||||||
"jest": "REDOCLY_TELEMETRY=off jest ./packages",
|
"jest": "REDOCLY_TELEMETRY=off jest ./packages",
|
||||||
"unit": "npm run jest -- --coverage --coverageReporters lcov text-summary",
|
"unit": "npm run jest -- --coverage --coverageReporters lcov text-summary",
|
||||||
|
"unit:watch": "REDOCLY_TELEMETRY=off jest --watch",
|
||||||
"coverage:cli": "npm run jest -- --roots packages/cli/src --coverage",
|
"coverage:cli": "npm run jest -- --roots packages/cli/src --coverage",
|
||||||
"coverage:core": "npm run jest -- --roots packages/core/src --coverage",
|
"coverage:core": "npm run jest -- --roots packages/core/src --coverage",
|
||||||
"typecheck": "tsc --noEmit --skipLibCheck",
|
"typecheck": "tsc --noEmit --skipLibCheck",
|
||||||
|
|||||||
@@ -48,33 +48,42 @@ export type Remote = {
|
|||||||
export type PushResponse = {
|
export type PushResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
remoteId: string;
|
remoteId: string;
|
||||||
|
isMainBranch: boolean;
|
||||||
|
isOutdated: boolean;
|
||||||
|
hasChanges: boolean;
|
||||||
|
replace: boolean;
|
||||||
|
scoutJobId: string | null;
|
||||||
|
uploadedFiles: Array<{ path: string; mimeType: string }>;
|
||||||
commit: {
|
commit: {
|
||||||
message: string;
|
|
||||||
branchName: string;
|
branchName: string;
|
||||||
sha: string | null;
|
message: string;
|
||||||
url: string | null;
|
|
||||||
createdAt: string | null;
|
createdAt: string | null;
|
||||||
namespace: string | null;
|
namespaceId: string | null;
|
||||||
repository: string | null;
|
repositoryId: string | null;
|
||||||
|
url: string | null;
|
||||||
|
sha: string | null;
|
||||||
author: {
|
author: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
image: string | null;
|
image: string | null;
|
||||||
};
|
};
|
||||||
|
statuses: Array<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
status: 'pending' | 'running' | 'success' | 'failed';
|
||||||
|
url: string | null;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
remote: {
|
remote: {
|
||||||
commits: {
|
commits: {
|
||||||
branchName: string;
|
|
||||||
sha: string;
|
sha: string;
|
||||||
|
branchName: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
hasChanges: boolean;
|
|
||||||
isOutdated: boolean;
|
|
||||||
isMainBranch: boolean;
|
|
||||||
status: PushStatusResponse;
|
status: PushStatusResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeploymentStatusResponse = {
|
export type DeploymentStatusResponse = {
|
||||||
deploy: {
|
deploy: {
|
||||||
url: string | null;
|
url: string | null;
|
||||||
status: DeploymentStatus;
|
status: DeploymentStatus;
|
||||||
@@ -96,6 +105,4 @@ export type ScorecardItem = {
|
|||||||
|
|
||||||
export type PushStatusBase = 'pending' | 'success' | 'running' | 'failed';
|
export type PushStatusBase = 'pending' | 'success' | 'running' | 'failed';
|
||||||
|
|
||||||
// export type BuildStatus = PushStatusBase | 'NOT_STARTED' | 'QUEUED';
|
|
||||||
|
|
||||||
export type DeploymentStatus = 'skipped' | PushStatusBase;
|
export type DeploymentStatus = 'skipped' | PushStatusBase;
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { handlePushStatus } from '../push-status';
|
import { handlePushStatus } from '../push-status';
|
||||||
import { PushResponse } from '../../api/types';
|
import { PushResponse } from '../../api/types';
|
||||||
import { exitWithError } from '../../../utils/miscellaneous';
|
|
||||||
|
|
||||||
const remotes = {
|
const remotes = {
|
||||||
getPush: jest.fn(),
|
getPush: jest.fn(),
|
||||||
getRemotesList: jest.fn(),
|
getRemotesList: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('../../../utils/miscellaneous');
|
|
||||||
|
|
||||||
jest.mock('colorette', () => ({
|
jest.mock('colorette', () => ({
|
||||||
green: (str: string) => str,
|
green: (str: string) => str,
|
||||||
yellow: (str: string) => str,
|
yellow: (str: string) => str,
|
||||||
@@ -25,28 +22,57 @@ jest.mock('../../api', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('@redocly/openapi-core', () => ({
|
||||||
|
pause: jest.requireActual('@redocly/openapi-core').pause,
|
||||||
|
}));
|
||||||
|
|
||||||
describe('handlePushStatus()', () => {
|
describe('handlePushStatus()', () => {
|
||||||
const mockConfig = { apis: {} } as any;
|
const mockConfig = { apis: {} } as any;
|
||||||
|
|
||||||
const pushResponseStub = {
|
const commitStub: PushResponse['commit'] = {
|
||||||
|
message: 'test-commit-message',
|
||||||
|
branchName: 'test-branch-name',
|
||||||
|
sha: null,
|
||||||
|
url: null,
|
||||||
|
createdAt: null,
|
||||||
|
namespaceId: null,
|
||||||
|
repositoryId: null,
|
||||||
|
author: {
|
||||||
|
name: 'test-author-name',
|
||||||
|
email: 'test-author-email',
|
||||||
|
image: null,
|
||||||
|
},
|
||||||
|
statuses: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushResponseStub: PushResponse = {
|
||||||
|
id: 'test-push-id',
|
||||||
|
remoteId: 'test-remote-id',
|
||||||
|
replace: false,
|
||||||
|
scoutJobId: null,
|
||||||
|
uploadedFiles: [],
|
||||||
|
commit: commitStub,
|
||||||
|
remote: { commits: [] },
|
||||||
|
isOutdated: false,
|
||||||
|
isMainBranch: false,
|
||||||
hasChanges: true,
|
hasChanges: true,
|
||||||
status: {
|
status: {
|
||||||
preview: {
|
preview: {
|
||||||
scorecard: [],
|
scorecard: [],
|
||||||
deploy: {
|
deploy: {
|
||||||
url: 'https://test-url',
|
url: 'https://preview-test-url',
|
||||||
status: 'success',
|
status: 'success',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
production: {
|
production: {
|
||||||
scorecard: [],
|
scorecard: [],
|
||||||
deploy: {
|
deploy: {
|
||||||
url: 'https://test-url',
|
url: 'https://production-test-url',
|
||||||
status: 'success',
|
status: 'success',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PushResponse;
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
||||||
@@ -58,23 +84,27 @@ describe('handlePushStatus()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if organization not provided', async () => {
|
it('should throw error if organization not provided', async () => {
|
||||||
await handlePushStatus(
|
await expect(
|
||||||
{
|
handlePushStatus(
|
||||||
domain: 'test-domain',
|
{
|
||||||
organization: '',
|
domain: 'test-domain',
|
||||||
project: 'test-project',
|
organization: '',
|
||||||
pushId: 'test-push-id',
|
project: 'test-project',
|
||||||
'max-execution-time': 1000,
|
pushId: 'test-push-id',
|
||||||
},
|
},
|
||||||
mockConfig
|
mockConfig
|
||||||
|
)
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"No organization provided, please use --organization option or specify the 'organization' field in the config file."`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(exitWithError).toHaveBeenCalledWith(
|
expect(process.stderr.write).toHaveBeenCalledWith(
|
||||||
"No organization provided, please use --organization option or specify the 'organization' field in the config file."
|
`No organization provided, please use --organization option or specify the 'organization' field in the config file.` +
|
||||||
|
'\n\n'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return success push status for preview-build', async () => {
|
it('should print success push status for preview-build', async () => {
|
||||||
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
remotes.getPush.mockResolvedValueOnce(pushResponseStub);
|
remotes.getPush.mockResolvedValueOnce(pushResponseStub);
|
||||||
|
|
||||||
@@ -84,17 +114,16 @@ describe('handlePushStatus()', () => {
|
|||||||
organization: 'test-org',
|
organization: 'test-org',
|
||||||
project: 'test-project',
|
project: 'test-project',
|
||||||
pushId: 'test-push-id',
|
pushId: 'test-push-id',
|
||||||
'max-execution-time': 1000,
|
|
||||||
},
|
},
|
||||||
mockConfig
|
mockConfig
|
||||||
);
|
);
|
||||||
expect(process.stdout.write).toHaveBeenCalledTimes(1);
|
expect(process.stdout.write).toHaveBeenCalledTimes(1);
|
||||||
expect(process.stdout.write).toHaveBeenCalledWith(
|
expect(process.stdout.write).toHaveBeenCalledWith(
|
||||||
'🚀 Preview deploy success.\nPreview URL: https://test-url\n'
|
'🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return success push status for preview and production builds', async () => {
|
it('should print success push status for preview and production builds', async () => {
|
||||||
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true });
|
remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true });
|
||||||
|
|
||||||
@@ -104,46 +133,50 @@ describe('handlePushStatus()', () => {
|
|||||||
organization: 'test-org',
|
organization: 'test-org',
|
||||||
project: 'test-project',
|
project: 'test-project',
|
||||||
pushId: 'test-push-id',
|
pushId: 'test-push-id',
|
||||||
'max-execution-time': 1000,
|
|
||||||
},
|
},
|
||||||
mockConfig
|
mockConfig
|
||||||
);
|
);
|
||||||
expect(process.stdout.write).toHaveBeenCalledTimes(2);
|
expect(process.stdout.write).toHaveBeenCalledTimes(2);
|
||||||
expect(process.stdout.write).toHaveBeenCalledWith(
|
expect(process.stdout.write).toHaveBeenCalledWith(
|
||||||
'🚀 Preview deploy success.\nPreview URL: https://test-url\n'
|
'🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n'
|
||||||
);
|
);
|
||||||
expect(process.stdout.write).toHaveBeenCalledWith(
|
expect(process.stdout.write).toHaveBeenCalledWith(
|
||||||
'🚀 Production deploy success.\nProduction URL: https://test-url\n'
|
'🚀 Production deploy success.\nProduction URL: https://production-test-url\n'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return failed push status for preview build', async () => {
|
it('should print failed push status for preview build', async () => {
|
||||||
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|
||||||
remotes.getPush.mockResolvedValue({
|
remotes.getPush.mockResolvedValue({
|
||||||
isOutdated: false,
|
isOutdated: false,
|
||||||
hasChanges: true,
|
hasChanges: true,
|
||||||
status: {
|
status: {
|
||||||
preview: { deploy: { status: 'failed', url: 'https://test-url' }, scorecard: [] },
|
preview: { deploy: { status: 'failed', url: 'https://preview-test-url' }, scorecard: [] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await handlePushStatus(
|
await expect(
|
||||||
{
|
handlePushStatus(
|
||||||
domain: 'test-domain',
|
{
|
||||||
organization: 'test-org',
|
domain: 'test-domain',
|
||||||
project: 'test-project',
|
organization: 'test-org',
|
||||||
pushId: 'test-push-id',
|
project: 'test-project',
|
||||||
'max-execution-time': 1000,
|
pushId: 'test-push-id',
|
||||||
},
|
},
|
||||||
mockConfig
|
mockConfig
|
||||||
);
|
)
|
||||||
expect(exitWithError).toHaveBeenCalledWith(
|
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||||
'❌ Preview deploy fail.\nPreview URL: https://test-url'
|
"❌ Preview deploy fail.
|
||||||
|
Preview URL: https://preview-test-url"
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(process.stderr.write).toHaveBeenCalledWith(
|
||||||
|
'❌ Preview deploy fail.\nPreview URL: https://preview-test-url' + '\n\n'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return success push status for preview build and print scorecards', async () => {
|
it('should print success push status for preview build and print scorecards', async () => {
|
||||||
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|
||||||
remotes.getPush.mockResolvedValue({
|
remotes.getPush.mockResolvedValue({
|
||||||
@@ -151,7 +184,7 @@ describe('handlePushStatus()', () => {
|
|||||||
hasChanges: true,
|
hasChanges: true,
|
||||||
status: {
|
status: {
|
||||||
preview: {
|
preview: {
|
||||||
deploy: { status: 'success', url: 'https://test-url' },
|
deploy: { status: 'success', url: 'https://preview-test-url' },
|
||||||
scorecard: [
|
scorecard: [
|
||||||
{
|
{
|
||||||
name: 'test-name',
|
name: 'test-name',
|
||||||
@@ -170,13 +203,12 @@ describe('handlePushStatus()', () => {
|
|||||||
organization: 'test-org',
|
organization: 'test-org',
|
||||||
project: 'test-project',
|
project: 'test-project',
|
||||||
pushId: 'test-push-id',
|
pushId: 'test-push-id',
|
||||||
'max-execution-time': 1000,
|
|
||||||
},
|
},
|
||||||
mockConfig
|
mockConfig
|
||||||
);
|
);
|
||||||
expect(process.stdout.write).toHaveBeenCalledTimes(4);
|
expect(process.stdout.write).toHaveBeenCalledTimes(4);
|
||||||
expect(process.stdout.write).toHaveBeenCalledWith(
|
expect(process.stdout.write).toHaveBeenCalledWith(
|
||||||
'🚀 Preview deploy success.\nPreview URL: https://test-url\n'
|
'🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n'
|
||||||
);
|
);
|
||||||
expect(process.stdout.write).toHaveBeenCalledWith('\nScorecard:');
|
expect(process.stdout.write).toHaveBeenCalledWith('\nScorecard:');
|
||||||
expect(process.stdout.write).toHaveBeenCalledWith(
|
expect(process.stdout.write).toHaveBeenCalledWith(
|
||||||
@@ -185,14 +217,18 @@ describe('handlePushStatus()', () => {
|
|||||||
expect(process.stdout.write).toHaveBeenCalledWith('\n');
|
expect(process.stdout.write).toHaveBeenCalledWith('\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display message if there is no changes', async () => {
|
it('should print message if there is no changes', async () => {
|
||||||
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|
||||||
remotes.getPush.mockResolvedValueOnce({
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
isOutdated: false,
|
isOutdated: false,
|
||||||
hasChanges: false,
|
hasChanges: false,
|
||||||
status: {
|
status: {
|
||||||
preview: { deploy: { status: 'skipped', url: 'https://test-url' }, scorecard: [] },
|
preview: { deploy: { status: 'skipped', url: 'https://preview-test-url' }, scorecard: [] },
|
||||||
|
production: {
|
||||||
|
deploy: { status: 'skipped', url: null },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,10 +239,400 @@ describe('handlePushStatus()', () => {
|
|||||||
project: 'test-project',
|
project: 'test-project',
|
||||||
pushId: 'test-push-id',
|
pushId: 'test-push-id',
|
||||||
wait: true,
|
wait: true,
|
||||||
'max-execution-time': 1000,
|
|
||||||
},
|
},
|
||||||
mockConfig
|
mockConfig
|
||||||
);
|
);
|
||||||
expect(process.stderr.write).toHaveBeenCalledWith('Files not uploaded. Reason: no changes.\n');
|
|
||||||
|
expect(process.stderr.write).toHaveBeenCalledWith(
|
||||||
|
'Files not added to your project. Reason: no changes.\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('return value', () => {
|
||||||
|
it('should return preview deployment info', async () => {
|
||||||
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: false });
|
||||||
|
|
||||||
|
const result = await handlePushStatus(
|
||||||
|
{
|
||||||
|
domain: 'test-domain',
|
||||||
|
organization: 'test-org',
|
||||||
|
project: 'test-project',
|
||||||
|
pushId: 'test-push-id',
|
||||||
|
},
|
||||||
|
mockConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
preview: {
|
||||||
|
deploy: {
|
||||||
|
status: 'success',
|
||||||
|
url: 'https://preview-test-url',
|
||||||
|
},
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: null,
|
||||||
|
commit: commitStub,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return preview and production deployment info', async () => {
|
||||||
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true });
|
||||||
|
|
||||||
|
const result = await handlePushStatus(
|
||||||
|
{
|
||||||
|
domain: 'test-domain',
|
||||||
|
organization: 'test-org',
|
||||||
|
project: 'test-project',
|
||||||
|
pushId: 'test-push-id',
|
||||||
|
},
|
||||||
|
mockConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
preview: {
|
||||||
|
deploy: {
|
||||||
|
status: 'success',
|
||||||
|
url: 'https://preview-test-url',
|
||||||
|
},
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
deploy: {
|
||||||
|
status: 'success',
|
||||||
|
url: 'https://production-test-url',
|
||||||
|
},
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
commit: commitStub,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('"wait" option', () => {
|
||||||
|
it('should wait for preview "success" deployment status', async () => {
|
||||||
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'pending', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'running', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'success', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handlePushStatus(
|
||||||
|
{
|
||||||
|
domain: 'test-domain',
|
||||||
|
organization: 'test-org',
|
||||||
|
project: 'test-project',
|
||||||
|
pushId: 'test-push-id',
|
||||||
|
'retry-interval': 0.5, // 500 ms
|
||||||
|
wait: true,
|
||||||
|
},
|
||||||
|
mockConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
preview: {
|
||||||
|
deploy: {
|
||||||
|
status: 'success',
|
||||||
|
url: 'https://preview-test-url',
|
||||||
|
},
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: null,
|
||||||
|
commit: commitStub,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wait for production "success" status after preview "success" status', async () => {
|
||||||
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
isMainBranch: true,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'success', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
deploy: { status: 'pending', url: 'https://production-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
isMainBranch: true,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'success', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
deploy: { status: 'running', url: 'https://production-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
isMainBranch: true,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'success', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
deploy: { status: 'success', url: 'https://production-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handlePushStatus(
|
||||||
|
{
|
||||||
|
domain: 'test-domain',
|
||||||
|
organization: 'test-org',
|
||||||
|
project: 'test-project',
|
||||||
|
pushId: 'test-push-id',
|
||||||
|
'retry-interval': 0.5, // 500 ms
|
||||||
|
wait: true,
|
||||||
|
},
|
||||||
|
mockConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'success', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
deploy: { status: 'success', url: 'https://production-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
commit: commitStub,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('"continue-on-deploy-failures" option', () => {
|
||||||
|
it('should throw error if option value is false', async () => {
|
||||||
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'failed', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handlePushStatus(
|
||||||
|
{
|
||||||
|
domain: 'test-domain',
|
||||||
|
organization: 'test-org',
|
||||||
|
project: 'test-project',
|
||||||
|
pushId: 'test-push-id',
|
||||||
|
'continue-on-deploy-failures': false,
|
||||||
|
},
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"❌ Preview deploy fail.
|
||||||
|
Preview URL: https://preview-test-url"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw error if option value is true', async () => {
|
||||||
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'failed', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handlePushStatus(
|
||||||
|
{
|
||||||
|
domain: 'test-domain',
|
||||||
|
organization: 'test-org',
|
||||||
|
project: 'test-project',
|
||||||
|
pushId: 'test-push-id',
|
||||||
|
'continue-on-deploy-failures': true,
|
||||||
|
},
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
).resolves.toStrictEqual({
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'failed', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: null,
|
||||||
|
commit: commitStub,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('"onRetry" callback', () => {
|
||||||
|
it('should be called when command retries request to API in wait mode for preview deploy', async () => {
|
||||||
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'pending', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'running', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
remotes.getPush.mockResolvedValueOnce({
|
||||||
|
...pushResponseStub,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'success', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onRetrySpy = jest.fn();
|
||||||
|
|
||||||
|
const result = await handlePushStatus(
|
||||||
|
{
|
||||||
|
domain: 'test-domain',
|
||||||
|
organization: 'test-org',
|
||||||
|
project: 'test-project',
|
||||||
|
pushId: 'test-push-id',
|
||||||
|
wait: true,
|
||||||
|
'retry-interval': 0.5, // 500 ms
|
||||||
|
onRetry: onRetrySpy,
|
||||||
|
},
|
||||||
|
mockConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onRetrySpy).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
// first retry
|
||||||
|
expect(onRetrySpy).toHaveBeenNthCalledWith(1, {
|
||||||
|
preview: {
|
||||||
|
deploy: {
|
||||||
|
status: 'pending',
|
||||||
|
url: 'https://preview-test-url',
|
||||||
|
},
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: null,
|
||||||
|
commit: commitStub,
|
||||||
|
});
|
||||||
|
|
||||||
|
// second retry
|
||||||
|
expect(onRetrySpy).toHaveBeenNthCalledWith(2, {
|
||||||
|
preview: {
|
||||||
|
deploy: {
|
||||||
|
status: 'running',
|
||||||
|
url: 'https://preview-test-url',
|
||||||
|
},
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: null,
|
||||||
|
commit: commitStub,
|
||||||
|
});
|
||||||
|
|
||||||
|
// final result
|
||||||
|
expect(result).toEqual({
|
||||||
|
preview: {
|
||||||
|
deploy: {
|
||||||
|
status: 'success',
|
||||||
|
url: 'https://preview-test-url',
|
||||||
|
},
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
production: null,
|
||||||
|
commit: commitStub,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('"max-execution-time" option', () => {
|
||||||
|
it('should throw error in case "max-execution-time" was exceeded', async () => {
|
||||||
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|
||||||
|
// Stuck deployment simulation
|
||||||
|
remotes.getPush.mockResolvedValue({
|
||||||
|
...pushResponseStub,
|
||||||
|
status: {
|
||||||
|
preview: {
|
||||||
|
deploy: { status: 'pending', url: 'https://preview-test-url' },
|
||||||
|
scorecard: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handlePushStatus(
|
||||||
|
{
|
||||||
|
domain: 'test-domain',
|
||||||
|
organization: 'test-org',
|
||||||
|
project: 'test-project',
|
||||||
|
pushId: 'test-push-id',
|
||||||
|
'retry-interval': 2, // seconds
|
||||||
|
'max-execution-time': 1, // seconds
|
||||||
|
wait: true,
|
||||||
|
},
|
||||||
|
mockConfig
|
||||||
|
)
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"✗ Failed to get push status. Reason: Timeout exceeded
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ describe('handlePush()', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
remotes.getDefaultBranch.mockResolvedValueOnce('test-default-branch');
|
remotes.getDefaultBranch.mockResolvedValueOnce('test-default-branch');
|
||||||
remotes.upsert.mockResolvedValueOnce({ id: 'test-remote-id' });
|
remotes.upsert.mockResolvedValueOnce({ id: 'test-remote-id', mountPath: 'test-mount-path' });
|
||||||
remotes.push.mockResolvedValueOnce({ branchName: 'uploaded-to-branch' });
|
remotes.push.mockResolvedValueOnce({ branchName: 'uploaded-to-branch', id: 'test-id' });
|
||||||
|
|
||||||
jest.spyOn(fs, 'createReadStream').mockReturnValue('stream' as any);
|
jest.spyOn(fs, 'createReadStream').mockReturnValue('stream' as any);
|
||||||
|
|
||||||
@@ -118,6 +118,44 @@ describe('handlePush()', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return push id', async () => {
|
||||||
|
const mockConfig = { apis: {} } as any;
|
||||||
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|
||||||
|
fsStatSyncSpy.mockReturnValueOnce({
|
||||||
|
isDirectory() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
pathResolveSpy.mockImplementationOnce((p) => p);
|
||||||
|
pathRelativeSpy.mockImplementationOnce((_, p) => p);
|
||||||
|
pathDirnameSpy.mockImplementation((_: string) => '.');
|
||||||
|
|
||||||
|
const result = await handlePush(
|
||||||
|
{
|
||||||
|
domain: 'test-domain',
|
||||||
|
'mount-path': 'test-mount-path',
|
||||||
|
organization: 'test-org',
|
||||||
|
project: 'test-project',
|
||||||
|
branch: 'test-branch',
|
||||||
|
namespace: 'test-namespace',
|
||||||
|
repository: 'test-repository',
|
||||||
|
'commit-sha': 'test-commit-sha',
|
||||||
|
'commit-url': 'test-commit-url',
|
||||||
|
'default-branch': 'test-branch',
|
||||||
|
'created-at': 'test-created-at',
|
||||||
|
author: 'TestAuthor <test-author@mail.com>',
|
||||||
|
message: 'Test message',
|
||||||
|
files: ['test-file'],
|
||||||
|
'max-execution-time': 10,
|
||||||
|
},
|
||||||
|
mockConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({ pushId: 'test-id' });
|
||||||
|
});
|
||||||
|
|
||||||
it('should collect files from directory and preserve file structure', async () => {
|
it('should collect files from directory and preserve file structure', async () => {
|
||||||
const mockConfig = { apis: {} } as any;
|
const mockConfig = { apis: {} } as any;
|
||||||
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||||
|
|||||||
62
packages/cli/src/cms/commands/__tests__/utils.test.ts
Normal file
62
packages/cli/src/cms/commands/__tests__/utils.test.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { retryUntilConditionMet } from '../utils';
|
||||||
|
|
||||||
|
jest.mock('@redocly/openapi-core', () => ({
|
||||||
|
pause: jest.requireActual('@redocly/openapi-core').pause,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('retryUntilConditionMet()', () => {
|
||||||
|
it('should retry until condition meet and return result', async () => {
|
||||||
|
const operation = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ status: 'pending' })
|
||||||
|
.mockResolvedValueOnce({ status: 'pending' })
|
||||||
|
.mockResolvedValueOnce({ status: 'done' });
|
||||||
|
|
||||||
|
const data = await retryUntilConditionMet({
|
||||||
|
operation,
|
||||||
|
condition: (result: any) => result?.status === 'done',
|
||||||
|
retryIntervalMs: 100,
|
||||||
|
retryTimeoutMs: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(data).toEqual({ status: 'done' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if condition not meet for desired timeout', async () => {
|
||||||
|
const operation = jest.fn().mockResolvedValue({ status: 'pending' });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
retryUntilConditionMet({
|
||||||
|
operation,
|
||||||
|
condition: (result: any) => result?.status === 'done',
|
||||||
|
retryIntervalMs: 100,
|
||||||
|
retryTimeoutMs: 1000,
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Timeout exceeded');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call "onConditionNotMet" and "onRetry" callbacks', async () => {
|
||||||
|
const operation = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ status: 'pending' })
|
||||||
|
.mockResolvedValueOnce({ status: 'pending' })
|
||||||
|
.mockResolvedValueOnce({ status: 'done' });
|
||||||
|
|
||||||
|
const onConditionNotMet = jest.fn();
|
||||||
|
const onRetry = jest.fn();
|
||||||
|
|
||||||
|
const data = await retryUntilConditionMet({
|
||||||
|
operation,
|
||||||
|
condition: (result: any) => result?.status === 'done',
|
||||||
|
retryIntervalMs: 100,
|
||||||
|
retryTimeoutMs: 1000,
|
||||||
|
onConditionNotMet,
|
||||||
|
onRetry,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(data).toEqual({ status: 'done' });
|
||||||
|
|
||||||
|
expect(onConditionNotMet).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onRetry).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
import * as colors from 'colorette';
|
import * as colors from 'colorette';
|
||||||
import { Config } from '@redocly/openapi-core';
|
import type { Config, OutputFormat } from '@redocly/openapi-core';
|
||||||
|
|
||||||
import { exitWithError, printExecutionTime } from '../../utils/miscellaneous';
|
import { exitWithError, printExecutionTime } from '../../utils/miscellaneous';
|
||||||
import { Spinner } from '../../utils/spinner';
|
import { Spinner } from '../../utils/spinner';
|
||||||
import { DeploymentError } from '../utils';
|
import { DeploymentError } from '../utils';
|
||||||
import { yellow } from 'colorette';
|
|
||||||
import { ReuniteApiClient, getApiKeys, getDomain } from '../api';
|
import { ReuniteApiClient, getApiKeys, getDomain } from '../api';
|
||||||
import { capitalize } from '../../utils/js-utils';
|
import { capitalize } from '../../utils/js-utils';
|
||||||
|
import type {
|
||||||
|
DeploymentStatus,
|
||||||
|
DeploymentStatusResponse,
|
||||||
|
PushResponse,
|
||||||
|
ScorecardItem,
|
||||||
|
} from '../api/types';
|
||||||
|
import { retryUntilConditionMet } from './utils';
|
||||||
|
|
||||||
import type { DeploymentStatus, PushResponse, ScorecardItem } from '../api/types';
|
const RETRY_INTERVAL_MS = 5000; // 5 sec
|
||||||
|
|
||||||
const INTERVAL = 5000;
|
|
||||||
|
|
||||||
export type PushStatusOptions = {
|
export type PushStatusOptions = {
|
||||||
organization: string;
|
organization: string;
|
||||||
@@ -17,12 +22,25 @@ export type PushStatusOptions = {
|
|||||||
pushId: string;
|
pushId: string;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
config?: string;
|
config?: string;
|
||||||
format?: 'stylish' | 'json';
|
format?: Extract<OutputFormat, 'stylish'>;
|
||||||
wait?: boolean;
|
wait?: boolean;
|
||||||
'max-execution-time': number;
|
'max-execution-time'?: number; // in seconds
|
||||||
|
'retry-interval'?: number; // in seconds
|
||||||
|
'start-time'?: number; // in milliseconds
|
||||||
|
'continue-on-deploy-failures'?: boolean;
|
||||||
|
onRetry?: (lasSummary: PushStatusSummary) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function handlePushStatus(argv: PushStatusOptions, config: Config) {
|
export interface PushStatusSummary {
|
||||||
|
preview: DeploymentStatusResponse;
|
||||||
|
production: DeploymentStatusResponse | null;
|
||||||
|
commit: PushResponse['commit'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePushStatus(
|
||||||
|
argv: PushStatusOptions,
|
||||||
|
config: Config
|
||||||
|
): Promise<PushStatusSummary | undefined> {
|
||||||
const startedAt = performance.now();
|
const startedAt = performance.now();
|
||||||
const spinner = new Spinner();
|
const spinner = new Spinner();
|
||||||
|
|
||||||
@@ -31,123 +49,198 @@ export async function handlePushStatus(argv: PushStatusOptions, config: Config)
|
|||||||
const orgId = organization || config.organization;
|
const orgId = organization || config.organization;
|
||||||
|
|
||||||
if (!orgId) {
|
if (!orgId) {
|
||||||
return exitWithError(
|
exitWithError(
|
||||||
`No organization provided, please use --organization option or specify the 'organization' field in the config file.`
|
`No organization provided, please use --organization option or specify the 'organization' field in the config file.`
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const domain = argv.domain || getDomain();
|
const domain = argv.domain || getDomain();
|
||||||
|
const maxExecutionTime = argv['max-execution-time'] || 1200; // 20 min
|
||||||
if (!domain) {
|
const retryIntervalMs = argv['retry-interval']
|
||||||
return exitWithError(
|
? argv['retry-interval'] * 1000
|
||||||
`No domain provided, please use --domain option or environment variable REDOCLY_DOMAIN.`
|
: RETRY_INTERVAL_MS;
|
||||||
);
|
const startTime = argv['start-time'] || Date.now();
|
||||||
}
|
const retryTimeoutMs = maxExecutionTime * 1000;
|
||||||
|
const continueOnDeployFailures = argv['continue-on-deploy-failures'] || false;
|
||||||
const maxExecutionTime = argv['max-execution-time'] || 600;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiKey = getApiKeys(domain);
|
const apiKey = getApiKeys(domain);
|
||||||
const client = new ReuniteApiClient(domain, apiKey);
|
const client = new ReuniteApiClient(domain, apiKey);
|
||||||
|
|
||||||
if (wait) {
|
let pushResponse: PushResponse;
|
||||||
const push = await waitForDeployment(client, 'preview');
|
|
||||||
|
|
||||||
if (push.isMainBranch && push.status.preview.deploy.status === 'success') {
|
pushResponse = await retryUntilConditionMet({
|
||||||
await waitForDeployment(client, 'production');
|
operation: () =>
|
||||||
}
|
client.remotes.getPush({
|
||||||
|
organizationId: orgId,
|
||||||
|
projectId,
|
||||||
|
pushId,
|
||||||
|
}),
|
||||||
|
condition: wait
|
||||||
|
? // Keep retrying if status is "pending" or "running" (returning false, so the operation will be retried)
|
||||||
|
(result) => !['pending', 'running'].includes(result.status['preview'].deploy.status)
|
||||||
|
: null,
|
||||||
|
onConditionNotMet: (lastResult) => {
|
||||||
|
displayDeploymentAndBuildStatus({
|
||||||
|
status: lastResult.status['preview'].deploy.status,
|
||||||
|
url: lastResult.status['preview'].deploy.url,
|
||||||
|
spinner,
|
||||||
|
buildType: 'preview',
|
||||||
|
continueOnDeployFailures,
|
||||||
|
wait,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onRetry: (lastResult) => {
|
||||||
|
if (argv.onRetry) {
|
||||||
|
argv.onRetry({
|
||||||
|
preview: lastResult.status.preview,
|
||||||
|
production: lastResult.isMainBranch ? lastResult.status.production : null,
|
||||||
|
commit: lastResult.commit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startTime,
|
||||||
|
retryTimeoutMs,
|
||||||
|
retryIntervalMs,
|
||||||
|
});
|
||||||
|
|
||||||
printPushStatusInfo();
|
printPushStatus({
|
||||||
return;
|
buildType: 'preview',
|
||||||
|
spinner,
|
||||||
|
wait,
|
||||||
|
push: pushResponse,
|
||||||
|
continueOnDeployFailures,
|
||||||
|
});
|
||||||
|
printScorecard(pushResponse.status.preview.scorecard);
|
||||||
|
|
||||||
|
const shouldWaitForProdDeployment =
|
||||||
|
pushResponse.isMainBranch &&
|
||||||
|
(wait ? pushResponse.status.preview.deploy.status === 'success' : true);
|
||||||
|
|
||||||
|
if (shouldWaitForProdDeployment) {
|
||||||
|
pushResponse = await retryUntilConditionMet({
|
||||||
|
operation: () =>
|
||||||
|
client.remotes.getPush({
|
||||||
|
organizationId: orgId,
|
||||||
|
projectId,
|
||||||
|
pushId,
|
||||||
|
}),
|
||||||
|
condition: wait
|
||||||
|
? // Keep retrying if status is "pending" or "running" (returning false, so the operation will be retried)
|
||||||
|
(result) => !['pending', 'running'].includes(result.status['production'].deploy.status)
|
||||||
|
: null,
|
||||||
|
onConditionNotMet: (lastResult) => {
|
||||||
|
displayDeploymentAndBuildStatus({
|
||||||
|
status: lastResult.status['production'].deploy.status,
|
||||||
|
url: lastResult.status['production'].deploy.url,
|
||||||
|
spinner,
|
||||||
|
buildType: 'production',
|
||||||
|
continueOnDeployFailures,
|
||||||
|
wait,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onRetry: (lastResult) => {
|
||||||
|
if (argv.onRetry) {
|
||||||
|
argv.onRetry({
|
||||||
|
preview: lastResult.status.preview,
|
||||||
|
production: lastResult.isMainBranch ? lastResult.status.production : null,
|
||||||
|
commit: lastResult.commit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startTime,
|
||||||
|
retryTimeoutMs,
|
||||||
|
retryIntervalMs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const pushPreview = await getAndPrintPushStatus(client, 'preview');
|
if (pushResponse.isMainBranch) {
|
||||||
printScorecard(pushPreview.status.preview.scorecard);
|
printPushStatus({
|
||||||
|
buildType: 'production',
|
||||||
if (pushPreview.isMainBranch) {
|
spinner,
|
||||||
await getAndPrintPushStatus(client, 'production');
|
wait,
|
||||||
printScorecard(pushPreview.status.production.scorecard);
|
push: pushResponse,
|
||||||
|
continueOnDeployFailures,
|
||||||
|
});
|
||||||
|
printScorecard(pushResponse.status.production.scorecard);
|
||||||
}
|
}
|
||||||
|
printPushStatusInfo({ orgId, projectId, pushId, startedAt });
|
||||||
|
|
||||||
printPushStatusInfo();
|
const summary: PushStatusSummary = {
|
||||||
|
preview: pushResponse.status.preview,
|
||||||
|
production: pushResponse.isMainBranch ? pushResponse.status.production : null,
|
||||||
|
commit: pushResponse.commit,
|
||||||
|
};
|
||||||
|
|
||||||
|
return summary;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
spinner.stop(); // Spinner can block process exit, so we need to stop it explicitly.
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
err instanceof DeploymentError
|
err instanceof DeploymentError
|
||||||
? err.message
|
? err.message
|
||||||
: `✗ Failed to get push status. Reason: ${err.message}\n`;
|
: `✗ Failed to get push status. Reason: ${err.message}\n`;
|
||||||
exitWithError(message);
|
exitWithError(message);
|
||||||
}
|
return;
|
||||||
|
} finally {
|
||||||
function printPushStatusInfo() {
|
spinner.stop(); // Spinner can block process exit, so we need to stop it explicitly.
|
||||||
process.stderr.write(
|
|
||||||
`\nProcessed push-status for ${colors.yellow(orgId!)}, ${colors.yellow(
|
|
||||||
projectId
|
|
||||||
)} and pushID ${colors.yellow(pushId)}.\n`
|
|
||||||
);
|
|
||||||
printExecutionTime('push-status', startedAt, 'Finished');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForDeployment(
|
|
||||||
client: ReuniteApiClient,
|
|
||||||
buildType: 'preview' | 'production'
|
|
||||||
): Promise<PushResponse> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (performance.now() - startedAt > maxExecutionTime * 1000) {
|
|
||||||
spinner.stop();
|
|
||||||
reject(new Error(`Time limit exceeded.`));
|
|
||||||
}
|
|
||||||
|
|
||||||
getAndPrintPushStatus(client, buildType)
|
|
||||||
.then((push) => {
|
|
||||||
if (!['pending', 'running'].includes(push.status[buildType].deploy.status)) {
|
|
||||||
printScorecard(push.status[buildType].scorecard);
|
|
||||||
resolve(push);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
const pushResponse = await waitForDeployment(client, buildType);
|
|
||||||
resolve(pushResponse);
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}, INTERVAL);
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAndPrintPushStatus(
|
|
||||||
client: ReuniteApiClient,
|
|
||||||
buildType: 'preview' | 'production'
|
|
||||||
) {
|
|
||||||
const push = await client.remotes.getPush({
|
|
||||||
organizationId: orgId!,
|
|
||||||
projectId,
|
|
||||||
pushId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (push.isOutdated || !push.hasChanges) {
|
|
||||||
process.stderr.write(
|
|
||||||
yellow(`Files not uploaded. Reason: ${push.isOutdated ? 'outdated' : 'no changes'}.\n`)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
displayDeploymentAndBuildStatus({
|
|
||||||
status: push.status[buildType].deploy.status,
|
|
||||||
previewUrl: push.status[buildType].deploy.url,
|
|
||||||
buildType,
|
|
||||||
spinner,
|
|
||||||
wait,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return push;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printScorecard(scorecard: ScorecardItem[]) {
|
function printPushStatusInfo({
|
||||||
if (!scorecard.length) {
|
orgId,
|
||||||
|
projectId,
|
||||||
|
pushId,
|
||||||
|
startedAt,
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
projectId: string;
|
||||||
|
pushId: string;
|
||||||
|
startedAt: number;
|
||||||
|
}) {
|
||||||
|
process.stderr.write(
|
||||||
|
`\nProcessed push-status for ${colors.yellow(orgId!)}, ${colors.yellow(
|
||||||
|
projectId
|
||||||
|
)} and pushID ${colors.yellow(pushId)}.\n`
|
||||||
|
);
|
||||||
|
printExecutionTime('push-status', startedAt, 'Finished');
|
||||||
|
}
|
||||||
|
|
||||||
|
function printPushStatus({
|
||||||
|
buildType,
|
||||||
|
spinner,
|
||||||
|
push,
|
||||||
|
continueOnDeployFailures,
|
||||||
|
}: {
|
||||||
|
buildType: 'preview' | 'production';
|
||||||
|
spinner: Spinner;
|
||||||
|
wait?: boolean;
|
||||||
|
push?: PushResponse | null;
|
||||||
|
continueOnDeployFailures: boolean;
|
||||||
|
}) {
|
||||||
|
if (!push) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (push.isOutdated || !push.hasChanges) {
|
||||||
|
process.stderr.write(
|
||||||
|
colors.yellow(
|
||||||
|
`Files not added to your project. Reason: ${push.isOutdated ? 'outdated' : 'no changes'}.\n`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
displayDeploymentAndBuildStatus({
|
||||||
|
status: push.status[buildType].deploy.status,
|
||||||
|
url: push.status[buildType].deploy.url,
|
||||||
|
buildType,
|
||||||
|
spinner,
|
||||||
|
continueOnDeployFailures,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printScorecard(scorecard?: ScorecardItem[]) {
|
||||||
|
if (!scorecard || scorecard.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
process.stdout.write(`\n${colors.magenta('Scorecard')}:`);
|
process.stdout.write(`\n${colors.magenta('Scorecard')}:`);
|
||||||
@@ -163,42 +256,71 @@ function printScorecard(scorecard: ScorecardItem[]) {
|
|||||||
|
|
||||||
function displayDeploymentAndBuildStatus({
|
function displayDeploymentAndBuildStatus({
|
||||||
status,
|
status,
|
||||||
previewUrl,
|
url,
|
||||||
spinner,
|
spinner,
|
||||||
buildType,
|
buildType,
|
||||||
|
continueOnDeployFailures,
|
||||||
|
wait,
|
||||||
|
}: {
|
||||||
|
status: DeploymentStatus;
|
||||||
|
url: string | null;
|
||||||
|
spinner: Spinner;
|
||||||
|
buildType: 'preview' | 'production';
|
||||||
|
continueOnDeployFailures: boolean;
|
||||||
|
wait?: boolean;
|
||||||
|
}) {
|
||||||
|
const message = getMessage({ status, url, buildType, wait });
|
||||||
|
|
||||||
|
if (status === 'failed' && !continueOnDeployFailures) {
|
||||||
|
spinner.stop();
|
||||||
|
throw new DeploymentError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wait && (status === 'pending' || status === 'running')) {
|
||||||
|
return spinner.start(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.stop();
|
||||||
|
return process.stdout.write(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessage({
|
||||||
|
status,
|
||||||
|
url,
|
||||||
|
buildType,
|
||||||
wait,
|
wait,
|
||||||
}: {
|
}: {
|
||||||
status: DeploymentStatus;
|
status: DeploymentStatus;
|
||||||
previewUrl: string | null;
|
url: string | null;
|
||||||
spinner: Spinner;
|
|
||||||
buildType: 'preview' | 'production';
|
buildType: 'preview' | 'production';
|
||||||
wait?: boolean;
|
wait?: boolean;
|
||||||
}) {
|
}): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'success':
|
|
||||||
spinner.stop();
|
|
||||||
return process.stdout.write(
|
|
||||||
`${colors.green(`🚀 ${capitalize(buildType)} deploy success.`)}\n${colors.magenta(
|
|
||||||
`${capitalize(buildType)} URL`
|
|
||||||
)}: ${colors.cyan(previewUrl!)}\n`
|
|
||||||
);
|
|
||||||
case 'failed':
|
|
||||||
spinner.stop();
|
|
||||||
throw new DeploymentError(
|
|
||||||
`${colors.red(`❌ ${capitalize(buildType)} deploy fail.`)}\n${colors.magenta(
|
|
||||||
`${capitalize(buildType)} URL`
|
|
||||||
)}: ${colors.cyan(previewUrl!)}`
|
|
||||||
);
|
|
||||||
case 'pending':
|
|
||||||
return wait
|
|
||||||
? spinner.start(`${colors.yellow(`Pending ${buildType}`)}`)
|
|
||||||
: process.stdout.write(`Status: ${colors.yellow(`Pending ${buildType}`)}\n`);
|
|
||||||
case 'skipped':
|
case 'skipped':
|
||||||
spinner.stop();
|
return `${colors.yellow(`Skipped ${buildType}`)}\n`;
|
||||||
return process.stdout.write(`${colors.yellow(`Skipped ${buildType}`)}\n`);
|
|
||||||
case 'running':
|
case 'pending': {
|
||||||
return wait
|
const message = `${colors.yellow(`Pending ${buildType}`)}`;
|
||||||
? spinner.start(`${colors.yellow(`Running ${buildType}`)}`)
|
return wait ? message : `Status: ${message}\n`;
|
||||||
: process.stdout.write(`Status: ${colors.yellow(`Running ${buildType}`)}\n`);
|
}
|
||||||
|
case 'running': {
|
||||||
|
const message = `${colors.yellow(`Running ${buildType}`)}`;
|
||||||
|
return wait ? message : `Status: ${message}\n`;
|
||||||
|
}
|
||||||
|
case 'success':
|
||||||
|
return `${colors.green(`🚀 ${capitalize(buildType)} deploy success.`)}\n${colors.magenta(
|
||||||
|
`${capitalize(buildType)} URL`
|
||||||
|
)}: ${colors.cyan(url || 'No URL yet.')}\n`;
|
||||||
|
|
||||||
|
case 'failed':
|
||||||
|
return `${colors.red(`❌ ${capitalize(buildType)} deploy fail.`)}\n${colors.magenta(
|
||||||
|
`${capitalize(buildType)} URL`
|
||||||
|
)}: ${colors.cyan(url || 'No URL yet.')}`;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
const message = `${colors.yellow(`No status yet for ${buildType} deploy`)}`;
|
||||||
|
|
||||||
|
return wait ? message : `Status: ${message}\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { Config, slash } from '@redocly/openapi-core';
|
import { slash } from '@redocly/openapi-core';
|
||||||
import { exitWithError, HandledError, printExecutionTime } from '../../utils/miscellaneous';
|
|
||||||
import { green, yellow } from 'colorette';
|
import { green, yellow } from 'colorette';
|
||||||
import pluralize = require('pluralize');
|
import pluralize = require('pluralize');
|
||||||
|
|
||||||
|
import type { OutputFormat, Config } from '@redocly/openapi-core';
|
||||||
|
|
||||||
|
import { exitWithError, HandledError, printExecutionTime } from '../../utils/miscellaneous';
|
||||||
import { handlePushStatus } from './push-status';
|
import { handlePushStatus } from './push-status';
|
||||||
import { ReuniteApiClient, getDomain, getApiKeys } from '../api';
|
import { ReuniteApiClient, getDomain, getApiKeys } from '../api';
|
||||||
|
|
||||||
@@ -29,13 +32,20 @@ export type PushOptions = {
|
|||||||
config?: string;
|
config?: string;
|
||||||
'wait-for-deployment'?: boolean;
|
'wait-for-deployment'?: boolean;
|
||||||
'max-execution-time': number;
|
'max-execution-time': number;
|
||||||
|
'continue-on-deploy-failures'?: boolean;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
|
format?: Extract<OutputFormat, 'stylish'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FileToUpload = { name: string; path: string };
|
type FileToUpload = { name: string; path: string };
|
||||||
|
|
||||||
export async function handlePush(argv: PushOptions, config: Config) {
|
export async function handlePush(
|
||||||
const startedAt = performance.now();
|
argv: PushOptions,
|
||||||
|
config: Config
|
||||||
|
): Promise<{ pushId: string } | void> {
|
||||||
|
const startedAt = performance.now(); // for printing execution time
|
||||||
|
const startTime = Date.now(); // for push-status command
|
||||||
|
|
||||||
const { organization, project: projectId, 'mount-path': mountPath, verbose } = argv;
|
const { organization, project: projectId, 'mount-path': mountPath, verbose } = argv;
|
||||||
|
|
||||||
const orgId = organization || config.organization;
|
const orgId = organization || config.organization;
|
||||||
@@ -111,8 +121,8 @@ export async function handlePush(argv: PushOptions, config: Config) {
|
|||||||
filesToUpload.forEach((f) => {
|
filesToUpload.forEach((f) => {
|
||||||
process.stderr.write(green(`✓ ${f.name}\n`));
|
process.stderr.write(green(`✓ ${f.name}\n`));
|
||||||
});
|
});
|
||||||
process.stdout.write('\n');
|
|
||||||
|
|
||||||
|
process.stdout.write('\n');
|
||||||
process.stdout.write(`Push ID: ${id}\n`);
|
process.stdout.write(`Push ID: ${id}\n`);
|
||||||
|
|
||||||
if (waitForDeployment) {
|
if (waitForDeployment) {
|
||||||
@@ -126,6 +136,8 @@ export async function handlePush(argv: PushOptions, config: Config) {
|
|||||||
wait: true,
|
wait: true,
|
||||||
domain,
|
domain,
|
||||||
'max-execution-time': maxExecutionTime,
|
'max-execution-time': maxExecutionTime,
|
||||||
|
'start-time': startTime,
|
||||||
|
'continue-on-deploy-failures': argv['continue-on-deploy-failures'],
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
@@ -139,6 +151,10 @@ export async function handlePush(argv: PushOptions, config: Config) {
|
|||||||
filesToUpload.length
|
filesToUpload.length
|
||||||
)} uploaded to organization ${orgId}, project ${projectId}. Push ID: ${id}.`
|
)} uploaded to organization ${orgId}, project ${projectId}. Push ID: ${id}.`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pushId: id,
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message =
|
const message =
|
||||||
err instanceof HandledError ? '' : `✗ File upload failed. Reason: ${err.message}`;
|
err instanceof HandledError ? '' : `✗ File upload failed. Reason: ${err.message}`;
|
||||||
|
|||||||
52
packages/cli/src/cms/commands/utils.ts
Normal file
52
packages/cli/src/cms/commands/utils.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { pause } from '@redocly/openapi-core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function retries an operation until a condition is met or a timeout is exceeded.
|
||||||
|
* If the condition is not met within the timeout, an error is thrown.
|
||||||
|
* @operation The operation to retry.
|
||||||
|
* @condition The condition to check after each operation result. Return false to continue retrying. Return true to stop retrying.
|
||||||
|
* If not provided, the first result will be returned.
|
||||||
|
* @param onConditionNotMet Will be called with the last result right after checking condition and before timeout and retrying.
|
||||||
|
* @param onRetry Will be called right before retrying operation with the last result before retrying.
|
||||||
|
* @param startTime The start time of the operation. Default is the current time.
|
||||||
|
* @param retryTimeoutMs The maximum time to retry the operation. Default is 10 minutes.
|
||||||
|
* @param retryIntervalMs The interval between retries. Default is 5 seconds.
|
||||||
|
*/
|
||||||
|
export async function retryUntilConditionMet<T>({
|
||||||
|
operation,
|
||||||
|
condition,
|
||||||
|
onConditionNotMet,
|
||||||
|
onRetry,
|
||||||
|
startTime = Date.now(),
|
||||||
|
retryTimeoutMs = 600000, // 10 min
|
||||||
|
retryIntervalMs = 5000, // 5 sec
|
||||||
|
}: {
|
||||||
|
operation: () => Promise<T>;
|
||||||
|
condition?: ((result: T) => boolean) | null;
|
||||||
|
onConditionNotMet?: (lastResult: T) => void;
|
||||||
|
onRetry?: (lastResult: T) => void | Promise<void>;
|
||||||
|
startTime?: number;
|
||||||
|
retryTimeoutMs?: number;
|
||||||
|
retryIntervalMs?: number;
|
||||||
|
}): Promise<T> {
|
||||||
|
async function attempt(): Promise<T> {
|
||||||
|
const result = await operation();
|
||||||
|
|
||||||
|
if (!condition) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (condition(result)) {
|
||||||
|
return result;
|
||||||
|
} else if (Date.now() - startTime > retryTimeoutMs) {
|
||||||
|
throw new Error('Timeout exceeded');
|
||||||
|
} else {
|
||||||
|
onConditionNotMet?.(result);
|
||||||
|
await pause(retryIntervalMs);
|
||||||
|
await onRetry?.(result);
|
||||||
|
return attempt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attempt();
|
||||||
|
}
|
||||||
@@ -204,9 +204,10 @@ yargs
|
|||||||
project: {
|
project: {
|
||||||
description: 'Name of the project to push to.',
|
description: 'Name of the project to push to.',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
required: true,
|
||||||
alias: 'p',
|
alias: 'p',
|
||||||
},
|
},
|
||||||
domain: { description: 'Specify a domain.', alias: 'd', type: 'string' },
|
domain: { description: 'Specify a domain.', alias: 'd', type: 'string', required: false },
|
||||||
wait: {
|
wait: {
|
||||||
description: 'Wait for build to finish.',
|
description: 'Wait for build to finish.',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -216,6 +217,11 @@ yargs
|
|||||||
description: 'Maximum execution time in seconds.',
|
description: 'Maximum execution time in seconds.',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
|
'continue-on-deploy-failures': {
|
||||||
|
description: 'Command does not fail even if the deployment fails.',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
(argv) => {
|
(argv) => {
|
||||||
process.env.REDOCLY_CLI_COMMAND = 'push-status';
|
process.env.REDOCLY_CLI_COMMAND = 'push-status';
|
||||||
@@ -377,10 +383,15 @@ yargs
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
'continue-on-deploy-failures': {
|
||||||
|
description: 'Command does not fail even if the deployment fails.',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
(argv) => {
|
(argv) => {
|
||||||
process.env.REDOCLY_CLI_COMMAND = 'push';
|
process.env.REDOCLY_CLI_COMMAND = 'push';
|
||||||
commandWrapper(commonPushHandler(argv))(argv as PushArguments);
|
commandWrapper(commonPushHandler(argv))(argv as Arguments<PushArguments>);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.command(
|
.command(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { lintConfigCallback } from './commands/lint';
|
|||||||
import type { CommandOptions } from './types';
|
import type { CommandOptions } from './types';
|
||||||
|
|
||||||
export function commandWrapper<T extends CommandOptions>(
|
export function commandWrapper<T extends CommandOptions>(
|
||||||
commandHandler?: (argv: T, config: Config, version: string) => Promise<void>
|
commandHandler?: (argv: T, config: Config, version: string) => Promise<unknown>
|
||||||
) {
|
) {
|
||||||
return async (argv: Arguments<T>) => {
|
return async (argv: Arguments<T>) => {
|
||||||
let code: ExitCode = 2;
|
let code: ExitCode = 2;
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ export type OutputFormat =
|
|||||||
| 'checkstyle'
|
| 'checkstyle'
|
||||||
| 'codeclimate'
|
| 'codeclimate'
|
||||||
| 'summary'
|
| 'summary'
|
||||||
| 'github-actions';
|
| 'github-actions'
|
||||||
|
| 'markdown';
|
||||||
|
|
||||||
export function getTotals(problems: (NormalizedProblem & { ignored?: boolean })[]): Totals {
|
export function getTotals(problems: (NormalizedProblem & { ignored?: boolean })[]): Totals {
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
export { BundleOutputFormat, readFileFromUrl, slash, doesYamlFileExist, isTruthy } from './utils';
|
export {
|
||||||
|
BundleOutputFormat,
|
||||||
|
readFileFromUrl,
|
||||||
|
slash,
|
||||||
|
doesYamlFileExist,
|
||||||
|
isTruthy,
|
||||||
|
pause,
|
||||||
|
} from './utils';
|
||||||
export { Oas3_1Types } from './types/oas3_1';
|
export { Oas3_1Types } from './types/oas3_1';
|
||||||
export { Oas3Types } from './types/oas3';
|
export { Oas3Types } from './types/oas3';
|
||||||
export { Oas2Types } from './types/oas2';
|
export { Oas2Types } from './types/oas2';
|
||||||
|
|||||||
@@ -271,6 +271,10 @@ export function nextTick() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function pause(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
function getUpdatedFieldName(updatedField: string, updatedObject?: string) {
|
function getUpdatedFieldName(updatedField: string, updatedObject?: string) {
|
||||||
return `${typeof updatedObject !== 'undefined' ? `${updatedObject}.` : ''}${updatedField}`;
|
return `${typeof updatedObject !== 'undefined' ? `${updatedObject}.` : ''}${updatedField}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user