@cedarjs/cli 8.0.0-canary.3427 → 8.0.0-canary.3429
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.
|
@@ -8,6 +8,10 @@ import { env as envInterpolation } from "string-env-interpolation";
|
|
|
8
8
|
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
|
+
import {
|
|
12
|
+
API_PROCESS_META_FILE_NAME,
|
|
13
|
+
PROCESS_META_DIR_NAME
|
|
14
|
+
} from "@cedarjs/project-config";
|
|
11
15
|
import { getPackageManager } from "@cedarjs/project-config/packageManager";
|
|
12
16
|
import { getConfig, getPaths } from "../../../lib/index.js";
|
|
13
17
|
import { MONITOR_ADAPTERS, MONITORS, isMonitor } from "./monitors.js";
|
|
@@ -17,6 +21,7 @@ const SYMLINK_FLAGS = "-nsf";
|
|
|
17
21
|
const CURRENT_RELEASE_SYMLINK_NAME = "current";
|
|
18
22
|
const LIFECYCLE_HOOKS = ["before", "after"];
|
|
19
23
|
const DEFAULT_HEALTH_CHECK_PATH = "/graphql/health";
|
|
24
|
+
const GENERATED_DATA_DIRS = [".cedar", ".redwood"];
|
|
20
25
|
const HEALTH_CHECK_INTERVAL_MS = 2e3;
|
|
21
26
|
const HEALTH_CHECK_REQUEST_TIMEOUT_SECONDS = 5;
|
|
22
27
|
const WEB_DIST_DIRS = ["web/dist", "web/dist/browser"];
|
|
@@ -114,21 +119,95 @@ const monitorTask = (command, ssh, serverConfig, skip) => {
|
|
|
114
119
|
...skip ? { skip } : {}
|
|
115
120
|
};
|
|
116
121
|
};
|
|
117
|
-
const
|
|
122
|
+
const healthCheckTarget = (serverConfig) => {
|
|
118
123
|
if (serverConfig.healthCheckUrl === false) {
|
|
119
124
|
return void 0;
|
|
120
125
|
}
|
|
121
126
|
if (serverConfig.healthCheckUrl) {
|
|
122
|
-
return serverConfig.healthCheckUrl;
|
|
127
|
+
return { url: serverConfig.healthCheckUrl };
|
|
123
128
|
}
|
|
124
129
|
if (!serverConfig.sides.includes("api")) {
|
|
125
130
|
return void 0;
|
|
126
131
|
}
|
|
127
|
-
return
|
|
132
|
+
return { apiPath: DEFAULT_HEALTH_CHECK_PATH };
|
|
133
|
+
};
|
|
134
|
+
const describeHealthCheckTarget = (target) => {
|
|
135
|
+
return "url" in target ? target.url : `${target.apiPath} on the api server`;
|
|
136
|
+
};
|
|
137
|
+
const API_PROCESS_META_FILES = GENERATED_DATA_DIRS.map(
|
|
138
|
+
(dir) => [
|
|
139
|
+
CURRENT_RELEASE_SYMLINK_NAME,
|
|
140
|
+
dir,
|
|
141
|
+
PROCESS_META_DIR_NAME,
|
|
142
|
+
API_PROCESS_META_FILE_NAME
|
|
143
|
+
].join("/")
|
|
144
|
+
);
|
|
145
|
+
const API_SERVER_DIST_DIRS = [
|
|
146
|
+
"node_modules/@cedarjs/api-server/dist",
|
|
147
|
+
"api/node_modules/@cedarjs/api-server/dist"
|
|
148
|
+
].map((dir) => `${CURRENT_RELEASE_SYMLINK_NAME}/${dir}`);
|
|
149
|
+
const releaseWritesApiProcessMeta = async (ssh, serverConfig) => {
|
|
150
|
+
try {
|
|
151
|
+
await ssh.exec(serverConfig.path, "grep", [
|
|
152
|
+
"-rqs",
|
|
153
|
+
"writeApiProcessMeta",
|
|
154
|
+
...API_SERVER_DIST_DIRS
|
|
155
|
+
]);
|
|
156
|
+
return true;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
if (typeof e === "object" && e !== null && "exitCode" in e && (e.exitCode === 1 || e.exitCode === 2)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const readApiProcessMeta = async (ssh, serverConfig) => {
|
|
165
|
+
const [preferredFile, fallbackFile] = API_PROCESS_META_FILES;
|
|
166
|
+
let contents = "";
|
|
167
|
+
try {
|
|
168
|
+
const result = await ssh.exec(serverConfig.path, "cat", [
|
|
169
|
+
preferredFile,
|
|
170
|
+
"2>/dev/null",
|
|
171
|
+
"||",
|
|
172
|
+
"cat",
|
|
173
|
+
fallbackFile
|
|
174
|
+
]);
|
|
175
|
+
contents = result.stdout;
|
|
176
|
+
} catch {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`The api server hasn't written ${preferredFile} yet. It does that once it's listening.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const jsonStart = contents.indexOf("{");
|
|
182
|
+
let meta;
|
|
183
|
+
try {
|
|
184
|
+
meta = JSON.parse(jsonStart === -1 ? contents : contents.slice(jsonStart));
|
|
185
|
+
} catch (e) {
|
|
186
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
187
|
+
throw new Error(`${preferredFile} isn't valid JSON: ${message}`);
|
|
188
|
+
}
|
|
189
|
+
if (typeof meta !== "object" || meta === null || !("port" in meta) || !Number.isInteger(meta.port) || !("host" in meta) || typeof meta.host !== "string" || "rootPath" in meta && typeof meta.rootPath !== "string") {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`${preferredFile} doesn't hold the api server's port and host: ` + contents.trim()
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
port: Number(meta.port),
|
|
196
|
+
host: meta.host,
|
|
197
|
+
..."rootPath" in meta && typeof meta.rootPath === "string" ? { rootPath: meta.rootPath } : {}
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
const apiServerUrl = ({
|
|
201
|
+
port,
|
|
202
|
+
host,
|
|
203
|
+
rootPath = "/"
|
|
204
|
+
}, apiPath) => {
|
|
205
|
+
const hostname = ["", "::", "0.0.0.0"].includes(host) ? "localhost" : host.includes(":") ? `[${host}]` : host;
|
|
206
|
+
return `http://${hostname}:${port}${rootPath.replace(/\/+$/, "")}${apiPath}`;
|
|
128
207
|
};
|
|
129
208
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
130
209
|
const waitForHealthCheck = async ({
|
|
131
|
-
|
|
210
|
+
target,
|
|
132
211
|
timeoutSeconds,
|
|
133
212
|
ssh,
|
|
134
213
|
serverConfig,
|
|
@@ -138,8 +217,24 @@ const waitForHealthCheck = async ({
|
|
|
138
217
|
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
139
218
|
let lastFailure = "";
|
|
140
219
|
let keepTrying = true;
|
|
220
|
+
let writesApiProcessMeta;
|
|
221
|
+
const apiProcessMeta = async () => {
|
|
222
|
+
try {
|
|
223
|
+
return await readApiProcessMeta(ssh, serverConfig);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
writesApiProcessMeta ??= await releaseWritesApiProcessMeta(
|
|
226
|
+
ssh,
|
|
227
|
+
serverConfig
|
|
228
|
+
);
|
|
229
|
+
if (writesApiProcessMeta) {
|
|
230
|
+
throw e;
|
|
231
|
+
}
|
|
232
|
+
return { port: getConfig().api.port, host: "" };
|
|
233
|
+
}
|
|
234
|
+
};
|
|
141
235
|
while (keepTrying) {
|
|
142
236
|
try {
|
|
237
|
+
const url = "url" in target ? target.url : apiServerUrl(await apiProcessMeta(), target.apiPath);
|
|
143
238
|
await ssh.exec(serverConfig.path, "curl", [
|
|
144
239
|
"--fail",
|
|
145
240
|
"--location",
|
|
@@ -163,7 +258,7 @@ const waitForHealthCheck = async ({
|
|
|
163
258
|
}
|
|
164
259
|
throw new Error(
|
|
165
260
|
[
|
|
166
|
-
`Health check failed: ${
|
|
261
|
+
`Health check failed: ${describeHealthCheckTarget(target)} did not respond successfully within ${timeoutSeconds} seconds of restarting the ${serverConfig.processNames?.join(", ")} process(es).`,
|
|
167
262
|
`Last attempt: ${lastFailure}`,
|
|
168
263
|
"",
|
|
169
264
|
...nextSteps,
|
|
@@ -171,15 +266,15 @@ const waitForHealthCheck = async ({
|
|
|
171
266
|
].join("\n")
|
|
172
267
|
);
|
|
173
268
|
};
|
|
174
|
-
const healthCheckTask = (
|
|
269
|
+
const healthCheckTask = (target, ssh, serverConfig, nextSteps) => {
|
|
175
270
|
return {
|
|
176
|
-
title:
|
|
271
|
+
title: target ? `Checking ${describeHealthCheckTarget(target)}...` : "Checking health...",
|
|
177
272
|
task: async (_ctx, task) => {
|
|
178
|
-
if (!
|
|
273
|
+
if (!target) {
|
|
179
274
|
return;
|
|
180
275
|
}
|
|
181
276
|
await waitForHealthCheck({
|
|
182
|
-
|
|
277
|
+
target,
|
|
183
278
|
timeoutSeconds: parseInt(String(serverConfig.healthCheckTimeout), 10),
|
|
184
279
|
ssh,
|
|
185
280
|
serverConfig,
|
|
@@ -189,7 +284,7 @@ const healthCheckTask = (url, ssh, serverConfig, nextSteps) => {
|
|
|
189
284
|
}
|
|
190
285
|
});
|
|
191
286
|
},
|
|
192
|
-
skip: () => !
|
|
287
|
+
skip: () => !target
|
|
193
288
|
};
|
|
194
289
|
};
|
|
195
290
|
const serverConfigWithDefaults = (serverConfig, yargs) => {
|
|
@@ -282,6 +377,10 @@ const rollbackTasks = (count, ssh, serverConfig) => {
|
|
|
282
377
|
ssh,
|
|
283
378
|
serverConfig.path
|
|
284
379
|
);
|
|
380
|
+
await ssh.exec(serverConfig.path, "rm", [
|
|
381
|
+
"-f",
|
|
382
|
+
...API_PROCESS_META_FILES
|
|
383
|
+
]);
|
|
285
384
|
} else {
|
|
286
385
|
throw new Error(
|
|
287
386
|
`Cannot rollback ${rollbackCount} release(s): ${dirs.length - deployedIndex - 1} previous release(s) available`
|
|
@@ -296,7 +395,7 @@ const rollbackTasks = (count, ssh, serverConfig) => {
|
|
|
296
395
|
tasks.push(monitorTask(adapter.restart(processName), ssh, serverConfig));
|
|
297
396
|
}
|
|
298
397
|
tasks.push(
|
|
299
|
-
healthCheckTask(
|
|
398
|
+
healthCheckTask(healthCheckTarget(serverConfig), ssh, serverConfig, [
|
|
300
399
|
"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."
|
|
301
400
|
])
|
|
302
401
|
);
|
|
@@ -700,13 +799,13 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
|
700
799
|
);
|
|
701
800
|
}
|
|
702
801
|
}
|
|
703
|
-
const
|
|
802
|
+
const target = healthCheckTarget(serverConfig);
|
|
704
803
|
tasks.push(
|
|
705
804
|
...commandWithLifecycleEvents({
|
|
706
805
|
name: "healthCheck",
|
|
707
806
|
config: { ...config, cmdPath: serverConfig.path },
|
|
708
|
-
skip: !yargs.restart || !
|
|
709
|
-
command: healthCheckTask(
|
|
807
|
+
skip: !yargs.restart || !target,
|
|
808
|
+
command: healthCheckTask(target, ssh, serverConfig, [
|
|
710
809
|
`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"])}\`.`
|
|
711
810
|
])
|
|
712
811
|
})
|
|
@@ -882,16 +981,20 @@ const handler = async (yargs) => {
|
|
|
882
981
|
export {
|
|
883
982
|
DEFAULT_SERVER_CONFIG,
|
|
884
983
|
RELEASE_DIR_PATTERN,
|
|
984
|
+
apiServerUrl,
|
|
885
985
|
commandWithLifecycleEvents,
|
|
886
986
|
commands,
|
|
887
987
|
deployTasks,
|
|
988
|
+
describeHealthCheckTarget,
|
|
888
989
|
handler,
|
|
889
|
-
|
|
990
|
+
healthCheckTarget,
|
|
890
991
|
keepPreviousAssets,
|
|
891
992
|
lifecycleTask,
|
|
892
993
|
maintenanceTasks,
|
|
893
994
|
manifestAssetFiles,
|
|
894
995
|
parseConfig,
|
|
996
|
+
readApiProcessMeta,
|
|
997
|
+
releaseWritesApiProcessMeta,
|
|
895
998
|
rollbackTasks,
|
|
896
999
|
serverConfigWithDefaults,
|
|
897
1000
|
throwMissingConfig,
|
package/dist/commands/serve.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as apiServerCLIConfig from "@cedarjs/api-server/apiCliConfig";
|
|
|
7
7
|
import * as bothServerCLIConfig from "@cedarjs/api-server/bothCliConfig";
|
|
8
8
|
import { colors as c } from "@cedarjs/cli-helpers/colors";
|
|
9
9
|
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
|
|
10
|
+
import { writeApiProcessMeta } from "@cedarjs/project-config";
|
|
10
11
|
import * as webServerCLIConfig from "@cedarjs/web-server";
|
|
11
12
|
import { getPaths, getConfig } from "../lib/index.js";
|
|
12
13
|
import { serverFileExists } from "../lib/project.js";
|
|
@@ -46,6 +47,17 @@ async function startUDServer(entryPath, host, port) {
|
|
|
46
47
|
});
|
|
47
48
|
server.serve();
|
|
48
49
|
await server.ready();
|
|
50
|
+
const address = server.node?.server?.address();
|
|
51
|
+
try {
|
|
52
|
+
writeApiProcessMeta(
|
|
53
|
+
address && typeof address !== "string" ? { port: address.port, host: address.address } : { port, host }
|
|
54
|
+
);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
57
|
+
console.warn(
|
|
58
|
+
c.warning(`Could not write the api process meta file: ${message}`)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
49
61
|
return server;
|
|
50
62
|
}
|
|
51
63
|
const command = "serve [side]";
|
|
@@ -128,8 +128,9 @@ const deployToml = ({
|
|
|
128
128
|
# After restarting the processes, the deploy requests the api side's health
|
|
129
129
|
# check endpoint on the server and fails if it doesn't respond in time:
|
|
130
130
|
#
|
|
131
|
-
# * healthCheckUrl - defaults to http://localhost:<
|
|
132
|
-
#
|
|
131
|
+
# * healthCheckUrl - defaults to http://localhost:<port>/graphql/health on
|
|
132
|
+
# servers that host the api side, at the port their api server listens on.
|
|
133
|
+
# Set to \`false\` to skip the check
|
|
133
134
|
# * healthCheckTimeout - seconds to keep retrying, defaults to 30
|
|
134
135
|
#
|
|
135
136
|
# See https://cedarjs.com/docs/deploy/baremetal for more info
|
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.3429",
|
|
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.10",
|
|
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.3429",
|
|
40
|
+
"@cedarjs/babel-config": "8.0.0-canary.3429",
|
|
41
|
+
"@cedarjs/cli-helpers": "8.0.0-canary.3429",
|
|
42
|
+
"@cedarjs/internal": "8.0.0-canary.3429",
|
|
43
|
+
"@cedarjs/prerender": "8.0.0-canary.3429",
|
|
44
|
+
"@cedarjs/project-config": "8.0.0-canary.3429",
|
|
45
|
+
"@cedarjs/structure": "8.0.0-canary.3429",
|
|
46
|
+
"@cedarjs/telemetry": "8.0.0-canary.3429",
|
|
47
|
+
"@cedarjs/utils": "8.0.0-canary.3429",
|
|
48
|
+
"@cedarjs/vite": "8.0.0-canary.3429",
|
|
49
|
+
"@cedarjs/web-server": "8.0.0-canary.3429",
|
|
50
50
|
"@listr2/prompt-adapter-enquirer": "4.3.3",
|
|
51
51
|
"@opentelemetry/api": "1.9.1",
|
|
52
52
|
"@opentelemetry/core": "2.12.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.3429",
|
|
98
98
|
"@prisma/dmmf": "7.10.0",
|
|
99
99
|
"@types/archiver": "^7.0.0",
|
|
100
100
|
"memfs": "4.80.0",
|