@naturalcycles/backend-lib 9.79.0 → 9.80.0

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.
Files changed (46) hide show
  1. package/dist/deploy/deployHealthCheck.d.ts +1 -5
  2. package/dist/deploy/deployHealthCheck.js +3 -34
  3. package/dist/deploy/index.d.ts +2 -9
  4. package/dist/deploy/index.js +1 -6
  5. package/dist/express/createDefaultApp.js +2 -2
  6. package/dist/server/serverStatusMiddleware.d.ts +2 -2
  7. package/dist/server/serverStatusMiddleware.js +5 -14
  8. package/dist/util.d.ts +0 -1
  9. package/dist/util.js +1 -4
  10. package/package.json +3 -13
  11. package/src/deploy/deployHealthCheck.ts +2 -48
  12. package/src/deploy/index.ts +2 -17
  13. package/src/express/createDefaultApp.ts +2 -2
  14. package/src/server/serverStatusMiddleware.ts +4 -20
  15. package/src/util.ts +1 -5
  16. package/bin/deploy-gae.js +0 -2
  17. package/bin/deploy-prepare.js +0 -2
  18. package/bin/undeploy-gae.js +0 -2
  19. package/dist/bin/deploy-gae.d.ts +0 -1
  20. package/dist/bin/deploy-gae.js +0 -12
  21. package/dist/bin/deploy-prepare.d.ts +0 -1
  22. package/dist/bin/deploy-prepare.js +0 -19
  23. package/dist/bin/undeploy-gae.d.ts +0 -1
  24. package/dist/bin/undeploy-gae.js +0 -13
  25. package/dist/deploy/backend.cfg.util.d.ts +0 -30
  26. package/dist/deploy/backend.cfg.util.js +0 -23
  27. package/dist/deploy/deploy.model.d.ts +0 -29
  28. package/dist/deploy/deploy.model.js +0 -1
  29. package/dist/deploy/deploy.util.d.ts +0 -7
  30. package/dist/deploy/deploy.util.js +0 -151
  31. package/dist/deploy/deployGae.d.ts +0 -10
  32. package/dist/deploy/deployGae.js +0 -90
  33. package/dist/deploy/deployInfo.util.d.ts +0 -1
  34. package/dist/deploy/deployInfo.util.js +0 -25
  35. package/dist/deploy/deployPrepare.d.ts +0 -13
  36. package/dist/deploy/deployPrepare.js +0 -85
  37. package/src/bin/deploy-gae.ts +0 -14
  38. package/src/bin/deploy-prepare.ts +0 -24
  39. package/src/bin/undeploy-gae.ts +0 -15
  40. package/src/deploy/backend.cfg.util.ts +0 -63
  41. package/src/deploy/deploy.model.ts +0 -32
  42. package/src/deploy/deploy.util.ts +0 -214
  43. package/src/deploy/deployGae.ts +0 -127
  44. package/src/deploy/deployInfo.util.ts +0 -27
  45. package/src/deploy/deployPrepare.ts +0 -113
  46. package/src/deploy/files-default/.gcloudignore +0 -7
