summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-30 00:26:08 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-30 00:26:08 -0700
commitc0724a62c5e5742469339ec7aef4d0f509e10559 (patch)
treeb9e646b400f49a8759b5f0d5ecbab3b80e8a3387
parent5fbc175e540803d919863f3d90dffc3c0645a90b (diff)
fix: align deploy flow with post-receive checkout (no .git in /var/www)
The working tree at /var/www/prism-v2 is populated by a post-receive hook that does `git --work-tree=... checkout -f`, so it has no .git directory. Drop git operations from deploy.sh and add scripts/post-receive.sample plus README setup for the bare repo + hook + sudoers wiring. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
-rw-r--r--AGENTS.md7
-rw-r--r--CLAUDE.md2
-rw-r--r--README.md55
-rwxr-xr-xscripts/deploy.sh52
-rwxr-xr-xscripts/post-receive.sample32
5 files changed, 94 insertions, 54 deletions
diff --git a/AGENTS.md b/AGENTS.md
index fbb2f2a..98e355e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -29,12 +29,7 @@ cd frontend && npm run lint && npm run build
Topology: uvicorn on `127.0.0.1:8001`, `next start` on `127.0.0.1:3001`, nginx TLS reverse-proxy (`/api/` → backend, `/` → frontend). See `README.md` for the full deploy and redeploy commands. Service user is `www-data`; code lives at `/var/www/prism-v2/`.
-Redeploy:
-
-```bash
-cd /var/www/prism-v2 && sudo ./scripts/deploy.sh
-# --no-pull to skip git pull; --install to also refresh systemd units + nginx site
-```
+Deploy flow: bare repo at `/srv/git/prism-v2.git`; pushing master triggers a post-receive hook that `git checkout -f`s the tree into `/var/www/prism-v2/` and runs `scripts/deploy.sh` (build + restart + smoke check). `/var/www/prism-v2/` has no `.git/`. Manual deploy on the server: `sudo ./scripts/deploy.sh` (add `--install` to refresh systemd units / nginx site). See README for first-time setup.
## Coding Style
diff --git a/CLAUDE.md b/CLAUDE.md
index 5fc6417..39040f7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -41,7 +41,7 @@ Key files:
Topology: uvicorn on `127.0.0.1:8001`, `next start` on `127.0.0.1:3001`, nginx TLS reverse-proxy at `prism.tylerhoang.xyz` (`/api/` → backend, `/` → frontend). Service user `www-data`, code at `/var/www/prism-v2/`. Full deploy instructions in `README.md`.
-**Redeploy:** `sudo ./scripts/deploy.sh` from `/var/www/prism-v2` (idempotent — pull, install deps as `www-data`, build, restart, smoke-check). Flags: `--no-pull`, `--install` (refresh systemd units + nginx site).
+**Deploy flow:** bare repo at `/srv/git/prism-v2.git`; pushing master triggers a post-receive hook that `git checkout -f`s into `/var/www/prism-v2/` and runs `scripts/deploy.sh`. `/var/www/prism-v2/` has NO `.git/` — never try to `git pull` there. Manual server-side deploy: `sudo ./scripts/deploy.sh` (`--install` also refreshes systemd units + nginx site). Sample hook lives at `scripts/post-receive.sample`.
**Logs / status:**
```bash
diff --git a/README.md b/README.md
index 9f6b77d..cf2eecb 100644
--- a/README.md
+++ b/README.md
@@ -51,36 +51,63 @@ SQLite lives at `backend/data/prism.db`. Backend seeds a `default` profile on st
## Production Deployment
-Target topology: backend on `127.0.0.1:8001`, frontend on `127.0.0.1:3001`, nginx terminates TLS and reverse-proxies. Code lives at `/var/www/prism-v2/` owned by `www-data`.
+Target topology:
-Use `scripts/deploy.sh` on the server — it is idempotent and handles both initial install and redeploys.
+- Bare repo at `/srv/git/prism-v2.git` (push target)
+- Working tree at `/var/www/prism-v2/` owned by `www-data` (no `.git/` — populated by post-receive hook via `git --work-tree=... checkout -f`)
+- Backend on `127.0.0.1:8001`, frontend on `127.0.0.1:3001`
+- nginx terminates TLS and reverse-proxies `/api/` → backend, `/` → frontend
-### First-time setup on a fresh server
+Workflow: `git push prod master` on your laptop → post-receive hook checks out the tree into `/var/www/prism-v2/` and runs `scripts/deploy.sh`, which builds + restarts.
+
+### First-time setup
```bash
-# As root / via sudo
-mkdir -p /var/www && chown www-data:www-data /var/www
-sudo -u www-data git clone <repo-url> /var/www/prism-v2
-cd /var/www/prism-v2
+# 1. Create bare repo
+sudo mkdir -p /srv/git && sudo git init --bare /srv/git/prism-v2.git
+
+# 2. Create working tree dir
+sudo install -d -o www-data -g www-data /var/www/prism-v2
-# Install systemd units + nginx site + build + start
+# 3. Push from your laptop so the working tree gets populated
+# (add the remote on your laptop: git remote add prod user@host:/srv/git/prism-v2.git)
+# git push prod master
+
+# 4. On the server: install systemd units + nginx site + build + start
+cd /var/www/prism-v2
sudo ./scripts/deploy.sh --install
-# First-time TLS (Certbot edits the nginx server block in-place)
+# 5. First-time TLS (Certbot edits the nginx server block in-place)
sudo certbot --nginx -d prism.tylerhoang.xyz
sudo systemctl reload nginx
+
+# 6. Install the post-receive hook so future pushes auto-deploy
+sudo cp /var/www/prism-v2/scripts/post-receive.sample /srv/git/prism-v2.git/hooks/post-receive
+sudo chmod +x /srv/git/prism-v2.git/hooks/post-receive
+
+# 7. Sudoers entry so the git-push user can run deploy.sh without a password
+# (replace GIT_USER with the account that owns /srv/git or receives the push)
+echo 'GIT_USER ALL=(root) NOPASSWD: /var/www/prism-v2/scripts/deploy.sh' | sudo tee /etc/sudoers.d/prism-v2-deploy
+sudo chmod 0440 /etc/sudoers.d/prism-v2-deploy
+```
+
+### Subsequent deploys
+
+From your laptop:
+
+```bash
+git push prod master # post-receive hook checks out + runs deploy.sh
```
-### Redeploy
+Manual deploy on the server (e.g. retry after a failed build, or to refresh systemd/nginx):
```bash
cd /var/www/prism-v2
-sudo ./scripts/deploy.sh # pull + build + restart + smoke check
-sudo ./scripts/deploy.sh --no-pull # rebuild + restart without git pull
-sudo ./scripts/deploy.sh --install # also refresh systemd units / nginx site
+sudo ./scripts/deploy.sh # build + restart + smoke check
+sudo ./scripts/deploy.sh --install # also refresh systemd units / nginx site
```
-The script runs all build steps as `www-data` (with `HOME` + `NPM_CONFIG_CACHE` set), restarts both services, and curls `/health` on the backend and `/` on the frontend before exiting.
+`deploy.sh` is idempotent: ensures ownership, builds the backend venv and `npm ci && npm run build` as `www-data` (with `HOME` + `NPM_CONFIG_CACHE` set), restarts both services, and curls `/health` and `/` before exiting. It does NOT touch git — the post-receive hook owns checkout.
### Ops
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index f192e72..5eae840 100755
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -2,12 +2,12 @@
#
# Production deploy script for Prism v2.
#
-# Runs on the production server (in /var/www/prism-v2). Idempotent — handles
-# both first-time install and redeploys.
+# Runs on the production server, AFTER the working tree has been updated
+# (typically by a git post-receive hook that does `git checkout -f` into
+# /var/www/prism-v2). This script does NOT pull — it only builds + restarts.
#
# Usage:
-# sudo ./scripts/deploy.sh # full deploy (pull + build + restart)
-# sudo ./scripts/deploy.sh --no-pull # build + restart only (skip git pull)
+# sudo ./scripts/deploy.sh # build + restart + smoke-check
# sudo ./scripts/deploy.sh --install # also install systemd units + nginx site
# sudo ./scripts/deploy.sh --help
@@ -20,17 +20,15 @@ DOMAIN="${DOMAIN:-prism.tylerhoang.xyz}"
BACKEND_SVC="prismv2-backend.service"
FRONTEND_SVC="prismv2-frontend.service"
-DO_PULL=1
DO_INSTALL=0
usage() {
- sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
+ sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
for arg in "$@"; do
case "$arg" in
- --no-pull) DO_PULL=0 ;;
--install) DO_INSTALL=1 ;;
-h|--help) usage ;;
*) echo "Unknown arg: $arg" >&2; exit 2 ;;
@@ -39,26 +37,13 @@ done
log() { printf '\n=== %s ===\n' "$*"; }
-require_root() {
- if [[ $EUID -ne 0 ]]; then
- echo "deploy.sh must be run as root (use sudo)" >&2
- exit 1
- fi
-}
-
-run_as_app() {
- # Run a command as APP_USER with HOME + NPM_CONFIG_CACHE set so npm/git work.
- sudo -u "$APP_USER" \
- env HOME="$APP_DIR/frontend" \
- NPM_CONFIG_CACHE="$APP_DIR/frontend/.npm" \
- "$@"
-}
-
-require_root
+if [[ $EUID -ne 0 ]]; then
+ echo "deploy.sh must be run as root (use sudo)" >&2
+ exit 1
+fi
-if [[ ! -d "$APP_DIR/.git" ]]; then
- echo "Not a checkout: $APP_DIR (expected $APP_DIR/.git)" >&2
- echo "Clone the repo to $APP_DIR first." >&2
+if [[ ! -d "$APP_DIR/backend" || ! -d "$APP_DIR/frontend" ]]; then
+ echo "Expected working tree at $APP_DIR (backend/ + frontend/ not found)" >&2
exit 1
fi
@@ -66,13 +51,15 @@ cd "$APP_DIR"
log "Ensuring ownership of $APP_DIR"
chown -R "$APP_USER:$APP_GROUP" "$APP_DIR"
-mkdir -p "$APP_DIR/frontend/.npm"
-chown -R "$APP_USER:$APP_GROUP" "$APP_DIR/frontend/.npm"
+install -d -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/frontend/.npm"
+install -d -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/backend/data"
-if [[ $DO_PULL -eq 1 ]]; then
- log "git pull origin master"
- sudo -u "$APP_USER" git -C "$APP_DIR" pull --ff-only origin master
-fi
+run_as_app() {
+ sudo -u "$APP_USER" \
+ env HOME="$APP_DIR/frontend" \
+ NPM_CONFIG_CACHE="$APP_DIR/frontend/.npm" \
+ "$@"
+}
log "Backend: venv + dependencies"
if [[ ! -x "$APP_DIR/backend/.venv/bin/pip" ]]; then
@@ -108,7 +95,6 @@ if curl -fsS http://127.0.0.1:8001/health >/dev/null; then
echo " backend /health OK"
else
echo " backend /health FAILED" >&2
- echo " journalctl -u $BACKEND_SVC -n 50 --no-pager:" >&2
journalctl -u "$BACKEND_SVC" -n 50 --no-pager >&2 || true
exit 1
fi
diff --git a/scripts/post-receive.sample b/scripts/post-receive.sample
new file mode 100755
index 0000000..3ee898d
--- /dev/null
+++ b/scripts/post-receive.sample
@@ -0,0 +1,32 @@
+#!/bin/bash
+#
+# Sample git post-receive hook for /srv/git/prism-v2.git on the VPS.
+#
+# Install:
+# sudo cp /var/www/prism-v2/scripts/post-receive.sample /srv/git/prism-v2.git/hooks/post-receive
+# sudo chmod +x /srv/git/prism-v2.git/hooks/post-receive
+#
+# Required sudoers entry so the git user can restart services without a password.
+# Adjust GIT_USER if your git is owned by a different account.
+# GIT_USER ALL=(root) NOPASSWD: /var/www/prism-v2/scripts/deploy.sh
+
+set -euo pipefail
+
+BRANCH="master"
+WEB_DIR="/var/www/prism-v2"
+GIT_DIR="/srv/git/prism-v2.git"
+LOG="/var/log/git-deploy.log"
+
+exec >> "$LOG" 2>&1
+
+while read -r oldrev newrev refname; do
+ if [ "$refname" = "refs/heads/$BRANCH" ]; then
+ echo "$(date -Is): Checking out $BRANCH to $WEB_DIR"
+ sudo -u www-data git --work-tree="$WEB_DIR" --git-dir="$GIT_DIR" checkout -f "$BRANCH"
+
+ echo "$(date -Is): Running deploy.sh"
+ sudo "$WEB_DIR/scripts/deploy.sh"
+
+ echo "$(date -Is): Done."
+ fi
+done