Skip to content

Commit fd00eb1

Browse files
iivvaannxxclaudePatrick Russell
authored
fix: aio app deploy fails when packages: {} is declared (#233)
* fix: skip dist check and guard ow.package when packages is empty When a manifest declares `packages: {}` the build step produces no output so the dist directory is never created. The existing dist check was throwing "missing files in dist/…, maybe you forgot to build your actions?" even though there was nothing to build. * deploy-actions.js: gate the dist check on `hasAnyActions` — only validate the build directory when at least one package actually defines actions. Two tests cover both branches of `pkg.actions || {}` (empty packages and package with no actions key), keeping 100% branch coverage on deploy-actions.js. * utils.js (replacePackagePlaceHolder): guard the `packageNames[0]` assignment so an empty packages object no longer clobbers `ow.package` with `undefined`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: warn when packages is empty instead of silently deploying a no-op Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * remove em dash * fix: throw missing-files error when build dir exists but is empty The dist-existence check had an operator-precedence bug: `!fs.readdirSync(dist).length === 0` parses as `(!length) === 0`, which is always false, so the empty-build-directory branch never fired. Correct it to `fs.readdirSync(dist).length === 0` so a dist directory that exists but contains no built actions now raises the "missing files" error as intended. Adds a test covering that branch. * fix: skip deploy as a no-op when no packages are declared When the manifest declares `packages: {}` (e.g. only to trigger database auto-provisioning), deployActions previously fell through to a full sync against an empty manifest, which undeploys every previously-deployed entity for the project. Return early instead so an empty manifest is a true no-op that leaves existing entities untouched. `aio app undeploy` remains the explicit way to remove everything. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Patrick Russell <parussel@adobe.com>
1 parent 86eba0a commit fd00eb1

4 files changed

Lines changed: 76 additions & 3 deletions

File tree

src/deploy-actions.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,24 @@ async function deployActions (config, deployConfig = {}, logFunc) {
5757
// checks
5858
// a. missing credentials
5959
utils.checkOpenWhiskCredentials(config)
60-
// b. missing build files
60+
// b. no packages declared (e.g. `packages: {}`, used only to trigger database
61+
// auto-provisioning). Skip deployment entirely. Proceeding would run a full
62+
// sync against an empty manifest, undeploying every previously-deployed entity
63+
// for the project. Use `aio app undeploy` to intentionally remove all entities.
64+
const packages = config.manifest.full.packages
65+
if (Object.keys(packages).length === 0) {
66+
log('Warning: no packages defined in the manifest, skipping deployment. Existing deployed entities are left untouched; use \'aio app undeploy\' to remove them.')
67+
return {}
68+
}
69+
70+
// c. missing build files — only required when at least one package defines actions
6171
const dist = config.actions.dist
72+
const hasAnyActions = Object.values(packages)
73+
.some(pkg => Object.keys(pkg.actions || {}).length > 0)
6274
if (
75+
hasAnyActions &&
6376
(!deployConfig.filterEntities || deployConfig.filterEntities.actions) &&
64-
(!fs.pathExistsSync(dist) || !fs.lstatSync(dist).isDirectory() || !fs.readdirSync(dist).length === 0)
77+
(!fs.pathExistsSync(dist) || !fs.lstatSync(dist).isDirectory() || fs.readdirSync(dist).length === 0)
6578
) {
6679
throw new Error(`missing files in ${utils._relApp(config.root, dist)}, maybe you forgot to build your actions ?`)
6780
}

src/utils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2104,7 +2104,9 @@ function replacePackagePlaceHolder (config) {
21042104
// Using custom package name.
21052105
// Set config.ow.package so that syncProject can use it as project name for annotations.
21062106
const packageNames = Object.keys(packages)
2107-
modifiedConfig.ow.package = packageNames[0]
2107+
if (packageNames.length > 0) {
2108+
modifiedConfig.ow.package = packageNames[0]
2109+
}
21082110
}
21092111
return modifiedConfig
21102112
}

test/deploy.actions.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,12 +577,30 @@ test('Deploy actions should fail if there are no build files and action filter',
577577
.rejects.toThrow('missing files in dist')
578578
})
579579

580+
test('Deploy actions should fail if the build directory exists but is empty', async () => {
581+
addSampleAppFiles()
582+
// dist exists as a directory but contains no built actions
583+
global.fakeFileSystem.addJson({ [global.sampleAppConfig.actions.dist]: null })
584+
await expect(deployActions(global.sampleAppConfig))
585+
.rejects.toThrow('missing files in dist')
586+
})
587+
580588
test('Deploy actions should pass if there are no build files and filter does not include actions', async () => {
581589
addSampleAppFiles()
582590
runtimeLibUtils.processPackage.mockReturnValue({})
583591
await expect(deployActions(global.sampleAppConfig, { filterEntities: { triggers: ['trigger1'] } })).resolves.toEqual({})
584592
})
585593

594+
test('Deploy actions should be a no-op (no undeploy) and warn when packages: {} (empty packages)', async () => {
595+
const emptyPackagesConfig = deepCopy(global.sampleAppConfig)
596+
emptyPackagesConfig.manifest.full.packages = {}
597+
const logSpy = jest.fn()
598+
await expect(deployActions(emptyPackagesConfig, {}, logSpy)).resolves.toEqual({})
599+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('no packages defined'))
600+
// must not run a full sync that would undeploy previously-deployed entities
601+
expect(runtimeLibUtils.syncProject).not.toHaveBeenCalled()
602+
})
603+
586604
// lonely
587605
test('if actions are deployed and part of the manifest it should return their url', async () => {
588606
addSampleAppReducedFiles()

test/utils.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3097,3 +3097,43 @@ describe('loadIMSCredentialsFromEnv', () => {
30973097
expect(result.scopes).toBe('not json')
30983098
})
30993099
})
3100+
3101+
describe('replacePackagePlaceHolder', () => {
3102+
test('leaves ow.package unchanged when packages is empty (packages: {})', () => {
3103+
const config = {
3104+
ow: { package: 'my-pkg' },
3105+
manifest: {
3106+
packagePlaceholder: '__APP_PACKAGE__',
3107+
full: { packages: {} }
3108+
}
3109+
}
3110+
const result = utils.replacePackagePlaceHolder(config)
3111+
expect(result.ow.package).toBe('my-pkg')
3112+
})
3113+
3114+
test('renames placeholder package to ow.package', () => {
3115+
const config = {
3116+
ow: { package: 'my-pkg' },
3117+
manifest: {
3118+
packagePlaceholder: '__APP_PACKAGE__',
3119+
full: { packages: { __APP_PACKAGE__: { actions: {} } } }
3120+
}
3121+
}
3122+
const result = utils.replacePackagePlaceHolder(config)
3123+
expect(result.ow.package).toBe('my-pkg')
3124+
expect(result.manifest.full.packages['my-pkg']).toBeDefined()
3125+
expect(result.manifest.full.packages.__APP_PACKAGE__).toBeUndefined()
3126+
})
3127+
3128+
test('sets ow.package to first package name when no placeholder matches', () => {
3129+
const config = {
3130+
ow: { package: 'ignored' },
3131+
manifest: {
3132+
packagePlaceholder: '__APP_PACKAGE__',
3133+
full: { packages: { 'custom-pkg': { actions: {} } } }
3134+
}
3135+
}
3136+
const result = utils.replacePackagePlaceHolder(config)
3137+
expect(result.ow.package).toBe('custom-pkg')
3138+
})
3139+
})

0 commit comments

Comments
 (0)