#!/usr/bin/env bash # deploy.sh — local native-quarto build + deploy for jayrup.me. # For machines with quarto installed natively (e.g. Arch laptop / voidlaptop). # The Docker-based path is deploy-docker.sh (used by meru's post-receive hook). # # Strict mode: a failed render exits BEFORE rsync --delete can touch the VPS. set -euo pipefail # Resolve the repo root from this script's location and capture the SHA up # front: the build runs inside a temp dir with no .git, so every later git # call must be pointed at $REPO explicitly. REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SHA="$(git -C "$REPO" rev-parse --short HEAD)" DEPLOY_HOST="${DEPLOY_HOST:-jayrup.me}" # 1. Render into a fresh temp dir so a partial build never gets deployed BUILD_DIR="$(mktemp -d /tmp/homepage-local.XXXXXX)" trap 'rm -rf "$BUILD_DIR"' EXIT cd "$BUILD_DIR" git -C "$REPO" archive --format=tar --output="$BUILD_DIR/tree.tar" HEAD tar xf tree.tar rm tree.tar # 2. Render the whole website (HTML) # This creates the directories and index.html files correctly quarto render --to html # 3. Render the Plain Text versions manually to ensure mirroring # We find every .qmd, and render it to a .txt in the corresponding public/ folder find . -name "*.qmd" -not -path "./.*" -not -path "./public/*" | while read -r file; do # Get the directory structure (e.g., ./projects/index.qmd -> projects) rel_dir=$(dirname "${file#./}") # Get the filename without extension (e.g., index) base_name=$(basename "$file" .qmd) # Render to plain text directly into the correct public sub-directory quarto render "$file" --to plain --output "${base_name}.txt" --output-dir "public/$rel_dir" done # 4. Add cv cp cv/index.pdf public/cv.pdf cp cv/index.txt public/cv.txt # 5. Add assets (static files bundled in the repo) mkdir -p public/assets cp -r assets/* public/ cp -r assets/* public/assets/ # .txt twin for curl/wget: normalize to exactly one line ending in a newline, # so `curl https://jayrup.me/public_key >> ~/.ssh/authorized_keys` appends a # clean key instead of gluing itself onto the previous line. KEY="$(tr -d '\r' < assets/public_key | sed -e 's/[[:space:]]*$//' | tr -d '\n')" [ -n "$KEY" ] || { echo "assets/public_key is empty" >&2; exit 1; } printf '%s\n' "$KEY" > public/public_key.txt # 6. Deploy marker printf 'deployed %s commit %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SHA" > public/last-deploy.txt echo "Pushing to vps" # 7. Content. Only reached if all renders succeeded (set -e) rsync -avz --delete public/ "$DEPLOY_HOST":~/homepage/public/ # 8. nginx config. It is bind-mounted into the container as a SINGLE FILE, so # it does not travel with public/, and rsync's rename-into-place would swap # the inode the mount points at: the container would keep reading the old # inode while `nginx -t` cheerfully validated it. Stage to a sidecar, then # copy in place (cat >) so the mounted inode keeps its identity. # Test before reloading; on failure restore the previous content the same # way and fail loudly instead of taking the running workers down. echo "Pushing nginx.conf" rsync -avz nginx.conf "$DEPLOY_HOST":~/homepage/nginx.conf.new ssh "$DEPLOY_HOST" 'cp -a ~/homepage/nginx.conf ~/homepage/nginx.conf.prev' ssh "$DEPLOY_HOST" 'cat ~/homepage/nginx.conf.new > ~/homepage/nginx.conf' if ssh "$DEPLOY_HOST" 'docker exec homepage nginx -t'; then ssh "$DEPLOY_HOST" 'docker exec homepage nginx -s reload' ssh "$DEPLOY_HOST" 'rm -f ~/homepage/nginx.conf.new' echo "nginx reloaded" else echo "!! nginx config failed its test - restoring previous config" >&2 ssh "$DEPLOY_HOST" 'cat ~/homepage/nginx.conf.prev > ~/homepage/nginx.conf' exit 1 fi # A single-file bind mount pins whatever inode existed when the container # started. If the host file was ever replaced by rename (an older deploy, an # editor, `cp`), the container keeps serving the old bytes while `nginx -t` # validates them happily. Compare what the container actually sees and restart # to re-resolve the path if it has drifted. HOST_MD5="$(ssh "$DEPLOY_HOST" 'md5sum ~/homepage/nginx.conf' | cut -d' ' -f1)" CTR_MD5="$(ssh "$DEPLOY_HOST" 'docker exec homepage md5sum /etc/nginx/conf.d/default.conf' | cut -d' ' -f1)" if [ "$HOST_MD5" != "$CTR_MD5" ]; then echo "!! container is serving a stale nginx.conf (bind mount detached) - restarting" >&2 ssh "$DEPLOY_HOST" 'docker restart homepage' CTR_MD5="$(ssh "$DEPLOY_HOST" 'docker exec homepage md5sum /etc/nginx/conf.d/default.conf' | cut -d' ' -f1)" [ "$HOST_MD5" = "$CTR_MD5" ] || { echo "!! nginx.conf still not mounted - aborting" >&2; exit 1; } fi