Skip to content

Commit 215f2c0

Browse files
authored
fix: ACNA-3864 - action URLs are output with deploy service URL (#873)
1 parent a1b8bda commit 215f2c0

6 files changed

Lines changed: 286 additions & 6 deletions

File tree

src/commands/app/deploy.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ const BaseCommand = require('../../BaseCommand')
1818
const BuildCommand = require('./build')
1919
const webLib = require('@adobe/aio-lib-web')
2020
const { Flags } = require('@oclif/core')
21-
const { runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata, getFilesCountWithExtension } = require('../../lib/app-helper')
21+
const {
22+
rewriteActionUrlInEntities, runInProcess,
23+
buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata,
24+
getFilesCountWithExtension
25+
} = require('../../lib/app-helper')
2226
const rtLib = require('@adobe/aio-lib-runtime')
2327
const LogForwarding = require('../../lib/log-forwarding')
2428
const { sendAppAssetsDeployedAuditLog, sendAppDeployAuditLog } = require('../../lib/audit-logger')
@@ -128,7 +132,7 @@ class Deploy extends BuildCommand {
128132
const k = keys[i]
129133
const v = setRuntimeApiHostAndAuthHandler(values[i])
130134

131-
await this.deploySingleConfig(k, v, flags, spinner)
135+
await this.deploySingleConfig({ name: k, config: v, originalConfig: values[i], flags, spinner })
132136
if (cliDetails?.accessToken && v.app.hasFrontend && flags['web-assets']) {
133137
const opItems = getFilesCountWithExtension(v.web.distProd)
134138
try {
@@ -167,7 +171,7 @@ class Deploy extends BuildCommand {
167171
this.log(chalk.green(chalk.bold('Successful deployment 🏄')))
168172
}
169173

170-
async deploySingleConfig (name, config, flags, spinner) {
174+
async deploySingleConfig ({ name, config, originalConfig, flags, spinner }) {
171175
const onProgress = !flags.verbose
172176
? info => {
173177
spinner.text = info
@@ -267,7 +271,8 @@ class Deploy extends BuildCommand {
267271

268272
// log deployed resources
269273
if (deployedRuntimeEntities.actions && deployedRuntimeEntities.actions.length > 0) {
270-
await logActions({ entities: deployedRuntimeEntities, log: (...rest) => this.log(chalk.bold(chalk.blue(...rest))) })
274+
const entities = await rewriteActionUrlInEntities({ entities: deployedRuntimeEntities, config: originalConfig })
275+
await logActions({ entities, log: (...rest) => this.log(chalk.bold(chalk.blue(...rest))) })
271276
}
272277

273278
// TODO urls should depend on extension point, exc shell only for exc shell extension point - use a post-app-deploy hook ?

src/lib/app-helper.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,49 @@ function getFilesCountWithExtension (directory) {
504504
return log
505505
}
506506

507+
/**
508+
* Rewrites action URLs in deployed runtime entities using URLs from the manifest configuration.
509+
*
510+
* This function takes deployed runtime entities and updates the URL property of each action
511+
* with the corresponding URL from the runtime manifest configuration. It creates a deep copy
512+
* of the entities to avoid mutating the original object.
513+
*
514+
* @param {object} params - Parameters object
515+
* @param {object} params.entities - The deployed runtime entities object
516+
* @param {object} params.config - The application configuration object containing runtime manifest
517+
* @returns {Promise<object>} A promise that resolves to a deep copy of the entities object with updated action URLs
518+
* @example
519+
* const entities = {
520+
* actions: [
521+
* { name: 'my-action', url: 'old-url' },
522+
* { name: 'another-action', url: 'another-old-url' }
523+
* ]
524+
* }
525+
* const config = {
526+
* actions: { devRemote: false },
527+
* // ... other config properties
528+
* }
529+
*
530+
* const rewrittenEntities = await rewriteActionUrlInEntities({ entities, config })
531+
* // rewrittenEntities.actions will have updated URLs from the manifest
532+
*/
533+
async function rewriteActionUrlInEntities ({ entities, config }) {
534+
const actionUrlsFromManifest = RuntimeLib.utils.getActionUrls(config, config.actions.devRemote)
535+
const rewrittenEntities = structuredClone(entities)
536+
537+
rewrittenEntities.actions = rewrittenEntities.actions?.map(action => {
538+
const retAction = structuredClone(action)
539+
const url = actionUrlsFromManifest[action.name]
540+
if (url) {
541+
retAction.url = url
542+
}
543+
return retAction
544+
})
545+
return rewrittenEntities
546+
}
547+
507548
module.exports = {
549+
rewriteActionUrlInEntities,
508550
getObjectValue,
509551
getObjectProp,
510552
createWebExportFilter,

src/lib/auth-helper.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ const bearerAuthHandler = {
6464
}
6565
}
6666

67-
const setRuntimeApiHostAndAuthHandler = (config) => {
67+
const setRuntimeApiHostAndAuthHandler = (_config) => {
68+
const config = structuredClone(_config)
6869
const aioConfig = (config && 'runtime' in config) ? config : null
6970
if (aioConfig) {
7071
const apiEndpoint = process.env.AIO_DEPLOY_SERVICE_URL ?? defaultDeployServiceUrl

test/commands/app/config/get/log-forwarding.test.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@ test('get log forwarding settings (expect init to be passed a config)', async ()
5252
lf.getServerConfig.mockResolvedValue(serverConfig)
5353

5454
await command.run()
55-
expect(LogForwarding.init).toHaveBeenCalledWith(command.appConfig.aio)
55+
// config should be deploy service settings
56+
const modifiedConfig = structuredClone(command.appConfig.aio)
57+
modifiedConfig.runtime.apihost = 'https://deploy-service.app-builder.adp.adobe.io/runtime'
58+
modifiedConfig.runtime.auth_handler = {
59+
getAuthHeader: expect.any(Function)
60+
}
61+
expect(LogForwarding.init).toHaveBeenCalledWith(modifiedConfig)
5662
})
5763

5864
test('get log forwarding settings (local and server are the same)', async () => {

test/commands/app/deploy.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ beforeEach(() => {
167167
helpers.buildExtensionPointPayloadWoMetadata.mockReset()
168168
helpers.buildExcShellViewExtensionMetadata.mockReset()
169169
helpers.createWebExportFilter.mockReset()
170+
helpers.rewriteActionUrlInEntities.mockReset()
170171
mockLogForwarding.isLocalConfigChanged.mockReset()
171172
mockLogForwarding.getLocalConfigWithSecrets.mockReset()
172173
mockLogForwarding.updateServerConfig.mockReset()
@@ -183,6 +184,11 @@ beforeEach(() => {
183184
'1 HTML page(s)'
184185
]
185186
})
187+
188+
helpers.rewriteActionUrlInEntities.mockImplementation(async ({ entities }) => {
189+
return entities
190+
})
191+
186192
authHelper.setRuntimeApiHostAndAuthHandler.mockImplementation((aioConfig) => aioConfig)
187193
authHelper.getAccessToken.mockImplementation(() => {
188194
return {

test/commands/lib/app-helper.test.js

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,3 +847,223 @@ describe('getFilesCountWithExtension', () => {
847847
])
848848
})
849849
})
850+
851+
describe('rewriteActionUrlInEntities', () => {
852+
const RuntimeLib = require('@adobe/aio-lib-runtime')
853+
854+
beforeEach(() => {
855+
RuntimeLib.utils.getActionUrls = jest.fn()
856+
})
857+
858+
afterEach(() => {
859+
jest.clearAllMocks()
860+
})
861+
862+
test('should rewrite action URLs with URLs from manifest', async () => {
863+
const entities = {
864+
actions: [
865+
{ name: 'action1', url: 'old-url-1' },
866+
{ name: 'action2', url: 'old-url-2' }
867+
]
868+
}
869+
const config = {
870+
actions: { devRemote: false }
871+
}
872+
const mockActionUrls = {
873+
action1: 'https://example.com/api/v1/web/action1',
874+
action2: 'https://example.com/api/v1/web/action2'
875+
}
876+
877+
RuntimeLib.utils.getActionUrls.mockReturnValue(mockActionUrls)
878+
879+
const result = await appHelper.rewriteActionUrlInEntities({ entities, config })
880+
881+
expect(RuntimeLib.utils.getActionUrls).toHaveBeenCalledWith(config, false)
882+
expect(result.actions).toEqual([
883+
{ name: 'action1', url: 'https://example.com/api/v1/web/action1' },
884+
{ name: 'action2', url: 'https://example.com/api/v1/web/action2' }
885+
])
886+
})
887+
888+
test('should use devRemote flag when calling getActionUrls', async () => {
889+
const entities = { actions: [] }
890+
const config = {
891+
actions: { devRemote: true }
892+
}
893+
894+
RuntimeLib.utils.getActionUrls.mockReturnValue({})
895+
896+
await appHelper.rewriteActionUrlInEntities({ entities, config })
897+
898+
expect(RuntimeLib.utils.getActionUrls).toHaveBeenCalledWith(config, true)
899+
})
900+
901+
test('should preserve original action when no URL found in manifest', async () => {
902+
const entities = {
903+
actions: [
904+
{ name: 'action1', url: 'original-url' },
905+
{ name: 'action2', url: 'another-original-url' }
906+
]
907+
}
908+
const config = {
909+
actions: { devRemote: false }
910+
}
911+
const mockActionUrls = {
912+
action1: 'https://example.com/api/v1/web/action1'
913+
// action2 is missing from manifest URLs
914+
}
915+
916+
RuntimeLib.utils.getActionUrls.mockReturnValue(mockActionUrls)
917+
918+
const result = await appHelper.rewriteActionUrlInEntities({ entities, config })
919+
920+
expect(result.actions).toEqual([
921+
{ name: 'action1', url: 'https://example.com/api/v1/web/action1' },
922+
{ name: 'action2', url: 'another-original-url' } // Original URL preserved
923+
])
924+
})
925+
926+
test('should handle entities with no actions array', async () => {
927+
const entities = {}
928+
const config = {
929+
actions: { devRemote: false }
930+
}
931+
932+
RuntimeLib.utils.getActionUrls.mockReturnValue({})
933+
934+
const result = await appHelper.rewriteActionUrlInEntities({ entities, config })
935+
936+
expect(result.actions).toBeUndefined()
937+
expect(RuntimeLib.utils.getActionUrls).toHaveBeenCalledWith(config, false)
938+
})
939+
940+
test('should handle entities with undefined actions', async () => {
941+
const entities = { actions: undefined }
942+
const config = {
943+
actions: { devRemote: false }
944+
}
945+
946+
RuntimeLib.utils.getActionUrls.mockReturnValue({})
947+
948+
const result = await appHelper.rewriteActionUrlInEntities({ entities, config })
949+
950+
expect(result.actions).toBeUndefined()
951+
})
952+
953+
test('should handle empty actions array', async () => {
954+
const entities = { actions: [] }
955+
const config = {
956+
actions: { devRemote: false }
957+
}
958+
959+
RuntimeLib.utils.getActionUrls.mockReturnValue({})
960+
961+
const result = await appHelper.rewriteActionUrlInEntities({ entities, config })
962+
963+
expect(result.actions).toEqual([])
964+
})
965+
966+
test('should create deep copy and not mutate original entities', async () => {
967+
const originalEntities = {
968+
actions: [
969+
{ name: 'action1', url: 'original-url', otherProp: 'value' }
970+
],
971+
otherProp: 'test'
972+
}
973+
const config = {
974+
actions: { devRemote: false }
975+
}
976+
const mockActionUrls = {
977+
action1: 'https://example.com/api/v1/web/action1'
978+
}
979+
980+
RuntimeLib.utils.getActionUrls.mockReturnValue(mockActionUrls)
981+
982+
const result = await appHelper.rewriteActionUrlInEntities({ entities: originalEntities, config })
983+
984+
// Original should be unchanged
985+
expect(originalEntities.actions[0].url).toBe('original-url')
986+
expect(originalEntities.otherProp).toBe('test')
987+
988+
// Result should have the new URL
989+
expect(result.actions[0].url).toBe('https://example.com/api/v1/web/action1')
990+
expect(result.actions[0].otherProp).toBe('value')
991+
expect(result.otherProp).toBe('test')
992+
993+
// Should be different objects
994+
expect(result).not.toBe(originalEntities)
995+
expect(result.actions).not.toBe(originalEntities.actions)
996+
expect(result.actions[0]).not.toBe(originalEntities.actions[0])
997+
})
998+
999+
test('should preserve all other action properties', async () => {
1000+
const entities = {
1001+
actions: [
1002+
{
1003+
name: 'action1',
1004+
url: 'old-url',
1005+
version: '1.0.0',
1006+
annotations: { 'web-export': true },
1007+
parameters: { key: 'value' },
1008+
exec: { kind: 'nodejs:18' }
1009+
}
1010+
]
1011+
}
1012+
const config = {
1013+
actions: { devRemote: false }
1014+
}
1015+
const mockActionUrls = {
1016+
action1: 'https://example.com/api/v1/web/action1'
1017+
}
1018+
1019+
RuntimeLib.utils.getActionUrls.mockReturnValue(mockActionUrls)
1020+
1021+
const result = await appHelper.rewriteActionUrlInEntities({ entities, config })
1022+
1023+
expect(result.actions[0]).toEqual({
1024+
name: 'action1',
1025+
url: 'https://example.com/api/v1/web/action1',
1026+
version: '1.0.0',
1027+
annotations: { 'web-export': true },
1028+
parameters: { key: 'value' },
1029+
exec: { kind: 'nodejs:18' }
1030+
})
1031+
})
1032+
1033+
test('should handle empty action URLs from manifest', async () => {
1034+
const entities = {
1035+
actions: [
1036+
{ name: 'action1', url: 'original-url' }
1037+
]
1038+
}
1039+
const config = {
1040+
actions: { devRemote: false }
1041+
}
1042+
1043+
RuntimeLib.utils.getActionUrls.mockReturnValue({}) // Empty object
1044+
1045+
const result = await appHelper.rewriteActionUrlInEntities({ entities, config })
1046+
1047+
expect(result.actions[0].url).toBe('original-url') // Should preserve original
1048+
})
1049+
1050+
test('should handle null/undefined URL from manifest', async () => {
1051+
const entities = {
1052+
actions: [
1053+
{ name: 'action1', url: 'original-url' }
1054+
]
1055+
}
1056+
const config = {
1057+
actions: { devRemote: false }
1058+
}
1059+
const mockActionUrls = {
1060+
action1: null // Null URL
1061+
}
1062+
1063+
RuntimeLib.utils.getActionUrls.mockReturnValue(mockActionUrls)
1064+
1065+
const result = await appHelper.rewriteActionUrlInEntities({ entities, config })
1066+
1067+
expect(result.actions[0].url).toBe('original-url') // Should preserve original when URL is falsy
1068+
})
1069+
})

0 commit comments

Comments
 (0)