#!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")/.." source deploy/env.sh CONTAINER_TOOL="${CONTAINER_TOOL:-}" AGE_KEY_FILE_CANDIDATES=( "${SOPS_AGE_KEY_FILE:-}" "$HOME/.config/age/keys.txt" "$PWD/creds/age-key.txt" ) AGE_KEY_FILE="" for candidate in "${AGE_KEY_FILE_CANDIDATES[@]}"; do if [[ -n "$candidate" && -f "$candidate" ]]; then AGE_KEY_FILE="$candidate" break fi done if [[ -z "$AGE_KEY_FILE" ]]; then AGE_KEY_FILE="$HOME/.config/age/keys.txt" fi AGE_RECIPIENTS=( "age1hkehkc2rryjl975c2mg5cghmjr54n4wjshncl292h2eg5l394fhs4uydrh" "age1ducvqxdzdhhluftu5hv4f2xsppmn803uh8tnnqj92v4n7nf6lprq9h3dqp" ) ENCRYPTED_SECRETS_FILE="${ENCRYPTED_SECRETS_FILE:-$PWD/creds/secrets.enc.env}" require_cmd() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then echo "Missing required tool: $cmd. Please install it before building." >&2 exit 1 fi } check_docker() { echo "Deprecated: check_docker is kept for compatibility. Use check_container_engine instead." >&2 check_container_engine } check_container_engine() { if [[ -n "${SKIP_DOCKER_CHECK:-}" || -n "${SKIP_CONTAINER_BUILD:-}" ]]; then return fi if [[ -n "$CONTAINER_TOOL" ]]; then require_cmd "$CONTAINER_TOOL" if "$CONTAINER_TOOL" info >/dev/null 2>&1; then return fi echo "$CONTAINER_TOOL is installed but the daemon/service is not reachable. Start it or choose another engine via CONTAINER_TOOL." >&2 exit 1 fi for candidate in docker podman nerdctl; do if command -v "$candidate" >/dev/null 2>&1 && "$candidate" info >/dev/null 2>&1; then CONTAINER_TOOL="$candidate" export CONTAINER_TOOL return fi done echo "No working container engine found (checked docker, podman, nerdctl). Start one or set CONTAINER_TOOL to a reachable engine." >&2 exit 1 } check_age_setup() { if [[ -n "${SKIP_AGE_CHECK:-}" ]]; then return fi require_cmd sops local repo_age_key="$PWD/creds/age-key.txt" if [[ ! -f "$AGE_KEY_FILE" ]]; then echo "Age key file not found at $AGE_KEY_FILE. Copy $repo_age_key or set SOPS_AGE_KEY_FILE." >&2 exit 1 fi local has_key="0" if command -v age-keygen >/dev/null 2>&1; then for recipient in "${AGE_RECIPIENTS[@]}"; do if age-keygen -y "$AGE_KEY_FILE" 2>/dev/null | grep -q "$recipient"; then has_key="1" break fi done else # Fallback: best-effort text check for the public key comment for recipient in "${AGE_RECIPIENTS[@]}"; do if grep -q "$recipient" "$AGE_KEY_FILE"; then has_key="1" break fi done fi if [[ "$has_key" != "1" ]]; then echo "Age key file at $AGE_KEY_FILE does not contain any expected public key: ${AGE_RECIPIENTS[*]}." >&2 if [[ -f "$repo_age_key" ]]; then cat >&2 <> "$AGE_KEY_FILE" Or set: SOPS_AGE_KEY_FILE="$repo_age_key" EOF else echo "Ensure your ~/.config/age/keys.txt includes the repo key (see creds/age-key.txt)." >&2 fi exit 1 fi export SOPS_AGE_KEY_FILE="${SOPS_AGE_KEY_FILE:-$AGE_KEY_FILE}" if [[ -f "$ENCRYPTED_SECRETS_FILE" ]]; then if ! sops -d "$ENCRYPTED_SECRETS_FILE" >/dev/null 2>&1; then echo "sops could not decrypt $ENCRYPTED_SECRETS_FILE with the configured keys." >&2 echo "Export SOPS_AGE_KEY_FILE to point at the correct key (e.g., creds/age-key.txt)." >&2 exit 1 fi fi } echo "Running pre-flight checks..." for tool in git npm; do require_cmd "$tool" done check_container_engine check_age_setup if [[ -z "${SKIP_DB_MIGRATION_CHECK:-}" ]]; then if command -v npx >/dev/null 2>&1; then echo "Checking for pending Prisma migrations..." if ! npx prisma migrate status >/dev/null 2>&1; then echo "Prisma migrate status failed. Ensure DATABASE_URL is set and migrations are up to date." >&2 exit 1 fi else echo "npx not found; skipping Prisma migration check." >&2 fi fi GIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || date +%s) BASE_TAG=${BUILD_TAG:-$GIT_SHA} # Optional dev override: set FORCE_DEV_TAG=1 to append a timestamp without committing if [[ -n "${FORCE_DEV_TAG:-}" ]]; then BASE_TAG="${BASE_TAG}-dev$(date +%s)" fi IMAGE_REPO="${REGISTRY}/${REGISTRY_REPO}" IMAGE="${IMAGE_REPO}:${BASE_TAG}" IMAGE_LATEST="${IMAGE_REPO}:latest" echo "Building image:" echo " $IMAGE" echo " $IMAGE_LATEST" if [[ -z "${SKIP_NPM_AUDIT:-}" ]]; then # npm audit (high severity and above) echo "Running npm audit (high)..." npm audit --audit-level=high || echo "npm audit reported issues above." else echo "Skipping npm audit (SKIP_NPM_AUDIT set)." fi if [[ -n "${SKIP_CONTAINER_BUILD:-}" ]]; then echo "Skipping container build (SKIP_CONTAINER_BUILD set)." exit 0 fi # Build "${CONTAINER_TOOL:-docker}" build --build-arg APP_VERSION="$GIT_SHA" -t "$IMAGE" -t "$IMAGE_LATEST" . echo "$IMAGE" > deploy/.last-image echo "Done. Last image: $IMAGE" # Trivy image scan (if available) if command -v trivy >/dev/null 2>&1; then MIN_TRIVY_VERSION="0.56.0" INSTALLED_TRIVY_VERSION="$(trivy --version 2>/dev/null | head -n1 | awk '{print $2}')" if [[ -n "$INSTALLED_TRIVY_VERSION" ]] && [[ "$(printf '%s\n%s\n' "$MIN_TRIVY_VERSION" "$INSTALLED_TRIVY_VERSION" | sort -V | head -n1)" != "$MIN_TRIVY_VERSION" ]]; then echo "Trivy version $INSTALLED_TRIVY_VERSION is older than recommended $MIN_TRIVY_VERSION." echo "Update recommended: brew upgrade trivy # macOS" echo "or: sudo apt-get install -y trivy # Debian/Ubuntu (Aqua repo)" echo "or: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin" fi echo "Running Trivy scan on $IMAGE ..." TRIVY_IGNORE_ARGS=() if [[ -f ".trivyignore" ]]; then TRIVY_IGNORE_ARGS+=(--ignorefile .trivyignore) fi trivy image --exit-code 0 "${TRIVY_IGNORE_ARGS[@]}" "$IMAGE" || true else echo "Trivy not installed; skipping image scan." fi