@naturalcycles/backend-lib 9.78.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 (49) 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/compressionMiddleware.d.ts +14 -1
  7. package/dist/server/compressionMiddleware.js +81 -27
  8. package/dist/server/serverStatusMiddleware.d.ts +2 -2
  9. package/dist/server/serverStatusMiddleware.js +5 -14
  10. package/dist/util.d.ts +0 -1
  11. package/dist/util.js +1 -4
  12. package/package.json +3 -13
  13. package/src/deploy/deployHealthCheck.ts +2 -48
  14. package/src/deploy/index.ts +2 -17
  15. package/src/express/createDefaultApp.ts +2 -2
  16. package/src/server/compressionMiddleware.ts +107 -28
  17. package/src/server/serverStatusMiddleware.ts +4 -20
  18. package/src/util.ts +1 -5
  19. package/bin/deploy-gae.js +0 -2
  20. package/bin/deploy-prepare.js +0 -2
  21. package/bin/undeploy-gae.js +0 -2
  22. package/dist/bin/deploy-gae.d.ts +0 -1
  23. package/dist/bin/deploy-gae.js +0 -12
  24. package/dist/bin/deploy-prepare.d.ts +0 -1
  25. package/dist/bin/deploy-prepare.js +0 -19
  26. package/dist/bin/undeploy-gae.d.ts +0 -1
  27. package/dist/bin/undeploy-gae.js +0 -13
  28. package/dist/deploy/backend.cfg.util.d.ts +0 -30
  29. package/dist/deploy/backend.cfg.util.js +0 -23
  30. package/dist/deploy/deploy.model.d.ts +0 -29
  31. package/dist/deploy/deploy.model.js +0 -1
  32. package/dist/deploy/deploy.util.d.ts +0 -7
  33. package/dist/deploy/deploy.util.js +0 -151
  34. package/dist/deploy/deployGae.d.ts +0 -10
  35. package/dist/deploy/deployGae.js +0 -90
  36. package/dist/deploy/deployInfo.util.d.ts +0 -1
  37. package/dist/deploy/deployInfo.util.js +0 -25
  38. package/dist/deploy/deployPrepare.d.ts +0 -13
  39. package/dist/deploy/deployPrepare.js +0 -85
  40. package/src/bin/deploy-gae.ts +0 -14
  41. package/src/bin/deploy-prepare.ts +0 -24
  42. package/src/bin/undeploy-gae.ts +0 -15
  43. package/src/deploy/backend.cfg.util.ts +0 -63
  44. package/src/deploy/deploy.model.ts +0 -32
  45. package/src/deploy/deploy.util.ts +0 -214
  46. package/src/deploy/deployGae.ts +0 -127
  47. package/src/deploy/deployInfo.util.ts +0 -27
  48. package/src/deploy/deployPrepare.ts +0 -113
  49. package/src/deploy/files-default/.gcloudignore +0 -7
@@ -31,6 +31,19 @@ export interface CompressionOptions {
31
31
  */
32
32
  threshold?: number | string
33
33
 
34
+ /**
35
+ * Bodies sent in one `res.end(chunk)` call (which is what `res.json` and `res.send` do) up to this
36
+ * size are compressed synchronously on the event loop, instead of through a zlib stream.
37
+ * For typical JSON responses the per-response stream setup and threadpool round-trip cost more
38
+ * than the compression itself, and the sync path also keeps an exact `Content-Length`
39
+ * (streamed responses are chunked).
40
+ * Bodies above this size, and bodies written in several `res.write` calls, are streamed.
41
+ * Set to `0` to always stream.
42
+ *
43
+ * @default 128kb
44
+ */
45
+ syncMaxBytes?: number | string
46
+
34
47
  /**
35
48
  * Encoding to use when Accept-Encoding header is not present.
36
49
  * @default 'identity'
@@ -71,7 +84,8 @@ const cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/i
71
84
  /**
72
85
  * Compression middleware with support for gzip, deflate, brotli, and zstd.
73
86
  *
74
- * This is a fork of the `compression` npm package with added zstd support.
87
+ * This is a fork of the `compression` npm package with added zstd support,
88
+ * and a synchronous fast path for single-chunk bodies (see `syncMaxBytes`).
75
89
  *
76
90
  * Encoding preference order: zstd > br > gzip > deflate
77
91
  */
@@ -88,10 +102,14 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
88
102
  }
