@cedarjs/cli 7.0.1-next.314 → 7.0.1-next.466
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/commands/deploy/baremetal/baremetalHandler.js +375 -76
- package/dist/commands/deploy/baremetal/monitors.js +66 -0
- package/dist/commands/deploy/baremetal.js +6 -0
- package/dist/commands/destroy/scaffold/scaffoldHandler.js +63 -25
- package/dist/commands/dev/devHandler.js +13 -1
- package/dist/commands/generate/dbAuth/dbAuth.js +6 -2
- package/dist/commands/generate/dbAuth/dbAuthHandler.js +57 -14
- package/dist/commands/generate/dbAuth/templates/changePassword.tsx.template +131 -0
- package/dist/commands/generate/dbAuth/templates/login.webAuthn.tsx.template +20 -12
- package/dist/commands/generate/directive/directive.js +15 -2
- package/dist/commands/generate/helpers.js +26 -0
- package/dist/commands/generate/ogImage/templates/ogImage.og.tsx.template +1 -1
- package/dist/commands/generate/package/templates/tsconfig.json.template +1 -0
- package/dist/commands/generate/realtime/templates/liveQueries/blank/blank.service.ts.template +12 -4
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.service.ts.template +2 -2
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.ts.template +3 -3
- package/dist/commands/generate/scaffold/scaffold.js +1 -1
- package/dist/commands/generate/scaffold/scaffoldHandler.js +83 -44
- package/dist/commands/generate/scaffold/templates/assets/scaffold.tailwind.css.template +12 -7
- package/dist/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +7 -7
- package/dist/commands/generate/scaffold/templates/components/NameForm.tsx.template +7 -6
- package/dist/commands/generate/scaffold/templates/layouts/NamesScaffoldLayout.tsx.template +18 -0
- package/dist/commands/generate/scaffold/templates/lib/formatters.tsx.template +3 -3
- package/dist/commands/generate/script/templates/tsconfig.json.template +1 -0
- package/dist/commands/generate/sdl/sdlHandler.js +8 -3
- package/dist/commands/generate/service/serviceHandler.js +32 -11
- package/dist/commands/generate/service/templates/service.ts.template +21 -8
- package/dist/commands/generate/service/templates/test.ts.template +7 -5
- package/dist/commands/jobsHandler.js +5 -3
- package/dist/commands/setup/cache/templates/memcached.ts.template +14 -17
- package/dist/commands/setup/cache/templates/redis.ts.template +12 -20
- package/dist/commands/setup/deploy/helpers/index.js +3 -1
- package/dist/commands/setup/deploy/providers/baremetal.js +7 -0
- package/dist/commands/setup/deploy/providers/baremetalHandler.js +128 -20
- package/dist/commands/setup/deploy/providers/netlifyHandler.js +25 -3
- package/dist/commands/setup/deploy/templates/baremetal.js +119 -9
- package/dist/commands/setup/deploy/templates/netlifyUD.js +3 -0
- package/dist/commands/setup/jobs/jobsHandler.js +2 -0
- package/dist/commands/setup/monitoring/sentry/sentryHandler.js +5 -2
- package/dist/commands/setup/monitoring/sentry/templates/sentryApi.ts.template +1 -4
- package/dist/commands/setup/monitoring/sentry/templates/sentryWeb.ts.template +7 -4
- package/dist/commands/setup/realtime/templates/defer/fastAndSlowFields/fastAndSlowFields.ts.template +4 -1
- package/dist/commands/setup/realtime/templates/liveQueries/auctions/auctions.ts.template +12 -4
- package/dist/commands/setup/realtime/templates/subscriptions/newMessage/newMessage.ts.template +1 -1
- package/dist/commands/setup/tenancy/signupCodemod.js +2 -1
- package/dist/commands/setup/tenancy/templates/InvitePage.tsx.template +10 -1
- package/dist/commands/setup/tenancy/templates/organizations.scenarios.ts.template +0 -2
- package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +0 -8
- package/dist/commands/setup/tenancy/templates/organizations.ts.template +1 -1
- package/dist/commands/setup/tenancy/templates/requireMembership.test.ts.template +1 -1
- package/dist/commands/setup/tenancy/tenancyHandler.js +1 -1
- package/dist/commands/setup/ui/libraries/tailwindcssHandler.js +61 -81
- package/dist/commands/setup/uploads/gitignore.js +17 -0
- package/dist/commands/setup/uploads/serverFile.js +93 -27
- package/dist/commands/setup/uploads/templates/uploads.ts.template +14 -5
- package/dist/commands/setup/uploads/uploadsHandler.js +22 -1
- package/dist/lib/index.js +10 -6
- package/package.json +24 -24
- package/dist/commands/setup/ui/templates/postcss.config.cjs.template +0 -9
|
@@ -9,21 +9,34 @@ 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
|
+
import { MONITOR_ADAPTERS, MONITORS, isMonitor } from "./monitors.js";
|
|
13
14
|
const CONFIG_FILENAME = "deploy.toml";
|
|
15
|
+
const DOCS_URL = "https://cedarjs.com/docs/deploy/baremetal";
|
|
14
16
|
const SYMLINK_FLAGS = "-nsf";
|
|
15
17
|
const CURRENT_RELEASE_SYMLINK_NAME = "current";
|
|
16
18
|
const LIFECYCLE_HOOKS = ["before", "after"];
|
|
19
|
+
const DEFAULT_HEALTH_CHECK_PATH = "/graphql/health";
|
|
20
|
+
const HEALTH_CHECK_INTERVAL_MS = 2e3;
|
|
21
|
+
const HEALTH_CHECK_REQUEST_TIMEOUT_SECONDS = 5;
|
|
22
|
+
const WEB_DIST_DIRS = ["web/dist", "web/dist/browser"];
|
|
23
|
+
const BUILD_MANIFEST_FILENAME = "client-build-manifest.json";
|
|
24
|
+
const LINK_BATCH_SIZE = 100;
|
|
25
|
+
const RELEASE_DIR_PATTERN = "^[0-9]{14}$";
|
|
26
|
+
const releaseDirRegExp = new RegExp(RELEASE_DIR_PATTERN);
|
|
27
|
+
const DEFAULT_MONITOR = "pm2";
|
|
17
28
|
const DEFAULT_SERVER_CONFIG = {
|
|
18
29
|
port: 22,
|
|
19
30
|
branch: "main",
|
|
20
31
|
packageManagerCommand: getPackageManager(),
|
|
21
|
-
|
|
32
|
+
monitor: DEFAULT_MONITOR,
|
|
22
33
|
sides: ["api", "web"],
|
|
23
34
|
keepReleases: 5,
|
|
24
|
-
freeSpaceRequired: 2048
|
|
35
|
+
freeSpaceRequired: 2048,
|
|
36
|
+
healthCheckTimeout: 30
|
|
25
37
|
};
|
|
26
38
|
const pathJoin = path.posix.join;
|
|
39
|
+
const shellQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
27
40
|
const throwMissingConfig = (name) => {
|
|
28
41
|
throw new Error(
|
|
29
42
|
`"${name}" config option not set. See https://cedarjs.com/docs/deployment/baremetal#deploytoml`
|
|
@@ -40,6 +53,17 @@ const verifyConfig = (config, yargs) => {
|
|
|
40
53
|
}
|
|
41
54
|
return true;
|
|
42
55
|
};
|
|
56
|
+
const isHttpUrl = (value) => {
|
|
57
|
+
if (typeof value !== "string") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const { protocol } = new URL(value);
|
|
62
|
+
return protocol === "http:" || protocol === "https:";
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
43
67
|
const verifyServerConfig = (config) => {
|
|
44
68
|
if (!config.host) {
|
|
45
69
|
throwMissingConfig("host");
|
|
@@ -53,6 +77,17 @@ const verifyServerConfig = (config) => {
|
|
|
53
77
|
if (!/^\d+$/.test(String(config.freeSpaceRequired))) {
|
|
54
78
|
throw new Error('"freeSpaceRequired" must be an integer >= 0');
|
|
55
79
|
}
|
|
80
|
+
if (config.healthCheckUrl !== void 0 && config.healthCheckUrl !== false && !isHttpUrl(config.healthCheckUrl)) {
|
|
81
|
+
throw new Error('"healthCheckUrl" must be an http(s) URL or `false`');
|
|
82
|
+
}
|
|
83
|
+
if (!/^\d+$/.test(String(config.healthCheckTimeout))) {
|
|
84
|
+
throw new Error('"healthCheckTimeout" must be an integer >= 0');
|
|
85
|
+
}
|
|
86
|
+
if (!isMonitor(config.monitor)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`"monitor" must be one of ${MONITORS.map((monitor) => `"${monitor}"`).join(", ")}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
56
91
|
return true;
|
|
57
92
|
};
|
|
58
93
|
const symlinkCurrentCommand = async (dir, ssh, deployPath) => {
|
|
@@ -62,17 +97,110 @@ const symlinkCurrentCommand = async (dir, ssh, deployPath) => {
|
|
|
62
97
|
CURRENT_RELEASE_SYMLINK_NAME
|
|
63
98
|
]);
|
|
64
99
|
};
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
const monitorAdapter = (serverConfig) => MONITOR_ADAPTERS[serverConfig.monitor];
|
|
101
|
+
const runMonitorCommand = async (command, ssh, serverConfig) => {
|
|
102
|
+
return await ssh.exec(
|
|
103
|
+
serverConfig.path,
|
|
104
|
+
serverConfig.monitorCommand,
|
|
105
|
+
command.args
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
const monitorTask = (command, ssh, serverConfig, skip) => {
|
|
109
|
+
return {
|
|
110
|
+
title: command.title,
|
|
111
|
+
task: async () => {
|
|
112
|
+
await runMonitorCommand(command, ssh, serverConfig);
|
|
113
|
+
},
|
|
114
|
+
...skip ? { skip } : {}
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
const healthCheckUrl = (serverConfig) => {
|
|
118
|
+
if (serverConfig.healthCheckUrl === false) {
|
|
119
|
+
return void 0;
|
|
120
|
+
}
|
|
121
|
+
if (serverConfig.healthCheckUrl) {
|
|
122
|
+
return serverConfig.healthCheckUrl;
|
|
123
|
+
}
|
|
124
|
+
if (!serverConfig.sides.includes("api")) {
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
return `http://localhost:${getConfig().api.port}${DEFAULT_HEALTH_CHECK_PATH}`;
|
|
128
|
+
};
|
|
129
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
130
|
+
const waitForHealthCheck = async ({
|
|
131
|
+
url,
|
|
132
|
+
timeoutSeconds,
|
|
133
|
+
ssh,
|
|
134
|
+
serverConfig,
|
|
135
|
+
nextSteps,
|
|
136
|
+
onFailedAttempt
|
|
137
|
+
}) => {
|
|
138
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
139
|
+
let lastFailure = "";
|
|
140
|
+
let keepTrying = true;
|
|
141
|
+
while (keepTrying) {
|
|
142
|
+
try {
|
|
143
|
+
await ssh.exec(serverConfig.path, "curl", [
|
|
144
|
+
"--fail",
|
|
145
|
+
"--location",
|
|
146
|
+
"--silent",
|
|
147
|
+
"--show-error",
|
|
148
|
+
"--output",
|
|
149
|
+
"/dev/null",
|
|
150
|
+
"--max-time",
|
|
151
|
+
String(HEALTH_CHECK_REQUEST_TIMEOUT_SECONDS),
|
|
152
|
+
shellQuote(url)
|
|
153
|
+
]);
|
|
154
|
+
return;
|
|
155
|
+
} catch (e) {
|
|
156
|
+
lastFailure = e instanceof Error ? e.message : String(e);
|
|
157
|
+
onFailedAttempt?.(lastFailure);
|
|
158
|
+
}
|
|
159
|
+
keepTrying = Date.now() + HEALTH_CHECK_INTERVAL_MS <= deadline;
|
|
160
|
+
if (keepTrying) {
|
|
161
|
+
await sleep(HEALTH_CHECK_INTERVAL_MS);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw new Error(
|
|
165
|
+
[
|
|
166
|
+
`Health check failed: ${url} did not respond successfully within ${timeoutSeconds} seconds of restarting the ${serverConfig.processNames?.join(", ")} process(es).`,
|
|
167
|
+
`Last attempt: ${lastFailure}`,
|
|
168
|
+
"",
|
|
169
|
+
...nextSteps,
|
|
170
|
+
`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`
|
|
171
|
+
].join("\n")
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
const healthCheckTask = (url, ssh, serverConfig, nextSteps) => {
|
|
175
|
+
return {
|
|
176
|
+
title: url ? `Checking ${url}...` : "Checking health...",
|
|
177
|
+
task: async (_ctx, task) => {
|
|
178
|
+
if (!url) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
await waitForHealthCheck({
|
|
182
|
+
url,
|
|
183
|
+
timeoutSeconds: parseInt(String(serverConfig.healthCheckTimeout), 10),
|
|
184
|
+
ssh,
|
|
185
|
+
serverConfig,
|
|
186
|
+
nextSteps,
|
|
187
|
+
onFailedAttempt: (reason) => {
|
|
188
|
+
task.output = reason;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
skip: () => !url
|
|
193
|
+
};
|
|
70
194
|
};
|
|
71
195
|
const serverConfigWithDefaults = (serverConfig, yargs) => {
|
|
196
|
+
const monitor = serverConfig.monitor ?? DEFAULT_SERVER_CONFIG.monitor;
|
|
197
|
+
const monitorCommand = serverConfig.monitorCommand ?? (isMonitor(monitor) ? MONITOR_ADAPTERS[monitor].defaultCommand : "");
|
|
72
198
|
return {
|
|
73
199
|
...DEFAULT_SERVER_CONFIG,
|
|
74
200
|
...serverConfig,
|
|
75
|
-
branch: yargs.branch || serverConfig.branch || DEFAULT_SERVER_CONFIG.branch
|
|
201
|
+
branch: yargs.branch || serverConfig.branch || DEFAULT_SERVER_CONFIG.branch,
|
|
202
|
+
monitor,
|
|
203
|
+
monitorCommand
|
|
76
204
|
};
|
|
77
205
|
};
|
|
78
206
|
const maintenanceTasks = (status, ssh, serverConfig) => {
|
|
@@ -94,26 +222,24 @@ const maintenanceTasks = (status, ssh, serverConfig) => {
|
|
|
94
222
|
}
|
|
95
223
|
});
|
|
96
224
|
if (serverConfig.processNames) {
|
|
97
|
-
tasks.push(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
});
|
|
225
|
+
tasks.push(
|
|
226
|
+
monitorTask(
|
|
227
|
+
monitorAdapter(serverConfig).stop(serverConfig.processNames),
|
|
228
|
+
ssh,
|
|
229
|
+
serverConfig
|
|
230
|
+
)
|
|
231
|
+
);
|
|
106
232
|
}
|
|
107
233
|
} else if (status === "down") {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
serverConfig
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
234
|
+
if (serverConfig.processNames) {
|
|
235
|
+
tasks.push(
|
|
236
|
+
monitorTask(
|
|
237
|
+
monitorAdapter(serverConfig).start(serverConfig.processNames),
|
|
238
|
+
ssh,
|
|
239
|
+
serverConfig
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
117
243
|
if (serverConfig.processNames) {
|
|
118
244
|
tasks.push({
|
|
119
245
|
title: `Disabling maintenance page...`,
|
|
@@ -141,8 +267,13 @@ const rollbackTasks = (count, ssh, serverConfig) => {
|
|
|
141
267
|
title: `Rolling back ${rollbackCount} release(s)...`,
|
|
142
268
|
task: async () => {
|
|
143
269
|
const currentLink = (await ssh.exec(serverConfig.path, "readlink", ["-f", "current"])).stdout.split("/").pop();
|
|
144
|
-
const dirs = (await ssh.exec(serverConfig.path, "ls", ["-t"])).stdout.split("\n").filter((
|
|
270
|
+
const dirs = (await ssh.exec(serverConfig.path, "ls", ["-t"])).stdout.split("\n").filter((dir) => releaseDirRegExp.test(dir));
|
|
145
271
|
const deployedIndex = dirs.indexOf(currentLink ?? "");
|
|
272
|
+
if (deployedIndex === -1) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Cannot rollback: \`current\` points to "${currentLink}", which is not a timestamp-named release directory`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
146
277
|
const rollbackIndex = deployedIndex + rollbackCount;
|
|
147
278
|
if (dirs[rollbackIndex]) {
|
|
148
279
|
console.info("Setting symlink");
|
|
@@ -153,26 +284,22 @@ const rollbackTasks = (count, ssh, serverConfig) => {
|
|
|
153
284
|
);
|
|
154
285
|
} else {
|
|
155
286
|
throw new Error(
|
|
156
|
-
`Cannot rollback ${rollbackCount} release(s): ${dirs.length -
|
|
287
|
+
`Cannot rollback ${rollbackCount} release(s): ${dirs.length - deployedIndex - 1} previous release(s) available`
|
|
157
288
|
);
|
|
158
289
|
}
|
|
159
290
|
}
|
|
160
291
|
}
|
|
161
292
|
];
|
|
162
293
|
if (serverConfig.processNames) {
|
|
294
|
+
const adapter = monitorAdapter(serverConfig);
|
|
163
295
|
for (const processName of serverConfig.processNames) {
|
|
164
|
-
tasks.push(
|
|
165
|
-
title: `Restarting ${processName} process...`,
|
|
166
|
-
task: async () => {
|
|
167
|
-
await restartProcessCommand(
|
|
168
|
-
processName,
|
|
169
|
-
ssh,
|
|
170
|
-
serverConfig,
|
|
171
|
-
serverConfig.path
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
296
|
+
tasks.push(monitorTask(adapter.restart(processName), ssh, serverConfig));
|
|
175
297
|
}
|
|
298
|
+
tasks.push(
|
|
299
|
+
healthCheckTask(healthCheckUrl(serverConfig), ssh, serverConfig, [
|
|
300
|
+
"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
|
+
])
|
|
302
|
+
);
|
|
176
303
|
}
|
|
177
304
|
return tasks;
|
|
178
305
|
};
|
|
@@ -203,6 +330,134 @@ const commandWithLifecycleEvents = ({
|
|
|
203
330
|
tasks.push(lifecycleTask("after", name, skip, config));
|
|
204
331
|
return tasks.flat().filter((t) => Boolean(t));
|
|
205
332
|
};
|
|
333
|
+
const manifestAssetFiles = (manifestJson) => {
|
|
334
|
+
const manifest = JSON.parse(manifestJson);
|
|
335
|
+
const files = /* @__PURE__ */ new Set();
|
|
336
|
+
if (!manifest || typeof manifest !== "object") {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
for (const entry of Object.values(manifest)) {
|
|
340
|
+
if (!entry || typeof entry !== "object") {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if ("file" in entry && typeof entry.file === "string") {
|
|
344
|
+
files.add(entry.file);
|
|
345
|
+
}
|
|
346
|
+
for (const key of ["css", "assets"]) {
|
|
347
|
+
if (!(key in entry) || !Array.isArray(entry[key])) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
for (const item of entry[key]) {
|
|
351
|
+
if (typeof item === "string") {
|
|
352
|
+
files.add(item);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return [...files];
|
|
358
|
+
};
|
|
359
|
+
const readBuildManifest = async (ssh, releasePath) => {
|
|
360
|
+
for (const distDir of WEB_DIST_DIRS) {
|
|
361
|
+
let stdout;
|
|
362
|
+
try {
|
|
363
|
+
;
|
|
364
|
+
({ stdout } = await ssh.exec(pathJoin(releasePath, distDir), "cat", [
|
|
365
|
+
BUILD_MANIFEST_FILENAME
|
|
366
|
+
]));
|
|
367
|
+
} catch {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const jsonStart = stdout.indexOf("{");
|
|
371
|
+
const json = jsonStart === -1 ? stdout : stdout.slice(jsonStart);
|
|
372
|
+
try {
|
|
373
|
+
return { distDir, files: manifestAssetFiles(json) };
|
|
374
|
+
} catch (e) {
|
|
375
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Could not parse ${pathJoin(releasePath, distDir, BUILD_MANIFEST_FILENAME)}: ${message}`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return void 0;
|
|
382
|
+
};
|
|
383
|
+
const keepPreviousAssets = async (yargs, ssh, serverConfig) => {
|
|
384
|
+
const newReleasePath = pathJoin(serverConfig.path, yargs.releaseDir);
|
|
385
|
+
let newRelease;
|
|
386
|
+
try {
|
|
387
|
+
newRelease = await readBuildManifest(ssh, newReleasePath);
|
|
388
|
+
} catch (e) {
|
|
389
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
390
|
+
return { status: "skipped", reason };
|
|
391
|
+
}
|
|
392
|
+
if (!newRelease) {
|
|
393
|
+
return {
|
|
394
|
+
status: "skipped",
|
|
395
|
+
reason: `No ${BUILD_MANIFEST_FILENAME} in ` + WEB_DIST_DIRS.map((dir) => pathJoin(yargs.releaseDir, dir)).join(
|
|
396
|
+
" or "
|
|
397
|
+
)
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const { stdout } = await ssh.exec(serverConfig.path, "ls");
|
|
401
|
+
const previousReleases = stdout.split("\n").map((line) => line.trim()).filter((dir) => releaseDirRegExp.test(dir) && dir !== yargs.releaseDir).sort().reverse().slice(0, Math.max(serverConfig.keepReleases - 1, 0));
|
|
402
|
+
const presentFiles = new Set(newRelease.files);
|
|
403
|
+
const warnings = [];
|
|
404
|
+
let linkedCount = 0;
|
|
405
|
+
for (const release of previousReleases) {
|
|
406
|
+
const releasePath = pathJoin(serverConfig.path, release);
|
|
407
|
+
let previousRelease;
|
|
408
|
+
try {
|
|
409
|
+
previousRelease = await readBuildManifest(ssh, releasePath);
|
|
410
|
+
} catch (e) {
|
|
411
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
412
|
+
warnings.push(`Skipping the web assets of release ${release}: ${message}`);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (!previousRelease) {
|
|
416
|
+
warnings.push(
|
|
417
|
+
`Release ${release} has no web build manifest, skipping its assets`
|
|
418
|
+
);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const missingFiles = previousRelease.files.filter(
|
|
422
|
+
(file) => !presentFiles.has(file)
|
|
423
|
+
);
|
|
424
|
+
if (missingFiles.length === 0) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const filesByDir = /* @__PURE__ */ new Map();
|
|
428
|
+
for (const file of missingFiles) {
|
|
429
|
+
const dir = path.posix.dirname(file);
|
|
430
|
+
filesByDir.set(dir, [...filesByDir.get(dir) ?? [], file]);
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
for (const [dir, files] of filesByDir) {
|
|
434
|
+
const targetDir = pathJoin(newReleasePath, newRelease.distDir, dir);
|
|
435
|
+
await ssh.exec(newReleasePath, "mkdir", ["-p", shellQuote(targetDir)]);
|
|
436
|
+
for (let i = 0; i < files.length; i += LINK_BATCH_SIZE) {
|
|
437
|
+
const sources = files.slice(i, i + LINK_BATCH_SIZE).map(
|
|
438
|
+
(file) => shellQuote(pathJoin(releasePath, previousRelease.distDir, file))
|
|
439
|
+
);
|
|
440
|
+
await ssh.exec(newReleasePath, "ln", [
|
|
441
|
+
"-f",
|
|
442
|
+
...sources,
|
|
443
|
+
shellQuote(targetDir)
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} catch (e) {
|
|
448
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
449
|
+
warnings.push(
|
|
450
|
+
`Could not keep the web assets of release ${release}: ${message}`
|
|
451
|
+
);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
for (const file of missingFiles) {
|
|
455
|
+
presentFiles.add(file);
|
|
456
|
+
}
|
|
457
|
+
linkedCount += missingFiles.length;
|
|
458
|
+
}
|
|
459
|
+
return { status: "done", linkedCount, warnings };
|
|
460
|
+
};
|
|
206
461
|
const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
207
462
|
const cmdPath = pathJoin(serverConfig.path, yargs.releaseDir);
|
|
208
463
|
const config = {
|
|
@@ -351,6 +606,46 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
|
351
606
|
})
|
|
352
607
|
);
|
|
353
608
|
}
|
|
609
|
+
tasks.push(
|
|
610
|
+
...commandWithLifecycleEvents({
|
|
611
|
+
name: "keepAssets",
|
|
612
|
+
config,
|
|
613
|
+
skip: !yargs.keepAssets || !yargs.build || !serverConfig.sides.includes("web"),
|
|
614
|
+
command: {
|
|
615
|
+
title: `Keeping web assets of previous releases...`,
|
|
616
|
+
task: async (_ctx, task) => {
|
|
617
|
+
const result = await keepPreviousAssets(yargs, ssh, serverConfig);
|
|
618
|
+
if (result.status === "skipped") {
|
|
619
|
+
return task.skip(
|
|
620
|
+
c.warning(
|
|
621
|
+
`Warning: ${result.reason}, not keeping the web assets of previous releases`
|
|
622
|
+
)
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
task.output = [
|
|
626
|
+
`Linked ${result.linkedCount} file(s) from previous releases`,
|
|
627
|
+
...result.warnings.map((warning) => c.warning(warning))
|
|
628
|
+
].join("\n");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
})
|
|
632
|
+
);
|
|
633
|
+
if (serverConfig.processNames && yargs.firstRun) {
|
|
634
|
+
const configFile = monitorAdapter(serverConfig).firstRunConfigFile;
|
|
635
|
+
if (configFile) {
|
|
636
|
+
tasks.push({
|
|
637
|
+
title: `Checking for ${configFile}...`,
|
|
638
|
+
task: () => {
|
|
639
|
+
if (!fs.existsSync(path.join(getPaths().base, configFile))) {
|
|
640
|
+
throw new Error(
|
|
641
|
+
`${configFile} is missing. The ${serverConfig.monitor} monitor reads it when starting processes for the first time. Run \`${formatCedarCommand(["setup", "deploy", "baremetal"])}\` to generate it, or deploy without \`--first-run\` if the processes are already running.`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
skip: () => !yargs.restart
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
354
649
|
tasks.push(
|
|
355
650
|
...commandWithLifecycleEvents({
|
|
356
651
|
name: "symlinkCurrent",
|
|
@@ -366,56 +661,56 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
|
366
661
|
})
|
|
367
662
|
);
|
|
368
663
|
if (serverConfig.processNames) {
|
|
664
|
+
const adapter = monitorAdapter(serverConfig);
|
|
665
|
+
const skipRestart = () => !yargs.restart;
|
|
666
|
+
if (yargs.firstRun) {
|
|
667
|
+
for (const command of adapter.firstRunSetup) {
|
|
668
|
+
tasks.push(monitorTask(command, ssh, serverConfig, skipRestart));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
369
671
|
for (const processName of serverConfig.processNames) {
|
|
370
672
|
if (yargs.firstRun) {
|
|
673
|
+
const [firstCommand, ...followUpCommands] = adapter.firstRun(processName);
|
|
674
|
+
if (!firstCommand) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
371
677
|
tasks.push(
|
|
372
678
|
...commandWithLifecycleEvents({
|
|
373
679
|
name: "restart",
|
|
374
680
|
config,
|
|
375
681
|
skip: !yargs.restart,
|
|
376
|
-
command:
|
|
377
|
-
title: `Starting ${processName} process for the first time...`,
|
|
378
|
-
task: async () => {
|
|
379
|
-
await ssh.exec(serverConfig.path, serverConfig.monitorCommand, [
|
|
380
|
-
"start",
|
|
381
|
-
pathJoin(CURRENT_RELEASE_SYMLINK_NAME, "ecosystem.config.js"),
|
|
382
|
-
"--only",
|
|
383
|
-
processName
|
|
384
|
-
]);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
682
|
+
command: monitorTask(firstCommand, ssh, serverConfig)
|
|
387
683
|
})
|
|
388
684
|
);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
await ssh.exec(serverConfig.path, serverConfig.monitorCommand, [
|
|
393
|
-
"save"
|
|
394
|
-
]);
|
|
395
|
-
},
|
|
396
|
-
skip: () => !yargs.restart
|
|
397
|
-
});
|
|
685
|
+
for (const command of followUpCommands) {
|
|
686
|
+
tasks.push(monitorTask(command, ssh, serverConfig, skipRestart));
|
|
687
|
+
}
|
|
398
688
|
} else {
|
|
399
689
|
tasks.push(
|
|
400
690
|
...commandWithLifecycleEvents({
|
|
401
691
|
name: "restart",
|
|
402
692
|
config,
|
|
403
693
|
skip: !yargs.restart,
|
|
404
|
-
command:
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
ssh,
|
|
410
|
-
serverConfig,
|
|
411
|
-
serverConfig.path
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
694
|
+
command: monitorTask(
|
|
695
|
+
adapter.restart(processName),
|
|
696
|
+
ssh,
|
|
697
|
+
serverConfig
|
|
698
|
+
)
|
|
415
699
|
})
|
|
416
700
|
);
|
|
417
701
|
}
|
|
418
702
|
}
|
|
703
|
+
const url = healthCheckUrl(serverConfig);
|
|
704
|
+
tasks.push(
|
|
705
|
+
...commandWithLifecycleEvents({
|
|
706
|
+
name: "healthCheck",
|
|
707
|
+
config: { ...config, cmdPath: serverConfig.path },
|
|
708
|
+
skip: !yargs.restart || !url,
|
|
709
|
+
command: healthCheckTask(url, ssh, serverConfig, [
|
|
710
|
+
`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
|
+
])
|
|
712
|
+
})
|
|
713
|
+
);
|
|
419
714
|
}
|
|
420
715
|
tasks.push(
|
|
421
716
|
...commandWithLifecycleEvents({
|
|
@@ -425,10 +720,10 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
|
425
720
|
command: {
|
|
426
721
|
title: `Cleaning up old deploys...`,
|
|
427
722
|
task: async () => {
|
|
428
|
-
const fileStartIndex = serverConfig.keepReleases +
|
|
723
|
+
const fileStartIndex = serverConfig.keepReleases + 1;
|
|
429
724
|
await ssh.exec(
|
|
430
725
|
serverConfig.path,
|
|
431
|
-
`ls -t | tail -n +${fileStartIndex} | xargs rm -rf`
|
|
726
|
+
`ls -t | grep -E '${RELEASE_DIR_PATTERN}' | tail -n +${fileStartIndex} | xargs -r rm -rf`
|
|
432
727
|
);
|
|
433
728
|
}
|
|
434
729
|
}
|
|
@@ -552,9 +847,8 @@ const warnIfUnpushedCommits = async () => {
|
|
|
552
847
|
};
|
|
553
848
|
const handler = async (yargs) => {
|
|
554
849
|
const { SshExecutor } = await import("./SshExecutor.js");
|
|
555
|
-
const tomlPath = path.join(getPaths().base,
|
|
556
|
-
|
|
557
|
-
if (!fs.existsSync(tomlPath) || !fs.existsSync(ecosystemPath)) {
|
|
850
|
+
const tomlPath = path.join(getPaths().base, CONFIG_FILENAME);
|
|
851
|
+
if (!fs.existsSync(tomlPath)) {
|
|
558
852
|
console.error(
|
|
559
853
|
c.error("\nError: Baremetal deploy has not been properly setup.\n") + `Please run \`${formatCedarCommand(["setup", "deploy", "baremetal"])}\` before deploying`
|
|
560
854
|
);
|
|
@@ -587,17 +881,22 @@ const handler = async (yargs) => {
|
|
|
587
881
|
};
|
|
588
882
|
export {
|
|
589
883
|
DEFAULT_SERVER_CONFIG,
|
|
884
|
+
RELEASE_DIR_PATTERN,
|
|
590
885
|
commandWithLifecycleEvents,
|
|
591
886
|
commands,
|
|
592
887
|
deployTasks,
|
|
593
888
|
handler,
|
|
889
|
+
healthCheckUrl,
|
|
890
|
+
keepPreviousAssets,
|
|
594
891
|
lifecycleTask,
|
|
595
892
|
maintenanceTasks,
|
|
893
|
+
manifestAssetFiles,
|
|
596
894
|
parseConfig,
|
|
597
895
|
rollbackTasks,
|
|
598
896
|
serverConfigWithDefaults,
|
|
599
897
|
throwMissingConfig,
|
|
600
898
|
verifyConfig,
|
|
601
899
|
verifyServerConfig,
|
|
900
|
+
waitForHealthCheck,
|
|
602
901
|
warnIfUnpushedCommits
|
|
603
902
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const MONITORS = ["pm2", "systemd-user", "systemd-system"];
|
|
2
|
+
const pm2 = {
|
|
3
|
+
defaultCommand: "pm2",
|
|
4
|
+
firstRunConfigFile: "ecosystem.config.js",
|
|
5
|
+
firstRunSetup: [],
|
|
6
|
+
firstRun: (processName) => [
|
|
7
|
+
{
|
|
8
|
+
title: `Starting ${processName} process for the first time...`,
|
|
9
|
+
args: ["start", "current/ecosystem.config.js", "--only", processName]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
title: `Saving ${processName} state for future startup...`,
|
|
13
|
+
args: ["save"]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
restart: (processName) => ({
|
|
17
|
+
title: `Restarting ${processName} process...`,
|
|
18
|
+
args: ["restart", processName]
|
|
19
|
+
}),
|
|
20
|
+
stop: (processNames) => ({
|
|
21
|
+
title: `Stopping ${processNames.join(", ")} processes...`,
|
|
22
|
+
args: ["stop", ...processNames]
|
|
23
|
+
}),
|
|
24
|
+
start: (processNames) => ({
|
|
25
|
+
title: `Starting ${processNames.join(", ")} processes...`,
|
|
26
|
+
args: ["start", ...processNames]
|
|
27
|
+
})
|
|
28
|
+
};
|
|
29
|
+
const systemd = (defaultCommand) => ({
|
|
30
|
+
defaultCommand,
|
|
31
|
+
firstRunSetup: [
|
|
32
|
+
{
|
|
33
|
+
title: "Reloading systemd unit files...",
|
|
34
|
+
args: ["daemon-reload"]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
firstRun: (processName) => [
|
|
38
|
+
{
|
|
39
|
+
title: `Enabling and starting ${processName} for the first time...`,
|
|
40
|
+
args: ["enable", "--now", processName]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
restart: (processName) => ({
|
|
44
|
+
title: `Restarting ${processName}...`,
|
|
45
|
+
args: ["restart", processName]
|
|
46
|
+
}),
|
|
47
|
+
stop: (processNames) => ({
|
|
48
|
+
title: `Stopping ${processNames.join(", ")}...`,
|
|
49
|
+
args: ["stop", ...processNames]
|
|
50
|
+
}),
|
|
51
|
+
start: (processNames) => ({
|
|
52
|
+
title: `Starting ${processNames.join(", ")}...`,
|
|
53
|
+
args: ["start", ...processNames]
|
|
54
|
+
})
|
|
55
|
+
});
|
|
56
|
+
const MONITOR_ADAPTERS = {
|
|
57
|
+
pm2,
|
|
58
|
+
"systemd-user": systemd("systemctl --user"),
|
|
59
|
+
"systemd-system": systemd("sudo systemctl")
|
|
60
|
+
};
|
|
61
|
+
const isMonitor = (value) => typeof value === "string" && MONITORS.includes(value);
|
|
62
|
+
export {
|
|
63
|
+
MONITORS,
|
|
64
|
+
MONITOR_ADAPTERS,
|
|
65
|
+
isMonitor
|
|
66
|
+
};
|
|
@@ -48,6 +48,11 @@ const builder = (yargs) => {
|
|
|
48
48
|
default: true,
|
|
49
49
|
type: "boolean"
|
|
50
50
|
});
|
|
51
|
+
yargs.option("keep-assets", {
|
|
52
|
+
describe: "Hardlink previous releases' hashed web assets into the new release so open browser tabs keep working after the deploy",
|
|
53
|
+
default: true,
|
|
54
|
+
type: "boolean"
|
|
55
|
+
});
|
|
51
56
|
yargs.option("releaseDir", {
|
|
52
57
|
describe: "Directory to create for the latest release, defaults to timestamp",
|
|
53
58
|
default: (/* @__PURE__ */ new Date()).toISOString().replace(/[:\-TZ]/g, "").replace(/\.\d+$/, ""),
|
|
@@ -95,6 +100,7 @@ async function handler(yargs) {
|
|
|
95
100
|
build: yargs.build,
|
|
96
101
|
restart: yargs.restart,
|
|
97
102
|
cleanup: yargs.cleanup,
|
|
103
|
+
keepAssets: yargs.keepAssets,
|
|
98
104
|
maintenance: yargs.maintenance,
|
|
99
105
|
rollback: yargs.rollback,
|
|
100
106
|
verbose: yargs.verbose,
|