@cedarjs/cli 8.0.0-canary.3344 → 8.0.0-canary.3346
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 +85 -70
- package/dist/commands/deploy/baremetal/monitors.js +66 -0
- 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/templates/baremetal.js +111 -9
- package/package.json +13 -13
|
@@ -10,6 +10,7 @@ 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
12
|
import { getConfig, getPaths } from "../../../lib/index.js";
|
|
13
|
+
import { MONITOR_ADAPTERS, MONITORS, isMonitor } from "./monitors.js";
|
|
13
14
|
const CONFIG_FILENAME = "deploy.toml";
|
|
14
15
|
const DOCS_URL = "https://cedarjs.com/docs/deploy/baremetal";
|
|
15
16
|
const SYMLINK_FLAGS = "-nsf";
|
|
@@ -23,11 +24,12 @@ const BUILD_MANIFEST_FILENAME = "client-build-manifest.json";
|
|
|
23
24
|
const LINK_BATCH_SIZE = 100;
|
|
24
25
|
const RELEASE_DIR_PATTERN = "^[0-9]{14}$";
|
|
25
26
|
const releaseDirRegExp = new RegExp(RELEASE_DIR_PATTERN);
|
|
27
|
+
const DEFAULT_MONITOR = "pm2";
|
|
26
28
|
const DEFAULT_SERVER_CONFIG = {
|
|
27
29
|
port: 22,
|
|
28
30
|
branch: "main",
|
|
29
31
|
packageManagerCommand: getPackageManager(),
|
|
30
|
-
|
|
32
|
+
monitor: DEFAULT_MONITOR,
|
|
31
33
|
sides: ["api", "web"],
|
|
32
34
|
keepReleases: 5,
|
|
33
35
|
freeSpaceRequired: 2048,
|
|
@@ -81,6 +83,11 @@ const verifyServerConfig = (config) => {
|
|
|
81
83
|
if (!/^\d+$/.test(String(config.healthCheckTimeout))) {
|
|
82
84
|
throw new Error('"healthCheckTimeout" must be an integer >= 0');
|
|
83
85
|
}
|
|
86
|
+
if (!isMonitor(config.monitor)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`"monitor" must be one of ${MONITORS.map((monitor) => `"${monitor}"`).join(", ")}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
84
91
|
return true;
|
|
85
92
|
};
|
|
86
93
|
const symlinkCurrentCommand = async (dir, ssh, deployPath) => {
|
|
@@ -90,11 +97,22 @@ const symlinkCurrentCommand = async (dir, ssh, deployPath) => {
|
|
|
90
97
|
CURRENT_RELEASE_SYMLINK_NAME
|
|
91
98
|
]);
|
|
92
99
|
};
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
};
|
|
98
116
|
};
|
|
99
117
|
const healthCheckUrl = (serverConfig) => {
|
|
100
118
|
if (serverConfig.healthCheckUrl === false) {
|
|
@@ -175,10 +193,14 @@ const healthCheckTask = (url, ssh, serverConfig, nextSteps) => {
|
|
|
175
193
|
};
|
|
176
194
|
};
|
|
177
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 : "");
|
|
178
198
|
return {
|
|
179
199
|
...DEFAULT_SERVER_CONFIG,
|
|
180
200
|
...serverConfig,
|
|
181
|
-
branch: yargs.branch || serverConfig.branch || DEFAULT_SERVER_CONFIG.branch
|
|
201
|
+
branch: yargs.branch || serverConfig.branch || DEFAULT_SERVER_CONFIG.branch,
|
|
202
|
+
monitor,
|
|
203
|
+
monitorCommand
|
|
182
204
|
};
|
|
183
205
|
};
|
|
184
206
|
const maintenanceTasks = (status, ssh, serverConfig) => {
|
|
@@ -200,26 +222,24 @@ const maintenanceTasks = (status, ssh, serverConfig) => {
|
|
|
200
222
|
}
|
|
201
223
|
});
|
|
202
224
|
if (serverConfig.processNames) {
|
|
203
|
-
tasks.push(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
});
|
|
225
|
+
tasks.push(
|
|
226
|
+
monitorTask(
|
|
227
|
+
monitorAdapter(serverConfig).stop(serverConfig.processNames),
|
|
228
|
+
ssh,
|
|
229
|
+
serverConfig
|
|
230
|
+
)
|
|
231
|
+
);
|
|
212
232
|
}
|
|
213
233
|
} else if (status === "down") {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
serverConfig
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
234
|
+
if (serverConfig.processNames) {
|
|
235
|
+
tasks.push(
|
|
236
|
+
monitorTask(
|
|
237
|
+
monitorAdapter(serverConfig).start(serverConfig.processNames),
|
|
238
|
+
ssh,
|
|
239
|
+
serverConfig
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
223
243
|
if (serverConfig.processNames) {
|
|
224
244
|
tasks.push({
|
|
225
245
|
title: `Disabling maintenance page...`,
|
|
@@ -271,18 +291,9 @@ const rollbackTasks = (count, ssh, serverConfig) => {
|
|
|
271
291
|
}
|
|
272
292
|
];
|
|
273
293
|
if (serverConfig.processNames) {
|
|
294
|
+
const adapter = monitorAdapter(serverConfig);
|
|
274
295
|
for (const processName of serverConfig.processNames) {
|
|
275
|
-
tasks.push(
|
|
276
|
-
title: `Restarting ${processName} process...`,
|
|
277
|
-
task: async () => {
|
|
278
|
-
await restartProcessCommand(
|
|
279
|
-
processName,
|
|
280
|
-
ssh,
|
|
281
|
-
serverConfig,
|
|
282
|
-
serverConfig.path
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
});
|
|
296
|
+
tasks.push(monitorTask(adapter.restart(processName), ssh, serverConfig));
|
|
286
297
|
}
|
|
287
298
|
tasks.push(
|
|
288
299
|
healthCheckTask(healthCheckUrl(serverConfig), ssh, serverConfig, [
|
|
@@ -619,6 +630,22 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
|
619
630
|
}
|
|
620
631
|
})
|
|
621
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
|
+
}
|
|
622
649
|
tasks.push(
|
|
623
650
|
...commandWithLifecycleEvents({
|
|
624
651
|
name: "symlinkCurrent",
|
|
@@ -634,52 +661,41 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
|
|
|
634
661
|
})
|
|
635
662
|
);
|
|
636
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
|
+
}
|
|
637
671
|
for (const processName of serverConfig.processNames) {
|
|
638
672
|
if (yargs.firstRun) {
|
|
673
|
+
const [firstCommand, ...followUpCommands] = adapter.firstRun(processName);
|
|
674
|
+
if (!firstCommand) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
639
677
|
tasks.push(
|
|
640
678
|
...commandWithLifecycleEvents({
|
|
641
679
|
name: "restart",
|
|
642
680
|
config,
|
|
643
681
|
skip: !yargs.restart,
|
|
644
|
-
command:
|
|
645
|
-
title: `Starting ${processName} process for the first time...`,
|
|
646
|
-
task: async () => {
|
|
647
|
-
await ssh.exec(serverConfig.path, serverConfig.monitorCommand, [
|
|
648
|
-
"start",
|
|
649
|
-
pathJoin(CURRENT_RELEASE_SYMLINK_NAME, "ecosystem.config.js"),
|
|
650
|
-
"--only",
|
|
651
|
-
processName
|
|
652
|
-
]);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
682
|
+
command: monitorTask(firstCommand, ssh, serverConfig)
|
|
655
683
|
})
|
|
656
684
|
);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
await ssh.exec(serverConfig.path, serverConfig.monitorCommand, [
|
|
661
|
-
"save"
|
|
662
|
-
]);
|
|
663
|
-
},
|
|
664
|
-
skip: () => !yargs.restart
|
|
665
|
-
});
|
|
685
|
+
for (const command of followUpCommands) {
|
|
686
|
+
tasks.push(monitorTask(command, ssh, serverConfig, skipRestart));
|
|
687
|
+
}
|
|
666
688
|
} else {
|
|
667
689
|
tasks.push(
|
|
668
690
|
...commandWithLifecycleEvents({
|
|
669
691
|
name: "restart",
|
|
670
692
|
config,
|
|
671
693
|
skip: !yargs.restart,
|
|
672
|
-
command:
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
ssh,
|
|
678
|
-
serverConfig,
|
|
679
|
-
serverConfig.path
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
694
|
+
command: monitorTask(
|
|
695
|
+
adapter.restart(processName),
|
|
696
|
+
ssh,
|
|
697
|
+
serverConfig
|
|
698
|
+
)
|
|
683
699
|
})
|
|
684
700
|
);
|
|
685
701
|
}
|
|
@@ -831,9 +847,8 @@ const warnIfUnpushedCommits = async () => {
|
|
|
831
847
|
};
|
|
832
848
|
const handler = async (yargs) => {
|
|
833
849
|
const { SshExecutor } = await import("./SshExecutor.js");
|
|
834
|
-
const tomlPath = path.join(getPaths().base,
|
|
835
|
-
|
|
836
|
-
if (!fs.existsSync(tomlPath) || !fs.existsSync(ecosystemPath)) {
|
|
850
|
+
const tomlPath = path.join(getPaths().base, CONFIG_FILENAME);
|
|
851
|
+
if (!fs.existsSync(tomlPath)) {
|
|
837
852
|
console.error(
|
|
838
853
|
c.error("\nError: Baremetal deploy has not been properly setup.\n") + `Please run \`${formatCedarCommand(["setup", "deploy", "baremetal"])}\` before deploying`
|
|
839
854
|
);
|
|
@@ -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
|
+
};
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { MONITORS } from "../../../deploy/baremetal/monitors.js";
|
|
1
2
|
import { createHandler } from "../helpers/helpers.js";
|
|
2
3
|
const command = "baremetal";
|
|
3
4
|
const description = "Setup Baremetal deploy";
|
|
5
|
+
const builder = (yargs) => yargs.option("monitor", {
|
|
6
|
+
description: "Process monitor to generate config for. Asks when not given. pm2 gets an ecosystem.config.js, systemd gets unit files in systemd/",
|
|
7
|
+
choices: MONITORS,
|
|
8
|
+
type: "string"
|
|
9
|
+
});
|
|
4
10
|
const handler = createHandler("baremetal");
|
|
5
11
|
export {
|
|
12
|
+
builder,
|
|
6
13
|
command,
|
|
7
14
|
description,
|
|
8
15
|
handler
|
|
@@ -10,38 +10,136 @@ import {
|
|
|
10
10
|
getPaths,
|
|
11
11
|
printSetupNotes
|
|
12
12
|
} from "../../../../lib/index.js";
|
|
13
|
+
import { MONITORS, isMonitor } from "../../../deploy/baremetal/monitors.js";
|
|
13
14
|
import { addFilesTask } from "../helpers/index.js";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
MAINTENANCE,
|
|
17
|
+
SYSTEMD_DIR,
|
|
18
|
+
deployToml,
|
|
19
|
+
ecosystemConfig,
|
|
20
|
+
systemdJobsUnit,
|
|
21
|
+
systemdServiceUnit
|
|
22
|
+
} from "../templates/baremetal.js";
|
|
15
23
|
const configFilename = "deploy.toml";
|
|
16
|
-
const
|
|
24
|
+
const MONITOR_CHOICES = [
|
|
17
25
|
{
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
title: "pm2",
|
|
27
|
+
description: "Node process manager, configured in ecosystem.config.js",
|
|
28
|
+
value: "pm2"
|
|
20
29
|
},
|
|
21
30
|
{
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
title: "systemd (user units)",
|
|
32
|
+
description: "Units in ~/.config/systemd/user, managed by the deploy user without sudo",
|
|
33
|
+
value: "systemd-user"
|
|
24
34
|
},
|
|
25
35
|
{
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
title: "systemd (system units)",
|
|
37
|
+
description: "Units in /etc/systemd/system, managed with sudo systemctl",
|
|
38
|
+
value: "systemd-system"
|
|
28
39
|
}
|
|
29
40
|
];
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
const appNameFromPath = (projectPath) => {
|
|
42
|
+
const name = path.basename(projectPath).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
43
|
+
return name || "cedar";
|
|
44
|
+
};
|
|
45
|
+
const setupFiles = ({
|
|
46
|
+
monitor,
|
|
47
|
+
jobs,
|
|
48
|
+
appName,
|
|
49
|
+
basePath,
|
|
50
|
+
webSrcPath
|
|
51
|
+
}) => {
|
|
52
|
+
const files = [];
|
|
53
|
+
let processNames;
|
|
54
|
+
if (monitor === "pm2") {
|
|
55
|
+
processNames = jobs ? ["serve", "jobs"] : ["serve"];
|
|
56
|
+
files.push({
|
|
57
|
+
path: path.join(basePath, "ecosystem.config.js"),
|
|
58
|
+
content: ecosystemConfig({ jobs })
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
const jobsUnit = `${appName}-jobs`;
|
|
62
|
+
processNames = jobs ? [appName, `${jobsUnit}@0`] : [appName];
|
|
63
|
+
files.push({
|
|
64
|
+
path: path.join(basePath, SYSTEMD_DIR, `${appName}.service`),
|
|
65
|
+
content: systemdServiceUnit({ appName, monitor })
|
|
66
|
+
});
|
|
67
|
+
if (jobs) {
|
|
68
|
+
files.push({
|
|
69
|
+
path: path.join(basePath, SYSTEMD_DIR, `${jobsUnit}@.service`),
|
|
70
|
+
content: systemdJobsUnit({ appName, monitor })
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
files.unshift({
|
|
75
|
+
path: path.join(basePath, configFilename),
|
|
76
|
+
content: deployToml({ monitor, processNames })
|
|
77
|
+
});
|
|
78
|
+
files.push({
|
|
79
|
+
path: path.join(webSrcPath, "maintenance.html"),
|
|
80
|
+
content: MAINTENANCE
|
|
81
|
+
});
|
|
82
|
+
return { files, processNames };
|
|
83
|
+
};
|
|
84
|
+
const notesFor = (monitor, jobs, processNames) => {
|
|
85
|
+
const notes = ["You are almost ready to go BAREMETAL!", ""];
|
|
86
|
+
if (monitor !== "pm2") {
|
|
87
|
+
notes.push(
|
|
88
|
+
`Copy the unit files in ${SYSTEMD_DIR}/ to the server before your first`,
|
|
89
|
+
"deploy. The comments at the top of each file say where they go.",
|
|
90
|
+
""
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (jobs) {
|
|
94
|
+
notes.push(
|
|
95
|
+
`Background jobs are set up, so deploy.toml lists a jobs worker process`,
|
|
96
|
+
`(${processNames[processNames.length - 1]}). Add one per entry in the`,
|
|
97
|
+
"`workers` array of api/src/lib/jobs.ts.",
|
|
98
|
+
""
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
notes.push(
|
|
102
|
+
"See https://cedarjs.com/docs/deploy/baremetal for the remaining",
|
|
103
|
+
"config and setup required before you can perform your first deploy."
|
|
104
|
+
);
|
|
105
|
+
return notes;
|
|
106
|
+
};
|
|
107
|
+
const promptForMonitor = async () => {
|
|
108
|
+
const { monitor } = await prompts({
|
|
109
|
+
type: "select",
|
|
110
|
+
name: "monitor",
|
|
111
|
+
message: "Which process monitor will run your app on the server?",
|
|
112
|
+
choices: MONITOR_CHOICES
|
|
113
|
+
});
|
|
114
|
+
return isMonitor(monitor) ? monitor : void 0;
|
|
115
|
+
};
|
|
116
|
+
const handler = async ({
|
|
117
|
+
force,
|
|
118
|
+
monitor: monitorOption
|
|
119
|
+
}) => {
|
|
37
120
|
recordTelemetryAttributes({
|
|
38
121
|
command: "setup deploy baremetal",
|
|
39
|
-
force
|
|
122
|
+
force,
|
|
123
|
+
monitor: monitorOption
|
|
40
124
|
});
|
|
41
|
-
if (
|
|
125
|
+
if (monitorOption !== void 0 && !isMonitor(monitorOption)) {
|
|
126
|
+
console.error(
|
|
127
|
+
c.error(
|
|
128
|
+
`Unknown monitor "${monitorOption}". Use one of ${MONITORS.join(", ")}.`
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
const monitor = monitorOption ?? await promptForMonitor();
|
|
134
|
+
if (!monitor) {
|
|
135
|
+
console.log("Aborting baremetal setup.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const paths = getPaths();
|
|
139
|
+
if (fs.existsSync(path.join(paths.base, ".pnp.cjs"))) {
|
|
42
140
|
console.warn(
|
|
43
141
|
c.warning(
|
|
44
|
-
"Your project uses Yarn PnP (Plug'n'Play), which is not officially supported for Baremetal deployments. The generated
|
|
142
|
+
"Your project uses Yarn PnP (Plug'n'Play), which is not officially supported for Baremetal deployments. The generated process config starts the app with node_modules/.bin/cedar, which will most likely not work under PnP.\n\nYou will need to manually configure the server. See also the packageManagerCommand field in deploy.toml."
|
|
45
143
|
)
|
|
46
144
|
);
|
|
47
145
|
console.log();
|
|
@@ -56,6 +154,14 @@ const handler = async ({ force }) => {
|
|
|
56
154
|
}
|
|
57
155
|
console.log();
|
|
58
156
|
}
|
|
157
|
+
const jobs = Boolean(paths.api.jobsConfig);
|
|
158
|
+
const { files, processNames } = setupFiles({
|
|
159
|
+
monitor,
|
|
160
|
+
jobs,
|
|
161
|
+
appName: appNameFromPath(paths.base),
|
|
162
|
+
basePath: paths.base,
|
|
163
|
+
webSrcPath: paths.web.src
|
|
164
|
+
});
|
|
59
165
|
const tasks = new Listr(
|
|
60
166
|
[
|
|
61
167
|
await addPackagesTask({
|
|
@@ -66,7 +172,7 @@ const handler = async ({ force }) => {
|
|
|
66
172
|
files,
|
|
67
173
|
force
|
|
68
174
|
}),
|
|
69
|
-
printSetupNotes(
|
|
175
|
+
printSetupNotes(notesFor(monitor, jobs, processNames))
|
|
70
176
|
],
|
|
71
177
|
{ rendererOptions: { collapseSubtasks: false } }
|
|
72
178
|
);
|
|
@@ -81,6 +187,8 @@ const handler = async ({ force }) => {
|
|
|
81
187
|
}
|
|
82
188
|
};
|
|
83
189
|
export {
|
|
190
|
+
appNameFromPath,
|
|
84
191
|
configFilename,
|
|
85
|
-
handler
|
|
192
|
+
handler,
|
|
193
|
+
setupFiles
|
|
86
194
|
};
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { getPackageManager } from "@cedarjs/project-config/packageManager";
|
|
2
|
-
const
|
|
2
|
+
const SYSTEMD_DIR = "systemd";
|
|
3
|
+
const JOBS_WORKER_BIN = "api/node_modules/.bin/cedar-jobs-worker";
|
|
4
|
+
const WORKER_STOP_TIMEOUT_SECONDS = 600;
|
|
5
|
+
const JOBS_WORKER_COMMENT = `# Runs one background jobs worker. \`--index\` picks the entry of the \`workers\`
|
|
6
|
+
# array in api/src/lib/jobs.ts and \`--id\` numbers the worker within that
|
|
7
|
+
# entry, so an entry with \`count: 2\` needs a second worker with \`--id=1\`.`;
|
|
8
|
+
const ecosystemConfig = ({ jobs }) => {
|
|
9
|
+
const jobsApp = jobs ? `
|
|
10
|
+
{
|
|
11
|
+
// Runs one background jobs worker. \`--index\` picks the entry of the
|
|
12
|
+
// \`workers\` array in api/src/lib/jobs.ts and \`--id\` numbers the worker
|
|
13
|
+
// within that entry, so an entry with \`count: 2\` needs a second app
|
|
14
|
+
// with \`--id=1\`.
|
|
15
|
+
name: 'jobs',
|
|
16
|
+
cwd: 'current',
|
|
17
|
+
script: '${JOBS_WORKER_BIN}',
|
|
18
|
+
args: '--index=0 --id=0',
|
|
19
|
+
// pm2 stops the worker with SIGINT, which lets it finish the job it is
|
|
20
|
+
// running. This is how long pm2 waits for that (in ms) before killing it.
|
|
21
|
+
kill_timeout: ${WORKER_STOP_TIMEOUT_SECONDS * 1e3},
|
|
22
|
+
},` : "";
|
|
23
|
+
return `module.exports = {
|
|
3
24
|
apps: [
|
|
4
25
|
{
|
|
5
26
|
name: 'serve',
|
|
@@ -10,10 +31,79 @@ const ECOSYSTEM = `module.exports = {
|
|
|
10
31
|
exec_mode: 'cluster',
|
|
11
32
|
wait_ready: true,
|
|
12
33
|
listen_timeout: 10000,
|
|
13
|
-
}
|
|
34
|
+
},${jobsApp}
|
|
14
35
|
],
|
|
15
|
-
}
|
|
16
|
-
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
};
|
|
39
|
+
const unitHeader = (appName, monitor) => {
|
|
40
|
+
const install = monitor === "systemd-system" ? `# sudo cp ${SYSTEMD_DIR}/*.service /etc/systemd/system/` : `# mkdir -p ~/.config/systemd/user && cp ${SYSTEMD_DIR}/*.service ~/.config/systemd/user/`;
|
|
41
|
+
const linger = monitor === "systemd-system" ? "" : `#
|
|
42
|
+
# User units only run while the user has a session, so let them run after a
|
|
43
|
+
# reboot too:
|
|
44
|
+
# sudo loginctl enable-linger $(whoami)
|
|
45
|
+
`;
|
|
46
|
+
return `# systemd unit for the ${appName} Cedar app, generated by \`cedar setup deploy baremetal\`.
|
|
47
|
+
#
|
|
48
|
+
# Copy the unit files to the server once, before the first deploy:
|
|
49
|
+
${install}
|
|
50
|
+
${linger}#
|
|
51
|
+
# Then deploy with \`--first-run\`, which reloads the unit files and enables and
|
|
52
|
+
# starts every process listed in deploy.toml.
|
|
53
|
+
#
|
|
54
|
+
# WorkingDirectory must be the \`path\` from deploy.toml followed by \`/current\`.`;
|
|
55
|
+
};
|
|
56
|
+
const unitService = (monitor, execStart, extraDirectives = "") => {
|
|
57
|
+
const user = monitor === "systemd-system" ? "\nUser=deploy" : "";
|
|
58
|
+
return `[Service]
|
|
59
|
+
WorkingDirectory=/var/www/app/current
|
|
60
|
+
# A login shell puts node and ${getPackageManager()} from a version manager such as nvm on the PATH
|
|
61
|
+
ExecStart=/bin/bash -lc '${execStart}'
|
|
62
|
+
Restart=always
|
|
63
|
+
RestartSec=5
|
|
64
|
+
Environment=NODE_ENV=production${user}${extraDirectives}`;
|
|
65
|
+
};
|
|
66
|
+
const JOBS_STOP_DIRECTIVES = `
|
|
67
|
+
# Stopping sends SIGINT so the worker finishes the job it is running before it
|
|
68
|
+
# exits. systemd waits this long for that before killing the worker.
|
|
69
|
+
KillSignal=SIGINT
|
|
70
|
+
TimeoutStopSec=${WORKER_STOP_TIMEOUT_SECONDS}`;
|
|
71
|
+
const unitInstall = (monitor) => `[Install]
|
|
72
|
+
WantedBy=${monitor === "systemd-system" ? "multi-user.target" : "default.target"}`;
|
|
73
|
+
const systemdServiceUnit = ({
|
|
74
|
+
appName,
|
|
75
|
+
monitor
|
|
76
|
+
}) => `${unitHeader(appName, monitor)}
|
|
77
|
+
|
|
78
|
+
[Unit]
|
|
79
|
+
Description=${appName} (Cedar)
|
|
80
|
+
After=network.target
|
|
81
|
+
|
|
82
|
+
${unitService(monitor, "exec node_modules/.bin/cedar serve")}
|
|
83
|
+
|
|
84
|
+
${unitInstall(monitor)}
|
|
85
|
+
`;
|
|
86
|
+
const systemdJobsUnit = ({
|
|
87
|
+
appName,
|
|
88
|
+
monitor
|
|
89
|
+
}) => `${unitHeader(appName, monitor)}
|
|
90
|
+
#
|
|
91
|
+
${JOBS_WORKER_COMMENT}
|
|
92
|
+
# Start one instance per \`workers\` entry: \`${appName}-jobs@0\`, \`${appName}-jobs@1\`
|
|
93
|
+
# and so on, and list each one in \`processNames\` in deploy.toml.
|
|
94
|
+
|
|
95
|
+
[Unit]
|
|
96
|
+
Description=${appName} background jobs worker %i (Cedar)
|
|
97
|
+
After=network.target
|
|
98
|
+
|
|
99
|
+
${unitService(monitor, `exec ${JOBS_WORKER_BIN} --index=%i --id=0`, JOBS_STOP_DIRECTIVES)}
|
|
100
|
+
|
|
101
|
+
${unitInstall(monitor)}
|
|
102
|
+
`;
|
|
103
|
+
const deployToml = ({
|
|
104
|
+
monitor,
|
|
105
|
+
processNames
|
|
106
|
+
}) => `# This file contains config for a baremetal deployment
|
|
17
107
|
#
|
|
18
108
|
# SSH connection options include:
|
|
19
109
|
#
|
|
@@ -26,6 +116,15 @@ const DEPLOY = `# This file contains config for a baremetal deployment
|
|
|
26
116
|
# * passphrase - used if your private key has a passphrase
|
|
27
117
|
# * agentForward - set to \`true\` to forward the client machine's ssh credentials
|
|
28
118
|
#
|
|
119
|
+
# Process monitor options:
|
|
120
|
+
#
|
|
121
|
+
# * monitor - "pm2", "systemd-user" or "systemd-system". Decides how processes
|
|
122
|
+
# are started for the first time, restarted and stopped
|
|
123
|
+
# * monitorCommand - overrides the command the monitor is invoked with. Defaults
|
|
124
|
+
# to \`pm2\`, \`systemctl --user\` or \`sudo systemctl\` depending on \`monitor\`,
|
|
125
|
+
# for example \`doppler run -- pm2\`
|
|
126
|
+
# * processNames - the pm2 app names or systemd unit names to (re)start
|
|
127
|
+
#
|
|
29
128
|
# After restarting the processes, the deploy requests the api side's health
|
|
30
129
|
# check endpoint on the server and fails if it doesn't respond in time:
|
|
31
130
|
#
|
|
@@ -41,9 +140,9 @@ username = "user"
|
|
|
41
140
|
agentForward = true
|
|
42
141
|
sides = ["api","web"]
|
|
43
142
|
packageManagerCommand = "${getPackageManager()}"
|
|
44
|
-
|
|
143
|
+
monitor = "${monitor}"
|
|
45
144
|
path = "/var/www/app"
|
|
46
|
-
processNames = ["
|
|
145
|
+
processNames = [${processNames.map((name) => `"${name}"`).join(", ")}]
|
|
47
146
|
repo = "git@github.com:myorg/myapp.git"
|
|
48
147
|
branch = "main"
|
|
49
148
|
keepReleases = 5
|
|
@@ -131,7 +230,10 @@ And take it back down with:
|
|
|
131
230
|
</html>
|
|
132
231
|
`;
|
|
133
232
|
export {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
233
|
+
MAINTENANCE,
|
|
234
|
+
SYSTEMD_DIR,
|
|
235
|
+
deployToml,
|
|
236
|
+
ecosystemConfig,
|
|
237
|
+
systemdJobsUnit,
|
|
238
|
+
systemdServiceUnit
|
|
137
239
|
};
|
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.3346",
|
|
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.
|
|
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.3346",
|
|
40
|
+
"@cedarjs/babel-config": "8.0.0-canary.3346",
|
|
41
|
+
"@cedarjs/cli-helpers": "8.0.0-canary.3346",
|
|
42
|
+
"@cedarjs/internal": "8.0.0-canary.3346",
|
|
43
|
+
"@cedarjs/prerender": "8.0.0-canary.3346",
|
|
44
|
+
"@cedarjs/project-config": "8.0.0-canary.3346",
|
|
45
|
+
"@cedarjs/structure": "8.0.0-canary.3346",
|
|
46
|
+
"@cedarjs/telemetry": "8.0.0-canary.3346",
|
|
47
|
+
"@cedarjs/utils": "8.0.0-canary.3346",
|
|
48
|
+
"@cedarjs/vite": "8.0.0-canary.3346",
|
|
49
|
+
"@cedarjs/web-server": "8.0.0-canary.3346",
|
|
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.
|
|
97
|
+
"@cedarjs/framework-tools": "8.0.0-canary.3346",
|
|
98
98
|
"@prisma/dmmf": "7.10.0",
|
|
99
99
|
"@types/archiver": "^7.0.0",
|
|
100
100
|
"memfs": "4.80.0",
|