89
103
 
90
104
  // Zstd options
105
+ // Level 1 instead of zstd's own default of 3: for typical JSON responses the CPU cost and
106
+ // compression ratio are the same (measured 2026-10 on 22 KB and 115 KB payloads), but a level 3
107
+ // stream holds ~850 KB of native memory per in-flight response vs ~150 KB at level 1.
108
+ // That per-response allocation churn is what MALLOC_ARENA_MAX=2 works around on Cloud Run.
91
109
  const optsZstd: zlib.ZstdOptions = {
92
110
  ...opts.zstd,
93
111
  params: {
94
- [zlib.constants.ZSTD_c_compressionLevel]: 3,
112
+ [zlib.constants.ZSTD_c_compressionLevel]: 1,
95
113
  ...opts.zstd?.params,
96
114
  },
97
115
  }
@@ -104,12 +122,19 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
104
122
  if (opts.windowBits !== undefined) zlibOpts.windowBits = opts.windowBits
105
123
  if (opts.chunkSize !== undefined) zlibOpts.chunkSize = opts.chunkSize
106
124
 
125
+ const codecOpts: CodecOptions = { zstd: optsZstd, brotli: optsBrotli, zlib: zlibOpts }
126
+
107
127
  const filter = opts.filter || shouldCompress
108
128
  const threshold = parseBytes(opts.threshold) ?? 1024
129
+ const syncMaxBytes = parseBytes(opts.syncMaxBytes) ?? 128 * 1024
109
130
  const enforceEncoding = opts.enforceEncoding || 'identity'
110
131
 
111
132
  return function compression(req, res, next) {
112
133
  let ended = false
134
+ // Set by the first `res.write`: the body comes in several chunks, so only the stream path applies
135
+ let wrote = false
136
+ // Set once the compress-or-not decision has been made for this response (by either path)
137
+ let decided = false
113
138
  let length: number | undefined
114
139
  let listeners: [string, (...args: any[]) => void][] | null = []
115
140
  let stream: zlib.Gzip | zlib.Deflate | zlib.BrotliCompress | zlib.ZstdCompress | undefined
@@ -136,6 +161,8 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
136
161
  return false
137
162
  }
138
163
 
164
+ wrote = true
165
+
139
166
  if (!headersSent(res)) {
140
167
  res.writeHead(res.statusCode)
141
168
  }
@@ -161,6 +188,30 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
161
188
  if (!res.getHeader('Content-Length')) {
162
189
  length = chunkLength(chunk, encodingOrCallback as BufferEncoding)
163
190
  }
191
+
192
+ // Single-chunk body (no prior `res.write`): the whole body is right here, so compress it
193
+ // synchronously instead of setting up a zlib stream for one chunk.
194
+ if (chunk && !wrote && !decided) {
195
+ const encoding = typeof encodingOrCallback === 'function' ? undefined : encodingOrCallback
196
+ const body = toBuffer(chunk, encoding)
197
+
198
+ if (body.length <= syncMaxBytes) {
199
+ const method = decide(body.length)
200
+
201
+ if (method) {
202
+ const compressed = compressSync(method, body, codecOpts)
203
+ res.setHeader('Content-Encoding', method)
204
+ res.setHeader('Content-Length', compressed.length)
205
+ ended = true
206
+ const cb = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback
207
+ // encoding is irrelevant for a Buffer chunk; it is passed to satisfy the `end` overload
208
+ return _end.call(res, compressed, 'utf8', cb)
209
+ }
210
+ // Not compressing: falls through to the plain `end`.
211
+ // The `onHeaders` listener below sees `decided` and does nothing.
212
+ }
213
+ }
214
+
164
215
  res.writeHead(res.statusCode)
165
216
  }
166
217
 
@@ -195,34 +246,27 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
195
246
  return res
196
247
  }
197
248
 
