@raisenow/tamaro-cli 1.0.5 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +0 -6
- package/.gitignore +2 -2
- package/.nvmrc +1 -1
- package/.prettierignore +1 -0
- package/dist/cli.js +476 -129
- package/dist/webpack.config.js +204 -159
- package/package.json +54 -58
- package/readme.html +55 -0
- package/readme.md +227 -0
- package/src/cli.ts +100 -16
- package/src/commands/build.ts +97 -0
- package/src/commands/deploy-email-config.ts +107 -0
- package/src/commands/deploy.ts +151 -0
- package/src/commands/dev.ts +61 -0
- package/src/commands/list-deployed.ts +102 -0
- package/src/commands/serve.ts +29 -0
- package/src/lib/aws.ts +119 -0
- package/src/lib/helpers.ts +104 -27
- package/src/webpack.config.ts +156 -121
- package/src/assets/rnw-logo.png +0 -0
- package/src/commands/build/index.ts +0 -41
- package/src/commands/deploy/index.ts +0 -25
- package/src/commands/dev/index.ts +0 -32
- package/src/commands/serve/index.ts +0 -12
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {existsSync} from 'fs'
|
|
2
|
+
import stripIndent from 'strip-indent'
|
|
3
|
+
import {
|
|
4
|
+
fail,
|
|
5
|
+
getIfCoreFns,
|
|
6
|
+
getPaths,
|
|
7
|
+
getWidgetUuid,
|
|
8
|
+
logCommand,
|
|
9
|
+
logDataTable,
|
|
10
|
+
logTitle,
|
|
11
|
+
notify,
|
|
12
|
+
} from 'lib/helpers'
|
|
13
|
+
import {assertProfileValid, AwsOptions, runAwsCommandSync} from 'lib/aws'
|
|
14
|
+
|
|
15
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
16
|
+
|
|
17
|
+
export type DeployOptions = AwsOptions & {
|
|
18
|
+
tag: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
22
|
+
|
|
23
|
+
export const AWS_S3_BUCKET = 'tamaro.raisenow.com'
|
|
24
|
+
export const CORE_CONFIG_NAME = 'tamaro-core'
|
|
25
|
+
const AWS_CLOUDFRONT_DISTRIBUTION_ID = 'EHJ1OM458YQ0I'
|
|
26
|
+
export const DEFAULT_TAG = 'latest'
|
|
27
|
+
|
|
28
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
29
|
+
|
|
30
|
+
export const deploy = async (options: DeployOptions): Promise<void> => {
|
|
31
|
+
const {ifCore} = getIfCoreFns()
|
|
32
|
+
const paths = getPaths(ifCore)
|
|
33
|
+
|
|
34
|
+
assertOptionsValid(options)
|
|
35
|
+
assertDistPathExists(paths.appDist)
|
|
36
|
+
|
|
37
|
+
const {tag} = options
|
|
38
|
+
const configName = ifCore(CORE_CONFIG_NAME, getWidgetUuid())
|
|
39
|
+
const deployUrl = `s3://${AWS_S3_BUCKET}/${configName}/${tag}`
|
|
40
|
+
const entryFilename = ifCore('index.js', 'widget.js')
|
|
41
|
+
|
|
42
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
43
|
+
// Sync all files except entrypoint (without deleting old files)
|
|
44
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
45
|
+
|
|
46
|
+
const cmdSyncAll = `
|
|
47
|
+
aws s3 sync ${paths.appDist} ${deployUrl}
|
|
48
|
+
--acl public-read
|
|
49
|
+
--size-only
|
|
50
|
+
--exclude ${paths.appDist}/${entryFilename}
|
|
51
|
+
--cache-control max-age=31536000
|
|
52
|
+
`
|
|
53
|
+
logTitle('Deploying to AWS S3 …')
|
|
54
|
+
logCommand(cmdSyncAll)
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
runAwsCommandSync(cmdSyncAll, {stdout: 'inherit'})
|
|
58
|
+
} catch (error: any) {
|
|
59
|
+
fail(error.stderr)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
63
|
+
// Copy entrypoint
|
|
64
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
65
|
+
|
|
66
|
+
const cmdCpEntry = `
|
|
67
|
+
aws s3 cp ${paths.appDist}/${entryFilename} ${deployUrl}/${entryFilename}
|
|
68
|
+
--acl public-read
|
|
69
|
+
--cache-control max-age=64800
|
|
70
|
+
`
|
|
71
|
+
logCommand(cmdCpEntry)
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
runAwsCommandSync(cmdCpEntry, {stdout: 'inherit'})
|
|
75
|
+
} catch (error: any) {
|
|
76
|
+
fail(error.stderr)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
80
|
+
// Invalidate cloudfront cache
|
|
81
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
82
|
+
|
|
83
|
+
const cmdInvalidateCache = `
|
|
84
|
+
aws cloudfront create-invalidation
|
|
85
|
+
--distribution-id ${AWS_CLOUDFRONT_DISTRIBUTION_ID}
|
|
86
|
+
--paths /${configName}/${tag}/*
|
|
87
|
+
`
|
|
88
|
+
logTitle('Invalidating edge cache …')
|
|
89
|
+
logCommand(cmdInvalidateCache)
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// ignore output
|
|
93
|
+
runAwsCommandSync(cmdInvalidateCache)
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
fail(error.stderr)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
99
|
+
|
|
100
|
+
const demoPage = `https://${AWS_S3_BUCKET}/${configName}/${tag}/index.html`
|
|
101
|
+
const entryPoint = `https://${AWS_S3_BUCKET}/${configName}/${tag}/${entryFilename}`
|
|
102
|
+
|
|
103
|
+
logDataTable(
|
|
104
|
+
{
|
|
105
|
+
'Demo page:': demoPage,
|
|
106
|
+
'Entry point:': entryPoint,
|
|
107
|
+
},
|
|
108
|
+
`Bundle for “${configName}” is deployed with tag “${tag}”`,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
112
|
+
|
|
113
|
+
notify({
|
|
114
|
+
title: 'deploy',
|
|
115
|
+
message: stripIndent(`
|
|
116
|
+
Bundle for “${configName}” is deployed with tag “${tag}”.
|
|
117
|
+
Demo page: ${demoPage}
|
|
118
|
+
`),
|
|
119
|
+
target: demoPage,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
124
|
+
|
|
125
|
+
const assertOptionsValid = (options: DeployOptions) => {
|
|
126
|
+
const {tag, profile} = options
|
|
127
|
+
|
|
128
|
+
assertTagValid(tag)
|
|
129
|
+
assertProfileValid(profile)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
133
|
+
|
|
134
|
+
const assertDistPathExists = (distPath: string) => {
|
|
135
|
+
if (!existsSync(distPath)) {
|
|
136
|
+
fail(`
|
|
137
|
+
Dist folder does not exists.
|
|
138
|
+
Make sure you have built the bundle before trying to deploy it.
|
|
139
|
+
`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
144
|
+
|
|
145
|
+
const assertTagValid = (tag: string) => {
|
|
146
|
+
const regex = /^[a-zA-Z0-9-_]+$/
|
|
147
|
+
|
|
148
|
+
if (!regex.test(tag)) {
|
|
149
|
+
fail(`Flag "--tag" has forbidden format. Allowed format: ${regex}.`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import resolveBin from 'resolve-bin'
|
|
2
|
+
import {
|
|
3
|
+
fail,
|
|
4
|
+
getIfCoreFns,
|
|
5
|
+
logCommand,
|
|
6
|
+
logTitle,
|
|
7
|
+
resolveOwn,
|
|
8
|
+
runCommandSync,
|
|
9
|
+
} from 'lib/helpers'
|
|
10
|
+
|
|
11
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
12
|
+
|
|
13
|
+
export type DevOptions = {
|
|
14
|
+
localCore: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
18
|
+
|
|
19
|
+
export const dev = async (options: DevOptions): Promise<void> => {
|
|
20
|
+
assertOptionsValid(options)
|
|
21
|
+
|
|
22
|
+
const flags = prepareFlags(options)
|
|
23
|
+
const {ifCore} = getIfCoreFns()
|
|
24
|
+
const title = ifCore(
|
|
25
|
+
'Running dev web-server for Tamaro Core …',
|
|
26
|
+
'Running dev web-server for customer configuration …',
|
|
27
|
+
)
|
|
28
|
+
const wpBin = resolveBin.sync('webpack')
|
|
29
|
+
const wpConfig = resolveOwn('dist/webpack.config.js')
|
|
30
|
+
const cmd = `
|
|
31
|
+
${wpBin} serve
|
|
32
|
+
--config ${wpConfig}
|
|
33
|
+
${flags}
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
logTitle(title)
|
|
37
|
+
logCommand(cmd)
|
|
38
|
+
runCommandSync(cmd, {stdio: 'inherit'})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
42
|
+
|
|
43
|
+
const assertOptionsValid = (options: DevOptions) => {
|
|
44
|
+
const {localCore} = options
|
|
45
|
+
const {ifCore} = getIfCoreFns()
|
|
46
|
+
|
|
47
|
+
if (ifCore() && localCore) {
|
|
48
|
+
fail('Flag "--local-core" is redundant if running in Tamaro Core context.')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
53
|
+
|
|
54
|
+
const prepareFlags = (options: DevOptions): string => {
|
|
55
|
+
const {localCore} = options
|
|
56
|
+
let flags: string[] = []
|
|
57
|
+
|
|
58
|
+
flags = localCore ? [...flags, '--env localCore'] : flags
|
|
59
|
+
|
|
60
|
+
return flags.join(' ')
|
|
61
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fail,
|
|
3
|
+
getIfCoreFns,
|
|
4
|
+
getWidgetUuid,
|
|
5
|
+
logCommand,
|
|
6
|
+
logTable,
|
|
7
|
+
logTitle,
|
|
8
|
+
} from 'lib/helpers'
|
|
9
|
+
import {assertProfileValid, AwsOptions, runAwsCommandSync} from 'lib/aws'
|
|
10
|
+
import {AWS_S3_BUCKET, CORE_CONFIG_NAME} from 'commands/deploy'
|
|
11
|
+
|
|
12
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
13
|
+
|
|
14
|
+
export type ListDeployedOptions = AwsOptions & {
|
|
15
|
+
config?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
19
|
+
|
|
20
|
+
export const listDeployed = async (
|
|
21
|
+
options: ListDeployedOptions,
|
|
22
|
+
): Promise<void> => {
|
|
23
|
+
assertOptionsValid(options)
|
|
24
|
+
|
|
25
|
+
const {config} = options
|
|
26
|
+
const {ifCore} = getIfCoreFns()
|
|
27
|
+
const configName = config ?? ifCore(CORE_CONFIG_NAME, getWidgetUuid())
|
|
28
|
+
const title =
|
|
29
|
+
!config && ifCore()
|
|
30
|
+
? `Listing deployments of Tamaro Core …`
|
|
31
|
+
: `Listing deployments of “${configName}” customer configuration …`
|
|
32
|
+
|
|
33
|
+
const deployUrl = `s3://${AWS_S3_BUCKET}/${configName}/`
|
|
34
|
+
const cmd = `aws s3 ls ${deployUrl}`
|
|
35
|
+
let out = ''
|
|
36
|
+
|
|
37
|
+
logTitle(title)
|
|
38
|
+
logCommand(cmd)
|
|
39
|
+
|
|
40
|
+
// If customer configuration folder does not exist on AWS S3,
|
|
41
|
+
// command fails with blank stderr, so we need to handle this case.
|
|
42
|
+
try {
|
|
43
|
+
const result = runAwsCommandSync(cmd)
|
|
44
|
+
out = result.stdout
|
|
45
|
+
} catch (error: any) {
|
|
46
|
+
out = error.stdout
|
|
47
|
+
|
|
48
|
+
if (error.stderr) {
|
|
49
|
+
fail(error.stderr)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lines = out.split('\n')
|
|
54
|
+
const tags = parseTags(lines)
|
|
55
|
+
let text = ''
|
|
56
|
+
|
|
57
|
+
if (tags.length === 0) {
|
|
58
|
+
text = 'No deployments found.'
|
|
59
|
+
} else {
|
|
60
|
+
text = tags
|
|
61
|
+
.map((tag, idx) => {
|
|
62
|
+
// prettier-ignore
|
|
63
|
+
return `${idx + 1}. https://${AWS_S3_BUCKET}/${configName}/${tag}/index.html`
|
|
64
|
+
})
|
|
65
|
+
.join('\n')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
logTable(text)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
72
|
+
|
|
73
|
+
const assertOptionsValid = (options: ListDeployedOptions) => {
|
|
74
|
+
const {config, profile} = options
|
|
75
|
+
|
|
76
|
+
assertConfigValid(config)
|
|
77
|
+
assertProfileValid(profile)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
81
|
+
|
|
82
|
+
const assertConfigValid = (config: string | undefined) => {
|
|
83
|
+
if (!config) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const regex = /^[a-zA-Z0-9-_]+$/
|
|
88
|
+
|
|
89
|
+
if (!regex.test(config)) {
|
|
90
|
+
fail(`Flag "--config" has forbidden format. Allowed format: ${regex}.`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
95
|
+
|
|
96
|
+
const parseTags = (lines: string[]): string[] => {
|
|
97
|
+
const regex = /^\s*PRE\s*/
|
|
98
|
+
|
|
99
|
+
return lines
|
|
100
|
+
.filter((line) => regex.test(line))
|
|
101
|
+
.map((line) => line.replace(regex, '').replace(/\/$/, ''))
|
|
102
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {getPortPromise} from 'portfinder'
|
|
2
|
+
import {
|
|
3
|
+
getIfCoreFns,
|
|
4
|
+
getPaths,
|
|
5
|
+
logCommand,
|
|
6
|
+
logTitle,
|
|
7
|
+
runCommandSync,
|
|
8
|
+
} from 'lib/helpers'
|
|
9
|
+
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
|
|
12
|
+
const DEFAULT_PORT = 1234
|
|
13
|
+
|
|
14
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
15
|
+
|
|
16
|
+
export const serve = async (): Promise<void> => {
|
|
17
|
+
const {ifCore} = getIfCoreFns()
|
|
18
|
+
const paths = getPaths(ifCore)
|
|
19
|
+
const port = await getPortPromise({port: DEFAULT_PORT})
|
|
20
|
+
const cmd = `
|
|
21
|
+
npx -y http-server ${paths.appDist}
|
|
22
|
+
--cors
|
|
23
|
+
--port ${port}
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
logTitle(`Running web-server for pre-built bundle …`)
|
|
27
|
+
logCommand(cmd)
|
|
28
|
+
runCommandSync(cmd, {stdio: 'inherit'})
|
|
29
|
+
}
|
package/src/lib/aws.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {commandSync, ExecaSyncReturnValue, SyncOptions} from 'execa'
|
|
2
|
+
import stripIndent from 'strip-indent'
|
|
3
|
+
import {fail, prepareCommand} from './helpers'
|
|
4
|
+
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
|
|
7
|
+
export type AwsOptions = {
|
|
8
|
+
profile: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_AWS_PROFILE = 'payments-prod-cs-deployer'
|
|
14
|
+
|
|
15
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
16
|
+
|
|
17
|
+
export const runAwsCommandSync = (
|
|
18
|
+
command: string,
|
|
19
|
+
options?: SyncOptions,
|
|
20
|
+
): ExecaSyncReturnValue => {
|
|
21
|
+
authenticate()
|
|
22
|
+
|
|
23
|
+
return commandSync(prepareCommand(command), options)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
27
|
+
|
|
28
|
+
export const authenticate = () => {
|
|
29
|
+
// If "AWS_ACCESS_KEY_ID" and "AWS_SECRET_ACCESS_KEY" environment variables are set,
|
|
30
|
+
// then don't try to authenticate the user.
|
|
31
|
+
// This may be useful for using this CLI in automated systems like pipelines.
|
|
32
|
+
if (isSetEnv()) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
checkIdentity()
|
|
38
|
+
} catch (error) {
|
|
39
|
+
login()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
44
|
+
|
|
45
|
+
const isSetEnv = () => {
|
|
46
|
+
return !!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
50
|
+
|
|
51
|
+
export const assertProfileValid = (profile: string) => {
|
|
52
|
+
// If "AWS_ACCESS_KEY_ID" and "AWS_SECRET_ACCESS_KEY" environment variables are set,
|
|
53
|
+
// then ignore "--profile" flag.
|
|
54
|
+
if (isSetEnv()) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const profiles = getAvailableProfiles()
|
|
59
|
+
|
|
60
|
+
if (!profiles.includes(profile)) {
|
|
61
|
+
console.log(`Using AWS profile: “${profile}”`)
|
|
62
|
+
|
|
63
|
+
let message
|
|
64
|
+
|
|
65
|
+
if (profiles.length === 0) {
|
|
66
|
+
message = stripIndent(`
|
|
67
|
+
No AWS profiles found.
|
|
68
|
+
`)
|
|
69
|
+
} else {
|
|
70
|
+
message = stripIndent(`
|
|
71
|
+
AWS profile “${profile}” has not been found.
|
|
72
|
+
Available profiles are: ${profiles.map((v) => `“${v}”`).join(', ')}.
|
|
73
|
+
`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
message += stripIndent(`
|
|
77
|
+
Run “aws configure sso” to set up SSO-enabled profile.
|
|
78
|
+
Check the wiki for more information:
|
|
79
|
+
https://raisenow.atlassian.net/wiki/x/lIrWvg
|
|
80
|
+
`)
|
|
81
|
+
|
|
82
|
+
fail(message)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Set it here, so we don't need to explicitly pass "--profile" flag
|
|
86
|
+
// to all subsequent "aws" commands.
|
|
87
|
+
// If "AWS_PROFILE" were set before, it will be overriden by "--profile" flag,
|
|
88
|
+
// which has default value, so may be omitted.
|
|
89
|
+
process.env.AWS_PROFILE = profile
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
93
|
+
|
|
94
|
+
const getAvailableProfiles = (): string[] => {
|
|
95
|
+
const {stdout} = commandSync('aws configure list-profiles')
|
|
96
|
+
|
|
97
|
+
return stdout.split('\n')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
101
|
+
|
|
102
|
+
// Returns caller identity data (output is ignored here).
|
|
103
|
+
// Fails in case of expired sso session.
|
|
104
|
+
const checkIdentity = () => {
|
|
105
|
+
commandSync('aws sts get-caller-identity')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
109
|
+
|
|
110
|
+
// Opens sso authentication page in the browser.
|
|
111
|
+
// Fails if user canceled authentication process.
|
|
112
|
+
const login = () => {
|
|
113
|
+
try {
|
|
114
|
+
commandSync('aws sso login', {stdio: 'inherit'})
|
|
115
|
+
console.log('')
|
|
116
|
+
} catch (error) {
|
|
117
|
+
fail('Login failed')
|
|
118
|
+
}
|
|
119
|
+
}
|