@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.
@@ -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
+ }