syntaxai/tdd.md · main · scripts / p620 / deploy-tdd-md.sh

deploy-tdd-md.sh 227 lines · 10722 bytes raw
#!/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