198
- function nocompress(_msg: string): void {
249
+ function nocompress(_msg: string): undefined {
199
250
  addListeners(res, _on, listeners!)
200
251
  listeners = null
201
252
  }
202
253
 
203
- // Release the compression stream when the response closes, freeing native
204
- // zlib memory even if the client disconnected before the response finished.
205
- // Registered before onHeaders so a close that happens before the stream
206
- // exists is not missed (the `closed` flag below handles a stream created
207
- // afterwards). Fixes CVE-2026-87776 / GHSA-vc2v-76pw-4v95.
208
- // We use `stream.destroy()` (not the `destroy` npm package upstream added)
209
- // because our Node target frees the zlib handle on destroy().
210
- _on.call(res, 'close', function onResponseClose() {
211
- closed = true
212
- stream?.destroy()
213
- })
254
+ /**
255
+ * The compress-or-not decision, shared by the sync path (`res.end` holding the whole body)
256
+ * and the stream path (`onHeaders`).
257
+ * Returns the encoding to use, or undefined when the response is to be sent as-is.
258
+ */
259
+ function decide(bodyLength: number | undefined): string | undefined {
260
+ decided = true
214
261
 
215
- onHeaders(res, function onResponseHeaders() {
216
262
  // determine if request is filtered
217
263
  if (!filter(req, res)) {
218
- nocompress('filtered')
219
- return
264
+ return nocompress('filtered')
220
265
  }
221
266
 
222
267
  // determine if the entity should be transformed
223
268
  if (!shouldTransform(req, res)) {
224
- nocompress('no transform')
225
- return
269
+ return nocompress('no transform')
226
270
  }
227
271
 
228
272
  // vary
@@ -231,24 +275,21 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
231
275
  // content-length below threshold
232
276
  if (
233
277
  Number(res.getHeader('Content-Length')) < threshold ||
234
- (length !== undefined && length < threshold)
278
+ (bodyLength !== undefined && bodyLength < threshold)
235
279
  ) {
236
- nocompress('size below threshold')
237
- return
280
+ return nocompress('size below threshold')
238
281
  }
239
282
 
240
283
  const encoding = (res.getHeader('Content-Encoding') as string) || 'identity'
241
284
 
242
285
  // already encoded
243
286
  if (encoding !== 'identity') {
244
- nocompress('already encoded')
245
- return
287
+ return nocompress('already encoded')
246
288
  }
247
289
 
248
290
  // head
249
291
  if (req.method === 'HEAD') {
250
- nocompress('HEAD request')
251
- return
292
+ return nocompress('HEAD request')
252
293
  }
253
294
 
254
295
  // compression method
@@ -266,10 +307,31 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
266
307
 
267
308
  // negotiation failed
268
309
  if (!method || method === 'identity') {
269
- nocompress('not acceptable')
270
- return
310
+ return nocompress('not acceptable')
271
311
  }
272
312
 
313
+ return method
314
+ }
315
+
316
+ // Release the compression stream when the response closes, freeing native
317
+ // zlib memory even if the client disconnected before the response finished.
318
+ // Registered before onHeaders so a close that happens before the stream
319
+ // exists is not missed (the `closed` flag below handles a stream created
320
+ // afterwards). Fixes CVE-2026-87776 / GHSA-vc2v-76pw-4v95.
321
+ // We use `stream.destroy()` (not the `destroy` npm package upstream added)
322
+ // because our Node target frees the zlib handle on destroy().
323
+ _on.call(res, 'close', function onResponseClose() {
324
+ closed = true
325
+ stream?.destroy()
326
+ })
327
+
328
+ onHeaders(res, function onResponseHeaders() {
329
+ // The sync path has already handled this response (compressed it, or declined to)
330
+ if (decided) return
331
+
332
+ const method = decide(length)
333
+ if (!method) return
334
+
273
335
  // compression stream
274
336
  if (method === 'zstd') {
275
337
  stream = zlib.createZstdCompress(optsZstd)
@@ -331,6 +393,23 @@ export function shouldCompress(_req: any, res: any): boolean {
331
393
  return true
332
394
  }
333
395
 
396
+ interface CodecOptions {
397
+ zstd: zlib.ZstdOptions
398
+ brotli: zlib.BrotliOptions
399
+ zlib: zlib.ZlibOptions
400
+ }
401
+
402
+ /**
403
+ * Compresses a whole body in one call, on the event loop.
404
+ * Same codec options as the streams, so the output is identical to the streamed one.
405
+ */
406
+ function compressSync(method: string, body: Buffer, opts: CodecOptions): Buffer {
407
+ if (method === 'zstd') return zlib.zstdCompressSync(body, opts.zstd)
408
+ if (method === 'br') return zlib.brotliCompressSync(body, opts.brotli)
409
+ if (method === 'gzip') return zlib.gzipSync(body, opts.zlib)
410
+ return zlib.deflateSync(body, opts.zlib)
411
+ }
412
+
334
413
  function addListeners(
335
414
  stream: any,
336
415
  on: (type: string, listener: (...args: any[]) => void) => void,
@@ -2,15 +2,11 @@ import os from 'node:os'
2
2
  import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js'
3
3
  import { _filterNullishValues } from '@naturalcycles/js-lib/object/object.util.js'
4
4
  import { memoryUsageFull, processSharedUtil } from '@naturalcycles/nodejs-lib'
5
- import { getDeployInfo } from '../deploy/deployInfo.util.js'
6
5
  import type { BackendRequestHandler } from './server.model.js'
7
6
 
8
7
  const { versions, arch, platform } = process
9
8
  const availableParallelismAtStart = os.availableParallelism()
10
9
  const {
11
- GAE_APPLICATION,
12
- GAE_SERVICE,
13
- GAE_VERSION,
14
10
  GOOGLE_CLOUD_PROJECT,
15
11
  K_SERVICE,
16
12
  K_REVISION,
@@ -23,33 +19,21 @@ const {
23
19
  BUILD_VERSION,
24
20
  } = process.env
25
21
 
26
- export function serverStatusMiddleware(projectDir?: string, extra?: any): BackendRequestHandler {
22
+ export function serverStatusMiddleware(extra?: any): BackendRequestHandler {
27
23
  return async (_req, res) => {
28
- res.json(getServerStatusData(projectDir, extra))
24
+ res.json(getServerStatusData(extra))
29
25
  }
30
26
  }
31
27
 
32
- export function getServerStatusData(
33
- projectDir: string = process.cwd(),
34
- extra?: any,
35
- ): Record<string, any> {
36
- let deployBuildTime = DEPLOY_BUILD_TIME
37
- if (!deployBuildTime) {
38
- const { ts } = getDeployInfo(projectDir)
39
- deployBuildTime = localTime(ts).toPretty()
40
- }
41
-
28
+ export function getServerStatusData(extra?: any): Record<string, any> {
42
29
  const availableParallelism = os.availableParallelism()
43
30
 
44
31
  return _filterNullishValues({
45
32
  nodeProcessStarted: getStartedStr(),
46
- deployBuildTime,
33
+ DEPLOY_BUILD_TIME,
47
34
  BUILD_VERSION,
48
35
  APP_ENV,
49
36
  GOOGLE_CLOUD_PROJECT,
50
- GAE_APPLICATION,
51
- GAE_SERVICE,
52
- GAE_VERSION,
53
37
  K_SERVICE,
54
38
  K_REVISION,
55
39
  processInfo: {
package/src/util.ts CHANGED
@@ -1,8 +1,4 @@
1
- const { GAE_INSTANCE, K_SERVICE } = process.env
2
-
3
- export function isGAE(): boolean {
4
- return !!GAE_INSTANCE
5
- }
1
+ const { K_SERVICE } = process.env
6
2
 
7
3
  export function isCloudRun(): boolean {
8
4
  return !!K_SERVICE
package/bin/deploy-gae.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import '../dist/bin/deploy-gae.js'
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import '../dist/bin/deploy-prepare.js'
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import '../dist/bin/undeploy-gae.js'
@@ -1 +0,0 @@
1
- export {};
@@ -1,12 +0,0 @@
1
- import { _parseArgs } from '@naturalcycles/nodejs-lib/args';
2
- import { runScript } from '@naturalcycles/nodejs-lib/runScript';
3
- import { deployGae } from '../deploy/deployGae.js';
4
- import { deployHealthCheckCliOptions } from '../deploy/deployHealthCheck.js';
5
- import { deployPrepareCliOptions } from '../deploy/deployPrepare.js';
6
- runScript(async () => {
7
- const opt = _parseArgs({
8
- ...deployPrepareCliOptions,
9
- ...deployHealthCheckCliOptions,
10
- });
11
- await deployGae(opt);
12
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,19 +0,0 @@
1
- /*
2
-
3
- yarn deploy-prepare
4
-
5
- */
6
- import { _parseArgs } from '@naturalcycles/nodejs-lib/args';
7
- import { runScript } from '@naturalcycles/nodejs-lib/runScript';
8
- import { deployPrepare, deployPrepareCliOptions } from '../deploy/deployPrepare.js';
9
- runScript(async () => {
10
- const opt = _parseArgs(deployPrepareCliOptions);
11
- await deployPrepare(opt);
12
- });
13
- // deploy strategy
14
- // gae project: from config
15
- // gae service: from config -- branch name
16
- // gae version: automatic form date
17
- // yarn build-prod
18
- // yarn deploy-prepare && ./tmp/deploy/app.yaml && ./tmp/deploy/deployInfo.json && json2env
19
- //
@@ -1 +0,0 @@
1
- export {};
@@ -1,13 +0,0 @@
1
- import { _parseArgs } from '@naturalcycles/nodejs-lib/args';
2
- import { runScript } from '@naturalcycles/nodejs-lib/runScript';
3
- import { undeployGae } from '../deploy/deployGae.js';
4
- runScript(async () => {
5
- const { branch } = _parseArgs({
6
- branch: {
7
- type: 'string',
8
- demandOption: true,
9
- desc: `Because Github Actions delete event happens after the branch is already deleted - you need to pass it manually`,
10
- },
11
- });
12
- await undeployGae(branch);
13
- });
@@ -1,30 +0,0 @@
1
- import type { StringMap } from '@naturalcycles/js-lib/types';
2
- export interface BackendCfg {
3
- gaeProject: string;
4
- gaeProjectByBranch?: StringMap;
5
- /**
6
- * @example default
7
- */
8
- gaeService: string;
9
- gaeServiceByBranch?: StringMap;
10
- /**
11
- * List of file patterns to include in deployment.
12
- */
13
- files?: string[];
14
- appEnvDefault: string;
15
- appEnvByBranch?: StringMap;
16
- /**
17
- * List of branches to use timestamps in gae version names (to keep previous versions).
18
- */
19
- branchesWithTimestampVersions?: string[];
20
- /**
21
- * If true - branch names are not passed into deployed urls as is, but are hashed.
22
- */
23
- hashedBranches?: boolean;
24
- /**
25
- * Comma-separated list of env variables that will be passed to app.yaml from process.env
26
- */
27
- appYamlPassEnv?: string;
28
- }
29
- export declare const backendCfgSchema: any;
30
- export declare function getBackendCfg(projectDir?: string): BackendCfg;
@@ -1,23 +0,0 @@
1
- import { j } from '@naturalcycles/nodejs-lib/ajv';
2
- import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
3
- import { yaml2 } from '@naturalcycles/nodejs-lib/yaml2';
4
- export const backendCfgSchema = j.object({
5
- gaeProject: j.string(),
6
- gaeProjectByBranch: j.object.stringMap(j.string()).optional(),
7
- gaeService: j.string(),
8
- gaeServiceByBranch: j.object.stringMap(j.string()).optional(),
9
- files: j.array(j.string()).optional(),
10
- appEnvDefault: j.string(),
11
- appEnvByBranch: j.object.stringMap(j.string()).optional(),
12
- branchesWithTimestampVersions: j.array(j.string()).optional(),
13
- hashedBranches: j.boolean().optional(),
14
- appYamlPassEnv: j.string().optional(),
15
- });
16
- export function getBackendCfg(projectDir = '.') {
17
- const backendCfgYamlPath = `${projectDir}/backend.cfg.yaml`;
18
- fs2.requireFileToExist(backendCfgYamlPath);
19
- const backendCfg = {
20
- ...yaml2.readYaml(backendCfgYamlPath),
21
- };
22
- return backendCfgSchema.validate(backendCfg);
23
- }
@@ -1,29 +0,0 @@
1
- import type { UnixTimestamp } from '@naturalcycles/js-lib/types';
2
- export interface DeployInfo {
3
- gaeProject: string;
4
- gaeService: string;
5
- gaeVersion: string;
6
- versionUrl: string;
7
- serviceUrl: string;
8
- gitRev: string;
9
- gitBranch: string;
10
- /**
11
- * Unix timestamp of deployInfo.json being generated.
12
- */
13
- ts: UnixTimestamp;
14
- }
15
- export interface AppYaml extends Record<string, any> {
16
- runtime: string;
17
- service: string;
18
- inbound_services?: string[];
19
- instance_class?: string;
20
- automatic_scaling?: {
21
- min_instances?: number;
22
- max_instances?: number;
23
- };
24
- env_variables: {
25
- APP_ENV: string;
26
- DEBUG: string;
27
- [k: string]: string;
28
- };
29
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,7 +0,0 @@
1
- import type { BackendCfg } from './backend.cfg.util.js';
2
- import type { AppYaml, DeployInfo } from './deploy.model.js';
3
- export declare function createAndSaveDeployInfo(backendCfg: BackendCfg, targetDir: string): Promise<DeployInfo>;
4
- export declare function createDeployInfo(backendCfg: BackendCfg, overrideBranch?: string): Promise<DeployInfo>;
5
- export declare function createAndSaveAppYaml(backendCfg: BackendCfg, deployInfo: DeployInfo, projectDir: string, targetDir: string, appYamlPassEnv?: string): AppYaml;
6
- export declare function createAppYaml(backendCfg: BackendCfg, deployInfo: DeployInfo, projectDir: string, appYamlPassEnv?: string): AppYaml;
7
- export declare function validateGAEServiceName(serviceName: string): string;
@@ -1,151 +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
- const APP_YAML_DEFAULT = () => ({
10
- runtime: 'nodejs22',
11
- service: 'default',
12
- inbound_services: ['warmup'],
13
- instance_class: 'F1',
14
- automatic_scaling: {
15
- min_instances: 0,
16
- max_instances: 1,
17
- },
18
- env_variables: {
19
- APP_ENV: 'prod',
20
- DEBUG: 'app*,nc:*',
21
- DEBUG_HIDE_DATE: 'true',
22
- // DEBUG_COLORS: 'true',
23
- TZ: 'UTC',
24
- },
25
- });
26
- export async function createAndSaveDeployInfo(backendCfg, targetDir) {
27
- const deployInfo = await createDeployInfo(backendCfg);
28
- const deployInfoPath = `${targetDir}/deployInfo.json`;
29
- fs2.writeJson(deployInfoPath, deployInfo, { spaces: 2 });
30
- console.log(`saved ${dimGrey(deployInfoPath)}`);
31
- return deployInfo;
32
- }
33
- export async function createDeployInfo(backendCfg, overrideBranch) {
34
- const { simpleGit } = await import('simple-git'); // lazy load
35
- const git = simpleGit('.');
36
- const now = localTime.now();
37
- const gitBranch = overrideBranch || (await git.status()).current;
38
- const gitRev = (await git.revparse(['HEAD'])).slice(0, 7);
39
- let { gaeProject, gaeProjectByBranch = {}, gaeService, gaeServiceByBranch = {}, branchesWithTimestampVersions = [], } = backendCfg;
40
- gaeProject = gaeProjectByBranch[gitBranch] || gaeProject;
41
- if (gaeServiceByBranch[gitBranch]) {
42
- gaeService = validateGAEServiceName(gaeServiceByBranch[gitBranch]);
43
- }
44
- else {
45
- let branchName = gitBranch;
46
- if (backendCfg.hashedBranches) {
47
- // Obfuscates the branch name by hashing it.
48
- // If there are Jira issue names in the branch name, the first one found will be used as a prefix.
49
- const jiraIssue = gitBranch.match(/([Dd][Ee][Vv]-\d+)/)?.[0];
50
- const branchHash = sha256(gitBranch).slice(0, 10);
51
- branchName = [jiraIssue, branchHash].filter(Boolean).join('-');
52
- }
53
- gaeService = validateGAEServiceName([branchName, gaeService].join('--'));
54
- }
55
- let gaeVersion = '1';
56
- if (branchesWithTimestampVersions.includes(gitBranch)) {
57
- // May only contain lowercase letters, digits, and hyphens. Must begin and end with a letter or digit. Must not exceed 63 characters.
58
- gaeVersion = [
59
- // now.format('YYYYMMDD-HHmm'), // 20190521-1721
60
- now.toStringCompact().replace('_', '-'),
61
- gitRev,
62
- ].join('-');
63
- }
64
- const versionUrl = `https://${[gaeVersion, gaeService, gaeProject].join('-dot-')}.appspot.com`;
65
- // Check the 63-char limit
66
- const versionUrlString = [gaeVersion, gaeService, gaeProject].join('-dot-');
67
- _assert(versionUrlString.length <= 63, `versionUrl length should be <= 63 characters, but it's ${versionUrlString.length} instead: ${versionUrlString}`);
68
- const serviceUrl = `https://${[gaeService, gaeProject].join('-dot-')}.appspot.com`;
69
- const deployInfo = {
70
- gaeProject,
71
- gaeService,
72
- gaeVersion,
73
- versionUrl,
74
- serviceUrl,
75
- gitBranch,
76
- gitRev,
77
- ts: now.unix,
78
- };
79
- console.log({ deployInfo });
80
- return deployInfo;
81
- }
82
- export function createAndSaveAppYaml(backendCfg, deployInfo, projectDir, targetDir, appYamlPassEnv = '') {
83
- const appYaml = createAppYaml(backendCfg, deployInfo, projectDir, appYamlPassEnv);
84
- const appYamlPath = `${targetDir}/app.yaml`;
85
- yaml2.writeYaml(appYamlPath, appYaml);
86
- console.log(`saved ${dimGrey(appYamlPath)}`);
87
- return appYaml;
88
- }
89
- export function createAppYaml(backendCfg, deployInfo, projectDir, appYamlPassEnv = '') {
90
- const { appEnvDefault, appEnvByBranch = {} } = backendCfg;
91
- const { gaeService: service, gitBranch } = deployInfo;
92
- const { APP_ENV: processAppEnv } = process.env;
93
- const APP_ENV = processAppEnv || appEnvByBranch[gitBranch] || appEnvDefault;
94
- if (processAppEnv) {
95
- console.log(`using APP_ENV=${dimGrey(processAppEnv)} from process.env`);
96
- }
97
- const appYaml = APP_YAML_DEFAULT();
98
- // Check existing app.yaml
99
- const appYamlPath = `${projectDir}/app.yaml`;
100
- if (fs2.pathExists(appYamlPath)) {
101
- console.log(`merging-in ${dimGrey(appYamlPath)}`);
102
- _merge(appYaml, yaml2.readYaml(appYamlPath));
103
- }
104
- const appEnvYamlPath = `${projectDir}/app.${APP_ENV}.yaml`;
105
- if (fs2.pathExists(appEnvYamlPath)) {
106
- console.log(`merging-in ${dimGrey(appEnvYamlPath)}`);
107
- _merge(appYaml, yaml2.readYaml(appEnvYamlPath));
108
- }
109
- // appYamlPassEnv
110
- const passEnv = appYamlPassEnv
111
- .split(',')
112
- .filter(Boolean)
113
- // oxlint-disable-next-line unicorn/no-array-reduce
114
- .reduce((map, key) => {
115
- const v = process.env[key];
116
- if (!v) {
117
- throw new Error(`appYamlPassEnv.${key} is requested, but process.env.${key} is not defined!`);
118
- }
119
- map[key] = v;
120
- return map;
121
- }, {});
122
- if (Object.keys(passEnv).length) {
123
- console.log(`will merge ${white(String(Object.keys(passEnv).length))} process.env keys to app.yaml: ${dimGrey(Object.keys(passEnv).join(', '))}`);
124
- }
125
- _merge(appYaml, {
126
- service,
127
- env_variables: {
128
- APP_ENV,
129
- ...passEnv,
130
- },
131
- });
132
- // Redacted appYaml to not show up secrets
133
- console.log({
134
- appYaml: redactedAppYaml(appYaml),
135
- });
136
- return appYaml;
137
- }
138
- function redactedAppYaml(appYaml) {
139
- return {
140
- ...appYaml,
141
- env_variables: _mapValues(appYaml.env_variables || {}, (_k, v) => _truncate(v, 7)),
142
- };
143
- }
144
- export function validateGAEServiceName(serviceName) {
145
- // May only contain lowercase letters, digits, and hyphens. Must begin and end with a letter or digit. Must not exceed 63 characters.
146
- return serviceName
147
- .replaceAll('_', '-')
148
- .toLowerCase()
149
- .replaceAll(/[^0-9a-z-]/gi, '')
150
- .slice(0, 40);
151
- }
@@ -1,10 +0,0 @@
1
- import type { DeployHealthCheckOptions } from './deployHealthCheck.js';
2
- import type { DeployPrepareOptions } from './deployPrepare.js';
3
- export interface DeployGaeOptions extends DeployPrepareOptions, DeployHealthCheckOptions {
4
- }
5
- export declare function deployGae(opt?: DeployGaeOptions): Promise<void>;
6
- /**
7
- * Undeploys/removes the GAE service/version, using the same rules as deployGae.
8
- * Detects the service/version by the same criteria: branch name, backend.cfg.yaml, etc.
9
- */
10
- export declare function undeployGae(branch: string): Promise<void>;