syntaxai/tdd.md · main · scripts / p620 / deploy-tdd-md.sh
#!/usr/bin/env bash
# Deploy de tdd.md Bun-server naar p620 (default ssh-host).
#
# DEFAULT MODE — git-pull from the LOCAL BARE REPO at
# /home/scri/repos/tdd.md.git on p620. That bare repo is the canonical
# source: dev pushes to it via SSH, admin web-edits commit to it via
# c14_git plumbing inside the container, the deploy reads from it
# here. Forgejo is no longer in this loop.
#
# Steps:
# 1. ssh p620: cd ~/src/tdd.md && git fetch /home/scri/repos/tdd.md.git
# + reset --hard FETCH_HEAD
# 2. snapshot git history + test results into content/git-history/
# 3. bundle sama-cli into public/sama-cli
# 4. podman build localhost/tdd-md:latest (only when source-hash changed)
# 5. Quadlet sync (tdd.pod, tdd-md.container)
# 6. systemd reload + (re)start; wait for /healthz
#
# BOOTSTRAP MODE (--bootstrap) — for first-time setup of a new p620:
# Clones from the local bare repo at /home/scri/repos/tdd.md.git into
# ~/src/tdd.md. Assumes the bare repo already exists (a one-time
# `git init --bare` or `git clone --bare` from somewhere else seeded it).
#
# RSYNC MODE (--rsync) — emergency / pre-Forgejo escape hatch:
# Skip the git pull and rsync the local working tree to p620. Use when
# you need to deploy uncommitted changes (e.g. debugging) and accept
# that the deploy will diverge from git.tdd.md until you commit + push.
#
# Usage:
# ./scripts/p620/deploy-tdd-md.sh # default: git-pull from Forgejo
# ./scripts/p620/deploy-tdd-md.sh --bootstrap # first-time clone setup
# ./scripts/p620/deploy-tdd-md.sh --rsync # rsync local tree (emergency)
# ./scripts/p620/deploy-tdd-md.sh --host other # different ssh host
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SSH_HOST="p620"
REMOTE_SRC_DIR="src/tdd.md" # relative to ssh-home
REMOTE_BARE_REPO="/home/scri/repos/tdd.md.git" # canonical bare repo
IMAGE_TAG="localhost/tdd-md:latest"
MODE="git" # git | rsync | bootstrap
while [[ $# -gt 0 ]]; do
case "$1" in
--host) SSH_HOST="$2"; shift 2 ;;
--rsync) MODE="rsync"; shift ;;
--bootstrap) MODE="bootstrap"; shift ;;
-h|--help) sed -n '2,29p' "$0" | sed 's/^# \?//'; exit 0 ;;
*) echo "✗ unknown arg: $1"; exit 1 ;;
esac
done
echo "→ preflight op $SSH_HOST (mode=$MODE)"
ssh "$SSH_HOST" 'command -v podman >/dev/null && command -v systemctl >/dev/null && command -v rsync >/dev/null && command -v git >/dev/null' \
|| { echo "✗ podman/systemctl/rsync/git ontbreekt op $SSH_HOST"; exit 1; }
need_restart=0
# ---------------------------------------------------------------------
# Bootstrap mode — one-time clone of syntaxai/tdd.md from git.tdd.md.
# Uses the same Forgejo admin token that the running container has, so
# we read it from the existing podman secret rather than asking the
# operator to paste it.
# ---------------------------------------------------------------------
if [[ "$MODE" == "bootstrap" ]]; then
echo "→ bootstrap: cloning local bare repo $REMOTE_BARE_REPO into ~/$REMOTE_SRC_DIR"
ssh "$SSH_HOST" "
set -e
if [[ ! -d $REMOTE_BARE_REPO ]]; then
echo '✗ bare repo $REMOTE_BARE_REPO does not exist on $SSH_HOST.'
echo ' Seed it once with: git init --bare $REMOTE_BARE_REPO'
echo ' Then push your dev tree: git remote add p620 ssh://$SSH_HOST$REMOTE_BARE_REPO && git push p620 main'
exit 1
fi
if [[ -d ~/$REMOTE_SRC_DIR/.git ]]; then
echo ' ✓ already a git working tree — nothing to bootstrap'
else
mkdir -p ~/$(dirname $REMOTE_SRC_DIR)
git clone $REMOTE_BARE_REPO ~/$REMOTE_SRC_DIR
echo ' ✓ cloned from local bare repo'
fi
"
echo "✓ bootstrap done — re-run without --bootstrap to deploy"
exit 0
fi
# ---------------------------------------------------------------------
# Source sync — either git pull (canonical) or rsync (emergency).
# ---------------------------------------------------------------------
if [[ "$MODE" == "git" ]]; then
echo "→ fetch from local bare repo $REMOTE_BARE_REPO into ~/$REMOTE_SRC_DIR"
ssh "$SSH_HOST" "
set -e
if [[ ! -d ~/$REMOTE_SRC_DIR/.git ]]; then
echo '✗ ~/$REMOTE_SRC_DIR is not a git working tree — run: ./scripts/p620/deploy-tdd-md.sh --bootstrap'
exit 1
fi
cd ~/$REMOTE_SRC_DIR
git fetch $REMOTE_BARE_REPO main
git reset --hard FETCH_HEAD
head=\$(git rev-parse --short HEAD)
echo \" ✓ at \$head ('\$(git log -1 --pretty=format:%s)')\"
"
else
# --rsync escape hatch — keeps the legacy path so we can deploy
# uncommitted dev changes when needed.
echo "→ snapshot git history → content/git-history/ (rsync mode runs locally)"
( cd "$REPO_ROOT" && bun scripts/p620/snapshot-git-history.ts ) \
|| { echo "✗ snapshot-git-history mislukt"; exit 1; }
echo "→ snapshot tests (bun test --reporter=junit) → content/git-history/"
( cd "$REPO_ROOT" && bun scripts/p620/snapshot-tests.ts ) \
|| { echo "✗ snapshot-tests mislukt"; exit 1; }
echo "→ bundle sama CLI → public/sama-cli"
( cd "$REPO_ROOT" && bun build scripts/sama-cli.ts --target=bun --outfile=public/sama-cli >/dev/null ) \
|| { echo "✗ sama-cli bundle mislukt"; exit 1; }
chmod +x "$REPO_ROOT/public/sama-cli"
echo "→ rsync local tree → $SSH_HOST:~/$REMOTE_SRC_DIR (⚠ overwrites Forgejo state until you commit+push)"
ssh "$SSH_HOST" "mkdir -p ~/$REMOTE_SRC_DIR"
rsync -az --delete \
--exclude='node_modules' \
--exclude='.git' \
--exclude='scripts' \
--exclude='.bun-cache' \
--exclude='.DS_Store' \
--exclude='*.log' \
--exclude='.auth' \
--exclude='e2e' \
--exclude='playwright.config.ts' \
--exclude='test-results' \
--exclude='playwright-report' \
"$REPO_ROOT"/ "$SSH_HOST:$REMOTE_SRC_DIR/"
fi
# ---------------------------------------------------------------------
# Snapshot git-history + bundle sama-cli — only meaningful in git mode
# (rsync mode already did this above against the local tree).
#
# p620 itself has no bun on the host PATH; the canonical Bun runtime
# lives in the tdd-md image. Run the scripts via `podman run` on the
# existing image, mounting the working tree so the writes land where
# the build context expects them. Surfaces non-zero exits — these
# steps used to be `2>/dev/null || echo ⚠ skipped` which silently hid
# 12 days of stale `/reports/live` data.
# ---------------------------------------------------------------------
if [[ "$MODE" == "git" ]]; then
image_present=$(ssh "$SSH_HOST" "podman image inspect $IMAGE_TAG >/dev/null 2>&1 && echo yes || echo no")
if [[ "$image_present" == "no" ]]; then
echo " ⚠ image $IMAGE_TAG not present yet — skipping snapshot+bundle (will run on the next deploy after first build)"
else
echo "→ snapshot git history via podman run → content/git-history/"
ssh "$SSH_HOST" "podman run --rm -v \$HOME/$REMOTE_SRC_DIR:/work:Z --workdir /work $IMAGE_TAG bun scripts/p620/snapshot-git-history.ts" \
|| { echo "✗ snapshot-git-history failed"; exit 1; }
echo "→ snapshot tests via podman run → content/git-history/"
# bun test needs node_modules; the image bakes them at /app/node_modules.
# Symlink them into /work so the script's bun test resolves marked,
# node-html-parser, etc. without re-installing per deploy.
ssh "$SSH_HOST" "podman run --rm -v \$HOME/$REMOTE_SRC_DIR:/work:Z --workdir /work --entrypoint sh $IMAGE_TAG -c 'ln -sfn /app/node_modules node_modules && bun scripts/p620/snapshot-tests.ts'" \
|| { echo "✗ snapshot-tests failed"; exit 1; }
echo "→ bundle sama CLI via podman run → public/sama-cli"
ssh "$SSH_HOST" "podman run --rm -v \$HOME/$REMOTE_SRC_DIR:/work:Z --workdir /work --entrypoint sh $IMAGE_TAG -c 'bun build scripts/sama-cli.ts --target=bun --outfile=public/sama-cli >/dev/null && chmod +x public/sama-cli'" \
|| { echo "✗ sama-cli bundle failed"; exit 1; }
fi
fi
echo "→ podman build $IMAGE_TAG op $SSH_HOST"
src_hash=$(ssh "$SSH_HOST" "find ~/$REMOTE_SRC_DIR -type f \\( -name '*.ts' -o -name '*.json' -o -name '*.md' -o -name '*.css' -o -name 'Containerfile' -o -name 'bun.lock' \\) -not -path '*/node_modules/*' -not -path '*/.git/*' -exec sha256sum {} + | sort | sha256sum | awk '{print \$1}'")
existing_label=$(ssh "$SSH_HOST" "podman image inspect $IMAGE_TAG --format '{{index .Labels \"src-hash\"}}' 2>/dev/null || true")
if [[ "$src_hash" != "$existing_label" ]]; then
ssh "$SSH_HOST" "cd ~/$REMOTE_SRC_DIR && podman build --label src-hash=$src_hash -t $IMAGE_TAG -f Containerfile ." \
|| { echo "✗ build mislukt"; exit 1; }
echo " ✓ image gebouwd (src-hash=${src_hash:0:12})"
need_restart=1
else
echo " ✓ image up-to-date (src-hash=${src_hash:0:12})"
fi
echo "→ Quadlet sync"
ssh "$SSH_HOST" 'mkdir -p ~/.config/containers/systemd'
sync_quadlet() {
local file="$1"
local local_h remote_h
local_h=$(sha256sum "$SCRIPT_DIR/$file" | awk '{print $1}')
remote_h=$(ssh "$SSH_HOST" "sha256sum ~/.config/containers/systemd/$file 2>/dev/null | awk '{print \$1}'" || true)
if [[ "$local_h" != "$remote_h" ]]; then
scp -q "$SCRIPT_DIR/$file" "$SSH_HOST:.config/containers/systemd/$file"
echo " ✓ $file bijgewerkt"
need_restart=1
else
echo " ✓ $file ongewijzigd"
fi
}
sync_quadlet tdd.pod
sync_quadlet tdd-md.container
echo "→ systemd apply (need_restart=$need_restart)"
ssh "$SSH_HOST" 'systemctl --user daemon-reload'
if [[ "$need_restart" -eq 1 ]]; then
ssh "$SSH_HOST" 'systemctl --user restart tdd-pod.service && systemctl --user restart tdd-md.service'
else
ssh "$SSH_HOST" 'systemctl --user start tdd-pod.service && systemctl --user start tdd-md.service'
fi
echo -n "→ wachten tot /healthz antwoordt op :44390 "
for _ in $(seq 1 40); do
code=$(ssh "$SSH_HOST" "curl -s -o /dev/null -w '%{http_code}' --max-time 3 http://localhost:44390/healthz 2>/dev/null || echo 000")
if [[ "$code" == "200" ]]; then
echo "✓"
echo "✓ deploy klaar — Bun draait op p620:44390 en is bereikbaar via https://tdd.md"
exit 0
fi
echo -n "."
sleep 2
done
echo ""
echo "⚠ /healthz reageert niet binnen 80s. Recente logs:"
ssh "$SSH_HOST" 'echo "--- tdd-md ---"; podman logs --tail 40 tdd-md 2>&1' | sed 's/^/ /'
exit 1