diff --git a/src/deploy-actions.js b/src/deploy-actions.js index 8138eb6..08591b7 100644 --- a/src/deploy-actions.js +++ b/src/deploy-actions.js @@ -57,11 +57,24 @@ async function deployActions (config, deployConfig = {}, logFunc) { // checks // a. missing credentials utils.checkOpenWhiskCredentials(config) - // b. missing build files + // b. no packages declared (e.g. `packages: {}`, used only to trigger database + // auto-provisioning). Skip deployment entirely. Proceeding would run a full + // sync against an empty manifest, undeploying every previously-deployed entity + // for the project. Use `aio app undeploy` to intentionally remove all entities. + const packages = config.manifest.full.packages + if (Object.keys(packages).length === 0) { + log('Warning: no packages defined in the manifest, skipping deployment. Existing deployed entities are left untouched; use \'aio app undeploy\' to remove them.') + return {} + } + + // c. missing build files — only required when at least one package defines actions const dist = config.actions.dist + const hasAnyActions = Object.values(packages) + .some(pkg => Object.keys(pkg.actions || {}).length > 0) if ( + hasAnyActions && (!deployConfig.filterEntities || deployConfig.filterEntities.actions) && - (!fs.pathExistsSync(dist) || !fs.lstatSync(dist).isDirectory() || !fs.readdirSync(dist).length === 0) + (!fs.pathExistsSync(dist) || !fs.lstatSync(dist).isDirectory() || fs.readdirSync(dist).length === 0) ) { throw new Error(`missing files in ${utils._relApp(config.root, dist)}, maybe you forgot to build your actions ?`) } diff --git a/src/utils.js b/src/utils.js index ff978af..04e947f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2104,7 +2104,9 @@ function replacePackagePlaceHolder (config) { // Using custom package name. // Set config.ow.package so that syncProject can use it as project name for annotations. const packageNames = Object.keys(packages) - modifiedConfig.ow.package = packageNames[0] + if (packageNames.length > 0) { + modifiedConfig.ow.package = packageNames[0] + } } return modifiedConfig } diff --git a/test/deploy.actions.test.js b/test/deploy.actions.test.js index 2912fa9..3a55c66 100644 --- a/test/deploy.actions.test.js +++ b/test/deploy.actions.test.js @@ -577,12 +577,30 @@ test('Deploy actions should fail if there are no build files and action filter', .rejects.toThrow('missing files in dist') }) +test('Deploy actions should fail if the build directory exists but is empty', async () => { + addSampleAppFiles() + // dist exists as a directory but contains no built actions + global.fakeFileSystem.addJson({ [global.sampleAppConfig.actions.dist]: null }) + await expect(deployActions(global.sampleAppConfig)) + .rejects.toThrow('missing files in dist') +}) + test('Deploy actions should pass if there are no build files and filter does not include actions', async () => { addSampleAppFiles() runtimeLibUtils.processPackage.mockReturnValue({}) await expect(deployActions(global.sampleAppConfig, { filterEntities: { triggers: ['trigger1'] } })).resolves.toEqual({}) }) +test('Deploy actions should be a no-op (no undeploy) and warn when packages: {} (empty packages)', async () => { + const emptyPackagesConfig = deepCopy(global.sampleAppConfig) + emptyPackagesConfig.manifest.full.packages = {} + const logSpy = jest.fn() + await expect(deployActions(emptyPackagesConfig, {}, logSpy)).resolves.toEqual({}) + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('no packages defined')) + // must not run a full sync that would undeploy previously-deployed entities + expect(runtimeLibUtils.syncProject).not.toHaveBeenCalled() +}) + // lonely test('if actions are deployed and part of the manifest it should return their url', async () => { addSampleAppReducedFiles() diff --git a/test/utils.test.js b/test/utils.test.js index 7a36de3..6be08f3 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -3097,3 +3097,43 @@ describe('loadIMSCredentialsFromEnv', () => { expect(result.scopes).toBe('not json') }) }) + +describe('replacePackagePlaceHolder', () => { + test('leaves ow.package unchanged when packages is empty (packages: {})', () => { + const config = { + ow: { package: 'my-pkg' }, + manifest: { + packagePlaceholder: '__APP_PACKAGE__', + full: { packages: {} } + } + } + const result = utils.replacePackagePlaceHolder(config) + expect(result.ow.package).toBe('my-pkg') + }) + + test('renames placeholder package to ow.package', () => { + const config = { + ow: { package: 'my-pkg' }, + manifest: { + packagePlaceholder: '__APP_PACKAGE__', + full: { packages: { __APP_PACKAGE__: { actions: {} } } } + } + } + const result = utils.replacePackagePlaceHolder(config) + expect(result.ow.package).toBe('my-pkg') + expect(result.manifest.full.packages['my-pkg']).toBeDefined() + expect(result.manifest.full.packages.__APP_PACKAGE__).toBeUndefined() + }) + + test('sets ow.package to first package name when no placeholder matches', () => { + const config = { + ow: { package: 'ignored' }, + manifest: { + packagePlaceholder: '__APP_PACKAGE__', + full: { packages: { 'custom-pkg': { actions: {} } } } + } + } + const result = utils.replacePackagePlaceHolder(config) + expect(result.ow.package).toBe('custom-pkg') + }) +})