mirror of
https://github.com/LukeHagar/redocly-cli.git
synced 2025-12-06 04:21:09 +00:00
chore: send user-agent to Reunite (#1676)
This commit is contained in:
@@ -1,23 +1,53 @@
|
||||
import AbortController from 'abort-controller';
|
||||
import fetchWithTimeout from '../utils/fetch-with-timeout';
|
||||
import nodeFetch from 'node-fetch';
|
||||
import { getProxyAgent } from '@redocly/openapi-core';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
|
||||
jest.mock('node-fetch');
|
||||
jest.mock('@redocly/openapi-core');
|
||||
|
||||
describe('fetchWithTimeout', () => {
|
||||
beforeAll(() => {
|
||||
// @ts-ignore
|
||||
global.setTimeout = jest.fn();
|
||||
global.clearTimeout = jest.fn();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
(getProxyAgent as jest.Mock).mockReturnValueOnce(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call node-fetch with signal', async () => {
|
||||
// @ts-ignore
|
||||
global.setTimeout = jest.fn();
|
||||
|
||||
global.clearTimeout = jest.fn();
|
||||
await fetchWithTimeout('url');
|
||||
await fetchWithTimeout('url', { timeout: 1000 });
|
||||
|
||||
expect(global.setTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(nodeFetch).toHaveBeenCalledWith('url', { signal: new AbortController().signal });
|
||||
expect(nodeFetch).toHaveBeenCalledWith('url', {
|
||||
signal: new AbortController().signal,
|
||||
agent: undefined,
|
||||
});
|
||||
expect(global.clearTimeout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call node-fetch with proxy agent', async () => {
|
||||
(getProxyAgent as jest.Mock).mockRestore();
|
||||
const proxyAgent = new HttpsProxyAgent('http://localhost');
|
||||
(getProxyAgent as jest.Mock).mockReturnValueOnce(proxyAgent);
|
||||
|
||||
await fetchWithTimeout('url');
|
||||
|
||||
expect(nodeFetch).toHaveBeenCalledWith('url', { agent: proxyAgent });
|
||||
});
|
||||
|
||||
it('should call node-fetch without signal when timeout is not passed', async () => {
|
||||
await fetchWithTimeout('url');
|
||||
|
||||
expect(global.setTimeout).not.toHaveBeenCalled();
|
||||
expect(nodeFetch).toHaveBeenCalledWith('url', { agent: undefined });
|
||||
expect(global.clearTimeout).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import * as FormData from 'form-data';
|
||||
|
||||
import { ReuniteApiClient, PushPayload } from '../api-client';
|
||||
import { ReuniteApiClient, PushPayload, ReuniteApiError } from '../api-client';
|
||||
|
||||
jest.mock('node-fetch', () => ({
|
||||
default: jest.fn(),
|
||||
@@ -16,12 +16,15 @@ describe('ApiClient', () => {
|
||||
const testDomain = 'test-domain.com';
|
||||
const testOrg = 'test-org';
|
||||
const testProject = 'test-project';
|
||||
const version = '1.0.0';
|
||||
const command = 'push';
|
||||
const expectedUserAgent = `redocly-cli/${version} ${command}`;
|
||||
|
||||
describe('getDefaultBranch()', () => {
|
||||
let apiClient: ReuniteApiClient;
|
||||
|
||||
beforeEach(() => {
|
||||
apiClient = new ReuniteApiClient(testDomain, testToken);
|
||||
apiClient = new ReuniteApiClient({ domain: testDomain, apiKey: testToken, version, command });
|
||||
});
|
||||
|
||||
it('should get default project branch', async () => {
|
||||
@@ -41,6 +44,7 @@ describe('ApiClient', () => {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${testToken}`,
|
||||
'user-agent': expectedUserAgent,
|
||||
},
|
||||
signal: expect.any(Object),
|
||||
}
|
||||
@@ -62,7 +66,7 @@ describe('ApiClient', () => {
|
||||
});
|
||||
|
||||
await expect(apiClient.remotes.getDefaultBranch(testOrg, testProject)).rejects.toThrow(
|
||||
new Error('Failed to fetch default branch: Project source not found')
|
||||
new ReuniteApiError('Failed to fetch default branch. Project source not found.', 404)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -76,7 +80,7 @@ describe('ApiClient', () => {
|
||||
});
|
||||
|
||||
await expect(apiClient.remotes.getDefaultBranch(testOrg, testProject)).rejects.toThrow(
|
||||
new Error('Failed to fetch default branch: Not found')
|
||||
new ReuniteApiError('Failed to fetch default branch. Not found.', 404)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -89,7 +93,7 @@ describe('ApiClient', () => {
|
||||
let apiClient: ReuniteApiClient;
|
||||
|
||||
beforeEach(() => {
|
||||
apiClient = new ReuniteApiClient(testDomain, testToken);
|
||||
apiClient = new ReuniteApiClient({ domain: testDomain, apiKey: testToken, version, command });
|
||||
});
|
||||
|
||||
it('should upsert remote', async () => {
|
||||
@@ -116,6 +120,7 @@ describe('ApiClient', () => {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${testToken}`,
|
||||
'user-agent': expectedUserAgent,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mountPath: remotePayload.mountPath,
|
||||
@@ -144,8 +149,9 @@ describe('ApiClient', () => {
|
||||
});
|
||||
|
||||
await expect(apiClient.remotes.upsert(testOrg, testProject, remotePayload)).rejects.toThrow(
|
||||
new Error(
|
||||
'Failed to upsert remote: Not allowed to mount remote outside of project content path: /docs'
|
||||
new ReuniteApiError(
|
||||
'Failed to upsert remote. Not allowed to mount remote outside of project content path: /docs.',
|
||||
403
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -153,6 +159,7 @@ describe('ApiClient', () => {
|
||||
it('should throw statusText error if response is not ok', async () => {
|
||||
mockFetchResponse({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not found',
|
||||
json: jest.fn().mockResolvedValue({
|
||||
unknownField: 'unknown-error',
|
||||
@@ -160,7 +167,7 @@ describe('ApiClient', () => {
|
||||
});
|
||||
|
||||
await expect(apiClient.remotes.upsert(testOrg, testProject, remotePayload)).rejects.toThrow(
|
||||
new Error('Failed to upsert remote: Not found')
|
||||
new ReuniteApiError('Failed to upsert remote. Not found.', 404)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -200,7 +207,7 @@ describe('ApiClient', () => {
|
||||
let apiClient: ReuniteApiClient;
|
||||
|
||||
beforeEach(() => {
|
||||
apiClient = new ReuniteApiClient(testDomain, testToken);
|
||||
apiClient = new ReuniteApiClient({ domain: testDomain, apiKey: testToken, version, command });
|
||||
});
|
||||
|
||||
it('should push to remote', async () => {
|
||||
@@ -234,6 +241,7 @@ describe('ApiClient', () => {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${testToken}`,
|
||||
'user-agent': expectedUserAgent,
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -258,12 +266,13 @@ describe('ApiClient', () => {
|
||||
|
||||
await expect(
|
||||
apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock)
|
||||
).rejects.toThrow(new Error('Failed to push: Cannot push to remote'));
|
||||
).rejects.toThrow(new ReuniteApiError('Failed to push. Cannot push to remote.', 403));
|
||||
});
|
||||
|
||||
it('should throw statusText error if response is not ok', async () => {
|
||||
mockFetchResponse({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not found',
|
||||
json: jest.fn().mockResolvedValue({
|
||||
unknownField: 'unknown-error',
|
||||
@@ -272,7 +281,7 @@ describe('ApiClient', () => {
|
||||
|
||||
await expect(
|
||||
apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock)
|
||||
).rejects.toThrow(new Error('Failed to push: Not found'));
|
||||
).rejects.toThrow(new ReuniteApiError('Failed to push. Not found.', 404));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import fetch from 'node-fetch';
|
||||
import * as FormData from 'form-data';
|
||||
import { getProxyAgent } from '@redocly/openapi-core';
|
||||
import fetchWithTimeout from '../../utils/fetch-with-timeout';
|
||||
import fetchWithTimeout, {
|
||||
type FetchWithTimeoutOptions,
|
||||
DEFAULT_FETCH_TIMEOUT,
|
||||
} from '../../utils/fetch-with-timeout';
|
||||
|
||||
import type { Response } from 'node-fetch';
|
||||
import type { ReadStream } from 'fs';
|
||||
@@ -12,23 +13,57 @@ import type {
|
||||
UpsertRemoteResponse,
|
||||
} from './types';
|
||||
|
||||
class RemotesApiClient {
|
||||
constructor(private readonly domain: string, private readonly apiKey: string) {}
|
||||
export class ReuniteApiError extends Error {
|
||||
constructor(message: string, public status: number) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private async getParsedResponse<T>(response: Response): Promise<T> {
|
||||
class ReuniteBaseApiClient {
|
||||
constructor(protected version: string, protected command: string) {}
|
||||
|
||||
protected async getParsedResponse<T>(response: Response): Promise<T> {
|
||||
const responseBody = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
return responseBody as T;
|
||||
}
|
||||
|
||||
throw new Error(responseBody.title || response.statusText);
|
||||
throw new ReuniteApiError(
|
||||
`${responseBody.title || response.statusText || 'Unknown error'}.`,
|
||||
response.status
|
||||
);
|
||||
}
|
||||
|
||||
protected request(url: string, options: FetchWithTimeoutOptions) {
|
||||
const headers = {
|
||||
...options.headers,
|
||||
'user-agent': `redocly-cli/${this.version.trim()} ${this.command}`,
|
||||
};
|
||||
|
||||
return fetchWithTimeout(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RemotesApiClient extends ReuniteBaseApiClient {
|
||||
constructor(
|
||||
private readonly domain: string,
|
||||
private readonly apiKey: string,
|
||||
version: string,
|
||||
command: string
|
||||
) {
|
||||
super(version, command);
|
||||
}
|
||||
|
||||
async getDefaultBranch(organizationId: string, projectId: string) {
|
||||
const response = await fetchWithTimeout(
|
||||
try {
|
||||
const response = await this.request(
|
||||
`${this.domain}/api/orgs/${organizationId}/projects/${projectId}/source`,
|
||||
{
|
||||
timeout: DEFAULT_FETCH_TIMEOUT,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -37,16 +72,17 @@ class RemotesApiClient {
|
||||
}
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`Failed to get default branch.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const source = await this.getParsedResponse<ProjectSourceResponse>(response);
|
||||
|
||||
return source.branchName;
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to fetch default branch: ${err.message || 'Unknown error'}`);
|
||||
const message = `Failed to fetch default branch. ${err.message}`;
|
||||
|
||||
if (err instanceof ReuniteApiError) {
|
||||
throw new ReuniteApiError(message, err.status);
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,9 +94,11 @@ class RemotesApiClient {
|
||||
mountBranchName: string;
|
||||
}
|
||||
): Promise<UpsertRemoteResponse> {
|
||||
const response = await fetchWithTimeout(
|
||||
try {
|
||||
const response = await this.request(
|
||||
`${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes`,
|
||||
{
|
||||
timeout: DEFAULT_FETCH_TIMEOUT,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -75,14 +113,15 @@ class RemotesApiClient {
|
||||
}
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`Failed to upsert.`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.getParsedResponse<UpsertRemoteResponse>(response);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to upsert remote: ${err.message || 'Unknown error'}`);
|
||||
const message = `Failed to upsert remote. ${err.message}`;
|
||||
|
||||
if (err instanceof ReuniteApiError) {
|
||||
throw new ReuniteApiError(message, err.status);
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,8 +149,8 @@ class RemotesApiClient {
|
||||
}
|
||||
|
||||
payload.isMainBranch && formData.append('isMainBranch', 'true');
|
||||
|
||||
const response = await fetch(
|
||||
try {
|
||||
const response = await this.request(
|
||||
`${this.domain}/api/orgs/${organizationId}/projects/${projectId}/pushes`,
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -119,21 +158,35 @@ class RemotesApiClient {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: formData,
|
||||
agent: getProxyAgent(),
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
return await this.getParsedResponse<PushResponse>(response);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to push: ${err.message || 'Unknown error'}`);
|
||||
const message = `Failed to push. ${err.message}`;
|
||||
|
||||
if (err instanceof ReuniteApiError) {
|
||||
throw new ReuniteApiError(message, err.status);
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
async getRemotesList(organizationId: string, projectId: string, mountPath: string) {
|
||||
const response = await fetchWithTimeout(
|
||||
async getRemotesList({
|
||||
organizationId,
|
||||
projectId,
|
||||
mountPath,
|
||||
}: {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
mountPath: string;
|
||||
}) {
|
||||
try {
|
||||
const response = await this.request(
|
||||
`${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes?filter=mountPath:/${mountPath}/`,
|
||||
{
|
||||
timeout: DEFAULT_FETCH_TIMEOUT,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -142,14 +195,15 @@ class RemotesApiClient {
|
||||
}
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`Failed to get remotes list.`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.getParsedResponse<ListRemotesResponse>(response);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to get remote list: ${err.message || 'Unknown error'}`);
|
||||
const message = `Failed to get remote list. ${err.message}`;
|
||||
|
||||
if (err instanceof ReuniteApiError) {
|
||||
throw new ReuniteApiError(message, err.status);
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,9 +216,11 @@ class RemotesApiClient {
|
||||
projectId: string;
|
||||
pushId: string;
|
||||
}) {
|
||||
const response = await fetchWithTimeout(
|
||||
try {
|
||||
const response = await this.request(
|
||||
`${this.domain}/api/orgs/${organizationId}/projects/${projectId}/pushes/${pushId}`,
|
||||
{
|
||||
timeout: DEFAULT_FETCH_TIMEOUT,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -173,14 +229,15 @@ class RemotesApiClient {
|
||||
}
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`Failed to get push status.`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.getParsedResponse<PushResponse>(response);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to get push status: ${err.message || 'Unknown error'}`);
|
||||
const message = `Failed to get push status. ${err.message}`;
|
||||
|
||||
if (err instanceof ReuniteApiError) {
|
||||
throw new ReuniteApiError(message, err.status);
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,8 +245,18 @@ class RemotesApiClient {
|
||||
export class ReuniteApiClient {
|
||||
remotes: RemotesApiClient;
|
||||
|
||||
constructor(public domain: string, private readonly apiKey: string) {
|
||||
this.remotes = new RemotesApiClient(this.domain, this.apiKey);
|
||||
constructor({
|
||||
domain,
|
||||
apiKey,
|
||||
version,
|
||||
command,
|
||||
}: {
|
||||
domain: string;
|
||||
apiKey: string;
|
||||
version: string;
|
||||
command: 'push' | 'push-status';
|
||||
}) {
|
||||
this.remotes = new RemotesApiClient(domain, apiKey, version, command);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -644,7 +644,7 @@ describe('handlePushStatus()', () => {
|
||||
version: 'cli-version',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"✗ Failed to get push status. Reason: Timeout exceeded
|
||||
"✗ Failed to get push status. Reason: Timeout exceeded.
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { handlePush } from '../push';
|
||||
import { ReuniteApiClient } from '../../api';
|
||||
import { ReuniteApiClient, ReuniteApiError } from '../../api';
|
||||
|
||||
const remotes = {
|
||||
push: jest.fn(),
|
||||
@@ -332,6 +332,53 @@ describe('handlePush()', () => {
|
||||
version: 'cli-version',
|
||||
});
|
||||
|
||||
expect(ReuniteApiClient).toBeCalledWith('test-domain-from-env', 'test-api-key');
|
||||
expect(ReuniteApiClient).toBeCalledWith({
|
||||
domain: 'test-domain-from-env',
|
||||
apiKey: 'test-api-key',
|
||||
version: 'cli-version',
|
||||
command: 'push',
|
||||
});
|
||||
});
|
||||
|
||||
it('should print error message', async () => {
|
||||
const mockConfig = { apis: {} } as any;
|
||||
process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
|
||||
|
||||
remotes.push.mockRestore();
|
||||
remotes.push.mockRejectedValueOnce(new ReuniteApiError('Deprecated.', 412));
|
||||
|
||||
fsStatSyncSpy.mockReturnValueOnce({
|
||||
isDirectory() {
|
||||
return false;
|
||||
},
|
||||
} as any);
|
||||
|
||||
pathResolveSpy.mockImplementationOnce((p) => p);
|
||||
pathRelativeSpy.mockImplementationOnce((_, p) => p);
|
||||
pathDirnameSpy.mockImplementation((_: string) => '.');
|
||||
|
||||
expect(
|
||||
handlePush({
|
||||
argv: {
|
||||
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,
|
||||
},
|
||||
config: mockConfig,
|
||||
version: 'cli-version',
|
||||
})
|
||||
).rejects.toThrow('✗ File upload failed. Reason: Deprecated.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('retryUntilConditionMet()', () => {
|
||||
retryIntervalMs: 100,
|
||||
retryTimeoutMs: 1000,
|
||||
})
|
||||
).rejects.toThrow('Timeout exceeded');
|
||||
).rejects.toThrow('Timeout exceeded.');
|
||||
});
|
||||
|
||||
it('should call "onConditionNotMet" and "onRetry" callbacks', async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Spinner } from '../../utils/spinner';
|
||||
import { DeploymentError } from '../utils';
|
||||
import { ReuniteApiClient, getApiKeys, getDomain } from '../api';
|
||||
import { capitalize } from '../../utils/js-utils';
|
||||
import { retryUntilConditionMet } from './utils';
|
||||
import { handleReuniteError, retryUntilConditionMet } from './utils';
|
||||
|
||||
import type { OutputFormat } from '@redocly/openapi-core';
|
||||
import type { CommandArgs } from '../../wrapper';
|
||||
@@ -41,7 +41,8 @@ export interface PushStatusSummary {
|
||||
export async function handlePushStatus({
|
||||
argv,
|
||||
config,
|
||||
}: CommandArgs<PushStatusOptions>): Promise<PushStatusSummary | undefined> {
|
||||
version,
|
||||
}: CommandArgs<PushStatusOptions>): Promise<PushStatusSummary | void> {
|
||||
const startedAt = performance.now();
|
||||
const spinner = new Spinner();
|
||||
|
||||
@@ -67,7 +68,7 @@ export async function handlePushStatus({
|
||||
|
||||
try {
|
||||
const apiKey = getApiKeys(domain);
|
||||
const client = new ReuniteApiClient(domain, apiKey);
|
||||
const client = new ReuniteApiClient({ domain, apiKey, version, command: 'push-status' });
|
||||
|
||||
let pushResponse: PushResponse;
|
||||
|
||||
@@ -178,12 +179,7 @@ export async function handlePushStatus({
|
||||
} catch (err) {
|
||||
spinner.stop(); // Spinner can block process exit, so we need to stop it explicitly.
|
||||
|
||||
const message =
|
||||
err instanceof DeploymentError
|
||||
? err.message
|
||||
: `✗ Failed to get push status. Reason: ${err.message}\n`;
|
||||
exitWithError(message);
|
||||
return;
|
||||
handleReuniteError('✗ Failed to get push status.', err);
|
||||
} finally {
|
||||
spinner.stop(); // Spinner can block process exit, so we need to stop it explicitly.
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import * as path from 'path';
|
||||
import { slash } from '@redocly/openapi-core';
|
||||
import { pluralize } from '@redocly/openapi-core/lib/utils';
|
||||
import { green, yellow } from 'colorette';
|
||||
import { exitWithError, HandledError, printExecutionTime } from '../../utils/miscellaneous';
|
||||
import { exitWithError, printExecutionTime } from '../../utils/miscellaneous';
|
||||
import { handlePushStatus } from './push-status';
|
||||
import { ReuniteApiClient, getDomain, getApiKeys } from '../api';
|
||||
import { handleReuniteError } from './utils';
|
||||
|
||||
import type { OutputFormat } from '@redocly/openapi-core';
|
||||
import type { CommandArgs } from '../../wrapper';
|
||||
@@ -52,7 +53,7 @@ export async function handlePush({
|
||||
const orgId = organization || config.organization;
|
||||
|
||||
if (!argv.message || !argv.author || !argv.branch) {
|
||||
exitWithError('Error: message, author and branch are required for push to the CMS.');
|
||||
exitWithError('Error: message, author and branch are required for push to the Reunite.');
|
||||
}
|
||||
|
||||
if (!orgId) {
|
||||
@@ -85,7 +86,7 @@ export async function handlePush({
|
||||
return printExecutionTime('push', startedAt, `No files to upload`);
|
||||
}
|
||||
|
||||
const client = new ReuniteApiClient(domain, apiKey);
|
||||
const client = new ReuniteApiClient({ domain, apiKey, version, command: 'push' });
|
||||
const projectDefaultBranch = await client.remotes.getDefaultBranch(orgId, projectId);
|
||||
const remote = await client.remotes.upsert(orgId, projectId, {
|
||||
mountBranchName: projectDefaultBranch,
|
||||
@@ -158,9 +159,7 @@ export async function handlePush({
|
||||
pushId: id,
|
||||
};
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof HandledError ? '' : `✗ File upload failed. Reason: ${err.message}`;
|
||||
exitWithError(message);
|
||||
handleReuniteError('✗ File upload failed.', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { pause } from '@redocly/openapi-core';
|
||||
import { DeploymentError } from '../utils';
|
||||
import { exitWithError } from '../../utils/miscellaneous';
|
||||
|
||||
import type { ReuniteApiError } from '../api';
|
||||
|
||||
/**
|
||||
* This function retries an operation until a condition is met or a timeout is exceeded.
|
||||
@@ -39,7 +43,7 @@ export async function retryUntilConditionMet<T>({
|
||||
if (condition(result)) {
|
||||
return result;
|
||||
} else if (Date.now() - startTime > retryTimeoutMs) {
|
||||
throw new Error('Timeout exceeded');
|
||||
throw new Error('Timeout exceeded.');
|
||||
} else {
|
||||
onConditionNotMet?.(result);
|
||||
await pause(retryIntervalMs);
|
||||
@@ -50,3 +54,13 @@ export async function retryUntilConditionMet<T>({
|
||||
|
||||
return attempt();
|
||||
}
|
||||
|
||||
export function handleReuniteError(
|
||||
message: string,
|
||||
error: ReuniteApiError | DeploymentError | Error
|
||||
) {
|
||||
const errorMessage =
|
||||
error instanceof DeploymentError ? error.message : `${message} Reason: ${error.message}\n`;
|
||||
|
||||
return exitWithError(errorMessage);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import nodeFetch from 'node-fetch';
|
||||
import nodeFetch, { type RequestInit } from 'node-fetch';
|
||||
import AbortController from 'abort-controller';
|
||||
import { getProxyAgent } from '@redocly/openapi-core';
|
||||
|
||||
const TIMEOUT = 3000;
|
||||
export const DEFAULT_FETCH_TIMEOUT = 3000;
|
||||
|
||||
export type FetchWithTimeoutOptions = RequestInit & {
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
export default async (url: string, { timeout, ...options }: FetchWithTimeoutOptions = {}) => {
|
||||
if (!timeout) {
|
||||
return nodeFetch(url, {
|
||||
...options,
|
||||
agent: getProxyAgent(),
|
||||
});
|
||||
}
|
||||
|
||||
export default async (url: string, options = {}) => {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, TIMEOUT);
|
||||
}, timeout);
|
||||
|
||||
const res = await nodeFetch(url, {
|
||||
signal: controller.signal,
|
||||
...options,
|
||||
agent: getProxyAgent(),
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import { deprecatedRefDocsSchema } from '@redocly/config/lib/reference-docs-conf
|
||||
import { outputExtensions } from '../types';
|
||||
import { version } from './update-version-notifier';
|
||||
import { DESTINATION_REGEX } from '../commands/push';
|
||||
import fetch from './fetch-with-timeout';
|
||||
import fetch, { DEFAULT_FETCH_TIMEOUT } from './fetch-with-timeout';
|
||||
|
||||
import type { Arguments } from 'yargs';
|
||||
import type {
|
||||
@@ -569,6 +569,7 @@ export async function sendTelemetry(
|
||||
spec_full_version,
|
||||
};
|
||||
await fetch(`https://api.redocly.com/registry/telemetry/cli`, {
|
||||
timeout: DEFAULT_FETCH_TIMEOUT,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { existsSync, writeFileSync, readFileSync, statSync } from 'fs';
|
||||
import { compare } from 'semver';
|
||||
import fetch from './fetch-with-timeout';
|
||||
import fetch, { DEFAULT_FETCH_TIMEOUT } from './fetch-with-timeout';
|
||||
import { cyan, green, yellow } from 'colorette';
|
||||
import { cleanColors } from './miscellaneous';
|
||||
|
||||
@@ -34,10 +34,16 @@ const isNewVersionAvailable = (current: string, latest: string) => compare(curre
|
||||
|
||||
const getLatestVersion = async (packageName: string): Promise<string | undefined> => {
|
||||
const latestUrl = `http://registry.npmjs.org/${packageName}/latest`;
|
||||
const response = await fetch(latestUrl);
|
||||
if (!response) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(latestUrl, { timeout: DEFAULT_FETCH_TIMEOUT });
|
||||
const info = await response.json();
|
||||
|
||||
return info.version;
|
||||
} catch {
|
||||
// Do nothing
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const cacheLatestVersion = () => {
|
||||
|
||||
Reference in New Issue
Block a user