diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index e34eeba1..ff9227a8 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -55,6 +55,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 && @@ -83,7 +84,18 @@ 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-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') { + 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 + } + } + + // 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 @@ -93,7 +105,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)}'`))) @@ -249,6 +261,12 @@ class Deploy extends BuildCommand { newPayload = await libConsoleCLI.updateExtensionPointsWithoutOverwrites(aioConfig.project.org, aioConfig.project, aioConfig.project.workspace, payload) return newPayload } + + async getApplicationExtension (libConsoleCLI, aioConfig) { + 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) + } } Deploy.description = `Build and deploy an Adobe I/O App @@ -322,8 +340,12 @@ Deploy.flags = { default: true, exclusive: ['action'] }), + 'force-deploy': Flags.boolean({ + description: '[default: false] Force deploy changes, regardless of production Workspace being published in Exchange.', + default: false + }), 'force-publish': Flags.boolean({ - description: 'Force publish extension(s) to Exchange, delete previously published extension points', + description: '[default: false] Force publish extension(s) to Exchange, delete previously published extension points', default: false, exclusive: ['action', 'publish'] // no-publish is excluded }), diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 126b01ee..096b59fb 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -114,9 +114,34 @@ const mockExtRegExcShellAndNuiPayload = () => { mockLibConsoleCLI.updateExtensionPoints.mockReturnValueOnce(payload) } +const mockGetExtensionPointsRetractedApp = () => { + const payload = [{ + status: 'RETRACTED', + appId: '1234' + }] + mockLibConsoleCLI.getApplicationExtensions.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 = { @@ -127,6 +152,7 @@ const mockLogForwarding = { afterAll(() => { jest.restoreAllMocks() + jest.resetAllMocks() }) beforeEach(() => { @@ -207,6 +233,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) @@ -796,6 +826,90 @@ 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('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({