@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.3339",
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.3339",
40
- "@cedarjs/babel-config": "8.0.0-canary.3339",
41
- "@cedarjs/cli-helpers": "8.0.0-canary.3339",
42
- "@cedarjs/internal": "8.0.0-canary.3339",
43
- "@cedarjs/prerender": "8.0.0-canary.3339",
44
- "@cedarjs/project-config": "8.0.0-canary.3339",
45
- "@cedarjs/structure": "8.0.0-canary.3339",
46
- "@cedarjs/telemetry": "8.0.0-canary.3339",
47
- "@cedarjs/utils": "8.0.0-canary.3339",
48
- "@cedarjs/vite": "8.0.0-canary.3339",
49
- "@cedarjs/web-server": "8.0.0-canary.3339",
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.3339",
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",