-
Notifications
You must be signed in to change notification settings - Fork 35
/
deploy.js
374 lines (338 loc) · 14.5 KB
/
deploy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
/*
Copyright 2019 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
const ora = require('ora')
const chalk = require('chalk')
const open = require('open')
const BaseCommand = require('../../BaseCommand')
const BuildCommand = require('./build')
const webLib = require('@adobe/aio-lib-web')
const { Flags } = require('@oclif/core')
const { createWebExportFilter, runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata } = require('../../lib/app-helper')
const rtLib = require('@adobe/aio-lib-runtime')
const LogForwarding = require('../../lib/log-forwarding')
const PRE_DEPLOY_EVENT_REG = 'pre-deploy-event-reg'
const POST_DEPLOY_EVENT_REG = 'post-deploy-event-reg'
class Deploy extends BuildCommand {
async run () {
// cli input
const { flags } = await this.parse(Deploy)
// flags
flags['web-assets'] = flags['web-assets'] && !flags.action
flags.publish = flags.publish && !flags.action
const deployConfigs = await this.getAppExtConfigs(flags)
const keys = Object.keys(deployConfigs)
const values = Object.values(deployConfigs)
const isStandaloneApp = (keys.length === 1 && keys[0] === 'application')
// if there are no extensions, then set publish to false
flags.publish = flags.publish && !isStandaloneApp
if (
(!flags.publish && !flags['web-assets'] && !flags.actions)
) {
this.error('Nothing to be done 🚫')
}
const spinner = ora()
try {
const aioConfig = (await 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?.project?.workspace &&
flags['log-forwarding-update'] &&
flags.actions) {
spinner.start('Updating log forwarding configuration')
try {
const lf = await LogForwarding.init(aioConfig)
if (lf.isLocalConfigChanged()) {
const lfConfig = lf.getLocalConfigWithSecrets()
if (lfConfig.isDefined()) {
await lf.updateServerConfig(lfConfig)
spinner.succeed(chalk.green(`Log forwarding is set to '${lfConfig.getDestination()}'`))
} else {
if (flags.verbose) {
spinner.info(chalk.dim('Log forwarding is not updated: no configuration is provided'))
}
}
} else {
spinner.info(chalk.dim('Log forwarding is not updated: configuration not changed since last update'))
}
} catch (error) {
spinner.fail(chalk.red('Log forwarding is not updated.'))
throw error
}
}
// 2. If workspace is prod and has extensions, check if the app is published
if (!isStandaloneApp && aioConfig?.project?.workspace?.name === 'Production') {
const extension = await this.getApplicationExtension(aioConfig)
if (extension && extension.status === 'PUBLISHED') {
flags.publish = false // if the app is production and published, then skip publish later on
// if the app is published and no force-deploy flag is set, then skip deployment
if (!flags['force-deploy']) {
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
for (let i = 0; i < keys.length; ++i) {
const k = keys[i]
const v = values[i]
await this.deploySingleConfig(k, v, flags, spinner)
}
// 4. deploy extension manifest
if (flags.publish) {
const payload = await this.publishExtensionPoints(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)}'`)))
} else {
this.log('skipping publish phase...')
}
} catch (error) {
spinner.stop()
// delegate to top handler
throw error
}
// final message
// TODO better output depending on which ext points/app and flags
this.log(chalk.green(chalk.bold('Successful deployment 🏄')))
}
async deploySingleConfig (name, config, flags, spinner) {
const onProgress = !flags.verbose
? info => {
spinner.text = info
}
: info => {
spinner.info(chalk.dim(`${info}`))
spinner.start()
}
// build phase
if (flags.build) {
await this.buildOneExt(name, config, flags, spinner)
}
// deploy phase
let deployedRuntimeEntities = {}
let deployedFrontendUrl = ''
const filterActions = flags.action
try {
await runInProcess(config.hooks['pre-app-deploy'], config)
const hookResults = await this.config.runHook(PRE_DEPLOY_EVENT_REG, { appConfig: config, force: flags['force-events'] })
if (hookResults?.failures?.length > 0) {
// output should be "Error : <plugin-name> : <error-message>\n" for each failure
this.error(hookResults.failures.map(f => `${f.plugin.name} : ${f.error.message}`).join('\nError: '), { exit: 1 })
}
} catch (err) {
this.error(err)
}
if (flags.actions) {
if (config.app.hasBackend) {
let filterEntities
if (filterActions) {
filterEntities = { actions: filterActions }
}
const message = `Deploying actions for '${name}'`
spinner.start(message)
try {
const script = await runInProcess(config.hooks['deploy-actions'], config)
if (!script) {
const hookResults = await this.config.runHook('deploy-actions', {
appConfig: config,
filterEntities: filterActions || [],
isLocalDev: false
})
if (hookResults?.failures?.length > 0) {
// output should be "Error : <plugin-name> : <error-message>\n" for each failure
this.error(hookResults.failures.map(f => `${f.plugin.name} : ${f.error.message}`).join('\nError: '), { exit: 1 })
}
deployedRuntimeEntities = await rtLib.deployActions(config, { filterEntities }, onProgress)
}
if (deployedRuntimeEntities.actions && deployedRuntimeEntities.actions.length > 0) {
spinner.succeed(chalk.green(`Deployed ${deployedRuntimeEntities.actions.length} action(s) for '${name}'`))
} else {
if (script) {
spinner.fail(chalk.green(`deploy-actions skipped by hook '${name}'`))
} else {
spinner.fail(chalk.green(`No actions deployed for '${name}'`))
}
}
} catch (err) {
spinner.fail(chalk.green(message))
throw err
}
} else {
this.log(`no backend, skipping action deploy '${name}'`)
}
}
if (flags['web-assets']) {
if (config.app.hasFrontend) {
const message = `Deploying web assets for '${name}'`
spinner.start(message)
try {
const script = await runInProcess(config.hooks['deploy-static'], config)
if (script) {
spinner.fail(chalk.green(`deploy-static skipped by hook '${name}'`))
} else {
deployedFrontendUrl = await webLib.deployWeb(config, onProgress)
spinner.succeed(chalk.green(message))
}
} catch (err) {
spinner.fail(chalk.green(message))
throw err
}
} else {
this.log(`no frontend, skipping frontend deploy '${name}'`)
}
}
// log deployed resources
if (deployedRuntimeEntities.actions && deployedRuntimeEntities.actions.length > 0) {
this.log(chalk.blue(chalk.bold('Your deployed actions:')))
const web = deployedRuntimeEntities.actions.filter(createWebExportFilter(true))
const nonWeb = deployedRuntimeEntities.actions.filter(createWebExportFilter(false))
if (web.length > 0) {
this.log('web actions:')
web.forEach(a => {
this.log(chalk.blue(chalk.bold(` -> ${a.url || a.name} `)))
})
}
if (nonWeb.length > 0) {
this.log('non-web actions:')
nonWeb.forEach(a => {
this.log(chalk.blue(chalk.bold(` -> ${a.url || a.name} `)))
})
}
}
// TODO urls should depend on extension point, exc shell only for exc shell extension point - use a post-app-deploy hook ?
if (deployedFrontendUrl) {
this.log(chalk.blue(chalk.bold(`To view your deployed application:\n -> ${deployedFrontendUrl}`)))
const launchUrl = this.getLaunchUrlPrefix() + deployedFrontendUrl
if (flags.open) {
this.log(chalk.blue(chalk.bold(`Opening your deployed application in the Experience Cloud shell:\n -> ${launchUrl}`)))
open(launchUrl)
} else {
this.log(chalk.blue(chalk.bold(`To view your deployed application in the Experience Cloud shell:\n -> ${launchUrl}`)))
}
}
try {
await runInProcess(config.hooks['post-app-deploy'], config)
const hookResults = await this.config.runHook(POST_DEPLOY_EVENT_REG, { appConfig: config, force: flags['force-events'] })
if (hookResults?.failures?.length > 0) {
// output should be "Error : <plugin-name> : <error-message>\n" for each failure
this.error(hookResults.failures.map(f => `${f.plugin.name} : ${f.error.message}`).join('\nError: '), { exit: 1 })
}
} catch (err) {
this.error(err)
}
}
async publishExtensionPoints (deployConfigs, aioConfig, force) {
const libConsoleCLI = await this.getLibConsoleCLI()
const payload = buildExtensionPointPayloadWoMetadata(deployConfigs)
// build metadata
if (payload.endpoints['dx/excshell/1'] && payload.endpoints['dx/excshell/1'].view) {
const metadata = await buildExcShellViewExtensionMetadata(libConsoleCLI, aioConfig)
payload.endpoints['dx/excshell/1'].view[0].metadata = metadata
}
let newPayload
if (force) {
// publish and overwrite any previous published endpoints (delete them)
newPayload = await libConsoleCLI.updateExtensionPoints(aioConfig.project.org, aioConfig.project, aioConfig.project.workspace, payload)
return newPayload
}
// publish without overwritting, meaning partial publish (for a subset of ext points) are supported
newPayload = await libConsoleCLI.updateExtensionPointsWithoutOverwrites(aioConfig.project.org, aioConfig.project, aioConfig.project.workspace, payload)
return newPayload
}
async getApplicationExtension (aioConfig) {
const libConsoleCLI = await this.getLibConsoleCLI()
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
This will always force a rebuild unless --no-force-build is set.
`
Deploy.flags = {
...BaseCommand.flags,
actions: Flags.boolean({
description: '[default: true] Deploy actions if any',
default: true,
allowNo: true,
exclusive: ['action'] // should be action exclusive --no-action but see https://github.com/oclif/oclif/issues/600
}),
action: Flags.string({
description: 'Deploy only a specific action, the flags can be specified multiple times, this will set --no-publish',
char: 'a',
exclusive: ['extension', { name: 'publish', when: async (flags) => flags.publish === true }],
multiple: true
}),
'web-assets': Flags.boolean({
description: '[default: true] Deploy web-assets if any',
default: true,
allowNo: true
}),
build: Flags.boolean({
description: '[default: true] Run the build phase before deployment',
default: true,
allowNo: true
}),
'force-build': Flags.boolean({
description: '[default: true] Force a build even if one already exists',
exclusive: ['no-build'], // no-build
default: true,
allowNo: true
}),
'content-hash': Flags.boolean({
description: '[default: true] Enable content hashing in browser code',
default: true,
allowNo: true
}),
open: Flags.boolean({
description: 'Open the default web browser after a successful deploy, only valid if your app has a front-end',
default: false
}),
extension: Flags.string({
description: 'Deploy only a specific extension, the flags can be specified multiple times',
exclusive: ['action'],
char: 'e',
multiple: true
}),
publish: Flags.boolean({
description: '[default: true] Publish extension(s) to Exchange',
allowNo: true,
default: true
}),
'force-deploy': Flags.boolean({
description: '[default: false] Force deploy changes, regardless of production Workspace being published in Exchange.',
default: false,
exclusive: ['publish', 'force-publish'] // publish is skipped if force-deploy is set and prod app is published
}),
'force-publish': Flags.boolean({
description: '[default: false] Force publish extension(s) to Exchange, delete previously published extension points',
default: false,
exclusive: ['action', 'publish'] // no-publish is excluded
}),
'force-events': Flags.boolean({
description: '[default: false] Force event registrations and delete any registrations not part of the config file',
default: false,
allowNo: true,
exclusive: ['action', 'publish'] // no-publish is excluded
}),
'web-optimize': Flags.boolean({
description: '[default: false] Enable optimization (minification) of web js/css/html',
default: false
}),
'log-forwarding-update': Flags.boolean({
description: '[default: true] Update log forwarding configuration on server',
default: true,
allowNo: true
})
}
Deploy.args = {}
module.exports = Deploy