@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 healthCheckUrl = (serverConfig) => {
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 `http://localhost:${getConfig().api.port}${DEFAULT_HEALTH_CHECK_PATH}`;
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
- url,
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: ${url} did not respond successfully within ${timeoutSeconds} seconds of restarting the ${serverConfig.processNames?.join(", ")} process(es).`,
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 = (url, ssh, serverConfig, nextSteps) => {
269
+ const healthCheckTask = (target, ssh, serverConfig, nextSteps) => {
175
270
  return {
176
- title: url ? `Checking ${url}...` : "Checking health...",
271
+ title: target ? `Checking ${describeHealthCheckTarget(target)}...` : "Checking health...",
177
272
  task: async (_ctx, task) => {
178
- if (!url) {
273
+ if (!target) {
179
274
  return;
180
275
  }
181
276
  await waitForHealthCheck({
182
- url,
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: () => !url
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(healthCheckUrl(serverConfig), ssh, serverConfig, [
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 url = healthCheckUrl(serverConfig);
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 || !url,
709
- command: healthCheckTask(url, ssh, serverConfig, [
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
- healthCheckUrl,
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,
@@ -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:<[api].port>/graphql/health
132
- # on servers that host the api side. Set to \`false\` to skip the check
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.3427",
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.3427",
40
- "@cedarjs/babel-config": "8.0.0-canary.3427",
41
- "@cedarjs/cli-helpers": "8.0.0-canary.3427",
42
- "@cedarjs/internal": "8.0.0-canary.3427",
43
- "@cedarjs/prerender": "8.0.0-canary.3427",
44
- "@cedarjs/project-config": "8.0.0-canary.3427",
45
- "@cedarjs/structure": "8.0.0-canary.3427",
46
- "@cedarjs/telemetry": "8.0.0-canary.3427",
47
- "@cedarjs/utils": "8.0.0-canary.3427",
48
- "@cedarjs/vite": "8.0.0-canary.3427",
49
- "@cedarjs/web-server": "8.0.0-canary.3427",
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.3427",
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",