Skip to content

Commit

Permalink
[ACNA-1304] Filter actions deployments. (#478)
Browse files Browse the repository at this point in the history
* [ACNA-1304] Filter actions deployments
  • Loading branch information
florind-ens authored Oct 11, 2021
1 parent bce1832 commit 9551029
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 30 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
node_modules
coverage
.vscode/
/.idea/

oclif.manifest.json
package-lock.json
Expand Down
2 changes: 1 addition & 1 deletion src/commands/app/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class Build extends BaseCommand {
aioLogger.debug(`run hook for 'build-actions' for actions in '${name}' returned ${script}`)
spinner.start(`Building actions for '${name}'`)
if (!script) {
builtList = await RuntimeLib.buildActions(config, filterActions)
builtList = await RuntimeLib.buildActions(config, filterActions, true)
}
if (builtList.length > 0) {
spinner.succeed(chalk.green(`Built ${builtList.length} action(s) for '${name}'`))
Expand Down
58 changes: 43 additions & 15 deletions src/lib/actions-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const upath = require('upath')
const chokidar = require('chokidar')
const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:actions-watcher', { provider: 'debug' })
const buildActions = require('./build-actions')
Expand Down Expand Up @@ -58,12 +58,13 @@ module.exports = async (watcherOptions) => {
* Builds and deploy the app.
*
* @param {WatcherOptions} watcherOptions the options for the watcher
* @param {Array<string>} filterActions add filters to deploy only specified OpenWhisk actions
*/
async function buildAndDeploy (watcherOptions) {
async function buildAndDeploy (watcherOptions, filterActions) {
const { config, isLocal, log } = watcherOptions

await buildActions(config)
await deployActions(config, isLocal, log)
await buildActions(config, filterActions)
await deployActions(config, isLocal, log, filterActions)
}

/**
Expand All @@ -75,30 +76,57 @@ async function buildAndDeploy (watcherOptions) {
function createChangeHandler (watcherOptions) {
const { watcher, log } = watcherOptions

let running = false
let changed = false
let deploymentInProgress = false
let fileChanged = false
let undeployedFile = ''

return async (filePath) => {
if (running) {
if (deploymentInProgress) {
aioLogger.debug(`${filePath} has changed. Deploy in progress. This change will be deployed after completion of current deployment.`)
changed = true
undeployedFile = filePath
fileChanged = true
return
}
running = true
deploymentInProgress = true
try {
aioLogger.debug(`${filePath} has changed. Redeploying actions.`)
await buildAndDeploy(watcherOptions)
aioLogger.debug('Deployment successful.')
const filterActions = getActionNameFromPath(filePath, watcherOptions)
await buildAndDeploy(watcherOptions, filterActions)
aioLogger.debug('Deployment successful')
} catch (err) {
log(' -> Error encountered while deploying actions. Stopping auto refresh.')
aioLogger.debug(err)
await watcher.close()
}
if (changed) {
if (fileChanged) {
aioLogger.debug('Code changed during deployment. Triggering deploy again.')
changed = running = false
await createChangeHandler(watcherOptions)(filePath)
fileChanged = deploymentInProgress = false
await createChangeHandler(watcherOptions)(undeployedFile)
}
running = false
deploymentInProgress = false
}
}

/**
* Util function which returns the actionName from the filePath.
*
* @param {string} filePath path of the file
* @param {WatcherOptions} watcherOptions the options for the watcher
* @returns {Array<string>} All of the actions which match the modified path
*/
function getActionNameFromPath (filePath, watcherOptions) {
const actionNames = []
const unixFilePath = upath.toUnix(filePath)
const { config } = watcherOptions
Object.entries(config.manifest.full.packages).forEach(([, pkg]) => {
if (pkg.actions) {
Object.entries(pkg.actions).forEach(([actionName, action]) => {
const unixActionFunction = upath.toUnix(action.function)
if (unixActionFunction.includes(unixFilePath)) {
actionNames.push(actionName)
}
})
}
})
return actionNames
}
5 changes: 3 additions & 2 deletions src/lib/build-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ const { buildActions } = require('@adobe/aio-lib-runtime')
* Builds actions.
*
* @param {object} config see src/lib/config-loader.js
* @param {Array<string>} filterActions add filters to deploy only specified OpenWhisk actions
*/
module.exports = async (config) => {
module.exports = async (config, filterActions) => {
utils.runScript(config.hooks['pre-app-build'])
const script = await utils.runScript(config.hooks['build-actions'])
if (!script) {
await buildActions(config)
await buildActions(config, filterActions)
}
utils.runScript(config.hooks['post-app-build'])
}
11 changes: 9 additions & 2 deletions src/lib/deploy-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@ const { deployActions } = require('@adobe/aio-lib-runtime')
* @param {object} config see src/lib/config-loader.js
* @param {boolean} isLocal=false set to true if it's a local deploy
* @param {Function} [log] a log function
* @param {boolean} filter true if a filter by built actions is desired.
*/
/** @private */
module.exports = async (config, isLocal = false, log = () => {}) => {
module.exports = async (config, isLocal = false, log = () => {}, filter = false) => {
utils.runScript(config.hooks['pre-app-deploy'])
const script = await utils.runScript(config.hooks['deploy-actions'])
if (!script) {
const entities = await deployActions(config, { isLocalDev: isLocal }, log)
const deployConfig = {
isLocalDev: isLocal,
filterEntities: {
byBuiltActions: filter
}
}
const entities = await deployActions(config, deployConfig, log)
if (entities.actions) {
const web = entities.actions.filter(utils.createWebExportFilter(true))
const nonWeb = entities.actions.filter(utils.createWebExportFilter(false))
Expand Down
2 changes: 1 addition & 1 deletion src/lib/run-dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ async function runDev (config, dataDir, options = {}, log = () => {}) {
// Deploy Phase - deploy actions
if (withBackend) {
log('redeploying actions..')
await deployActions(devConfig, isLocal, log)
await deployActions(devConfig, isLocal, log, true)
}

// Deploy Phase - serve the web UI
Expand Down
2 changes: 1 addition & 1 deletion test/commands/app/build.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ describe('run', () => {
expect(spinner.succeed).toBeCalledWith(expect.stringContaining('Built 3 action(s) for \'application\''))
expect(command.error).toHaveBeenCalledTimes(0)
expect(mockRuntimeLib.buildActions).toHaveBeenCalledTimes(1)
expect(mockRuntimeLib.buildActions).toHaveBeenCalledWith(appConfig.application, ['a', 'b', 'c'])
expect(mockRuntimeLib.buildActions).toHaveBeenCalledWith(appConfig.application, ['a', 'b', 'c'], true)
expect(mockWebLib.bundle).toHaveBeenCalledTimes(0)
})

Expand Down
81 changes: 73 additions & 8 deletions test/commands/lib/actions-watcher.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ const mockLogger = require('@adobe/aio-lib-core-logging')
const buildActions = require('../../../src/lib/build-actions')
const deployActions = require('../../../src/lib/deploy-actions')
const util = require('util')
const dataMocks = require('../../data-mocks/config-loader')
const sleep = util.promisify(setTimeout)
const cloneDeep = require('lodash.clonedeep')

jest.mock('chokidar')
jest.mock('../../../src/lib/build-actions')
jest.mock('../../../src/lib/deploy-actions')
jest.mock('../../../src/lib/app-helper')

const CONFIG = {
actions: {
src: 'actions'
}
const createAppConfig = (aioConfig = {}, appFixtureName = 'legacy-app') => {
const appConfig = dataMocks(appFixtureName, aioConfig).all
appConfig.application = { ...appConfig.application, ...aioConfig }
return appConfig
}

beforeEach(() => {
Expand Down Expand Up @@ -56,14 +58,15 @@ test('run and cleanup', async () => {
chokidar.watch.mockImplementation(() => mockWatcherInstance)

const log = jest.fn()
const { watcher, cleanup } = await actionsWatcher({ config: CONFIG, log })
const { application } = createAppConfig()
const { watcher, cleanup } = await actionsWatcher({ config: application, log })
expect(typeof watcher).toEqual('object')
expect(typeof cleanup).toEqual('function')

cleanup()

expect(mockWatcherInstance.on).toHaveBeenCalledWith('change', onChangeHandler)
expect(chokidar.watch).toHaveBeenCalledWith(CONFIG.actions.src)
expect(chokidar.watch).toHaveBeenCalledWith(application.actions.src)
expect(mockWatcherInstance.close).toHaveBeenCalled()
})

Expand All @@ -80,7 +83,8 @@ test('onChange handler', async () => {
chokidar.watch.mockImplementation(() => mockWatcherInstance)

const log = jest.fn()
await actionsWatcher({ config: CONFIG, log })
const { application } = createAppConfig()
await actionsWatcher({ config: application, log })
expect(typeof onChangeHandler).toEqual('function')

// first onchange
Expand All @@ -102,7 +106,8 @@ test('onChange handler called multiple times', async () => {
chokidar.watch.mockImplementation(() => mockWatcherInstance)

const log = jest.fn()
await actionsWatcher({ config: CONFIG, log })
const { application } = createAppConfig()
await actionsWatcher({ config: application, log })
expect(typeof onChangeHandler).toEqual('function')

// first onchange
Expand All @@ -118,3 +123,63 @@ test('onChange handler called multiple times', async () => {
expect(buildActions).toHaveBeenCalledTimes(1)
expect(deployActions).toHaveBeenCalledTimes(1)
})

test('onChange handler calls buildActions with filterActions', async () => {
let onChangeHandler = null
const mockWatcherInstance = {
on: jest.fn((event, handler) => {
if (event === 'change') {
onChangeHandler = handler
}
}),
close: jest.fn()
}
chokidar.watch.mockImplementation(() => mockWatcherInstance)

const log = jest.fn()
const { application } = createAppConfig()
await actionsWatcher({ config: application, log })
expect(typeof onChangeHandler).toEqual('function')

const filePath = process.platform === 'win32' ? '\\myactions\\action.js' : '/myactions/action.js'

deployActions.mockImplementation(async () => await sleep(5000))
onChangeHandler(filePath)

await jest.runAllTimers()

expect(buildActions).toHaveBeenCalledWith(
application, ['action']
)
})

test('onChange handler calls buildActions without filterActions when actions are undefined', async () => {
const { application } = createAppConfig()
const cloneApplication = cloneDeep(application)
Object.entries(cloneApplication.manifest.full.packages).forEach(([, pkg]) => {
if (pkg.actions) {
delete pkg.actions
}
})
let onChangeHandler = null
const mockWatcherInstance = {
on: jest.fn((event, handler) => {
if (event === 'change') {
onChangeHandler = handler
}
}),
close: jest.fn()
}
chokidar.watch.mockImplementation(() => mockWatcherInstance)

const log = jest.fn()
await actionsWatcher({ config: cloneApplication, log })
expect(typeof onChangeHandler).toEqual('function')

deployActions.mockImplementation(async () => await sleep(2000))
onChangeHandler('/myactions/action.js')

await jest.runAllTimers()

expect(buildActions).toHaveBeenCalledWith(cloneApplication, [])
})
21 changes: 21 additions & 0 deletions test/commands/lib/deploy-actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,27 @@ test('deploy-actions app hook available', async () => {
expect(utils.runScript).toHaveBeenNthCalledWith(3, undefined) // post-app-deploy
})

test('it should deploy actions with filter param, (coverage)', async () => {
utils.runScript.mockImplementation(() => false)

await deployActions({
hooks: {
'deploy-actions': 'deploy-actions'
},
filterByBuiltActions: true
})

expect(rtDeployActions).toHaveBeenCalled()
expect(rtDeployActions).toHaveBeenCalledWith(
expect.objectContaining({
hooks: { 'deploy-actions': 'deploy-actions' },
filterByBuiltActions: true
}),
expect.any(Object),
expect.any(Function)
)
})

test('no deploy-actions app hook available (use inbuilt)', async () => {
await deployActions({ hooks: {} })

Expand Down

0 comments on commit 9551029

Please sign in to comment.