From 53f4df6e3c067558e5bd395fd37d42e0e11ac4ba Mon Sep 17 00:00:00 2001 From: Clawd Agent Date: Tue, 3 Mar 2026 17:20:23 +0100 Subject: [PATCH] feat(07-02): Add CI/CD deployment scripts - scripts/deploy.sh: Full deploy flow with fresh builds (--no-cache) - scripts/build-check.sh: Pre-flight staleness check - Docker labels for build tracking (git commit + timestamp) - Prevents stale container bug from recurring --- backend/Dockerfile | 5 +++++ docker-compose.yml | 11 +++++++++ frontend/Dockerfile | 5 +++++ scripts/build-check.sh | 41 +++++++++++++++++++++++++++++++++ scripts/deploy.sh | 51 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100755 scripts/build-check.sh create mode 100755 scripts/deploy.sh diff --git a/backend/Dockerfile b/backend/Dockerfile index ca1059d..dcc893a 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,10 @@ FROM node:20-alpine +ARG GIT_COMMIT=unknown +ARG BUILD_DATE=unknown +LABEL org.opencontainers.image.revision=$GIT_COMMIT \ + org.opencontainers.image.created=$BUILD_DATE + WORKDIR /app COPY package*.json ./ diff --git a/docker-compose.yml b/docker-compose.yml index e1f7026..f20b338 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,9 @@ services: build: context: ./backend dockerfile: Dockerfile + args: + GIT_COMMIT: ${GIT_COMMIT:-unknown} + BUILD_DATE: ${BUILD_DATE:-unknown} restart: unless-stopped environment: - DB_HOST=postgres @@ -16,12 +19,18 @@ services: - homelab expose: - "3001" + labels: + - "org.opencontainers.image.revision=${GIT_COMMIT:-unknown}" + - "org.opencontainers.image.created=${BUILD_DATE:-unknown}" gravl-frontend: container_name: gravl-frontend build: context: ./frontend dockerfile: Dockerfile + args: + GIT_COMMIT: ${GIT_COMMIT:-unknown} + BUILD_DATE: ${BUILD_DATE:-unknown} restart: unless-stopped depends_on: - gravl-backend @@ -37,6 +46,8 @@ services: - "traefik.http.routers.gravl-secure.tls=true" - "traefik.http.routers.gravl-secure.service=gravl" - "traefik.http.services.gravl.loadbalancer.server.port=80" + - "org.opencontainers.image.revision=${GIT_COMMIT:-unknown}" + - "org.opencontainers.image.created=${BUILD_DATE:-unknown}" networks: proxy: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 4027fdf..3ede330 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -10,6 +10,11 @@ RUN npm run build FROM nginx:alpine +ARG GIT_COMMIT=unknown +ARG BUILD_DATE=unknown +LABEL org.opencontainers.image.revision=$GIT_COMMIT \ + org.opencontainers.image.created=$BUILD_DATE + COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/scripts/build-check.sh b/scripts/build-check.sh new file mode 100755 index 0000000..5cca7ef --- /dev/null +++ b/scripts/build-check.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Compare deployed container versions against local git HEAD +# Warns if containers are stale (built from an older commit) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(dirname "$SCRIPT_DIR")" + +cd "$REPO_DIR" + +LOCAL_COMMIT=$(git rev-parse HEAD) +echo "Local HEAD: $(git rev-parse --short HEAD) ($LOCAL_COMMIT)" +echo "" + +check() { + local name="$1" + + if ! docker inspect "$name" &>/dev/null; then + echo "[$name] Not running" + return + fi + + local commit date + commit=$(docker inspect "$name" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null) + date=$(docker inspect "$name" --format '{{index .Config.Labels "org.opencontainers.image.created"}}' 2>/dev/null) + + if [ -z "$commit" ] || [ "$commit" = "unknown" ]; then + echo "[$name] WARNING: no build label found — redeploy with scripts/deploy.sh to add tracking" + return + fi + + echo "[$name] Built: ${commit:0:7} on ${date:-unknown}" + + if [ "$commit" = "$LOCAL_COMMIT" ]; then + echo "[$name] OK: up to date" + else + echo "[$name] STALE: container is behind local code — run scripts/deploy.sh" + fi +} + +check "gravl-backend" +check "gravl-frontend" diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..14d5fda --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Gravl deployment script +# Prevents stale containers by always building fresh with --no-cache + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(dirname "$SCRIPT_DIR")" +LOG_FILE="$REPO_DIR/logs/deploy.log" +BACKEND_HEALTH="http://localhost:3001/api/health" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" +} + +mkdir -p "$REPO_DIR/logs" +cd "$REPO_DIR" + +log "=== Deploy started ===" + +# Pull latest code +log "Pulling latest code..." +git pull + +# Capture build metadata +GIT_COMMIT=$(git rev-parse HEAD) +BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +log "Commit: $(git rev-parse --short HEAD) | Date: $BUILD_DATE" + +# Build fresh images — no-cache prevents stale assets +log "Building images (--no-cache)..." +export GIT_COMMIT BUILD_DATE +docker compose build --no-cache + +# Restart containers with new images +log "Starting containers..." +docker compose up -d --force-recreate + +# Health check: wait up to 60s for backend +log "Health check..." +for i in $(seq 1 12); do + if curl -sf "$BACKEND_HEALTH" >/dev/null 2>&1; then + log "Backend healthy" + break + fi + [ "$i" -eq 12 ] && { log "ERROR: Health check failed after 60s"; exit 1; } + log "Waiting... ($i/12)" + sleep 5 +done + +log "=== Deploy complete: ${GIT_COMMIT:0:7} ==="