@cedarjs/cli 7.0.1-next.333 → 7.0.1-next.467
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 +363 -72
- 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/templates/login.webAuthn.tsx.template +20 -12
- 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/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,23 +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;
|
|
17
25
|
const RELEASE_DIR_PATTERN = "^[0-9]{14}$";
|
|
18
26
|
const releaseDirRegExp = new RegExp(RELEASE_DIR_PATTERN);
|
|
27
|
+
const DEFAULT_MONITOR = "pm2";
|
|
19
28
|
const DEFAULT_SERVER_CONFIG = {
|
|
20
29
|
port: 22,
|
|
21
30
|
branch: "main",
|
|
22
31
|
packageManagerCommand: getPackageManager(),
|
|
23
|
-
|
|
32
|
+
monitor: DEFAULT_MONITOR,
|
|
24
33
|
sides: ["api", "web"],
|
|
25
34
|
keepReleases: 5,
|
|
26
|
-
freeSpaceRequired: 2048
|
|
35
|
+
freeSpaceRequired: 2048,
|
|
36
|
+
healthCheckTimeout: 30
|
|
27
37
|
};
|
|
28
38
|
const pathJoin = path.posix.join;
|
|
39
|
+
const shellQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
29
40
|
const throwMissingConfig = (name) => {
|
|
30
41
|
throw new Error(
|
|
31
42
|
`"${name}" config option not set. See https://cedarjs.com/docs/deployment/baremetal#deploytoml`
|
|
@@ -42,6 +53,17 @@ const verifyConfig = (config, yargs) => {
|
|
|
42
53
|
}
|
|
43
54
|
return true;
|
|
44
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
|
+
};
|
|
45
67
|
const verifyServerConfig = (config) => {
|
|
46
68
|
if (!config.host) {
|
|
47
69
|
throwMissingConfig("host");
|
|
@@ -55,6 +77,17 @@ const verifyServerConfig = (config) => {
|
|
|
55
77
|
if (!/^\d+$/.test(String(config.freeSpaceRequired))) {
|
|
56
78
|
throw new Error('"freeSpaceRequired" must be an integer >= 0');
|
|
57
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
|
+
}
|
|
58
91
|
return true;
|
|
59
92
|
};
|
|
60
93
|
const symlinkCurrentCommand = async (dir, ssh, deployPath) => {
|
|
@@ -64,17 +97,110 @@ const symlinkCurrentCommand = async (dir, ssh, deployPath) => {
|
|
|
64
97
|
CURRENT_RELEASE_SYMLINK_NAME
|
|
65
98
|
]);
|
|
66
99
|
};
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
};
|
|
72
194
|
};
|
|
73
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 : "");
|
|
74
198
|
return {
|
|
75
199
|
...DEFAULT_SERVER_CONFIG,
|
|
76
200
|
...serverConfig,
|
|
77
|
-
branch: yargs.branch || serverConfig.branch || DEFAULT_SERVER_CONFIG.branch
|
|
201
|
+
branch: yargs.branch || serverConfig.branch || DEFAULT_SERVER_CONFIG.branch,
|
|
202
|
+
monitor,
|
|
203
|
+
monitorCommand
|
|
78
204
|
};
|
|
79
205
|
};
|
|
80
206
|
const maintenanceTasks = (status, ssh, serverConfig) => {
|
|
@@ -96,26 +222,24 @@ const maintenanceTasks = (status, ssh, serverConfig) => {
|
|
|
96
222
|
}
|
|
97
223
|
});
|
|
98
224
|
if (serverConfig.processNames) {
|
|
99
|
-
tasks.push(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
});
|
|
225
|
+
tasks.push(
|
|
226
|
+
monitorTask(
|
|
227
|
+
monitorAdapter(serverConfig).stop(serverConfig.processNames),
|
|
228
|
+
ssh,
|
|
229
|
+
serverConfig
|
|
230
|
+
)
|
|
231
|
+
);
|
|
108
232
|
}
|
|
109
233
|
} else if (status === "down") {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
serverConfig
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
234
|
+
if (serverConfig.processNames) {
|
|
235
|
+
tasks.push(
|
|
236
|
+
monitorTask(
|
|
237
|
+
monitorAdapter(serverConfig).start(serverConfig.processNames),
|
|
238
|
+
ssh,
|
|
239
|
+
serverConfig
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
119
243
|
if (serverConfig.processNames) {
|
|
120
244
|
tasks.push({
|
|
121
245
|
title: `Disabling maintenance page...`,
|
|
@@ -167,19 +291,15 @@ const rollbackTasks = (count, ssh, serverConfig) => {
|
|
|
167
291
|
}
|
|
168
292
|
];
|
|
169
293
|
if (serverConfig.processNames) {
|
|
294
|
+
const adapter = monitorAdapter(serverConfig);
|
|
170
295
|
for (const processName of serverConfig.processNames) {
|
|
171
|
-
tasks.push(
|
|
172
|
-
title: `Restarting ${processName} process...`,
|
|
173
|
-
task: async () => {
|
|
174
|
-
await restartProcessCommand(
|
|
175
|
-
processName,
|
|
176
|
-
ssh,
|
|
177
|
-
serverConfig,
|
|
178
|
-
serverConfig.path
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
296
|
+
tasks.push(monitorTask(adapter.restart(processName), ssh, serverConfig));
|
|
182
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
|
+
);
|
|
183
303
|
}
|
|
184
304
|
return tasks;
|
|
185
305
|
};
|
|
@@ -210,6 +330,134 @@ const commandWithLifecycleEvents = ({
|
|
|
210
330
|
tasks.push(lifecycleTask("after", name, skip, config));
|
|
211
331
|
return tasks.flat().filter((t) => Boolean(t));
|
|
212
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
|
+
};
|
|
213
461
|
const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
214
462
|
const cmdPath = pathJoin(serverConfig.path, yargs.releaseDir);
|
|
215
463
|
const config = {
|
|
@@ -358,6 +606,46 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
|
358
606
|
})
|
|
359
607
|
);
|
|
360
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
|
+
}
|
|
361
649
|
tasks.push(
|
|
362
650
|
...commandWithLifecycleEvents({
|
|
363
651
|
name: "symlinkCurrent",
|
|
@@ -373,56 +661,56 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
|
373
661
|
})
|
|
374
662
|
);
|
|
375
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
|
+
}
|
|
376
671
|
for (const processName of serverConfig.processNames) {
|
|
377
672
|
if (yargs.firstRun) {
|
|
673
|
+
const [firstCommand, ...followUpCommands] = adapter.firstRun(processName);
|
|
674
|
+
if (!firstCommand) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
378
677
|
tasks.push(
|
|
379
678
|
...commandWithLifecycleEvents({
|
|
380
679
|
name: "restart",
|
|
381
680
|
config,
|
|
382
681
|
skip: !yargs.restart,
|
|
383
|
-
command:
|
|
384
|
-
title: `Starting ${processName} process for the first time...`,
|
|
385
|
-
task: async () => {
|
|
386
|
-
await ssh.exec(serverConfig.path, serverConfig.monitorCommand, [
|
|
387
|
-
"start",
|
|
388
|
-
pathJoin(CURRENT_RELEASE_SYMLINK_NAME, "ecosystem.config.js"),
|
|
389
|
-
"--only",
|
|
390
|
-
processName
|
|
391
|
-
]);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
682
|
+
command: monitorTask(firstCommand, ssh, serverConfig)
|
|
394
683
|
})
|
|
395
684
|
);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
await ssh.exec(serverConfig.path, serverConfig.monitorCommand, [
|
|
400
|
-
"save"
|
|
401
|
-
]);
|
|
402
|
-
},
|
|
403
|
-
skip: () => !yargs.restart
|
|
404
|
-
});
|
|
685
|
+
for (const command of followUpCommands) {
|
|
686
|
+
tasks.push(monitorTask(command, ssh, serverConfig, skipRestart));
|
|
687
|
+
}
|
|
405
688
|
} else {
|
|
406
689
|
tasks.push(
|
|
407
690
|
...commandWithLifecycleEvents({
|
|
408
691
|
name: "restart",
|
|
409
692
|
config,
|
|
410
693
|
skip: !yargs.restart,
|
|
411
|
-
command:
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
ssh,
|
|
417
|
-
serverConfig,
|
|
418
|
-
serverConfig.path
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
694
|
+
command: monitorTask(
|
|
695
|
+
adapter.restart(processName),
|
|
696
|
+
ssh,
|
|
697
|
+
serverConfig
|
|
698
|
+
)
|
|
422
699
|
})
|
|
423
700
|
);
|
|
424
701
|
}
|
|
425
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
|
+
);
|
|
426
714
|
}
|
|
427
715
|
tasks.push(
|
|
428
716
|
...commandWithLifecycleEvents({
|
|
@@ -559,9 +847,8 @@ const warnIfUnpushedCommits = async () => {
|
|
|
559
847
|
};
|
|
560
848
|
const handler = async (yargs) => {
|
|
561
849
|
const { SshExecutor } = await import("./SshExecutor.js");
|
|
562
|
-
const tomlPath = path.join(getPaths().base,
|
|
563
|
-
|
|
564
|
-
if (!fs.existsSync(tomlPath) || !fs.existsSync(ecosystemPath)) {
|
|
850
|
+
const tomlPath = path.join(getPaths().base, CONFIG_FILENAME);
|
|
851
|
+
if (!fs.existsSync(tomlPath)) {
|
|
565
852
|
console.error(
|
|
566
853
|
c.error("\nError: Baremetal deploy has not been properly setup.\n") + `Please run \`${formatCedarCommand(["setup", "deploy", "baremetal"])}\` before deploying`
|
|
567
854
|
);
|
|
@@ -599,13 +886,17 @@ export {
|
|
|
599
886
|
commands,
|
|
600
887
|
deployTasks,
|
|
601
888
|
handler,
|
|
889
|
+
healthCheckUrl,
|
|
890
|
+
keepPreviousAssets,
|
|
602
891
|
lifecycleTask,
|
|
603
892
|
maintenanceTasks,
|
|
893
|
+
manifestAssetFiles,
|
|
604
894
|
parseConfig,
|
|
605
895
|
rollbackTasks,
|
|
606
896
|
serverConfigWithDefaults,
|
|
607
897
|
throwMissingConfig,
|
|
608
898
|
verifyConfig,
|
|
609
899
|
verifyServerConfig,
|
|
900
|
+
waitForHealthCheck,
|
|
610
901
|
warnIfUnpushedCommits
|
|
611
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,
|