172 lines
6.3 KiB
Bash
Executable File
172 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Deploy Solid Scribe. Run this ON the server, from inside the repo.
|
|
#
|
|
# cd /path/to/SolidScribe && ./deploy.sh
|
|
#
|
|
# Replaces buildAndUpdateProd.sh, which built the old Vue 2 client and rsynced a
|
|
# tarball to /home/mab/pi -- a directory that no longer exists.
|
|
#
|
|
# Assumes:
|
|
# - this repo is a git clone with a tracking branch
|
|
# - node + pnpm are on PATH for the user running this (on NixOS that means
|
|
# environment.systemPackages or the user's profile -- NOT only inside a
|
|
# nix-shell, or this will fail at the install step)
|
|
# - pm2 is already managing NoteServer (pm2 resurrect on boot)
|
|
# - nginx serves $WEBROOT/current with an SPA fallback to index.html
|
|
#
|
|
# nginx is NOT touched here. On NixOS it is declarative -- see configs/.
|
|
#
|
|
# --- the web root --------------------------------------------------------
|
|
#
|
|
# The built client is published to /var/www/ss, NOT served out of the repo.
|
|
# nginx runs as its own user, so serving from a clone under /home/mab meant
|
|
# loosening the home directory's permissions for every parent dir. /var/www is
|
|
# the conventional place for this and sidesteps the whole problem.
|
|
#
|
|
# Layout, which is the standard symlink-swap pattern:
|
|
#
|
|
# /var/www/ss/
|
|
# releases/2026-10-28_1147/ <- one directory per deploy
|
|
# releases/2026-10-28_0930/
|
|
# current -> releases/2026-10-28_1147
|
|
#
|
|
# nginx points at the stable path $WEBROOT/current. Publishing is a rename of
|
|
# that symlink, which is a single atomic syscall: no request ever sees a
|
|
# half-copied directory, and no window exists where the root is missing.
|
|
# Rolling back is the same rename pointing at the previous release.
|
|
#
|
|
# Only the *client* moves. The API server still runs from the repo under pm2,
|
|
# and user uploads are still served by Express from $REPO/staticFiles at
|
|
# /api/static -- deliberately, so deploys never touch user data.
|
|
#
|
|
# ONE-TIME SETUP on a new server (needs root):
|
|
#
|
|
# sudo mkdir -p /var/www/ss/releases
|
|
# sudo chown -R "$USER" /var/www/ss
|
|
# sudo chmod 755 /var/www /var/www/ss
|
|
#
|
|
# Override the location for a staging box with: WEBROOT=/var/www/ss-staging ./deploy.sh
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$REPO"
|
|
|
|
WEBROOT="${WEBROOT:-/var/www/ss}"
|
|
RELEASE="$WEBROOT/releases/$(date +%Y-%m-%d_%H%M%S)"
|
|
# Number of old releases to keep around for rollback.
|
|
KEEP_RELEASES=5
|
|
|
|
step(){ printf '\n\033[32m::--:: %s\033[0m\n' "$1"; }
|
|
|
|
# --- sanity checks before changing anything ------------------------------
|
|
|
|
for tool in git pnpm pm2 rsync; do
|
|
command -v "$tool" >/dev/null || { echo "$tool is not on PATH" >&2; exit 1; }
|
|
done
|
|
|
|
# Fail here rather than after a three-minute build. A missing web root is
|
|
# almost always a server that never had the one-time setup above run on it.
|
|
if [ ! -d "$WEBROOT/releases" ]; then
|
|
echo "$WEBROOT/releases does not exist -- run the one-time setup at the top of this script." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -w "$WEBROOT/releases" ]; then
|
|
echo "$WEBROOT/releases is not writable by $(whoami) -- check the chown in the one-time setup." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
|
|
echo "Working tree has local modifications. Commit or stash them first:" >&2
|
|
git status --short >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --- pull ----------------------------------------------------------------
|
|
|
|
step "Pulling $(git rev-parse --abbrev-ref HEAD)..."
|
|
# --ff-only: refuse to create a merge commit on the server. If this fails the
|
|
# server has diverged and wants a human.
|
|
git pull --ff-only
|
|
|
|
# --- dependencies --------------------------------------------------------
|
|
|
|
step "Installing server dependencies..."
|
|
pnpm install --frozen-lockfile
|
|
|
|
step "Installing client dependencies..."
|
|
pnpm install --frozen-lockfile --dir client
|
|
|
|
# --- build ---------------------------------------------------------------
|
|
|
|
# dist is gitignored, so the server builds it. The build stays inside the repo
|
|
# and is only copied to $WEBROOT once it has been checked, so a failed build
|
|
# cannot touch what is currently being served.
|
|
step "Building client..."
|
|
rm -rf client/dist
|
|
pnpm --dir client exec vite build
|
|
|
|
# manifest.json is checked in under client/public and copied verbatim by Vite;
|
|
# if it is missing, the PWA installs without icons, scope or a share target.
|
|
for required in index.html sw.js manifest.json; do
|
|
if [ ! -f "client/dist/$required" ]; then
|
|
echo "Build output looks wrong (missing $required) -- $WEBROOT left untouched." >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# --- publish -------------------------------------------------------------
|
|
|
|
step "Publishing to $RELEASE..."
|
|
mkdir -p "$RELEASE"
|
|
# Trailing slash on the source: copy the *contents* of dist into the release
|
|
# dir. --delete is harmless on a fresh dir but keeps a re-run of the same
|
|
# timestamp honest.
|
|
rsync -a --delete client/dist/ "$RELEASE/"
|
|
|
|
# nginx reads these as its own user, so they must be world-readable.
|
|
chmod -R a+rX "$RELEASE"
|
|
|
|
step "Pointing $WEBROOT/current at the new release..."
|
|
# Two steps because `ln -sfn` over an existing symlink unlinks before relinking,
|
|
# which leaves a brief window with no web root at all. Creating a scratch
|
|
# symlink and renaming it over the old one is a single atomic rename(2).
|
|
ln -sfn "$RELEASE" "$WEBROOT/current.new"
|
|
mv -T "$WEBROOT/current.new" "$WEBROOT/current"
|
|
|
|
# --- prune ---------------------------------------------------------------
|
|
|
|
# Keep the most recent $KEEP_RELEASES for rollback, drop the rest. Never remove
|
|
# whatever `current` actually resolves to, however old it is.
|
|
live="$(readlink -f "$WEBROOT/current")"
|
|
ls -1d "$WEBROOT"/releases/*/ 2>/dev/null \
|
|
| sort -r \
|
|
| tail -n +$((KEEP_RELEASES + 1)) \
|
|
| while read -r stale; do
|
|
[ "$(readlink -f "$stale")" = "$live" ] && continue
|
|
echo " pruning $(basename "$stale")"
|
|
rm -rf "$stale"
|
|
done
|
|
|
|
# --- restart -------------------------------------------------------------
|
|
|
|
step "Reloading the API server..."
|
|
# reload, not restart: graceful in cluster mode. --update-env so changes to
|
|
# ecosystem.config.js are picked up.
|
|
pm2 reload "$REPO/server/ecosystem.config.js" --env production --update-env
|
|
|
|
# Persist the process list so `pm2 resurrect` on boot gets the current state.
|
|
pm2 save
|
|
|
|
step "Done."
|
|
echo " serving $(readlink -f "$WEBROOT/current")"
|
|
echo
|
|
echo " To roll back, repoint the symlink at the previous release:"
|
|
echo " ls $WEBROOT/releases"
|
|
echo " ln -sfn $WEBROOT/releases/<older> $WEBROOT/current.new && mv -T $WEBROOT/current.new $WEBROOT/current"
|
|
echo
|
|
pm2 list
|