@leverege/build-tools 2.50.9 → 2.50.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.50.9",
3
+ "version": "2.50.10",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -54,7 +54,7 @@
54
54
  "author": "Leverege Devs",
55
55
  "license": "SEE LICENSE IN LICENSE.md",
56
56
  "dependencies": {
57
- "@google-cloud/artifact-registry": "^3.3.0",
57
+ "@google-cloud/artifact-registry": "^3.4.0",
58
58
  "ansi-colors": "^4.1.3",
59
59
  "chalk": "^5.3.0",
60
60
  "command-line-args": "^5.2.1",
@@ -62,22 +62,22 @@
62
62
  "deepmerge": "^4.3.1",
63
63
  "enquirer": "^2.4.1",
64
64
  "execa": "^8.0.1",
65
- "glob": "^10.3.12",
65
+ "glob": "^10.3.16",
66
66
  "handlebars": "^4.7.8",
67
- "inquirer": "^9.2.20",
67
+ "inquirer": "^9.2.22",
68
68
  "js-yaml": "^4.1.0",
69
69
  "ms": "^2.1.3",
70
- "npm-registry-fetch": "^17.0.0",
70
+ "npm-registry-fetch": "^17.0.1",
71
71
  "package-up": "^5.0.0",
72
72
  "parse-gitignore": "^2.0.0",
73
73
  "read-pkg": "^9.0.1",
74
74
  "readline-sync": "^1.4.10",
75
- "semver": "^7.6.0",
75
+ "semver": "^7.6.2",
76
76
  "simple-git": "^3.24.0",
77
- "zx": "^8.0.2"
77
+ "zx": "^8.1.0"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@leverege/eslint-config-leverege": "^4.2.0",
81
- "npm": "^10.7.0"
81
+ "npm": "^10.8.0"
82
82
  }
83
83
  }
@@ -0,0 +1,20 @@
1
+ registry:
2
+ - root: us-docker.pkg.dev
3
+ - repositories:
4
+ - name: stack
5
+ charts:
6
+ - api-server
7
+ - authz-server
8
+ - emailer
9
+ - message-processor
10
+ - name: leverege
11
+ charts:
12
+ - pubsub-pulse
13
+ - pusher
14
+ - overdose
15
+ - name: cox-health
16
+ charts:
17
+ - actions-server
18
+ - analytics-server
19
+ - centrak-healthz
20
+ - centrak-ingestor
package/src/Utils.mjs CHANGED
@@ -349,6 +349,10 @@ export const parseHelmChart = async ( helmroot = './helm' ) => {
349
349
  const chartVersion = chartYaml.version
350
350
  const chartPackage = `${chartName}-${chartVersion}.tgz`
351
351
  const valuesYaml = YAML.load( readFileSync( `${helmroot}/values.yaml`, 'utf8' ) )
352
+ debug( valuesYaml, '<== helm/values.yaml' )
353
+ if ( !valuesYaml.image ) {
354
+ errorExit( 'Error: legacy helm chart detected - must upgrade to latest ignition template to proceed' )
355
+ }
352
356
  const imageRegistry = valuesYaml.image?.registry
353
357
  const registryComponents = imageRegistry.split( '/' )
354
358
  /* eslint-enable security/detect-non-literal-fs-filename */
package/src/bash-funcs CHANGED
@@ -169,13 +169,20 @@ function ifPluggedIn() {
169
169
  local INVOKED_AS=`basename $0`
170
170
  local PLUGIN=
171
171
  case "$INVOKED_AS" in
172
- "helmup"|"helmwhat" )
172
+ "helmup"|"helmwhat"|"helmcycle" )
173
173
  PLUGIN="$1/helmup.plugin"
174
174
  ;;
175
175
  "helmdn" )
176
176
  PLUGIN="$1/helmdn.plugin"
177
177
  ;;
178
178
  *)
179
+ cat<<PLUGIN_INVOKED_AS_ERROR
180
+
181
+
182
+ $RED_ERROR bash-funcs ifPluggedIn: unknown INVOKED_AS=[`color r $INVOKED_AS`]
183
+
184
+ PLUGIN_INVOKED_AS_ERROR
185
+ exit 1
179
186
  ;;
