1// Deploys the web server. The site is built here, where the fonts are and
2// with a fresh copy of production's databases. The source and the build's
3// cache are then copied over. The server builds from that cache when it
4// starts, which leaves it nothing to do but serve.
5const remote = "paperclover.net";
6/** the server's node.js, a link to the version that builds here run on */
7const node = "~/.local/node/bin";
8const cwd = Path.resolve(".");
9
10export async function main(graph: incremental.Graph) {
11 // a cache is only kept by the node.js that wrote it, and a server that
12 // builds without one would need the fonts
13 const stdout = await subprocess.execute(["ssh", remote, `${node}/node --version`]);
14 ASSERT(
15 stdout.trim() === process.version,
16 `${remote} runs node.js ${stdout.trim()}, and this build's cache is for ${process.version}`,
17 );
18
19 await download("cache.sqlite");
20 await download("questions.sqlite");
21
22 // any error stops the deploy, including the ones a build can still be served with
23 await graph.incrementalBuild({ progress, buildAndWriteOutput });
24 // the cache is copied as one file
25 graph.db.sql.exec("pragma wal_checkpoint(truncate);");
26
27 await rsync.spawn({
28 cwd,
29 args: [
30 "-a",
31 "-r",
32 "--delete",
33 "--exclude=node_modules",
34 "run.js",
35 "package.json",
36 "package-lock.json",
37 "src",
38 "framework",
39 "lib",
40 `${remote}:~/paperclover.net/`,
41 ],
42 progress: progress.start("copy site files"),
43 });
44 await rsync.spawn({
45 cwd,
46 args: [
47 "-a",
48 ".clover/incremental.db",
49 `${remote}:~/paperclover.net/.clover/incremental.db.next`,
50 ],
51 progress: progress.start("copy build cache"),
52 });
53 await subprocess.spawn([
54 "ssh",
55 remote,
56 "cd paperclover.net && "
57 + `PATH=${node}:$PATH npm i && `
58 + "pm2 stop site && "
59 // the server leaves a write-ahead log beside the cache it had open, which is not this cache's
60 + "rm -f .clover/incremental.db-wal .clover/incremental.db-shm && "
61 + "mv .clover/incremental.db.next .clover/incremental.db && "
62 // started again with this checkout's command. secrets are in the server's `.env`
63 + "pm2 delete site && "
64 + `pm2 start run.js --name site --interpreter ${node}/node --kill-timeout 5000 `
65 + `--node-args="--env-file=.env --experimental-vm-modules --no-warnings" -- --prod && `
66 + "pm2 save",
67 ], { stdin: "inherit" }).wait();
68}
69
70/**
71 * replace the local copy of one of the server's databases. the server has it
72 * open, with its latest writes in a write-ahead log that a copy of the file
73 * alone would miss, so the server writes a consistent copy first.
74 */
75async function download(name: string) {
76 const snapshot = `/tmp/deploy-${name}`;
77 const vacuum = `new (require("node:sqlite").DatabaseSync)(".clover/${name}", { readOnly: true })`
78 + `.prepare("vacuum into ?").run("${snapshot}")`;
79 await subprocess.execute(["ssh", remote, `rm -f ${snapshot} && cd paperclover.net && ${node}/node -e '${vacuum}'`]);
80 await rsync.spawn({
81 cwd,
82 args: ["-a", `${remote}:${snapshot}`, `.clover/${name}.next`],
83 progress: progress.start(`download ${name}`),
84 });
85 // the server's /tmp is memory
86 await subprocess.execute(["ssh", remote, `rm ${snapshot}`]);
87 // a write-ahead log beside the old copy would be applied to the new one
88 for (const log of ["-wal", "-shm"]) fs.rmSync(`.clover/${name}${log}`, { force: true });
89 fs.renameSync(`.clover/${name}.next`, `.clover/${name}`);
90}
91
92import { Path } from "#sitegen/path";
93import { ASSERT } from "@clo/lib/assert";
94import type * as incremental from "@clo/lib/incremental";
95import * as progress from "@clo/lib/progress";
96import * as subprocess from "@clo/lib/subprocess";
97import * as fs from "node:fs";
98import * as rsync from "../file-viewer/rsync.ts";
99import { buildAndWriteOutput } from "../paperclover.net.ts";