#!/bin/bash # Build a deploy tarball from server/ and push it to the production NAS. # Excludes anything that should NEVER be overwritten on the server: # - data/ (user accounts, check states, list content) # - .env (admin password, Gmail credentials) # - node_modules (rebuilt by Docker) # # Usage: ./build-deploy.sh # # Requires: # - /tmp/sshstdin.exp (created on demand) # - /tmp/deploy.sh (created on demand) # - Christian's SSH password, from the login keychain (see below) set -e ROOT="$(cd "$(dirname "$0")" && pwd)" TARBALL="$ROOT/checklist-deploy.tgz" NAS_HOST="10.1.1.5" NAS_USER="Christian" NAS_PATH="/volume2/docker/recurring-checklist" # The NAS password is NOT stored in this file — it would end up in the repo history, # where rotating it means rewriting history rather than changing one keychain entry. # It is read from the login keychain, or from NAS_PASS in the environment for a machine # that has no keychain (CI, a Linux box). # # To store it, or after a password change: # security add-generic-password -U -s checklist-nas -a Christian -w # (omit the value and security prompts for it, so it never reaches your shell history) NAS_PASS="${NAS_PASS:-$(security find-generic-password -s checklist-nas -a Christian -w 2>/dev/null || true)}" if [ -z "$NAS_PASS" ]; then echo "No NAS password available." >&2 echo "Store it once with: security add-generic-password -U -s checklist-nas -a Christian -w" >&2 echo "or export NAS_PASS before running this script." >&2 exit 1 fi echo "==> Building tarball (excluding data/, .env, node_modules)" tar \ --exclude='./node_modules' \ --exclude='./data' \ --exclude='./data-smoke' \ --exclude='./.DS_Store' \ --exclude='./.env' \ -czf "$TARBALL" -C "$ROOT/server" . echo "==> Sanity check: tarball must not contain data/ or .env" if tar -tzf "$TARBALL" | grep -E '^\./?(data|\.env)(/|$)' >/dev/null; then echo "REFUSING to deploy — tarball contains data/ or .env" tar -tzf "$TARBALL" | grep -E '^\./?(data|\.env)(/|$)' exit 1 fi echo " OK ($(ls -lh "$TARBALL" | awk '{print $5}'))" # Re-create helper scripts if missing if [ ! -f /tmp/sshstdin.exp ]; then cat > /tmp/sshstdin.exp <<'EXP' #!/usr/bin/expect -f set timeout 300 log_user 1 set host [lindex $argv 0] set user [lindex $argv 1] set pass [lindex $argv 2] set cmd [lindex $argv 3] set localfile [lindex $argv 4] spawn bash -c "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=20 -o PreferredAuthentications=password -o PubkeyAuthentication=no -o GSSAPIAuthentication=no -o NumberOfPasswordPrompts=1 $user@$host \"$cmd\" < $localfile" expect { -nocase "password:" { send "$pass\r"; exp_continue } eof } catch wait result exit [lindex $result 3] EXP fi cat > /tmp/deploy.sh <<'DEPLOY' #!/bin/sh set -e cd /volume2/docker/recurring-checklist echo "--- DEPLOY-SIDE SANITY CHECK ---" if tar -tzf checklist-deploy.tgz | grep -E '^\./?(data|\.env)(/|$)' >/dev/null; then echo "ABORT: incoming tarball contains data/ or .env — refusing to extract" exit 1 fi echo "--- EXTRACT (data/ and .env are protected because tarball doesn't include them) ---" tar -xzf checklist-deploy.tgz rm -f checklist-deploy.tgz rm -rf PaxHeader find . -name "._*" -delete echo "--- VERIFY DATA UNTOUCHED ---" ls -la data/checklists.json 2>/dev/null && echo "data/checklists.json preserved" || echo "(no checklists.json yet)" ls -la .env 2>/dev/null && echo ".env preserved" || echo "(no .env)" echo "--- DEPLOY: recreate in place (NO prior 'down') ---" # Deliberately NO 'docker-compose down' first. 'down' removes the container AND the network, so a # deploy interrupted between 'down' and 'up' leaves the whole stack torn down (caused an outage once). # 'up -d --build --remove-orphans' rebuilds the image and recreates ONLY the app container when its # image changed, in place — the network and the data volume persist. If interrupted, the worst case is # the old container still running, never a fully removed stack. --remove-orphans clears stale containers. echo "$REMOTE_PASS" | sudo -S /usr/local/bin/docker-compose -f /volume2/docker/recurring-checklist/docker-compose.yml up -d --build --remove-orphans 2>&1 | tail -14 echo "--- STATUS ---" echo "$REMOTE_PASS" | sudo -S /usr/local/bin/docker ps --filter name=recurring --format "{{.Names}} {{.Status}}" echo "--- HEALTHCHECK ---" sleep 4 curl -sS -o /dev/null -w "GET /api/me => HTTP %{http_code}\n" http://127.0.0.1:3000/api/me DEPLOY echo "==> Uploading to $NAS_USER@$NAS_HOST:$NAS_PATH" expect /tmp/sshstdin.exp "$NAS_HOST" "$NAS_USER" "$NAS_PASS" \ "cat > $NAS_PATH/checklist-deploy.tgz && echo UPLOAD_OK" \ "$TARBALL" 2>&1 | tail -3 echo "==> Running deploy script on NAS" expect /tmp/sshstdin.exp "$NAS_HOST" "$NAS_USER" "$NAS_PASS" \ "REMOTE_PASS='$NAS_PASS' bash -s" \ /tmp/deploy.sh 2>&1 | tail -15 echo "==> Done"