#!/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/ $WEBROOT/current.new && mv -T $WEBROOT/current.new $WEBROOT/current" echo pm2 list