From 73c39b69004d3382590f5cdc6582286755f7a404 Mon Sep 17 00:00:00 2001 From: Michael Goberling Date: Mon, 18 Jul 2022 12:36:18 -0400 Subject: [PATCH 1/5] Bail on deploy if attempting to overwrite published application --- src/commands/app/deploy.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index aecdb37b..b24155b7 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -57,6 +57,7 @@ class Deploy extends BuildCommand { try { const aioConfig = this.getFullConfig().aio + // 1. update log forwarding configuration // note: it is possible that .aio file does not exist, which means there is no local lg config if (aioConfig && @@ -85,7 +86,23 @@ class Deploy extends BuildCommand { throw error } } - // 2. deploy actions and web assets for each extension + + // 2. Bail if workspace is production and application status is PUBLISHED, honor force-publish + if (aioConfig.project.workspace.name === "Production" && flags.publish && !flags['force-publish']) { + try { + let extension = await this.getApplicationExtension(libConsoleCLI, aioConfig, spinner) + spinner.info(chalk.dim(JSON.stringify(extension))) + if (extension.status === 'PUBLISHED') { + spinner.info(chalk.red('This application is published and the current workspace is Production, deployment will be skipped. You must first retract this application in Adobe Exchange to deploy updates.')) + return + } + } catch (err) { + spinner.fail(chalk.red('Error checking extension status')) + throw err + } + } + + // 3. deploy actions and web assets for each extension // Possible improvements: // - parallelize // - break into smaller pieces deploy, allowing to first deploy all actions then all web assets @@ -251,6 +268,12 @@ class Deploy extends BuildCommand { newPayload = await libConsoleCLI.updateExtensionPointsWithoutOverwrites(aioConfig.project.org, aioConfig.project, aioConfig.project.workspace, payload) return newPayload } + + async getApplicationExtension (libConsoleCLI, aioConfig, spinner) { + let { appId } = await libConsoleCLI.getProject(aioConfig.project.org.id, aioConfig.project.id) + let applicationExtensions = await libConsoleCLI.getApplicationExtensions(aioConfig.project.org.id, appId) + return applicationExtensions.find(extension => extension.appId === appId) + } } Deploy.description = `Build and deploy an Adobe I/O App From f0cb12fc6124586bec694eb294a239c6b0430c82 Mon Sep 17 00:00:00 2001 From: Michael Goberling Date: Mon, 18 Jul 2022 16:39:51 -0400 Subject: [PATCH 2/5] Add force deploy option --- src/commands/app/deploy.js | 27 ++++++----- test/commands/app/deploy.test.js | 78 +++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index b24155b7..e236c130 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -87,18 +87,13 @@ class Deploy extends BuildCommand { } } - // 2. Bail if workspace is production and application status is PUBLISHED, honor force-publish - if (aioConfig.project.workspace.name === "Production" && flags.publish && !flags['force-publish']) { - try { - let extension = await this.getApplicationExtension(libConsoleCLI, aioConfig, spinner) - spinner.info(chalk.dim(JSON.stringify(extension))) - if (extension.status === 'PUBLISHED') { - spinner.info(chalk.red('This application is published and the current workspace is Production, deployment will be skipped. You must first retract this application in Adobe Exchange to deploy updates.')) - return - } - } catch (err) { - spinner.fail(chalk.red('Error checking extension status')) - throw err + // 2. Bail if workspace is production and application status is PUBLISHED, honor force-deploy + if (aioConfig.project.workspace.name === "Production" && flags.publish && !flags['force-deploy']) { + let extension = await this.getApplicationExtension(libConsoleCLI, aioConfig, spinner) + spinner.info(chalk.dim(JSON.stringify(extension))) + if (extension && extension.status === 'PUBLISHED') { + spinner.info(chalk.red('This application is published and the current workspace is Production, deployment will be skipped. You must first retract this application in Adobe Exchange to deploy updates.')) + return } } @@ -112,7 +107,7 @@ class Deploy extends BuildCommand { await this.deploySingleConfig(k, v, flags, spinner) } - // 3. deploy extension manifest + // 4. deploy extension manifest if (flags.publish) { const payload = await this.publishExtensionPoints(libConsoleCLI, deployConfigs, aioConfig, flags['force-publish']) this.log(chalk.blue(chalk.bold(`New Extension Point(s) in Workspace '${aioConfig.project.workspace.name}': '${Object.keys(payload.endpoints)}'`))) @@ -269,7 +264,7 @@ class Deploy extends BuildCommand { return newPayload } - async getApplicationExtension (libConsoleCLI, aioConfig, spinner) { + async getApplicationExtension (libConsoleCLI, aioConfig) { let { appId } = await libConsoleCLI.getProject(aioConfig.project.org.id, aioConfig.project.id) let applicationExtensions = await libConsoleCLI.getApplicationExtensions(aioConfig.project.org.id, appId) return applicationExtensions.find(extension => extension.appId === appId) @@ -347,6 +342,10 @@ Deploy.flags = { default: true, exclusive: ['action'] }), + 'force-deploy': flags.boolean({ + description: '[default: false] Force deploy extensions regardless of published status or workspace', + default: false + }), 'force-publish': flags.boolean({ description: 'Force publish extension(s) to Exchange, delete previously published extension points', default: false, diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 87bbdc8a..0cc3248c 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -103,9 +103,26 @@ const mockExtRegExcShellAndNuiPayload = () => { mockLibConsoleCLI.updateExtensionPoints.mockReturnValueOnce(payload) } +const mockGetExtensionPointsPublishedApp = () => { + const payload = [{ + status: 'PUBLISHED', + appId: '1234' + }] + mockLibConsoleCLI.getApplicationExtensions.mockReturnValueOnce(payload) +} + +const mockGetProject = () => { + const payload = { + appId: '1234' + } + mockLibConsoleCLI.getProject.mockReturnValueOnce(payload) +} + const mockLibConsoleCLI = { updateExtensionPoints: jest.fn(), - updateExtensionPointsWithoutOverwrites: jest.fn() + updateExtensionPointsWithoutOverwrites: jest.fn(), + getProject: jest.fn(), + getApplicationExtensions: jest.fn() } const mockLogForwarding = { @@ -196,6 +213,10 @@ test('flags', async () => { expect(TheCommand.flags.publish.allowNo).toEqual(true) expect(TheCommand.flags.publish.exclusive).toEqual(['action']) + expect(typeof TheCommand.flags['force-deploy']).toBe('object') + expect(typeof TheCommand.flags['force-deploy'].description).toBe('string') + expect(TheCommand.flags['force-deploy'].default).toEqual(false) + expect(typeof TheCommand.flags['force-publish']).toBe('object') expect(typeof TheCommand.flags['force-publish'].description).toBe('string') expect(TheCommand.flags['force-publish'].default).toEqual(false) @@ -785,6 +806,61 @@ describe('run', () => { expect(command.error).toHaveBeenCalledWith(expect.stringMatching(/Nothing to be done/)) }) + test('deploy for PUBLISHED Production extension - no publish', async () => { + command.getAppExtConfigs.mockReturnValueOnce(createAppConfig(command.appConfig, 'exc')) + mockGetExtensionPointsPublishedApp() + mockGetProject() + command.getFullConfig.mockReturnValue({ + aio: { + project: { + workspace: { + name: 'Production' + }, + org: { + id: '1111' + } + } + } + }) + mockExtRegExcShellPayload() + await command.run() + + expect(mockLibConsoleCLI.getProject).toHaveBeenCalledTimes(1) + expect(mockLibConsoleCLI.getApplicationExtensions).toHaveBeenCalledTimes(1) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(0) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(0) + expect(mockLibConsoleCLI.updateExtensionPoints).toHaveBeenCalledTimes(0) + expect(mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites).toHaveBeenCalledTimes(0) + }) + + test('deploy for PUBLISHED Production extension - force deploy', async () => { + command.getAppExtConfigs.mockReturnValueOnce(createAppConfig(command.appConfig, 'exc')) + mockGetExtensionPointsPublishedApp() + mockGetProject() + command.getFullConfig.mockReturnValue({ + aio: { + project: { + workspace: { + name: 'Production' + }, + org: { + id: '1111' + } + } + } + }) + mockExtRegExcShellPayload() + command.argv = ['--force-deploy'] + await command.run() + + expect(mockLibConsoleCLI.getProject).toHaveBeenCalledTimes(0) + expect(mockLibConsoleCLI.getApplicationExtensions).toHaveBeenCalledTimes(0) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + expect(mockLibConsoleCLI.updateExtensionPoints).toHaveBeenCalledTimes(0) + expect(mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites).toHaveBeenCalledTimes(1) + }) + test('publish phase (no force, exc+nui payload)', async () => { command.getAppExtConfigs.mockReturnValueOnce(createAppConfig(command.appConfig, 'app-exc-nui')) command.getFullConfig.mockReturnValue({ From 02d69f23c21c3146262cba54b61cd8041a7f3597 Mon Sep 17 00:00:00 2001 From: Michael Goberling Date: Mon, 18 Jul 2022 17:02:20 -0400 Subject: [PATCH 3/5] Add test case for RETRACTED extension --- src/commands/app/deploy.js | 8 +++--- test/commands/app/deploy.test.js | 46 +++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index e236c130..9031a780 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -88,8 +88,8 @@ class Deploy extends BuildCommand { } // 2. Bail if workspace is production and application status is PUBLISHED, honor force-deploy - if (aioConfig.project.workspace.name === "Production" && flags.publish && !flags['force-deploy']) { - let extension = await this.getApplicationExtension(libConsoleCLI, aioConfig, spinner) + if (aioConfig.project.workspace.name === 'Production' && flags.publish && !flags['force-deploy']) { + const extension = await this.getApplicationExtension(libConsoleCLI, aioConfig, spinner) spinner.info(chalk.dim(JSON.stringify(extension))) if (extension && extension.status === 'PUBLISHED') { spinner.info(chalk.red('This application is published and the current workspace is Production, deployment will be skipped. You must first retract this application in Adobe Exchange to deploy updates.')) @@ -265,8 +265,8 @@ class Deploy extends BuildCommand { } async getApplicationExtension (libConsoleCLI, aioConfig) { - let { appId } = await libConsoleCLI.getProject(aioConfig.project.org.id, aioConfig.project.id) - let applicationExtensions = await libConsoleCLI.getApplicationExtensions(aioConfig.project.org.id, appId) + const { appId } = await libConsoleCLI.getProject(aioConfig.project.org.id, aioConfig.project.id) + const applicationExtensions = await libConsoleCLI.getApplicationExtensions(aioConfig.project.org.id, appId) return applicationExtensions.find(extension => extension.appId === appId) } } diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 0cc3248c..1bcf5faf 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -103,6 +103,14 @@ const mockExtRegExcShellAndNuiPayload = () => { mockLibConsoleCLI.updateExtensionPoints.mockReturnValueOnce(payload) } +const mockGetExtensionPointsRetractedApp = () => { + const payload = [{ + status: 'RETRACTED', + appId: '1234' + }] + mockLibConsoleCLI.getApplicationExtensions.mockReturnValueOnce(payload) +} + const mockGetExtensionPointsPublishedApp = () => { const payload = [{ status: 'PUBLISHED', @@ -114,14 +122,14 @@ const mockGetExtensionPointsPublishedApp = () => { const mockGetProject = () => { const payload = { appId: '1234' - } + } mockLibConsoleCLI.getProject.mockReturnValueOnce(payload) } const mockLibConsoleCLI = { updateExtensionPoints: jest.fn(), updateExtensionPointsWithoutOverwrites: jest.fn(), - getProject: jest.fn(), + getProject: jest.fn(), getApplicationExtensions: jest.fn() } @@ -133,6 +141,7 @@ const mockLogForwarding = { afterAll(() => { jest.restoreAllMocks() + jest.resetAllMocks() }) beforeEach(() => { @@ -816,7 +825,7 @@ describe('run', () => { workspace: { name: 'Production' }, - org: { + org: { id: '1111' } } @@ -843,7 +852,7 @@ describe('run', () => { workspace: { name: 'Production' }, - org: { + org: { id: '1111' } } @@ -861,6 +870,35 @@ describe('run', () => { expect(mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites).toHaveBeenCalledTimes(1) }) + test('deploy for RETRACTED Production extension - publish', async () => { + mockLibConsoleCLI.getApplicationExtensions.mockReset() + + command.getAppExtConfigs.mockReturnValueOnce(createAppConfig(command.appConfig, 'exc')) + mockGetExtensionPointsRetractedApp() + mockGetProject() + command.getFullConfig.mockReturnValue({ + aio: { + project: { + workspace: { + name: 'Production' + }, + org: { + id: '1111' + } + } + } + }) + mockExtRegExcShellPayload() + await command.run() + + expect(mockLibConsoleCLI.getProject).toHaveBeenCalledTimes(1) + expect(mockLibConsoleCLI.getApplicationExtensions).toHaveBeenCalledTimes(1) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + expect(mockLibConsoleCLI.updateExtensionPoints).toHaveBeenCalledTimes(0) + expect(mockLibConsoleCLI.updateExtensionPointsWithoutOverwrites).toHaveBeenCalledTimes(1) + }) + test('publish phase (no force, exc+nui payload)', async () => { command.getAppExtConfigs.mockReturnValueOnce(createAppConfig(command.appConfig, 'app-exc-nui')) command.getFullConfig.mockReturnValue({ From 89b4c87637639dbb3dc0cf79c3d09dc5e3b43223 Mon Sep 17 00:00:00 2001 From: Michael Goberling Date: Mon, 18 Jul 2022 17:05:04 -0400 Subject: [PATCH 4/5] Remove debug code --- src/commands/app/deploy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index 9031a780..b06b8a20 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -89,7 +89,7 @@ class Deploy extends BuildCommand { // 2. Bail if workspace is production and application status is PUBLISHED, honor force-deploy if (aioConfig.project.workspace.name === 'Production' && flags.publish && !flags['force-deploy']) { - const extension = await this.getApplicationExtension(libConsoleCLI, aioConfig, spinner) + const extension = await this.getApplicationExtension(libConsoleCLI, aioConfig) spinner.info(chalk.dim(JSON.stringify(extension))) if (extension && extension.status === 'PUBLISHED') { spinner.info(chalk.red('This application is published and the current workspace is Production, deployment will be skipped. You must first retract this application in Adobe Exchange to deploy updates.')) From 61e8c1de72499ce346aae768ca5ce768cd4aa51e Mon Sep 17 00:00:00 2001 From: Michael Goberling Date: Tue, 19 Jul 2022 11:42:38 -0400 Subject: [PATCH 5/5] Remove publish from deployment check --- src/commands/app/deploy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index a3bc4153..ff9227a8 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -86,7 +86,7 @@ class Deploy extends BuildCommand { } // 2. Bail if workspace is production and application status is PUBLISHED, honor force-deploy - if (aioConfig.project.workspace.name === 'Production' && flags.publish && !flags['force-deploy']) { + if (aioConfig.project.workspace.name === 'Production' && !flags['force-deploy']) { const extension = await this.getApplicationExtension(libConsoleCLI, aioConfig) spinner.info(chalk.dim(JSON.stringify(extension))) if (extension && extension.status === 'PUBLISHED') {