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