chore(07-03): Stage deployment scripts and documentation updates

This commit is contained in:
2026-03-03 19:24:29 +01:00
parent fa766b21f7
commit 1104f6360e
4 changed files with 722 additions and 377 deletions
+64 -26
View File
@@ -1,68 +1,106 @@
#!/bin/bash
# Compare deployed container versions against local git HEAD
# Warns if containers are stale (built from an older commit)
# Gravl Build Status Checker
#
# Purpose:
# Verifies that deployed containers match the current git HEAD.
# Warns if containers are stale (built from older commits).
# Helps you catch situations where code was updated but not redeployed.
#
# How it works:
# 1. Gets current local git commit (HEAD)
# 2. Queries each container's build labels
# 3. Compares container label commit vs local HEAD
# 4. Reports status: "OK", "STALE", or "WARNING"
#
# Exit codes:
# 0 = All checks completed (see output for individual status)
# (Warnings don't cause non-zero exit)
#
# Usage:
# ./scripts/build-check.sh
#
# What it shows:
# - Local git HEAD commit SHA
# - Each container's built commit SHA (from Docker labels)
# - Whether containers are up-to-date or stale
# - Warnings if labels are missing (pre-07-02 containers)
# Example output:
# Local HEAD: abc1234 (abc1234567890abcdef...)
#
# Label fields read:
# - org.opencontainers.image.revision = Git commit SHA embedded by deploy.sh
# - org.opencontainers.image.created = Build timestamp (ISO 8601 format)
#
# Exit codes:
# 0 = All containers up to date
# 1+ = Warnings or stale containers detected
#
# See: /docs/DEPLOYMENT.md for troubleshooting
# [gravl-backend] Built: abc1234 on 2026-03-03T18:21:00Z
# [gravl-backend] OK: up to date
# [gravl-frontend] Built: abc1234 on 2026-03-03T18:21:00Z
# [gravl-frontend] OK: up to date
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
cd "$REPO_DIR"
# Get the current local git commit (what's checked out locally)
LOCAL_COMMIT=$(git rev-parse HEAD)
echo "Local HEAD: $(git rev-parse --short HEAD) ($LOCAL_COMMIT)"
echo ""
# Check a single container's build status
# Args: $1 = container name
# ============================================================================
# check() helper function
# ============================================================================
# Queries a container's build labels and compares against local HEAD.
#
# Parameters:
# $1 = Container name (e.g., "gravl-backend")
#
# Label fields used:
# org.opencontainers.image.revision = commit hash when image was built
# Format: 40-character SHA (same as git rev-parse HEAD)
# Set by: scripts/deploy.sh -> docker compose build args
#
# org.opencontainers.image.created = RFC3339 timestamp when image was built
# Format: 2026-03-03T18:21:00Z
# Set by: scripts/deploy.sh -> docker compose build args
# Purpose: Shows humans when the image was built (for diagnostics)
#
# Status outcomes:
# - "Not running": Container doesn't exist or isn't running
# - "WARNING": Container exists but has no revision label
# Fix: Re-deploy with scripts/deploy.sh
# - "OK": Container label commit = local HEAD (up to date)
# - "STALE": Container label commit != local HEAD
# Fix: Run scripts/deploy.sh to update container
check() {
local name="$1"
# Container not running
# Check if container exists and is running
if ! docker inspect "$name" &>/dev/null; then
echo "[$name] Not running"
return
fi
# Read Docker labels set by deploy.sh
# If labels are missing, container was built before phase 07-02
# Extract build labels from container config
# These labels are set in the docker-compose.yml build args,
# and the Dockerfile COPYs them into image labels.
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)
# Check if revision label exists
if [ -z "$commit" ] || [ "$commit" = "unknown" ]; then
echo "[$name] WARNING: no build label found — redeploy with scripts/deploy.sh to add tracking"
return
fi
# Display container info
# Display when this container's image was built
echo "[$name] Built: ${commit:0:7} on ${date:-unknown}"
# Compare container commit to local HEAD
# Compare container's commit against local HEAD
# If they match, container is up to date.
# If they differ, code has changed locally but container hasn't been redeployed.
if [ "$commit" = "$LOCAL_COMMIT" ]; then
echo "[$name] OK: up to date"
echo "[$name] OK: up to date"
else
echo "[$name] STALE: container is behind local code — run scripts/deploy.sh"
echo "[$name] STALE: container is behind local code — run scripts/deploy.sh"
fi
}
# Check both containers
# ============================================================================
# Check Each Service
# ============================================================================
# These are the service names defined in docker-compose.yml.
# Adjust if you rename services.
check "gravl-backend"
check "gravl-frontend"
+98 -34
View File
@@ -1,24 +1,31 @@
#!/bin/bash
# Gravl deployment script
# Prevents stale containers by always building fresh with --no-cache
#
# Gravl Deployment Script
#
# Purpose:
# Automates the deployment of Gravl services to production/staging.
# Ensures fresh builds and verifies service health after startup.
#
# Prevents stale containers by always building fresh with --no-cache:
# The --no-cache flag rebuilds all Docker layers from scratch.
# This prevents stale application code, assets, or dependencies
# from being cached and deployed. Essential for reliable deployments.
#
# Workflow:
# 1. Pull latest code from git
# 2. Capture build metadata (commit hash, timestamp)
# 3. Build Docker images (--no-cache for freshness)
# 4. Start containers with new images
# 5. Health check: wait for backend to respond
#
# Exit codes:
# 0 = Success (deployment complete, services healthy)
# 1 = Failure (see error message in logs)
#
# Usage:
# ./scripts/deploy.sh
#
# What it does:
# 1. Pulls latest code from git
# 2. Captures build metadata (commit SHA, timestamp)
# 3. Builds fresh Docker images with --no-cache (no layer caching)
# 4. Restarts containers to use new images
# 5. Polls /api/health endpoint until backend is ready
# 6. Logs all steps to logs/deploy.log
#
# Rationale for --no-cache:
# Docker caching can hide stale assets (JS, CSS, images) when source files change.
# Using --no-cache ensures all layers rebuild fresh, guaranteeing new code is deployed.
# Trade-off: Slightly slower builds (30-60s vs 10-20s with cache), but safer.
#
# See: /docs/DEPLOYMENT.md for troubleshooting
# Logs:
# All output saved to logs/deploy.log (see tail to follow)
set -euo pipefail
@@ -27,49 +34,106 @@ REPO_DIR="$(dirname "$SCRIPT_DIR")"
LOG_FILE="$REPO_DIR/logs/deploy.log"
BACKEND_HEALTH="http://localhost:3001/api/health"
# Logging helper: prints timestamp + message to both stdout and log file
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# Ensure logs directory exists
mkdir -p "$REPO_DIR/logs"
cd "$REPO_DIR"
log "=== Deploy started ==="
# Pull latest code from remote
# Fails if there are local changes or merge conflicts
# ============================================================================
# STEP 1: Git Pull
# ============================================================================
# Fetches latest code from remote and merges into current branch.
# Fails if there are merge conflicts (manual intervention required).
log "Pulling latest code..."
git pull
# Capture build metadata to embed in Docker image labels
# These labels allow build-check.sh to verify deployed containers match local code
# ============================================================================
# STEP 2: Capture Build Metadata
# ============================================================================
# Build labels are attached to Docker images and stored in container labels.
# These are used by build-check.sh to verify deployed containers match local HEAD.
#
# Labels:
# org.opencontainers.image.revision = git commit hash (40-char SHA)
# Purpose: Track which commit the image was built from
# Example: abc1234567890abcdef1234567890abcdef123456
#
# org.opencontainers.image.created = RFC3339 timestamp
# Purpose: Track when the image was built
# Example: 2026-03-03T18:21:00Z
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 Docker layer caching
# This is critical for frontend deployments where CSS/JS changes might not be obvious
# to Docker's layer detection algorithm
log "Building images (--no-cache)..."
# ============================================================================
# STEP 3: Build Docker Images (--no-cache)
# ============================================================================
# Why --no-cache?
# Docker layer caching can hide stale assets (CSS, JS bundles, dependencies).
# Example: If package.json changes but npm install is cached, old dependencies are used.
# --no-cache forces full rebuild of all layers every time.
#
# Build args are passed to Dockerfile via export, allowing them to be used
# in RUN instructions or referenced in labels (see docker-compose.yml).
log "Building images (--no-cache to prevent stale assets)..."
export GIT_COMMIT BUILD_DATE
docker compose build --no-cache
# Restart containers with new images
# --force-recreate stops old containers and removes them before starting new ones
# ============================================================================
# STEP 4: Start Containers with New Images
# ============================================================================
# docker compose up -d --force-recreate:
# -d = Run in background (detached mode)
# --force-recreate = Stop and remove existing containers, start fresh
# Ensures old containers with old images are not reused.
#
# This step also networks containers (creates/reuses docker network).
log "Starting containers..."
docker compose up -d --force-recreate
# Health check: poll /api/health endpoint until it responds with 200 OK
# Timeout: 60 seconds (12 retries × 5 seconds each)
# This prevents deployment from completing if the backend is broken
log "Health check..."
# ============================================================================
# STEP 5: Health Check
# ============================================================================
# Waits for backend to respond on /api/health endpoint.
# This proves the service started correctly and is ready for traffic.
#
# Timeout configuration:
# Loop: 12 iterations
# Interval: 5 seconds per iteration
# Total: 60 seconds max wait time
#
# Why 60 seconds?
# - Docker startup: ~5-10 seconds
# - Node.js app initialization: ~5 seconds
# - Database connection: ~5-10 seconds
# - Buffer for system load: ~30 seconds
#
# If this timeout is too short, you may see false negatives (healthy app fails check).
# If too long, deployment takes unnecessarily long to fail.
#
# Endpoint details:
# URL: http://localhost:3001/api/health
# Method: GET
# Expected status: 200
# Should complete in <1 second
log "Health check: waiting for backend (60s timeout)..."
for i in $(seq 1 12); do
if curl -sf "$BACKEND_HEALTH" >/dev/null 2>&1; then
log "Backend healthy"
log "Backend healthy"
break
fi
[ "$i" -eq 12 ] && { log "ERROR: Health check failed after 60s"; exit 1; }
log "Waiting... ($i/12)"
if [ "$i" -eq 12 ]; then
log "✗ ERROR: Health check failed after 60s"
log " Try: docker logs gravl-backend | tail -20"
exit 1
fi
log " Waiting... ($i/12 attempts, 5s intervals)"
sleep 5
done