180
187
  esac
181
188
 
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * chart-to-registry will...
4
+ */
5
+ import fs from 'node:fs'
6
+
7
+ import chalk from 'chalk'
8
+ import commandLineArgs from 'command-line-args'
9
+ import commandLineUsage from 'command-line-usage'
10
+ import { lt as semverLt } from 'semver'
11
+ import YAML from 'js-yaml'
12
+
13
+ import {
14
+ condir,
15
+ debug,
16
+ errorExit,
17
+ log,
18
+ warning,
19
+ getGitRootDirectory,
20
+ parsePackageJson,
21
+ parseHelmChart,
22
+ shellCmd } from './Utils.mjs'
23
+
24
+ const commandLineOptions = [ // Use commandLineOptions to tie into the Usage statements
25
+ /* eslint-disable max-len */
26
+ {
27
+ name : 'location',
28
+ type : String,
29
+ description : '{green the location of the artifact registry the chart will be pushed to (default us-docker.pkg.dev)}',
30
+ },
31
+ {
32
+ name : 'project',
33
+ type : String,
34
+ description : '{green the name of the google project containing the npmrc and slack config secrets (default leverege-registry)}',
35
+ },
36
+ {
37
+ name : 'repository',
38
+ type : String,
39
+ description : '{green the target repository to receive the pushed chart}',
40
+ },
41
+ {
42
+ name : 'dry-run',
43
+ type : Boolean,
44
+ description : '{yellow perform everything except the actual chart push}',
45
+ },
46
+ {
47
+ name : 'help',
48
+ type : Boolean,
49
+ description : '{green display this help screen}',
50
+ },
51
+ /* eslint-enable max-len */
52
+ ]
53
+
54
+ const sections = [
55
+ {
56
+ header : 'Leverege Helm Chart Compass (for helmup)',
57
+ content : `{green This tool helps helmup navigate the helm charts stored in the
58
+ artifact-registries.}`
59
+ },
60
+ { header : 'Options',
61
+ optionList : commandLineOptions,
62
+ },
63
+ ]
64
+
65
+ const args = commandLineArgs( commandLineOptions, { camelCase : true, partial : true } )
66
+ const usage = commandLineUsage( sections )
67
+
68
+ if ( args.help ) {
69
+ log( usage )
70
+ process.exit( 0 )
71
+ }
72
+
73
+ /* eslint-disable no-underscore-dangle */
74
+ if ( args._unknown ) {
75
+ log( usage )
76
+ log( `\nUnrecognized argument [${chalk.bold.red( args._unknown )}]\n` )
77
+ process.exit( 1 )
78
+ }
79
+ /* eslint-enable no-underscore-dangle */
80
+
81
+ const minNodejsVersion = '18.0.0'
82
+ if ( semverLt( process.version, minNodejsVersion ) ) {
83
+ errorExit( `\n***ERROR: must be running at least node ${minNodejsVersion}\n` )
84
+ }
85
+
86
+ // First of all, fail if we are not in a git repository
87
+ let gitRoot
88
+ try {
89
+ gitRoot = await getGitRootDirectory()
90
+ } catch ( error ) {
91
+ errorExit( chalk.red.bold( error ), { errorCode : 5 } )
92
+ }
93
+
94
+ const chartCompass = YAML.load( fs.readFileSync( './registry-compass.yaml', 'utf8' ) )
95
+
96
+ condir( { chartCompass }, '<==Navigation' )
@@ -12,5 +12,9 @@ spec:
12
12
  pgbouncer:
13
13
  poolMode: session
14
14
  parameters:
15
+ # https://www.pgbouncer.org/config.html#generic-settings
15
16
  max_client_conn: "500"
16
17
  default_pool_size: "10"
18
+ # https://www.pgbouncer.org/config.html#log-settings
19
+ log_connections: "0"
20
+ log_disconnections: "0"
@@ -13,24 +13,35 @@ spec:
13
13
  imageName: us-docker.pkg.dev/leverege-registry/system/images/leverege-pgsql:16.2-cnpg-tsdb2-beta.4
14
14
  bootstrap:
15
15
  initdb:
16
- database: models
16
+ # database: models
17
17
  postInitSQL:
18
+ # - CREATE DATABASE app; # operator expects this
19
+ # - CREATE ROLE app; # and this to exist on restore
20
+ # create the stack databases
18
21
  - CREATE DATABASE authz;
19
22
  - CREATE DATABASE fota;
20
23
  - CREATE DATABASE geotile;
21
24
  - CREATE DATABASE models;
22
25
  - CREATE DATABASE scheduler;
23
- owner: imagine
24
- secret:
25
- name: cnpg-db-psql-stack-imagine-pw # imagine db pw
26
+ # owner: imagine
27
+ # secret:
28
+ # name: cnpg-db-psql-stack-imagine-pw # imagine db pw
26
29
 
27
30
  enableSuperuserAccess: true
28
31
  superuserSecret:
29
32
  name: cnpg-db-psql-stack-postgres-pw # postgres db pw
30
33
 
31
34
  affinity:
32
- # enablePodAntiAffinity: true
33
- # topologyKey: kubernetes.io/hostname # default value
35
+ enablePodAntiAffinity: true
36
+ topologyKey: kubernetes.io/hostname # default value
37
+ nodeAffinity:
38
+ requiredDuringSchedulingIgnoredDuringExecution:
39
+ nodeSelectorTerms:
40
+ - matchExpressions:
41
+ - key: target-env
42
+ operator: In
43
+ values:
44
+ - database
34
45
  tolerations:
35
46
  - key: "database"
36
47
  operator: "Equal"
@@ -83,6 +94,7 @@ spec:
83
94
  cnpgCluster: cnpg-db-psql-stack
84
95
  wal:
85
96
  compression: gzip
97
+ maxParallel: 4
86
98
  ---
87
99
  apiVersion: postgresql.cnpg.io/v1
88
100
  kind: ScheduledBackup
@@ -91,8 +103,9 @@ metadata:
91
103
  namespace: cnpg-operands
92
104
  spec:
93
105
  # https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format
94
- schedule: "0 0 * * * *"
106
+ schedule: "0 0 */12 * * *"
95
107
  cluster:
96
- method: volumeSnapshot
108
+ name: cnpg-db-psql-stack
109
+ method: barmanObjectStore # volumeSnapshot
97
110
  backupOwnerReference: self
98
111
  immediate: true
@@ -12,5 +12,9 @@ spec:
12
12
  pgbouncer:
13
13
  poolMode: session
14
14
  parameters:
15
+ # https://www.pgbouncer.org/config.html#generic-settings
15
16
  max_client_conn: "500"
16
17
  default_pool_size: "10"
18
+ # https://www.pgbouncer.org/config.html#log-settings
19
+ log_connections: "0"
20
+ log_disconnections: "0"
@@ -19,6 +19,8 @@ spec:
19
19
  postInitTemplateSQL:
20
20
  - CREATE EXTENSION timescaledb;
21
21
  - CREATE EXTENSION jsquery;
22
+ - CREATE DATABASE app; # for the operator restores
23
+ - CREATE ROLE app; # same
22
24
  database: imagine
23
25
  owner: imagine
24
26
  secret:
@@ -94,6 +96,7 @@ spec:
94
96
  cnpgCluster: cnpg-db-tsdb-basic
95
97
  wal:
96
98
  compression: gzip
99
+ maxParallel: 4
97
100
  ---
98
101
  apiVersion: postgresql.cnpg.io/v1
99
102
  kind: ScheduledBackup
@@ -12,5 +12,9 @@ spec:
12
12
  pgbouncer:
13
13
  poolMode: session
14
14
  parameters:
15
+ # https://www.pgbouncer.org/config.html#generic-settings
15
16
  max_client_conn: "500"
16
17
  default_pool_size: "10"
18
+ # https://www.pgbouncer.org/config.html#log-settings
19
+ log_connections: "0"
20
+ log_disconnections: "0"
@@ -19,6 +19,8 @@ spec:
19
19
  postInitTemplateSQL:
20
20
  - CREATE EXTENSION timescaledb;
21
21
  - CREATE EXTENSION jsquery;
22
+ - CREATE DATABASE app; # for the operator restores
23
+ - CREATE ROLE app; # same
22
24
  database: imagine
23
25
  owner: imagine
