@naturalcycles/backend-lib 9.79.0 → 9.81.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.
- package/dist/deploy/deployHealthCheck.d.ts +1 -5
- package/dist/deploy/deployHealthCheck.js +3 -34
- package/dist/deploy/index.d.ts +2 -9
- package/dist/deploy/index.js +1 -6
- package/dist/express/createDefaultApp.d.ts +23 -0
- package/dist/express/createDefaultApp.js +5 -3
- package/dist/server/serverStatusMiddleware.d.ts +2 -2
- package/dist/server/serverStatusMiddleware.js +5 -14
- package/dist/util.d.ts +0 -1
- package/dist/util.js +1 -4
- package/package.json +3 -13
- package/src/admin/login.html +103 -76
- package/src/deploy/deployHealthCheck.ts +2 -48
- package/src/deploy/index.ts +2 -17
- package/src/express/createDefaultApp.ts +35 -3
- package/src/server/serverStatusMiddleware.ts +4 -20
- package/src/util.ts +1 -5
- package/bin/deploy-gae.js +0 -2
- package/bin/deploy-prepare.js +0 -2
- package/bin/undeploy-gae.js +0 -2
- package/dist/bin/deploy-gae.d.ts +0 -1
- package/dist/bin/deploy-gae.js +0 -12
- package/dist/bin/deploy-prepare.d.ts +0 -1
- package/dist/bin/deploy-prepare.js +0 -19
- package/dist/bin/undeploy-gae.d.ts +0 -1
- package/dist/bin/undeploy-gae.js +0 -13
- package/dist/deploy/backend.cfg.util.d.ts +0 -30
- package/dist/deploy/backend.cfg.util.js +0 -23
- package/dist/deploy/deploy.model.d.ts +0 -29
- package/dist/deploy/deploy.model.js +0 -1
- package/dist/deploy/deploy.util.d.ts +0 -7
- package/dist/deploy/deploy.util.js +0 -151
- package/dist/deploy/deployGae.d.ts +0 -10
- package/dist/deploy/deployGae.js +0 -90
- package/dist/deploy/deployInfo.util.d.ts +0 -1
- package/dist/deploy/deployInfo.util.js +0 -25
- package/dist/deploy/deployPrepare.d.ts +0 -13
- package/dist/deploy/deployPrepare.js +0 -85
- package/src/bin/deploy-gae.ts +0 -14
- package/src/bin/deploy-prepare.ts +0 -24
- package/src/bin/undeploy-gae.ts +0 -15
- package/src/deploy/backend.cfg.util.ts +0 -63
- package/src/deploy/deploy.model.ts +0 -32
- package/src/deploy/deploy.util.ts +0 -214
- package/src/deploy/deployGae.ts +0 -127
- package/src/deploy/deployInfo.util.ts +0 -27
- package/src/deploy/deployPrepare.ts +0 -113
- package/src/deploy/files-default/.gcloudignore +0 -7
|
@@ -14,10 +14,11 @@ import type {
|
|
|
14
14
|
BackendRequestHandler,
|
|
15
15
|
} from '../server/server.model.js'
|
|
16
16
|
import { simpleRequestLoggerMiddleware } from '../server/simpleRequestLoggerMiddleware.js'
|
|
17
|
-
import {
|
|
17
|
+
import { isCloudRun } from '../util.js'
|
|
18
18
|
|
|
19
19
|
const isTest = process.env['APP_ENV'] === 'test'
|
|
20
20
|
const isDev = process.env['APP_ENV'] === 'dev'
|
|
21
|
+
const isGAE = !!process.env['GAE_INSTANCE']
|
|
21
22
|
|
|
22
23
|
export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendApplication> {
|
|
23
24
|
const { sentryService } = cfg
|
|
@@ -30,7 +31,7 @@ export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendAppli
|
|
|
30
31
|
|
|
31
32
|
app.disable('etag')
|
|
32
33
|
app.disable('x-powered-by')
|
|
33
|
-
app.set('trust proxy',
|
|
34
|
+
app.set('trust proxy', cfg.trustProxy ?? 2)
|
|
34
35
|
|
|
35
36
|
// preHandlers
|
|
36
37
|
useHandlers(app, cfg.preHandlers)
|
|
@@ -69,8 +70,9 @@ export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendAppli
|
|
|
69
70
|
}),
|
|
70
71
|
)
|
|
71
72
|
|
|
72
|
-
if (!isGAE
|
|
73
|
+
if (!isGAE && (isTest || isCloudRun())) {
|
|
73
74
|
// compression is not used in AppEngine, because AppEngine provides it by default
|
|
75
|
+
// Cloud Run doesn't compress responses, so we do it ourselves
|
|
74
76
|
app.use(compressionMiddleware())
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -195,4 +197,34 @@ export interface DefaultAppCfg {
|
|
|
195
197
|
corsOptions?: CorsOptions
|
|
196
198
|
|
|
197
199
|
genericErrorMwCfg?: GenericErrorMiddlewareCfg
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Value for Express `trust proxy` setting, which defines how `req.ip`, `req.ips`,
|
|
203
|
+
* `req.protocol` and `req.hostname` are resolved from `X-Forwarded-*` headers.
|
|
204
|
+
*
|
|
205
|
+
* Defaults to `2` - trust exactly 2 hops: the socket peer (Google Front End) and the
|
|
206
|
+
* rightmost `X-Forwarded-For` entry (appended by the Google Cloud Load Balancer).
|
|
207
|
+
* This is the setup of Cloud Run behind a Load Balancer.
|
|
208
|
+
* Any further entries to the left could be injected by the client, so they are not trusted,
|
|
209
|
+
* which prevents IP address spoofing via `X-Forwarded-For` header.
|
|
210
|
+
*
|
|
211
|
+
* Do NOT set it to `true` in production - it trusts the leftmost `X-Forwarded-For` entry,
|
|
212
|
+
* which is fully client-controlled.
|
|
213
|
+
*
|
|
214
|
+
* Adjust it to match the hosting environment (number of proxies in front of the app).
|
|
215
|
+
*
|
|
216
|
+
* @default 2
|
|
217
|
+
*/
|
|
218
|
+
trustProxy?: TrustProxy
|
|
198
219
|
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Accepted values of Express `trust proxy` setting.
|
|
223
|
+
* See https://expressjs.com/en/guide/behind-proxies.html
|
|
224
|
+
*/
|
|
225
|
+
export type TrustProxy =
|
|
226
|
+
| boolean
|
|
227
|
+
| number
|
|
228
|
+
| string
|
|
229
|
+
| string[]
|
|
230
|
+
| ((ip: string | undefined, hopIndex: number) => boolean)
|
|
@@ -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(
|
|
22
|
+
export function serverStatusMiddleware(extra?: any): BackendRequestHandler {
|
|
27
23
|
return async (_req, res) => {
|
|
28
|
-
res.json(getServerStatusData(
|
|
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
|
-
|
|
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
package/bin/deploy-gae.js
DELETED
package/bin/deploy-prepare.js
DELETED
package/bin/undeploy-gae.js
DELETED
package/dist/bin/deploy-gae.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/bin/deploy-gae.js
DELETED
|
@@ -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 {};
|
package/dist/bin/undeploy-gae.js
DELETED
|
@@ -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>;
|
package/dist/deploy/deployGae.js
DELETED
|
@@ -1,90 +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 { deployHealthCheck } from './deployHealthCheck.js';
|
|
10
|
-
import { deployPrepare } from './deployPrepare.js';
|
|
11
|
-
export async function deployGae(opt = {}) {
|
|
12
|
-
const { logOnFailure, logOnSuccess } = opt;
|
|
13
|
-
// 1. build
|
|
14
|
-
const packageManager = detectPackageManager();
|
|
15
|
-
exec2.spawn(`${packageManager} build`);
|
|
16
|
-
// 2. deploy-prepare
|
|
17
|
-
const deployInfo = await deployPrepare();
|
|
18
|
-
const targetDir = './tmp/deploy';
|
|
19
|
-
const appYamlPath = `${targetDir}/app.yaml`;
|
|
20
|
-
const { gaeProject, gaeService, gaeVersion, versionUrl, serviceUrl } = deployInfo;
|
|
21
|
-
_objectAssign(opt, {
|
|
22
|
-
gaeProject,
|
|
23
|
-
gaeService,
|
|
24
|
-
gaeVersion,
|
|
25
|
-
});
|
|
26
|
-
await pRetry(async () => {
|
|
27
|
-
try {
|
|
28
|
-
exec2.spawn(`gcloud app deploy ${appYamlPath} --project ${gaeProject} --version ${gaeVersion} --quiet --no-promote`);
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
if (logOnFailure) {
|
|
32
|
-
logs(gaeProject, gaeService, gaeVersion);
|
|
33
|
-
}
|
|
34
|
-
throw err;
|
|
35
|
-
}
|
|
36
|
-
}, {
|
|
37
|
-
name: 'deploy',
|
|
38
|
-
maxAttempts: 2,
|
|
39
|
-
delay: 30_000,
|
|
40
|
-
// todo: this doesn't work, as the error is different from what is logged.
|
|
41
|
-
// We shoud somehow capture the logged text
|
|
42
|
-
predicate: err => _anyToError(err).message.includes('operation is already in progress'),
|
|
43
|
-
});
|
|
44
|
-
// Health check (versionUrl)
|
|
45
|
-
// yarn deploy-health-check --url $deployInfo_versionUrl --repeat 3 --timeoutSec 180 --intervalSec 2
|
|
46
|
-
await deployHealthCheck(versionUrl, opt);
|
|
47
|
-
// Only if "timestamped version" is used ('1' is default)
|
|
48
|
-
if (gaeVersion !== '1') {
|
|
49
|
-
// Rollout (promote versionUrl to serviceUrl)
|
|
50
|
-
// gcloud app services set-traffic $deployInfo_gaeService --project $deployInfo_gaeProject --splits $deployInfo_gaeVersion=1 --quiet
|
|
51
|
-
exec2.spawn(`gcloud app services set-traffic ${gaeService} --project ${gaeProject} --splits ${gaeVersion}=1 --quiet`);
|
|
52
|
-
// Health check (serviceUrl)
|
|
53
|
-
// yarn deploy-health-check --url $deployInfo_serviceUrl --repeat 3 --timeoutSec 60 --intervalSec 2
|
|
54
|
-
await deployHealthCheck(serviceUrl, opt);
|
|
55
|
-
}
|
|
56
|
-
appendToGithubSummary(`Deployed ${serviceUrl}`);
|
|
57
|
-
// Logs
|
|
58
|
-
if (logOnSuccess) {
|
|
59
|
-
logs(gaeProject, gaeService, gaeVersion);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Undeploys/removes the GAE service/version, using the same rules as deployGae.
|
|
64
|
-
* Detects the service/version by the same criteria: branch name, backend.cfg.yaml, etc.
|
|
65
|
-
*/
|
|
66
|
-
export async function undeployGae(branch) {
|
|
67
|
-
const projectDir = '.';
|
|
68
|
-
const backendCfg = getBackendCfg(projectDir);
|
|
69
|
-
const { gaeProject, gaeService, gaeVersion } = await createDeployInfo(backendCfg, branch);
|
|
70
|
-
// if (prod) {
|
|
71
|
-
// console.log(`undeployGae (branch: ${branch}): not removing prod version (safety check)`)
|
|
72
|
-
// return
|
|
73
|
-
// }
|
|
74
|
-
console.log(`undeployGae (branch: ${branch}): going to remove ${gaeProject}/${gaeService}/${gaeVersion}`);
|
|
75
|
-
exec2.spawn(`gcloud app versions delete --project ${gaeProject} --service ${gaeService} ${gaeVersion} --quiet`);
|
|
76
|
-
appendToGithubSummary(`removed ${gaeProject}/${gaeService}/${gaeVersion}`);
|
|
77
|
-
}
|
|
78
|
-
function logs(gaeProject, gaeService, gaeVersion) {
|
|
79
|
-
try {
|
|
80
|
-
exec2.spawn(`gcloud app logs read --project ${gaeProject} --service ${gaeService} --version ${gaeVersion}`);
|
|
81
|
-
}
|
|
82
|
-
catch { }
|
|
83
|
-
}
|
|
84
|
-
function detectPackageManager() {
|
|
85
|
-
const pj = fs2.readText('package.json');
|
|
86
|
-
if (pj.includes('pnpm')) {
|
|
87
|
-
return 'pnpm';
|
|
88
|
-
}
|
|
89
|
-
return 'yarn'; // yarn is still the default
|
|
90
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const getDeployInfo: any;
|
|
@@ -1,25 +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
|
-
export const getDeployInfo = _memoized((projectDir) => {
|
|
5
|
-
const deployInfoPath = `${projectDir}/deployInfo.json`;
|
|
6
|
-
try {
|
|
7
|
-
return fs2.readJson(deployInfoPath);
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
// console.error(`cannot read ${deployInfoPath}, returning empty version`)
|
|
11
|
-
return getDeployInfoStub();
|
|
12
|
-
}
|
|
13
|
-
});
|
|
14
|
-
function getDeployInfoStub(stub = '') {
|
|
15
|
-
return {
|
|
16
|
-
gaeProject: stub,
|
|
17
|
-
gaeService: stub,
|
|
18
|
-
gaeVersion: stub,
|
|
19
|
-
serviceUrl: stub,
|
|
20
|
-
versionUrl: stub,
|
|
21
|
-
gitBranch: stub,
|
|
22
|
-
gitRev: stub,
|
|
23
|
-
ts: localTime.nowUnix(),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { DeployInfo } from './deploy.model.js';
|
|
2
|
-
export interface DeployPrepareOptions {
|
|
3
|
-
projectDir?: string;
|
|
4
|
-
targetDir?: string;
|
|
5
|
-
createNpmrc?: boolean;
|
|
6
|
-
/**
|
|
7
|
-
* Comma-separated list of env variables that will be passed to app.yaml from process.env.
|
|
8
|
-
* Use it to pass secrets.
|
|
9
|
-
*/
|
|
10
|
-
appYamlPassEnv?: string;
|
|
11
|
-
}
|
|
12
|
-
export declare const deployPrepareCliOptions: CliOptions;
|
|
13
|
-
export declare function deployPrepare(opt?: DeployPrepareOptions): Promise<DeployInfo>;
|