@@ -1,214 +0,0 @@
1
- import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js'
2
- import { _assert } from '@naturalcycles/js-lib/error/assert.js'
3
- import { _mapValues, _merge } from '@naturalcycles/js-lib/object/object.util.js'
4
- import { _truncate } from '@naturalcycles/js-lib/string/string.util.js'
5
- import { sha256 } from '@naturalcycles/nodejs-lib'
6
- import { dimGrey, white } from '@naturalcycles/nodejs-lib/colors'
7
- import { fs2 } from '@naturalcycles/nodejs-lib/fs2'
8
- import { yaml2 } from '@naturalcycles/nodejs-lib/yaml2'
9
- import type { BackendCfg } from './backend.cfg.util.js'
10
- import type { AppYaml, DeployInfo } from './deploy.model.js'
11
-
12
- const APP_YAML_DEFAULT = (): AppYaml => ({
13
- runtime: 'nodejs22',
14
- service: 'default',
15
- inbound_services: ['warmup'],
16
- instance_class: 'F1',
17
- automatic_scaling: {
18
- min_instances: 0,
19
- max_instances: 1,
20
- },
21
- env_variables: {
22
- APP_ENV: 'prod',
23
- DEBUG: 'app*,nc:*',
24
- DEBUG_HIDE_DATE: 'true',
25
- // DEBUG_COLORS: 'true',
26
- TZ: 'UTC',
27
- },
28
- })
29
-
30
- export async function createAndSaveDeployInfo(
31
- backendCfg: BackendCfg,
32
- targetDir: string,
33
- ): Promise<DeployInfo> {
34
- const deployInfo = await createDeployInfo(backendCfg)
35
- const deployInfoPath = `${targetDir}/deployInfo.json`
36
- fs2.writeJson(deployInfoPath, deployInfo, { spaces: 2 })
37
- console.log(`saved ${dimGrey(deployInfoPath)}`)
38
- return deployInfo
39
- }
40
-
41
- export async function createDeployInfo(
42
- backendCfg: BackendCfg,
43
- overrideBranch?: string,
44
- ): Promise<DeployInfo> {
45
- const { simpleGit } = await import('simple-git') // lazy load
46
- const git = simpleGit('.')
47
-
48
- const now = localTime.now()
49
- const gitBranch = overrideBranch || (await git.status()).current!
50
- const gitRev = (await git.revparse(['HEAD'])).slice(0, 7)
51
-
52
- let {
53
- gaeProject,
54
- gaeProjectByBranch = {},
55
- gaeService,
56
- gaeServiceByBranch = {},
57
- branchesWithTimestampVersions = [],
58
- } = backendCfg
59
-
60
- gaeProject = gaeProjectByBranch[gitBranch] || gaeProject
61
-
62
- if (gaeServiceByBranch[gitBranch]) {
63
- gaeService = validateGAEServiceName(gaeServiceByBranch[gitBranch])
64
- } else {
65
- let branchName = gitBranch
66
-
67
- if (backendCfg.hashedBranches) {
68
- // Obfuscates the branch name by hashing it.
69
- // If there are Jira issue names in the branch name, the first one found will be used as a prefix.
70
- const jiraIssue = gitBranch.match(/([Dd][Ee][Vv]-\d+)/)?.[0]
71
- const branchHash = sha256(gitBranch).slice(0, 10)
72
- branchName = [jiraIssue, branchHash].filter(Boolean).join('-')
73
- }
74
-
75
- gaeService = validateGAEServiceName([branchName, gaeService].join('--'))
76
- }
77
-
78
- let gaeVersion = '1'
79
-
80
- if (branchesWithTimestampVersions.includes(gitBranch)) {
81
- // May only contain lowercase letters, digits, and hyphens. Must begin and end with a letter or digit. Must not exceed 63 characters.
82
- gaeVersion = [
83
- // now.format('YYYYMMDD-HHmm'), // 20190521-1721
84
- now.toStringCompact().replace('_', '-'),
85
- gitRev,
86
- ].join('-')
87
- }
88
-
89
- const versionUrl = `https://${[gaeVersion, gaeService, gaeProject].join('-dot-')}.appspot.com`
90
-
91
- // Check the 63-char limit
92
- const versionUrlString = [gaeVersion, gaeService, gaeProject].join('-dot-')
93
- _assert(
94
- versionUrlString.length <= 63,
95
- `versionUrl length should be <= 63 characters, but it's ${versionUrlString.length} instead: ${versionUrlString}`,
96
- )
97
-
98
- const serviceUrl = `https://${[gaeService, gaeProject].join('-dot-')}.appspot.com`
99
-
100
- const deployInfo: DeployInfo = {
101
- gaeProject,
102
- gaeService,
103
- gaeVersion,
104
- versionUrl,
105
- serviceUrl,
106
- gitBranch,
107
- gitRev,
108
- ts: now.unix,
109
- }
110
-
111
- console.log({ deployInfo })
112
-
113
- return deployInfo
114
- }
115
-
116
- export function createAndSaveAppYaml(
117
- backendCfg: BackendCfg,
118
- deployInfo: DeployInfo,
119
- projectDir: string,
120
- targetDir: string,
121
- appYamlPassEnv = '',
122
- ): AppYaml {
123
- const appYaml = createAppYaml(backendCfg, deployInfo, projectDir, appYamlPassEnv)
124
- const appYamlPath = `${targetDir}/app.yaml`
125
- yaml2.writeYaml(appYamlPath, appYaml)
126
- console.log(`saved ${dimGrey(appYamlPath)}`)
127
- return appYaml
128
- }
129
-
130
- export function createAppYaml(
131
- backendCfg: BackendCfg,
132
- deployInfo: DeployInfo,
133
- projectDir: string,
134
- appYamlPassEnv = '',
135
- ): AppYaml {
136
- const { appEnvDefault, appEnvByBranch = {} } = backendCfg
137
- const { gaeService: service, gitBranch } = deployInfo
138
-
139
- const { APP_ENV: processAppEnv } = process.env
140
- const APP_ENV = processAppEnv || appEnvByBranch[gitBranch] || appEnvDefault
141
- if (processAppEnv) {
142
- console.log(`using APP_ENV=${dimGrey(processAppEnv)} from process.env`)
143
- }
144
-
145
- const appYaml = APP_YAML_DEFAULT()
146
-
147
- // Check existing app.yaml
148
- const appYamlPath = `${projectDir}/app.yaml`
149
- if (fs2.pathExists(appYamlPath)) {
150
- console.log(`merging-in ${dimGrey(appYamlPath)}`)
151
- _merge(appYaml, yaml2.readYaml(appYamlPath))
152
- }
153
-
154
- const appEnvYamlPath = `${projectDir}/app.${APP_ENV}.yaml`
155
- if (fs2.pathExists(appEnvYamlPath)) {
156
- console.log(`merging-in ${dimGrey(appEnvYamlPath)}`)
157
- _merge(appYaml, yaml2.readYaml(appEnvYamlPath))
158
- }
159
-
160
- // appYamlPassEnv
161
- const passEnv = appYamlPassEnv
162
- .split(',')
163
- .filter(Boolean)
164
- // oxlint-disable-next-line unicorn/no-array-reduce
165
- .reduce<Record<string, string>>((map, key) => {
166
- const v = process.env[key]
167
- if (!v) {
168
- throw new Error(
169
- `appYamlPassEnv.${key} is requested, but process.env.${key} is not defined!`,
170
- )
171
- }
172
- map[key] = v
173
- return map
174
- }, {})
175
-
176
- if (Object.keys(passEnv).length) {
177
- console.log(
178
- `will merge ${white(
179
- String(Object.keys(passEnv).length),
180
- )} process.env keys to app.yaml: ${dimGrey(Object.keys(passEnv).join(', '))}`,
181
- )
182
- }
183
-
184
- _merge(appYaml, {
185
- service,
186
- env_variables: {
187
- APP_ENV,
188
- ...passEnv,
189
- },
190
- })
191
-
192
- // Redacted appYaml to not show up secrets
193
- console.log({
194
- appYaml: redactedAppYaml(appYaml),
195
- })
196
-
197
- return appYaml
198
- }
199
-
200
- function redactedAppYaml(appYaml: AppYaml): AppYaml {
201
- return {
202
- ...appYaml,
203
- env_variables: _mapValues(appYaml.env_variables || {}, (_k, v) => _truncate(v, 7)),
204
- }
205
- }
206
-
207
- export function validateGAEServiceName(serviceName: string): string {
208
- // May only contain lowercase letters, digits, and hyphens. Must begin and end with a letter or digit. Must not exceed 63 characters.
209
- return serviceName
210
- .replaceAll('_', '-')
211
- .toLowerCase()
212
- .replaceAll(/[^0-9a-z-]/gi, '')
213
- .slice(0, 40)
214
- }
@@ -1,127 +0,0 @@
1
- import { _anyToError } from '@naturalcycles/js-lib/error'
2
- import { pRetry } from '@naturalcycles/js-lib/promise/pRetry.js'
3
- import { _objectAssign } from '@naturalcycles/js-lib/types'
4
- import { appendToGithubSummary } from '@naturalcycles/nodejs-lib/env'
5
- import { exec2 } from '@naturalcycles/nodejs-lib/exec2'
6
- import { fs2 } from '@naturalcycles/nodejs-lib/fs2'
7
- import { getBackendCfg } from './backend.cfg.util.js'
8
- import { createDeployInfo } from './deploy.util.js'
9
- import type { DeployHealthCheckOptions } from './deployHealthCheck.js'
10
- import { deployHealthCheck } from './deployHealthCheck.js'
11
- import type { DeployPrepareOptions } from './deployPrepare.js'
12
- import { deployPrepare } from './deployPrepare.js'
13
-
14
- export interface DeployGaeOptions extends DeployPrepareOptions, DeployHealthCheckOptions {}
15
-
16
- export async function deployGae(opt: DeployGaeOptions = {}): Promise<void> {
17
- const { logOnFailure, logOnSuccess } = opt
18
-
19
- // 1. build
20
-
21
- const packageManager = detectPackageManager()
22
-
23
- exec2.spawn(`${packageManager} build`)
24
-
25
- // 2. deploy-prepare
26
-
27
- const deployInfo = await deployPrepare()
28
-
29
- const targetDir = './tmp/deploy'
30
- const appYamlPath = `${targetDir}/app.yaml`
31
-
32
- const { gaeProject, gaeService, gaeVersion, versionUrl, serviceUrl } = deployInfo
33
- _objectAssign(opt, {
34
- gaeProject,
35
- gaeService,
36
- gaeVersion,
37
- })
38
-
39
- await pRetry(
40
- async () => {
41
- try {
42
- exec2.spawn(
43
- `gcloud app deploy ${appYamlPath} --project ${gaeProject} --version ${gaeVersion} --quiet --no-promote`,
44
- )
45
- } catch (err) {
46
- if (logOnFailure) {
47
- logs(gaeProject, gaeService, gaeVersion)
48
- }
49
- throw err
50
- }
51
- },
52
- {
53
- name: 'deploy',
54
- maxAttempts: 2,
55
- delay: 30_000,
56
- // todo: this doesn't work, as the error is different from what is logged.
57
- // We shoud somehow capture the logged text
58
- predicate: err => _anyToError(err).message.includes('operation is already in progress'),
59
- },
60
- )
61
-
62
- // Health check (versionUrl)
63
- // yarn deploy-health-check --url $deployInfo_versionUrl --repeat 3 --timeoutSec 180 --intervalSec 2
64
- await deployHealthCheck(versionUrl, opt)
65
-
66
- // Only if "timestamped version" is used ('1' is default)
67
- if (gaeVersion !== '1') {
68
- // Rollout (promote versionUrl to serviceUrl)
69
- // gcloud app services set-traffic $deployInfo_gaeService --project $deployInfo_gaeProject --splits $deployInfo_gaeVersion=1 --quiet
70
- exec2.spawn(
71
- `gcloud app services set-traffic ${gaeService} --project ${gaeProject} --splits ${gaeVersion}=1 --quiet`,
72
- )
73
-
74
- // Health check (serviceUrl)
75
- // yarn deploy-health-check --url $deployInfo_serviceUrl --repeat 3 --timeoutSec 60 --intervalSec 2
76
- await deployHealthCheck(serviceUrl, opt)
77
- }
78
-
79
- appendToGithubSummary(`Deployed ${serviceUrl}`)
80
-
81
- // Logs
82
- if (logOnSuccess) {
83
- logs(gaeProject, gaeService, gaeVersion)
84
- }
85
- }
86
-
87
- /**
88
- * Undeploys/removes the GAE service/version, using the same rules as deployGae.
89
- * Detects the service/version by the same criteria: branch name, backend.cfg.yaml, etc.
90
- */
91
- export async function undeployGae(branch: string): Promise<void> {
92
- const projectDir = '.'
93
- const backendCfg = getBackendCfg(projectDir)
94
-
95
- const { gaeProject, gaeService, gaeVersion } = await createDeployInfo(backendCfg, branch)
96
-
97
- // if (prod) {
98
- // console.log(`undeployGae (branch: ${branch}): not removing prod version (safety check)`)
99
- // return
100
- // }
101
-
102
- console.log(
103
- `undeployGae (branch: ${branch}): going to remove ${gaeProject}/${gaeService}/${gaeVersion}`,
104
- )
105
-
106
- exec2.spawn(
107
- `gcloud app versions delete --project ${gaeProject} --service ${gaeService} ${gaeVersion} --quiet`,
108
- )
109
-
110
- appendToGithubSummary(`removed ${gaeProject}/${gaeService}/${gaeVersion}`)
111
- }
112
-
113
- function logs(gaeProject: string, gaeService: string, gaeVersion: string): void {
114
- try {
115
- exec2.spawn(
116
- `gcloud app logs read --project ${gaeProject} --service ${gaeService} --version ${gaeVersion}`,
117
- )
118
- } catch {}
119
- }
120
-
121
- function detectPackageManager(): string {
122
- const pj = fs2.readText('package.json')
123
- if (pj.includes('pnpm')) {
124
- return 'pnpm'
125
- }
126
- return 'yarn' // yarn is still the default
127
- }
@@ -1,27 +0,0 @@
1
- import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js'
2
- import { _memoized } from '@naturalcycles/js-lib/decorators'
3
- import { fs2 } from '@naturalcycles/nodejs-lib/fs2'
4
- import type { DeployInfo } from './deploy.model.js'
5
-
6
- export const getDeployInfo = _memoized((projectDir: string): DeployInfo => {
7
- const deployInfoPath = `${projectDir}/deployInfo.json`
8
- try {
9
- return fs2.readJson(deployInfoPath)
10
- } catch {
11
- // console.error(`cannot read ${deployInfoPath}, returning empty version`)
12
- return getDeployInfoStub()
13
- }
14
- })
15
-
16
- function getDeployInfoStub(stub = ''): DeployInfo {
17
- return {
18
- gaeProject: stub,
19
- gaeService: stub,
20
- gaeVersion: stub,
21
- serviceUrl: stub,
22
- versionUrl: stub,
23
- gitBranch: stub,
24
- gitRev: stub,
25
- ts: localTime.nowUnix(),
26
- }
27
- }
@@ -1,113 +0,0 @@
1
- import { loadEnvFileIfExists } from '@naturalcycles/nodejs-lib'
2
- import type { CliOptions } from '@naturalcycles/nodejs-lib/args'
3
- import { dimGrey } from '@naturalcycles/nodejs-lib/colors'
4
- import { fs2 } from '@naturalcycles/nodejs-lib/fs2'
5
- import { kpySync } from '@naturalcycles/nodejs-lib/kpy'
6
- import { srcDir } from '../paths.cnst.js'
7
- import { getBackendCfg } from './backend.cfg.util.js'
8
- import type { DeployInfo } from './deploy.model.js'
9
- import { createAndSaveAppYaml, createAndSaveDeployInfo } from './deploy.util.js'
10
-
11
- export interface DeployPrepareOptions {
12
- projectDir?: string
13
- targetDir?: string
14
- createNpmrc?: boolean
15
-
16
- /**
17
- * Comma-separated list of env variables that will be passed to app.yaml from process.env.
18
- * Use it to pass secrets.
19
- */
20
- appYamlPassEnv?: string
21
- }
22
-
23
- export const deployPrepareCliOptions = {
24
- projectDir: {
25
- type: 'string',
26
- default: '.',
27
- },
28
- targetDir: {
29
- type: 'string',
30
- default: `./tmp/deploy`,
31
- },
32
- createNpmrc: {
33
- type: 'boolean',
34
- desc: 'Create .npmrc in targetDir if NPM_TOKEN env var is set',
35
- default: true,
36
- },
37
- appYamlPassEnv: {
38
- type: 'string',
39
- desc: 'Comma-separated list of env variables that will be passed to app.yaml from process.env',
40
- },
41
- } as const satisfies CliOptions
42
-
43
- const DEFAULT_FILES = [
44
- 'dist',
45
- // '!dist/test',
46
- // '!dist/**/*.test.*',
47
- // '!dist/**/*.mock.*',
48
- '!dist/**/*.script.*',
49
- 'src', // For Sentry
50
- '!src/test',
51
- '!src/**/*.test.*',
52
- '!src/**/*.mock.*',
53
- '!src/**/*.script.*',
54
- '!**/__snapshots__',
55
- '!**/__exclude',
56
- 'static',
57
- 'secret/**/*.enc', // encrypted secrets
58
- 'package.json',
59
- 'yarn.lock',
60
- '.yarnrc',
61
- 'pnpm-lock.yaml',
62
- 'pnpm-workspace.yaml',
63
- // 'tsconfig.json', // for path-mapping to work!
64
- // 'tsconfig.dist.json',
65
- '.gcloudignore',
66
- 'app.yaml',
67
- 'patches', // to allow patch-package
68
- 'resources',
69
- ]
70
-
71
- const defaultFilesDir = `${srcDir}/deploy/files-default`
72
-
73
- export async function deployPrepare(opt: DeployPrepareOptions = {}): Promise<DeployInfo> {
74
- loadEnvFileIfExists()
75
-
76
- const { projectDir = '.', targetDir = './tmp/deploy', createNpmrc = true } = opt
77
-
78
- const backendCfg = getBackendCfg(projectDir)
79
- const inputPatterns = backendCfg.files || DEFAULT_FILES
80
- const appYamlPassEnv = opt.appYamlPassEnv || backendCfg.appYamlPassEnv
81
-
82
- console.log(`1. Copy files to ${dimGrey(targetDir)}`)
83
-
84
- // Clean targetDir
85
- fs2.emptyDir(targetDir)
86
-
87
- kpySync({
88
- baseDir: defaultFilesDir,
89
- outputDir: targetDir,
90
- dotfiles: true,
91
- })
92
-
93
- kpySync({
94
- baseDir: projectDir,
95
- inputPatterns,
96
- outputDir: targetDir,
97
- dotfiles: true,
98
- })
99
-
100
- const { NPM_TOKEN } = process.env
101
- if (NPM_TOKEN && createNpmrc) {
102
- const npmrcPath = `${targetDir}/.npmrc`
103
- const npmrc = `//registry.npmjs.org/:_authToken=${NPM_TOKEN}`
104
- fs2.writeFile(npmrcPath, npmrc)
105
- }
106
-
107
- console.log(`2. Generate ${dimGrey('deployInfo.json')} and ${dimGrey('app.yaml')} in targetDir`)
108
-
109
- const deployInfo = await createAndSaveDeployInfo(backendCfg, targetDir)
110
- createAndSaveAppYaml(backendCfg, deployInfo, projectDir, targetDir, appYamlPassEnv)
111
-
112
- return deployInfo
113
- }
@@ -1,7 +0,0 @@
1
- #
2
- # .gcloudignore
3
- #
4
- .gcloudignore
5
- .git
6
- .gitignore
7
- node_modules/