Files
recurring-checklist/build-deploy.sh
T
Waters ChurchandClaude Opus 5 469db5bc60 Bring the checklist app under version control
The project has lived only as a folder on the Mac Studio and, briefly, as a
transfer tarball. This is the first commit: the Express server and its web
front end, the Capacitor iOS and Android wrappers, and the release and backup
scripts.

The repo is the source, not a backup of the machine. Left out on purpose, by
the .gitignore files that were already in the tree: node_modules, build output,
mobile/www (regenerated from server/public on every build), and server/data,
which holds the live checklists and belongs on the NAS rather than in git.

Every credential is excluded too — server/.env, the Play upload keystore and
its properties file, the App Store Connect key and ios-signing.config. The one
secret that was not in an ignored file was the NAS SSH password, hard-coded in
build-deploy.sh and grepped back out of it by backup-to-drive.sh. Both now read
it from the login keychain (service checklist-nas, account Christian), falling
back to NAS_PASS in the environment, so rotating it is one keychain entry
rather than a history rewrite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:00:57 -04:00

125 lines
4.8 KiB
Bash
Executable File

#!/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"