24
26
  secret:
@@ -93,6 +95,7 @@ spec:
93
95
  cnpgCluster: cnpg-db-tsdb-dense
94
96
  wal:
95
97
  compression: gzip
98
+ maxParallel: 4
96
99
  ---
97
100
  apiVersion: postgresql.cnpg.io/v1
98
101
  kind: ScheduledBackup
@@ -12,5 +12,9 @@ spec:
12
12
  pgbouncer:
13
13
  poolMode: session
14
14
  parameters:
15
+ # https://www.pgbouncer.org/config.html#generic-settings
15
16
  max_client_conn: "500"
16
17
  default_pool_size: "10"
18
+ # https://www.pgbouncer.org/config.html#log-settings
19
+ log_connections: "0"
20
+ log_disconnections: "0"
package/src/helmup.sh CHANGED
@@ -794,7 +794,13 @@ doHelmup() {
794
794
  [ ! -z "$FROM_VERSIONS" ] && from="`color y \"<= from Versions.json\"`"
795
795
  printf "$doing $from\n"
796
796
  eval $helmup
797
- sendToSlack "$helmup"
797
+ case "$INVOKED_AS" in
798
+ "helmup"|"helmdn")
799
+ sendToSlack "$helmup"
800
+ ;;
801
+ *)
802
+ ;;
803
+ esac
798
804
  }
799
805
 
800
806
  # not a fan of global vars but bash forced me to - set by getServiceAndChartVersion
