@cedarjs/cli 8.0.0-canary.3339 → 8.0.0-canary.3341
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.
|
@@ -9,11 +9,15 @@ import { titleCase } from "title-case";
|
|
|
9
9
|
import { colors as c } from "@cedarjs/cli-helpers/colors";
|
|
10
10
|
import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
|
|
11
11
|
import { getPackageManager } from "@cedarjs/project-config/packageManager";
|
|
12
|
-
import { getPaths } from "../../../lib/index.js";
|
|
12
|
+
import { getConfig, getPaths } from "../../../lib/index.js";
|
|
13
13
|
const CONFIG_FILENAME = "deploy.toml";
|
|
14
|
+
const DOCS_URL = "https://cedarjs.com/docs/deploy/baremetal";
|
|
14
15
|
const SYMLINK_FLAGS = "-nsf";
|
|
15
16
|
const CURRENT_RELEASE_SYMLINK_NAME = "current";
|
|
16
17
|
const LIFECYCLE_HOOKS = ["before", "after"];
|
|
18
|
+
const DEFAULT_HEALTH_CHECK_PATH = "/graphql/health";
|
|
19
|
+
const HEALTH_CHECK_INTERVAL_MS = 2e3;
|
|
20
|
+
const HEALTH_CHECK_REQUEST_TIMEOUT_SECONDS = 5;
|
|
17
21
|
const WEB_DIST_DIRS = ["web/dist", "web/dist/browser"];
|
|
18
22
|
const BUILD_MANIFEST_FILENAME = "client-build-manifest.json";
|
|
19
23
|
const LINK_BATCH_SIZE = 100;
|
|
@@ -26,9 +30,11 @@ const DEFAULT_SERVER_CONFIG = {
|
|
|
26
30
|
monitorCommand: "pm2",
|
|
27
31
|
sides: ["api", "web"],
|
|
28
32
|
keepReleases: 5,
|
|
29
|
-
freeSpaceRequired: 2048
|
|
33
|
+
freeSpaceRequired: 2048,
|
|
34
|
+
healthCheckTimeout: 30
|
|
30
35
|
};
|
|
31
36
|
const pathJoin = path.posix.join;
|
|
37
|
+
const shellQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
32
38
|
const throwMissingConfig = (name) => {
|
|
33
39
|
throw new Error(
|
|
34
40
|
`"${name}" config option not set. See https://cedarjs.com/docs/deployment/baremetal#deploytoml`
|
|
@@ -45,6 +51,17 @@ const verifyConfig = (config, yargs) => {
|
|
|
45
51
|
}
|
|
46
52
|
return true;
|
|
47
53
|
};
|
|
54
|
+
const isHttpUrl = (value) => {
|
|
55
|
+
if (typeof value !== "string") {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const { protocol } = new URL(value);
|
|
60
|
+
return protocol === "http:" || protocol === "https:";
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
48
65
|
const verifyServerConfig = (config) => {
|
|
49
66
|
if (!config.host) {
|
|
50
67
|
throwMissingConfig("host");
|
|
@@ -58,6 +75,12 @@ const verifyServerConfig = (config) => {
|
|
|
58
75
|
if (!/^\d+$/.test(String(config.freeSpaceRequired))) {
|
|
59
76
|
throw new Error('"freeSpaceRequired" must be an integer >= 0');
|
|
60
77
|
}
|
|
78
|
+
if (config.healthCheckUrl !== void 0 && config.healthCheckUrl !== false && !isHttpUrl(config.healthCheckUrl)) {
|
|
79
|
+
throw new Error('"healthCheckUrl" must be an http(s) URL or `false`');
|
|
80
|
+
}
|
|
81
|
+
if (!/^\d+$/.test(String(config.healthCheckTimeout))) {
|
|
82
|
+
throw new Error('"healthCheckTimeout" must be an integer >= 0');
|
|
83
|
+
}
|
|
61
84
|
return true;
|
|
62
85
|
};
|
|
63
86
|
const symlinkCurrentCommand = async (dir, ssh, deployPath) => {
|
|
@@ -73,6 +96,84 @@ const restartProcessCommand = async (processName, ssh, serverConfig, deployPath)
|
|
|
73
96
|
processName
|
|
74
97
|
]);
|
|
75
98
|
};
|
|
99
|
+
const healthCheckUrl = (serverConfig) => {
|
|
100
|
+
if (serverConfig.healthCheckUrl === false) {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
if (serverConfig.healthCheckUrl) {
|
|
104
|
+
return serverConfig.healthCheckUrl;
|
|
105
|
+
}
|
|
106
|
+
if (!serverConfig.sides.includes("api")) {
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
return `http://localhost:${getConfig().api.port}${DEFAULT_HEALTH_CHECK_PATH}`;
|
|
110
|
+
};
|
|
111
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
112
|
+
const waitForHealthCheck = async ({
|
|
113
|
+
url,
|
|
114
|
+
timeoutSeconds,
|
|
115
|
+
ssh,
|
|
116
|
+
serverConfig,
|
|
117
|
+
nextSteps,
|
|
118
|
+
onFailedAttempt
|
|
119
|
+
}) => {
|
|
120
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
121
|
+
let lastFailure = "";
|
|
122
|
+
let keepTrying = true;
|
|
123
|
+
while (keepTrying) {
|
|
124
|
+
try {
|
|
125
|
+
await ssh.exec(serverConfig.path, "curl", [
|
|
126
|
+
"--fail",
|
|
127
|
+
"--location",
|
|
128
|
+
"--silent",
|
|
129
|
+
"--show-error",
|
|
130
|
+
"--output",
|
|
131
|
+
"/dev/null",
|
|
132
|
+
"--max-time",
|
|
133
|
+
String(HEALTH_CHECK_REQUEST_TIMEOUT_SECONDS),
|
|
134
|
+
shellQuote(url)
|
|
135
|
+
]);
|
|
136
|
+
return;
|
|
137
|
+
} catch (e) {
|
|
138
|
+
lastFailure = e instanceof Error ? e.message : String(e);
|
|
139
|
+
onFailedAttempt?.(lastFailure);
|
|
140
|
+
}
|
|
141
|
+
keepTrying = Date.now() + HEALTH_CHECK_INTERVAL_MS <= deadline;
|
|
142
|
+
if (keepTrying) {
|
|
143
|
+
await sleep(HEALTH_CHECK_INTERVAL_MS);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
throw new Error(
|
|
147
|
+
[
|
|
148
|
+
`Health check failed: ${url} did not respond successfully within ${timeoutSeconds} seconds of restarting the ${serverConfig.processNames?.join(", ")} process(es).`,
|
|
149
|
+
`Last attempt: ${lastFailure}`,
|
|
150
|
+
"",
|
|
151
|
+
...nextSteps,
|
|
152
|
+
`If your app serves its health check somewhere else, set \`healthCheckUrl\` in deploy.toml. Set it to \`false\` to skip the check. See ${DOCS_URL}#health-check`
|
|
153
|
+
].join("\n")
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
const healthCheckTask = (url, ssh, serverConfig, nextSteps) => {
|
|
157
|
+
return {
|
|
158
|
+
title: url ? `Checking ${url}...` : "Checking health...",
|
|
159
|
+
task: async (_ctx, task) => {
|
|
160
|
+
if (!url) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
await waitForHealthCheck({
|
|
164
|
+
url,
|
|
165
|
+
timeoutSeconds: parseInt(String(serverConfig.healthCheckTimeout), 10),
|
|
166
|
+
ssh,
|
|
167
|
+
serverConfig,
|
|
168
|
+
nextSteps,
|
|
169
|
+
onFailedAttempt: (reason) => {
|
|
170
|
+
task.output = reason;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
skip: () => !url
|
|
175
|
+
};
|
|
176
|
+
};
|
|
76
177
|
const serverConfigWithDefaults = (serverConfig, yargs) => {
|
|
77
178
|
return {
|
|
78
179
|
...DEFAULT_SERVER_CONFIG,
|
|
@@ -183,6 +284,11 @@ const rollbackTasks = (count, ssh, serverConfig) => {
|
|
|
183
284
|
}
|
|
184
285
|
});
|
|
185
286
|
}
|
|
287
|
+
tasks.push(
|
|
288
|
+
healthCheckTask(healthCheckUrl(serverConfig), ssh, serverConfig, [
|
|
289
|
+
"The release you rolled back to is live as `current`. Check the process logs on the server, or run the rollback again with `--rollback` to move one more release back."
|
|
290
|
+
])
|
|
291
|
+
);
|
|
186
292
|
}
|
|
187
293
|
return tasks;
|
|
188
294
|
};
|
|
@@ -239,7 +345,6 @@ const manifestAssetFiles = (manifestJson) => {
|
|
|
239
345
|
}
|
|
240
346
|
return [...files];
|
|
241
347
|
};
|
|
242
|
-
const shellQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
243
348
|
const readBuildManifest = async (ssh, releasePath) => {
|
|
244
349
|
for (const distDir of WEB_DIST_DIRS) {
|
|
245
350
|
let stdout;
|
|
@@ -579,6 +684,17 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
|
579
684
|
);
|
|
580
685
|
}
|
|
581
686
|
}
|
|
687
|
+
const url = healthCheckUrl(serverConfig);
|
|
688
|
+
tasks.push(
|
|
689
|
+
...commandWithLifecycleEvents({
|
|
690
|
+
name: "healthCheck",
|
|
691
|
+
config: { ...config, cmdPath: serverConfig.path },
|
|
692
|
+
skip: !yargs.restart || !url,
|
|
693
|
+
command: healthCheckTask(url, ssh, serverConfig, [
|
|
694
|
+
`The new release is live as \`current\`. Check the process logs on the server, or roll back to the previous release with \`${formatCedarCommand(["deploy", "baremetal", yargs.environment, "--rollback"])}\`.`
|
|
695
|
+
])
|
|
696
|
+
})
|
|
697
|
+
);
|
|
582
698
|
}
|
|
583
699
|
tasks.push(
|
|
584
700
|
...commandWithLifecycleEvents({
|
|
@@ -755,6 +871,7 @@ export {
|
|
|
755
871
|
commands,
|
|
756
872
|
deployTasks,
|
|
757
873
|
handler,
|
|
874
|
+
healthCheckUrl,
|
|
758
875
|
keepPreviousAssets,
|
|
759
876
|
lifecycleTask,
|
|
760
877
|
maintenanceTasks,
|
|
@@ -765,5 +882,6 @@ export {
|
|
|
765
882
|
throwMissingConfig,
|
|
766
883
|
verifyConfig,
|
|
767
884
|
verifyServerConfig,
|
|
885
|
+
waitForHealthCheck,
|
|
768
886
|
warnIfUnpushedCommits
|
|
769
887
|
};
|
|
@@ -26,6 +26,13 @@ const DEPLOY = `# This file contains config for a baremetal deployment
|
|
|
26
26
|
# * passphrase - used if your private key has a passphrase
|
|
27
27
|
# * agentForward - set to \`true\` to forward the client machine's ssh credentials
|
|
28
28
|
#
|
|
29
|
+
# After restarting the processes, the deploy requests the api side's health
|
|
30
|
+
# check endpoint on the server and fails if it doesn't respond in time:
|
|
31
|
+
#
|
|
32
|
+
# * healthCheckUrl - defaults to http://localhost:<[api].port>/graphql/health
|
|
33
|
+
# on servers that host the api side. Set to \`false\` to skip the check
|
|
34
|
+
# * healthCheckTimeout - seconds to keep retrying, defaults to 30
|
|
35
|
+
#
|
|
29
36
|
# See https://cedarjs.com/docs/deploy/baremetal for more info
|
|
30
37
|
|
|
31
38
|
[[production.servers]]
|
|
@@ -41,6 +48,7 @@ repo = "git@github.com:myorg/myapp.git"
|
|
|
41
48
|
branch = "main"
|
|
42
49
|
keepReleases = 5
|
|
43
50
|
freeSpaceRequired = 2048
|
|
51
|
+
healthCheckTimeout = 30
|
|
44
52
|
|
|
45
53
|
# If you have separate api and web servers:
|
|
46
54
|
#
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/cli",
|
|
3
|
-
"version": "8.0.0-canary.
|
|
3
|
+
"version": "8.0.0-canary.3341",
|
|
4
4
|
"description": "The CedarJS Command Line",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"@babel/preset-typescript": "7.29.7",
|
|
37
37
|
"@babel/traverse": "7.29.8",
|
|
38
38
|
"@babel/types": "7.29.8",
|
|
39
|
-
"@cedarjs/api-server": "8.0.0-canary.
|
|
40
|
-
"@cedarjs/babel-config": "8.0.0-canary.
|
|
41
|
-
"@cedarjs/cli-helpers": "8.0.0-canary.
|
|
42
|
-
"@cedarjs/internal": "8.0.0-canary.
|
|
43
|
-
"@cedarjs/prerender": "8.0.0-canary.
|
|
44
|
-
"@cedarjs/project-config": "8.0.0-canary.
|
|
45
|
-
"@cedarjs/structure": "8.0.0-canary.
|
|
46
|
-
"@cedarjs/telemetry": "8.0.0-canary.
|
|
47
|
-
"@cedarjs/utils": "8.0.0-canary.
|
|
48
|
-
"@cedarjs/vite": "8.0.0-canary.
|
|
49
|
-
"@cedarjs/web-server": "8.0.0-canary.
|
|
39
|
+
"@cedarjs/api-server": "8.0.0-canary.3341",
|
|
40
|
+
"@cedarjs/babel-config": "8.0.0-canary.3341",
|
|
41
|
+
"@cedarjs/cli-helpers": "8.0.0-canary.3341",
|
|
42
|
+
"@cedarjs/internal": "8.0.0-canary.3341",
|
|
43
|
+
"@cedarjs/prerender": "8.0.0-canary.3341",
|
|
44
|
+
"@cedarjs/project-config": "8.0.0-canary.3341",
|
|
45
|
+
"@cedarjs/structure": "8.0.0-canary.3341",
|
|
46
|
+
"@cedarjs/telemetry": "8.0.0-canary.3341",
|
|
47
|
+
"@cedarjs/utils": "8.0.0-canary.3341",
|
|
48
|
+
"@cedarjs/vite": "8.0.0-canary.3341",
|
|
49
|
+
"@cedarjs/web-server": "8.0.0-canary.3341",
|
|
50
50
|
"@listr2/prompt-adapter-enquirer": "4.3.3",
|
|
51
51
|
"@opentelemetry/api": "1.9.1",
|
|
52
52
|
"@opentelemetry/core": "2.11.0",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"yargs": "17.7.3"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
|
-
"@cedarjs/framework-tools": "8.0.0-canary.
|
|
97
|
+
"@cedarjs/framework-tools": "8.0.0-canary.3341",
|
|
98
98
|
"@prisma/dmmf": "7.10.0",
|
|
99
99
|
"@types/archiver": "^7.0.0",
|
|
100
100
|
"memfs": "4.80.0",
|