Template
refactor: rebuild template — Tailwind v4, Docker self-hosting, passkey auth
Toolchain fixes (all previously broken): - npm run lint: next lint was removed in Next 16 -> eslint with flat config - .eslintrc.json was ignored by ESLint 9 -> eslint.config.mjs - npm test / test:e2e exited 1 with no tests -> passWithNoTests - Vitest scanned tests/ and would have run Playwright specs -> scoped to src/ - Playwright targeted Mobile Safari while docs installed only chromium - reuseExistingServer silently tested a foreign server on :3000 -> E2E_PORT - 11 high-severity vulns -> npm audit fix - deny rule Read(**/.env*) also blocked .env.example -> precise patterns Stack changes: - Tailwind v3 -> v4 (CSS-first, no tailwind.config.ts, tw-animate-css) - shadcn regenerated for v4; 33 components -> 6 core, bundled radix-ui - Supabase removed entirely; data layer is now chosen per project - Vercel removed; Docker Compose self-hosting with scripts/update.sh - Better Auth + passkey plugin as the binding auth standard - ThemeProvider and Sonner mounted; dark mode and toasts now actually work Workflow system: - Sub-agents wired: /frontend, /backend, /qa delegate to .claude/agents - Checklists folded into their SKILL.md; one file per skill - Rules rewritten: DB-agnostic backend, passkey security, v4 frontend - Spec language settled as German, skill instructions English
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Pull the latest commit, rebuild the image and restart the stack.
|
||||
# Rolls back to the previous commit if the new container never turns healthy.
|
||||
#
|
||||
# Usage: ./scripts/update.sh [--force] [--branch <name>]
|
||||
# --force rebuild and restart even when there is no new commit
|
||||
# --branch branch to deploy (default: the currently checked out branch)
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$REPO_DIR"
|
||||
|
||||
FORCE=0
|
||||
BRANCH=""
|
||||
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-120}"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--force) FORCE=1; shift ;;
|
||||
--branch) BRANCH="${2:?--branch needs a value}"; shift 2 ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
||||
err() { printf '\033[1;31m!!\033[0m %s\n' "$*" >&2; }
|
||||
|
||||
compose() { docker compose "$@"; }
|
||||
|
||||
# --- preconditions -----------------------------------------------------------
|
||||
|
||||
command -v git >/dev/null || { err "git not found"; exit 1; }
|
||||
docker compose version >/dev/null 2>&1 || { err "docker compose not available"; exit 1; }
|
||||
|
||||
[ -f .env ] || { err ".env missing — copy .env.example to .env and fill it in"; exit 1; }
|
||||
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
err "working tree has uncommitted changes — refusing to update"
|
||||
git status --short >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ -n "$BRANCH" ] || BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
PREV_COMMIT="$(git rev-parse HEAD)"
|
||||
|
||||
# --- fetch -------------------------------------------------------------------
|
||||
|
||||
log "Fetching origin/$BRANCH"
|
||||
git fetch --quiet origin "$BRANCH"
|
||||
|
||||
TARGET_COMMIT="$(git rev-parse "origin/$BRANCH")"
|
||||
|
||||
if [ "$PREV_COMMIT" = "$TARGET_COMMIT" ] && [ "$FORCE" -eq 0 ]; then
|
||||
log "Already at $(git rev-parse --short HEAD) — nothing to do (use --force to rebuild anyway)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Updating $(git rev-parse --short "$PREV_COMMIT") -> $(git rev-parse --short "$TARGET_COMMIT")"
|
||||
git checkout --quiet "$BRANCH"
|
||||
git merge --ff-only --quiet "origin/$BRANCH"
|
||||
|
||||
# --- health helper -----------------------------------------------------------
|
||||
|
||||
container_id() { compose ps -q app; }
|
||||
|
||||
wait_for_health() {
|
||||
local deadline=$((SECONDS + HEALTH_TIMEOUT))
|
||||
local cid status
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
cid="$(container_id)"
|
||||
if [ -n "$cid" ]; then
|
||||
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$cid" 2>/dev/null || echo missing)"
|
||||
case "$status" in
|
||||
healthy|no-healthcheck) return 0 ;;
|
||||
unhealthy) return 1 ;;
|
||||
esac
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
rollback() {
|
||||
err "Deployment failed — rolling back to $(git rev-parse --short "$PREV_COMMIT")"
|
||||
compose logs --tail 50 app >&2 || true
|
||||
git reset --hard --quiet "$PREV_COMMIT"
|
||||
compose build app
|
||||
compose up -d app
|
||||
if wait_for_health; then
|
||||
err "Rolled back successfully. The stack runs the previous commit again."
|
||||
else
|
||||
err "ROLLBACK ALSO UNHEALTHY — manual intervention required."
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- build & restart ---------------------------------------------------------
|
||||
|
||||
log "Building image"
|
||||
compose build app || rollback
|
||||
|
||||
log "Restarting stack"
|
||||
compose up -d app || rollback
|
||||
|
||||
log "Waiting for healthcheck (timeout ${HEALTH_TIMEOUT}s)"
|
||||
wait_for_health || rollback
|
||||
|
||||
# --- cleanup -----------------------------------------------------------------
|
||||
|
||||
log "Pruning dangling images"
|
||||
docker image prune -f >/dev/null
|
||||
|
||||
log "Deployed $(git rev-parse --short HEAD) — stack is healthy"
|
||||
Reference in New Issue
Block a user