Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it harder to overwrite published, Production apps #559

Merged
merged 10 commits into from
Sep 26, 2022
28 changes: 25 additions & 3 deletions src/commands/app/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down Expand Up @@ -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
Expand All @@ -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)}'`)))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}),
Expand Down
116 changes: 115 additions & 1 deletion test/commands/app/deploy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -127,6 +152,7 @@ const mockLogForwarding = {

afterAll(() => {
jest.restoreAllMocks()
jest.resetAllMocks()
})

beforeEach(() => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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({
Expand Down