diff --git a/packages/plugins/apps/src/upload.test.ts b/packages/plugins/apps/src/upload.test.ts index 1fc85046..0a4756e2 100644 --- a/packages/plugins/apps/src/upload.test.ts +++ b/packages/plugins/apps/src/upload.test.ts @@ -2,7 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { getData, getIntakeUrl, uploadArchive } from '@dd/apps-plugin/upload'; +import { getData, getIntakeUrl, getReleaseUrl, uploadArchive } from '@dd/apps-plugin/upload'; import { getDDEnvValue } from '@dd/core/helpers/env'; import { getFile } from '@dd/core/helpers/fs'; import { @@ -73,11 +73,43 @@ describe('Apps Plugin - upload', () => { expect(getIntakeUrl('datadoghq.com', 'my-app')).toBe('https://custom.apps'); }); - test('Should fallback to default intake url', () => { + test('Should prefix for all Datadog sites', () => { getDDEnvValueMock.mockReturnValue(undefined); + expect(getIntakeUrl('datadoghq.com', 'my-app')).toBe( + 'https://api.datadoghq.com/api/unstable/app-builder-code/apps/my-app/upload', + ); expect(getIntakeUrl('datadoghq.eu', 'my-app')).toBe( 'https://api.datadoghq.eu/api/unstable/app-builder-code/apps/my-app/upload', ); + expect(getIntakeUrl('ddog-gov.com', 'my-app')).toBe( + 'https://api.ddog-gov.com/api/unstable/app-builder-code/apps/my-app/upload', + ); + expect(getIntakeUrl('us5.datadoghq.com', 'my-app')).toBe( + 'https://api.us5.datadoghq.com/api/unstable/app-builder-code/apps/my-app/upload', + ); + expect(getIntakeUrl('dd.datad0g.com', 'my-app')).toBe( + 'https://api.dd.datad0g.com/api/unstable/app-builder-code/apps/my-app/upload', + ); + }); + }); + + describe('getReleaseUrl', () => { + test('Should prefix for all Datadog sites', () => { + expect(getReleaseUrl('datadoghq.com', 'my-app')).toBe( + 'https://api.datadoghq.com/api/unstable/app-builder-code/apps/my-app/release/live', + ); + expect(getReleaseUrl('datadoghq.eu', 'my-app')).toBe( + 'https://api.datadoghq.eu/api/unstable/app-builder-code/apps/my-app/release/live', + ); + expect(getReleaseUrl('ddog-gov.com', 'my-app')).toBe( + 'https://api.ddog-gov.com/api/unstable/app-builder-code/apps/my-app/release/live', + ); + expect(getReleaseUrl('us5.datadoghq.com', 'my-app')).toBe( + 'https://api.us5.datadoghq.com/api/unstable/app-builder-code/apps/my-app/release/live', + ); + expect(getReleaseUrl('dd.datad0g.com', 'my-app')).toBe( + 'https://api.dd.datad0g.com/api/unstable/app-builder-code/apps/my-app/release/live', + ); }); }); @@ -226,6 +258,40 @@ describe('Apps Plugin - upload', () => { ); }); + test('Should make PUT request to release version when APPS_VERSION_NAME is set', async () => { + getDDEnvValueMock.mockImplementation((key) => { + if (key === 'APPS_VERSION_NAME') { + return 'my-version'; + } + return undefined; + }); + doRequestMock + .mockResolvedValueOnce({ + version_id: 'v123', + application_id: 'app123', + app_builder_id: 'builder123', + }) + .mockResolvedValueOnce({}); + + const { errors, warnings } = await uploadArchive(archive, context, logger); + + expect(errors).toHaveLength(0); + expect(warnings).toHaveLength(0); + expect(doRequestMock).toHaveBeenCalledTimes(2); + expect(doRequestMock).toHaveBeenNthCalledWith(2, { + auth: { apiKey: 'api-key', appKey: 'app-key' }, + url: 'https://api.datadoghq.com/api/unstable/app-builder-code/apps/repo:app/release/live', + method: 'PUT', + type: 'json', + getData: expect.any(Function), + onRetry: expect.any(Function), + }); + expect(mockLogFn).toHaveBeenCalledWith( + expect.stringContaining('Released version'), + 'info', + ); + }); + test('Should collect warnings on retries', async () => { doRequestMock.mockImplementation(async (opts) => { opts.onRetry?.(new Error('network'), 2); diff --git a/packages/plugins/apps/src/upload.ts b/packages/plugins/apps/src/upload.ts index de7a2c0c..f432a8d1 100644 --- a/packages/plugins/apps/src/upload.ts +++ b/packages/plugins/apps/src/upload.ts @@ -14,6 +14,7 @@ import { prettyObject } from '@dd/core/helpers/strings'; import type { Logger } from '@dd/core/types'; import chalk from 'chalk'; import prettyBytes from 'pretty-bytes'; +import { Readable } from 'stream'; import type { Archive } from './archive'; import { APPS_API_PATH, ARCHIVE_FILENAME } from './constants'; @@ -41,6 +42,10 @@ export const getIntakeUrl = (site: string, appId: string) => { return envIntake || `https://api.${site}/${APPS_API_PATH}/${appId}/upload`; }; +export const getReleaseUrl = (site: string, appId: string) => { + return `https://api.${site}/${APPS_API_PATH}/${appId}/release/live`; +}; + export const getData = (archivePath: string, defaultHeaders: Record = {}, name: string) => async (): Promise => { @@ -135,6 +140,32 @@ Would have uploaded ${summary}`, `Your application is available at:\n${bold('Standalone :')}\n ${cyan(appUrl)}\n\n${bold('AppBuilder :')}\n ${cyan(appBuilderUrl)}`, ); } + + const versionName = getDDEnvValue('APPS_VERSION_NAME')?.trim(); + if (versionName) { + const releaseUrl = getReleaseUrl(context.site, context.identifier); + await doRequest({ + auth: { apiKey: context.apiKey, appKey: context.appKey }, + url: releaseUrl, + method: 'PUT', + type: 'json', + getData: async () => ({ + data: Readable.from(JSON.stringify({ version_id: versionName })), + headers: { + 'Content-Type': 'application/json', + ...defaultHeaders, + }, + }), + onRetry: (error: Error, attempt: number) => { + const message = `Failed to release version (attempt ${yellow( + `${attempt}/${NB_RETRIES}`, + )}): ${error.message}`; + warnings.push(message); + log.warn(message); + }, + }); + log.info(`Released version ${bold(versionName)} to live.`); + } } catch (error: unknown) { const err = error instanceof Error ? error : new Error(String(error)); errors.push(err);