226 lines
8.2 KiB
Bash
Executable file
226 lines
8.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# deploy.sh — Idempotent deployment script for PDF Accessibility Checker
|
|
#
|
|
# Usage:
|
|
# cd /opt/pdf-accessibility && ./deploy.sh
|
|
#
|
|
# Architecture:
|
|
# - Apache (host) serves frontend + api.php from /var/www/html/pdf-accessibility
|
|
# - Docker Compose runs: PostgreSQL
|
|
# - PDF processing via Google Cloud Run (synchronous HTTP call from api.php)
|
|
#
|
|
set -euo pipefail
|
|
|
|
# ── Configuration ─────────────────────────────────────────────────
|
|
|
|
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
WEB_DIR="/var/www/html/pdf-accessibility"
|
|
COMPOSE_FILE="docker-compose.prod.yml"
|
|
ENV_FILE="${REPO_DIR}/.env"
|
|
MIN_PHP_VERSION="8.0"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m'
|
|
|
|
log() { echo -e "${GREEN}[DEPLOY]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
err() { echo -e "${RED}[ERROR]${NC} $*"; }
|
|
|
|
# ── Preflight Checks ─────────────────────────────────────────────
|
|
|
|
log "Starting deployment from ${REPO_DIR}"
|
|
|
|
# Check Docker
|
|
if ! command -v docker &>/dev/null; then
|
|
err "Docker is not installed. Install it first:"
|
|
err " curl -fsSL https://get.docker.com | sh"
|
|
err " sudo usermod -aG docker \$USER"
|
|
exit 1
|
|
fi
|
|
|
|
# Check Docker Compose (v2 plugin)
|
|
if ! docker compose version &>/dev/null; then
|
|
err "Docker Compose v2 is not available. Install it:"
|
|
err " sudo apt-get install docker-compose-plugin"
|
|
exit 1
|
|
fi
|
|
|
|
# Check PHP
|
|
if ! command -v php &>/dev/null; then
|
|
warn "PHP is not installed. api.php requires PHP ${MIN_PHP_VERSION}+ with extensions:"
|
|
warn " sudo apt-get install php8.2 php8.2-pgsql php8.2-curl php8.2-mbstring"
|
|
else
|
|
PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
|
|
log "PHP version: ${PHP_VER}"
|
|
|
|
# Check required extensions
|
|
MISSING_EXT=""
|
|
php -m | grep -qi pgsql || MISSING_EXT="${MISSING_EXT} php-pgsql"
|
|
php -m | grep -qi curl || MISSING_EXT="${MISSING_EXT} php-curl"
|
|
php -m | grep -qi openssl || MISSING_EXT="${MISSING_EXT} php-openssl"
|
|
|
|
if [ -n "${MISSING_EXT}" ]; then
|
|
warn "Missing PHP extensions:${MISSING_EXT}"
|
|
warn "Install with: sudo apt-get install${MISSING_EXT}"
|
|
fi
|
|
fi
|
|
|
|
# ── Pull Latest Code ─────────────────────────────────────────────
|
|
|
|
log "Pulling latest code..."
|
|
cd "${REPO_DIR}"
|
|
|
|
if [ -d .git ]; then
|
|
git config core.fileMode false
|
|
# Run git as the repo owner (not root) so SSH keys work
|
|
REPO_OWNER=$(stat -c '%U' "${REPO_DIR}/.git")
|
|
if [ "$(id -u)" = "0" ] && [ "${REPO_OWNER}" != "root" ]; then
|
|
sudo -u "${REPO_OWNER}" git -C "${REPO_DIR}" fetch --all
|
|
sudo -u "${REPO_OWNER}" git -C "${REPO_DIR}" reset --hard origin/$(git rev-parse --abbrev-ref HEAD)
|
|
else
|
|
git fetch --all
|
|
git reset --hard origin/$(git rev-parse --abbrev-ref HEAD)
|
|
fi
|
|
log "Code updated to $(git log --oneline -1)"
|
|
else
|
|
warn "Not a git repo — using existing files"
|
|
fi
|
|
|
|
# ── Environment File ─────────────────────────────────────────────
|
|
|
|
if [ ! -f "${ENV_FILE}" ]; then
|
|
log "Creating .env from .env.example (first run)..."
|
|
cp "${REPO_DIR}/.env.example" "${ENV_FILE}"
|
|
|
|
# Override Docker hostnames with localhost for host-side PHP
|
|
sed -i 's/^DB_HOST=postgres/DB_HOST=127.0.0.1/' "${ENV_FILE}"
|
|
sed -i 's/^DEV_MODE=true/DEV_MODE=false/' "${ENV_FILE}"
|
|
|
|
warn "Review and update ${ENV_FILE} with production values:"
|
|
warn " - DB_PASSWORD (change from default!)"
|
|
warn " - ANTHROPIC_API_KEY"
|
|
warn " - GOOGLE_API_KEY"
|
|
warn " - CLOUD_RUN_URL"
|
|
warn " - GCP_SA_KEY_PATH (copy pdf-api-invoker-key.json to server)"
|
|
warn " - AZURE_* settings"
|
|
else
|
|
log "Using existing .env file"
|
|
fi
|
|
|
|
# ── Build Docker Containers ──────────────────────────────────────
|
|
|
|
log "Building Docker containers (using cache)..."
|
|
docker compose -f "${COMPOSE_FILE}" build
|
|
|
|
log "Starting/restarting Docker services..."
|
|
docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans
|
|
|
|
# Wait for PostgreSQL to be ready
|
|
log "Waiting for PostgreSQL to be healthy..."
|
|
RETRIES=30
|
|
until docker compose -f "${COMPOSE_FILE}" exec -T postgres pg_isready -U pdf_checker &>/dev/null || [ $RETRIES -eq 0 ]; do
|
|
sleep 1
|
|
RETRIES=$((RETRIES - 1))
|
|
done
|
|
|
|
if [ $RETRIES -eq 0 ]; then
|
|
err "PostgreSQL failed to start. Check logs:"
|
|
err " docker compose -f ${COMPOSE_FILE} logs postgres"
|
|
exit 1
|
|
fi
|
|
|
|
log "PostgreSQL is ready"
|
|
|
|
# Database init.sql runs automatically on first compose up via
|
|
# /docker-entrypoint-initdb.d/init.sql — no migration tool needed.
|
|
# For future migrations, add numbered SQL files to db/ and apply:
|
|
if [ -d "${REPO_DIR}/db/migrations" ]; then
|
|
for migration in "${REPO_DIR}"/db/migrations/*.sql; do
|
|
[ -f "$migration" ] || continue
|
|
MIGRATION_NAME=$(basename "$migration")
|
|
log "Applying migration: ${MIGRATION_NAME}"
|
|
docker compose -f "${COMPOSE_FILE}" exec -T postgres \
|
|
psql -U pdf_checker -d pdf_checker -f "/dev/stdin" < "$migration" 2>/dev/null || \
|
|
warn "Migration ${MIGRATION_NAME} may have already been applied"
|
|
done
|
|
fi
|
|
|
|
# ── Deploy Frontend Files ─────────────────────────────────────────
|
|
|
|
log "Deploying frontend to ${WEB_DIR}..."
|
|
|
|
# Create web directory if it doesn't exist
|
|
sudo mkdir -p "${WEB_DIR}"
|
|
|
|
# Clean old frontend files (but preserve uploads, results, .env, logs)
|
|
log "Cleaning old frontend files..."
|
|
sudo rm -f "${WEB_DIR}/index.html" "${WEB_DIR}/history.html"
|
|
sudo rm -rf "${WEB_DIR}/css" "${WEB_DIR}/js"
|
|
sudo rm -f "${WEB_DIR}/api.php" "${WEB_DIR}/auth.php"
|
|
|
|
# Copy frontend files
|
|
sudo cp "${REPO_DIR}/index.html" "${WEB_DIR}/"
|
|
sudo cp "${REPO_DIR}/history.html" "${WEB_DIR}/"
|
|
sudo cp -r "${REPO_DIR}/css" "${WEB_DIR}/"
|
|
sudo cp -r "${REPO_DIR}/js" "${WEB_DIR}/"
|
|
|
|
# Copy PHP backend files
|
|
sudo cp "${REPO_DIR}/api.php" "${WEB_DIR}/"
|
|
sudo cp "${REPO_DIR}/auth.php" "${WEB_DIR}/"
|
|
|
|
# Copy Python scripts (needed if api.php fallback exec() is used)
|
|
sudo cp "${REPO_DIR}/enterprise_pdf_checker.py" "${WEB_DIR}/"
|
|
sudo cp "${REPO_DIR}/pdf_remediation.py" "${WEB_DIR}/"
|
|
sudo cp "${REPO_DIR}/logger_config.py" "${WEB_DIR}/"
|
|
sudo cp "${REPO_DIR}/retry_helper.py" "${WEB_DIR}/"
|
|
|
|
# Copy .env for PHP (if not already there)
|
|
if [ ! -f "${WEB_DIR}/.env" ]; then
|
|
sudo cp "${ENV_FILE}" "${WEB_DIR}/.env"
|
|
log "Copied .env to web directory"
|
|
else
|
|
# Update .env in web dir from repo .env
|
|
sudo cp "${ENV_FILE}" "${WEB_DIR}/.env"
|
|
fi
|
|
|
|
# Create runtime directories
|
|
sudo mkdir -p "${WEB_DIR}/uploads" "${WEB_DIR}/results" "${WEB_DIR}/logs" "${WEB_DIR}/rate_limits"
|
|
|
|
# Set ownership for Apache
|
|
sudo chown -R www-data:www-data "${WEB_DIR}"
|
|
sudo chmod -R 755 "${WEB_DIR}"
|
|
sudo chmod -R 775 "${WEB_DIR}/uploads" "${WEB_DIR}/results" "${WEB_DIR}/logs" "${WEB_DIR}/rate_limits"
|
|
|
|
# ── Verify ────────────────────────────────────────────────────────
|
|
|
|
log ""
|
|
log "============================================="
|
|
log " Deployment complete!"
|
|
log "============================================="
|
|
log ""
|
|
log "Services status:"
|
|
docker compose -f "${COMPOSE_FILE}" ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
|
|
log ""
|
|
log "Frontend: ${WEB_DIR}"
|
|
log "Docker: PostgreSQL (127.0.0.1:1221)"
|
|
log "Cloud Run: ${CLOUD_RUN_URL:-$(grep '^CLOUD_RUN_URL=' "${ENV_FILE}" 2>/dev/null | cut -d= -f2 || echo 'not set')}"
|
|
log ""
|
|
|
|
# Quick health check
|
|
if docker compose -f "${COMPOSE_FILE}" exec -T postgres pg_isready -U pdf_checker &>/dev/null; then
|
|
log "PostgreSQL: OK"
|
|
fi
|
|
|
|
log ""
|
|
log "Reloading Apache..."
|
|
sudo systemctl reload apache2 && log "Apache reloaded" || warn "Apache reload failed — run: sudo systemctl reload apache2"
|
|
|
|
log ""
|
|
log "Next steps (if first deploy):"
|
|
log " 1. Ensure pdf-api-invoker-key.json is at the GCP_SA_KEY_PATH location"
|
|
log " 2. Review ${WEB_DIR}/.env (especially CLOUD_RUN_URL and API keys)"
|
|
log ""
|