@@ -814,7 +820,7 @@ function doInstall() {
814
820
  # slap additional helm syntax in there if the version is set
815
821
  [ ! -z "$CHARTVER" ] && CHARTVER="--version $CHARTVER"
816
822
 
817
- local INVOKED_AS=`basename $0`
823
+ INVOKED_AS=`basename $0`
818
824
  # set the dry-run flag on what
819
825
  [ "$INVOKED_AS" == "helmwhat" ] && HELM_WHAT="--dry-run --debug" && K8S_WHAT="--dry-run=server -o yaml"
820
826
 
package/src/zx-testing ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ // https://github.com/google/zx
4
+ /* eslint-disable max-len */
5
+ import pkg from 'enquirer'
6
+ import colors from 'ansi-colors'
7
+ import { $, fs, argv, path, chalk } from 'zx'
8
+
9
+ const { prompt } = pkg
10
+
11
+ // This will preserve coloration from spawned child processes
12
+ process.env.FORCE_COLORS = 3
13
+ process.env.FORCE_COLOR = '1'
14
+
15
+ let TARGET
16
+ let CHANNEL
17
+
18
+ // A single unnamed parameter is assumed to be a deploy target.
19
+ // If named parameters exist, they may not be stored in argv,
20
+ // so don't look for a larger length of argv to decide whether to look for named paramters
21
+ if ( argv._.length === 1 ) {
22
+ TARGET = argv._[0]
23
+ } else {
24
+ TARGET = argv.target
25
+
26
+ if ( argv.channelName ) {
27
+ CHANNEL = {}
28
+ CHANNEL.name = argv.channelName
29
+ CHANNEL.expiration = argv.channelExpiration ?? '7d'
30
+ }
31
+ }
32
+
33
+ // If target has not been defined on the command line,
34
+ // then enter interactive mode
35
+ const INTERACTIVE_MODE = !TARGET
36
+
37
+ let exited = false
38
+
39
+ const SECRETS_DIR = 'secrets'
40
+ const EXIT_EVENTS = [ 'SIGINT', 'exit', 'uncaughException', 'unhandledRejection' ]
41
+
42
+ const [
43
+ firebaserc,
44
+ firebaseJson,
45
+ gitBranch
46
+ ] = await Promise.all( [
47
+ fs.exists( path.join( process.cwd(), '.firebaserc' ) ),
48
+ fs.exists( path.join( process.cwd(), 'firebase.json' ) ),
49
+ $`git rev-parse --abbrev-ref HEAD -C ${process.cwd()}`
50
+ .then( ( { stdout } ) => {
51
+ return stdout.split( '\n' )[0]
52
+ } )
53
+ ] )
54
+
55
+ if ( !firebaserc ) {
56
+ console.log( chalk.red( 'No .firebaserc file found. Make sure that you are executing firebaseDeploy from within a firebase project directory.' ) )
57
+ }
58
+
59
+ if ( !firebaseJson ) {
60
+ console.log( chalk.red( 'No firebase.json file found. Make sure that you are executing firebaseDeploy from within a firebase project directory.' ) )
61
+ }
62
+
63
+ if ( !firebaserc || !firebaseJson ) {
64
+ process.exit()
65
+ }
66
+
67
+ const firebasercContent = JSON.parse( await fs.readFile( path.join( process.cwd(), '.firebaserc' ), { encoding : 'utf-8' } ) )
68
+
69
+ let maxSiteIdLength = 0
70
+
71
+ const targets = Object
72
+ .entries( firebasercContent.targets )
73
+ .reduce( ( prev, [ projectId, config ] ) => {
74
+ Object
75
+ .entries( config.hosting )
76
+ .forEach( ( [ siteId, aliases ] ) => {
77
+ aliases.forEach( ( alias ) => {
78
+ // Store the longest length of site id for formatting later on
79
+ maxSiteIdLength = Math.max( siteId.length, maxSiteIdLength )
80
+
81
+ // eslint-disable-next-line no-param-reassign
82
+ prev[alias] = {
83
+ projectId,
84
+ siteId,
85
+ alias,
86
+ production : config.production
87
+ }
88
+ } )
89
+ } )
90
+
91
+ return prev
92
+ }, {} )
93
+
94
+ if ( INTERACTIVE_MODE ) {
95
+ const { targetEnv } = await prompt( [ {
96
+ type : 'autocomplete',
97
+ name : 'targetEnv',
98
+ message : 'To which environment would you like to deploy?',
99
+ choices : Object.values( targets ).map( ( t ) => {
100
+ const productionLabel = t.production ? colors.bold.red( '--PRODUCTION--' ) : ''
101
+ return { message : `${colors.bold.cyan( 'Site: ' )}${t.siteId.padEnd( maxSiteIdLength )} ${colors.bold.green( 'Project: ' )}${t.projectId} ${productionLabel}`, value : t.alias }
102
+ } )
103
+ } ] )
104
+
105
+ TARGET = targets[targetEnv]
106
+
107
+ if ( TARGET.production ) {
108
+ const { confirmProductionDeploy } = await prompt( [ {
109
+ type : 'confirm',
110
+ name : 'confirmProductionDeploy',
111
+ message : `${colors.bold.red( 'WARNING: ' )} This is a ${colors.bold.yellow( 'PRODUCTION' )} environment. Are you sure you want to deploy here?`
112
+ } ] )
113
+ if ( !confirmProductionDeploy ) {
114
+ console.log( 'Cancelling deployment...' )
115
+ process.exit()
116
+ }
117
+ }
118
+
119
+ const { useChannel } = await prompt( [ {
120
+ type : 'confirm',
121
+ name : 'useChannel',
122
+ message : 'Do you want to deploy to a temporary preview channel?'
123
+ } ] )
124
+
125
+ if ( useChannel ) {
126
+ const { channelName, channelExpiration } = await prompt( [ {
127
+ type : 'input',
128
+ name : 'channelName',
129
+ message : 'To which channel would you like to deploy?',
130
+ initial : gitBranch ?? 'temp-deploy'
131
+ }, {
132
+ type : 'input',
133
+ name : 'channelExpiration',
134
+ message : 'When should the channel expire?',
135
+ initial : '7d'
136
+ } ] )
137
+
138
+ CHANNEL = { name : channelName, expiration : channelExpiration }
139
+ }
140
+ } else {
141
+ TARGET = targets[TARGET]
142
+ }
143
+
144
+ if ( !TARGET ) {
145
+ console.log( chalk.red( 'No such target exists in .firebaserc config.' ) )
146
+ process.exit()
147
+ }
148
+
149
+ const [ aliasFileExists, siteIdFileExists, sharedFileExists ] = await Promise.all( [
150
+ fs.exists( path.join( process.cwd(), SECRETS_DIR, `${TARGET.alias}.env` ) ),
151
+ fs.exists( path.join( process.cwd(), SECRETS_DIR, `${TARGET.siteId}.env` ) ),
152
+ fs.exists( path.join( process.cwd(), SECRETS_DIR, 'shared.env' ) )
153
+ ] )
154
+
155
+ if ( !aliasFileExists && !siteIdFileExists ) {
156
+ console.log( chalk.red( `No env file for ${TARGET.alias} exists. Check your secrets directory and try again` ) )
157
+ process.exit()
158
+ }
159
+
160
+ const exec = []
161
+
162
+ if ( sharedFileExists ) {
163
+ exec.push( fs.readFile( path.join( process.cwd(), SECRETS_DIR, 'shared.env' ), { encoding : 'utf-8' } ) )
164
+ }
165
+
166
+ if ( aliasFileExists ) {
167
+ exec.push( fs.readFile( path.join( process.cwd(), SECRETS_DIR, `${TARGET.alias}.env` ), { encoding : 'utf-8' } ) )
168
+ } else {
169
+ exec.push( fs.readFile( path.join( process.cwd(), SECRETS_DIR, `${TARGET.siteId}.env` ), { encoding : 'utf-8' } ) )
170
+ }
171
+
172
+ console.log( `\n\n\n${colors.bold.green( '====== Deployment Summary ======' )}` )
173
+ console.log( `${colors.blue( 'Firebase project:' )} ${TARGET.projectId} ${TARGET.production ? colors.yellow( '--PRODUCTION--' ) : ''}` )
174
+ console.log( `${colors.blue( 'Site ID:' )} ${TARGET.siteId}` )
175
+ console.log( `${colors.blue( 'Channel Name:' )} ${CHANNEL?.name ?? colors.italic.gray( '(none)' )}` )
176
+ console.log( `${colors.blue( 'Channel Expiration date:' )} ${CHANNEL?.name ? CHANNEL.expiration ?? colors.italic.gray( '(none)' ) : colors.italic.gray( '(n/a)' )}` )
177
+ console.log( `${colors.blue( 'Shared .env file:' )} ${sharedFileExists ? `${SECRETS_DIR}/shared.env` : colors.italic.gray( '(none)' )}` )
178
+ console.log( `${colors.blue( 'Additional .env overrides:' )} ${aliasFileExists ? `${SECRETS_DIR}/${TARGET.alias}.env` : `${SECRETS_DIR}/${TARGET.siteId}.env`}` )
179
+ console.log( `${colors.bold.green( '===============================' )}` )
180
+
181
+ if ( INTERACTIVE_MODE ) {
182
+ const { confirmSelections } = await prompt( [ {
183
+ type : 'confirm',
184
+ name : 'confirmSelections',
185
+ message : 'Proceed with this deployment?'
186
+ } ] )
187
+
188
+ if ( !confirmSelections ) {
189
+ console.log( 'Cancelling deployment...' )
190
+ process.exit()
191
+ }
192
+ }
193
+ const envFileContent = ( await Promise.all( exec ) ).join( '\n' )
194
+
195
+ EXIT_EVENTS
196
+ .forEach( ( event ) => {
197
+ process.on( event, () => {
198
+ if ( !exited ) {
199
+ exited = true
200
+ fs.removeSync( path.join( process.cwd(), '.env.temp' ) )
201
+ }
202
+ } )
203
+ } )
204
+
205
+ await fs.writeFile( path.join( process.cwd(), '.env.temp' ), envFileContent )
206
+
207
+ try {
208
+
209
+ // deploying to a channel requires slightly different parameter structure
210
+ const deployType = CHANNEL ? `hosting:channel:deploy ${CHANNEL.name} ${CHANNEL.expiration ? `--expires ${CHANNEL.expiration}` : ''}`.split( ' ' ) : 'deploy'
211
+ const hostingPrefix = CHANNEL ? '' : 'hosting:'
212
+
213
+ await $`npm run clean && DOTENV_CONFIG_PATH=${path.join( process.cwd(), '.env.temp' )} DEPLOYMENT_TARGET=${TARGET.siteId} npm run build && firebase use ${TARGET.projectId} && firebase ${deployType} --only ${hostingPrefix}${TARGET.siteId}`
214
+ } catch ( err ) {
215
+ console.error( err )
216
+ }