#!/usr/bin/env bash # DashCaddy Post-Deploy Verifier # Runs AFTER the host-side update script copies staging files into the API source # directory, but BEFORE the Docker build. # # Historical role: this script ORIGINALLY applied require-path patches to work # around v1.14.4-era bugs (broken relative requires, missing license-keygen.js # at root). After the build-pipeline-fix (which ships a clean src/ tree in # every release tarball starting v1.14.8), those patches are no-ops. # # Current role: DEFENSIVE VERIFIER. Empirically measured 2026-07-13 against # v1.14.4, v1.14.8, v1.14.9 (latest), and origin/main — every patch is a # no-op against all four. We keep the script running on every update as a # verification gate: if a future release reintroduces one of these classes of # bug, we FAIL THE BUILD with a clear error instead of silently letting a # crash-looping container reach production. This is the inverse of the old # behavior (which would patch-and-continue, hiding the regression). # # Idempotent: safe to run multiple times. Exits 0 if everything checks out, # exits 1 if any required file is missing or a known-bad pattern is detected. # # Usage: dashcaddy-post-deploy-patches.sh # api_source_dir: e.g. /opt/dashcaddy/dashcaddy-api set -uo pipefail API_DIR="${1:-/opt/dashcaddy/dashcaddy-api}" log() { echo "[dashcaddy-verify] $(date '+%Y-%m-%d %H:%M:%S') $*"; } fail() { echo "[dashcaddy-verify] FAIL: $*" >&2; exit 1; } if [[ ! -d "$API_DIR" ]]; then log "ERROR: API source directory not found: $API_DIR" exit 1 fi # Resolve to absolute path so the file-existence checks below don't depend # on the cwd set by `cd "$API_DIR"` below. API_DIR="$(cd "$API_DIR" && pwd)" cd "$API_DIR" || exit 1 TOTAL_CHECKS=0 TOTAL_OK=0 FAILED_CHECKS=() # ──────────────────────────────────────────────────────────────────────────── # Check 1: Root-level server.js — must use './src/...' not '../src/...' # ──────────────────────────────────────────────────────────────────────────── TOTAL_CHECKS=$((TOTAL_CHECKS + 1)) SERVER_JS="$API_DIR/server.js" if [[ ! -f "$SERVER_JS" ]]; then FAILED_CHECKS+=("server.js: file missing at $SERVER_JS") log "FAIL: server.js: file missing" elif grep -q "require('\.\./src/" "$SERVER_JS"; then FAILED_CHECKS+=("server.js: still contains require('../src/...') (should be './src/...')") log "FAIL: server.js: contains require('../src/...') — build would produce crash-looping container" else TOTAL_OK=$((TOTAL_OK + 1)) log "OK: server.js — uses './src/...' requires" fi # ──────────────────────────────────────────────────────────────────────────── # Check 2: license-manager.js — must use '../../license-keygen' (correct # relative path from src/managers/ to API root) # ──────────────────────────────────────────────────────────────────────────── TOTAL_CHECKS=$((TOTAL_CHECKS + 1)) LICENSE_MGR="$API_DIR/src/managers/license-manager.js" if [[ ! -f "$LICENSE_MGR" ]]; then # Check if license-manager even exists — if src/managers/ doesn't have it, # that's only OK if the license module is somewhere else. if [[ -f "$API_DIR/src/managers/license-manager.js.bak" || -f "$API_DIR/license-manager.js" ]]; then TOTAL_OK=$((TOTAL_OK + 1)) log "OK: license-manager.js — relocated out of src/managers/ (acceptable)" else FAILED_CHECKS+=("license-manager.js: missing from src/managers/") log "FAIL: license-manager.js: missing from src/managers/" fi elif grep -q "require('\./license-keygen')" "$LICENSE_MGR"; then FAILED_CHECKS+=("license-manager.js: uses broken './license-keygen' (should be '../../license-keygen')") log "FAIL: license-manager.js: uses broken './license-keygen' — would MODULE_NOT_FOUND at runtime" else TOTAL_OK=$((TOTAL_OK + 1)) log "OK: license-manager.js — correct license-keygen path" fi # ──────────────────────────────────────────────────────────────────────────── # Check 3: src/ subdirectory present and non-empty (the bug that broke v1.14.4) # v1.14.4 tarballs literally didn't include src/ at all — every auto-update # resulted in a crash-looping container. We refuse to build without it. # ──────────────────────────────────────────────────────────────────────────── TOTAL_CHECKS=$((TOTAL_CHECKS + 1)) if [[ ! -d "$API_DIR/src" ]]; then FAILED_CHECKS+=("src/: missing — tarball did not ship src/ tree (v1.14.4-class bug)") log "FAIL: src/: directory missing — tarball did not ship src/ tree" elif [[ -z "$(ls -A "$API_DIR/src" 2>/dev/null)" ]]; then FAILED_CHECKS+=("src/: empty — tarball shipped empty src/ tree") log "FAIL: src/: directory is empty" elif [[ ! -f "$API_DIR/src/app.js" ]]; then FAILED_CHECKS+=("src/app.js: missing — src/ tree incomplete") log "FAIL: src/app.js: missing — src/ tree incomplete" else TOTAL_OK=$((TOTAL_OK + 1)) src_file_count=$(find "$API_DIR/src" -type f -name "*.js" -not -path "*/__tests__/*" 2>/dev/null | wc -l) log "OK: src/ — present with ${src_file_count} .js files" fi # ──────────────────────────────────────────────────────────────────────────── # Check 4: license-keygen.js exists at API root (was missing in v1.14.4) # ──────────────────────────────────────────────────────────────────────────── TOTAL_CHECKS=$((TOTAL_CHECKS + 1)) LICENSE_ROOT="$API_DIR/license-keygen.js" if [[ ! -f "$LICENSE_ROOT" ]]; then FAILED_CHECKS+=("license-keygen.js: missing at API root") log "FAIL: license-keygen.js: missing at API root — would MODULE_NOT_FOUND at runtime" else TOTAL_OK=$((TOTAL_OK + 1)) log "OK: license-keygen.js — present at API root" fi # ──────────────────────────────────────────────────────────────────────────── # Check 5: Generic src/**/*.js require path check — for each src/ file, walk # any `require('./name')` pattern and verify the module resolves from the # file's directory. If the require points at a file that does NOT exist in # the same subdir but DOES exist higher up, we report a likely-broken path. # # NOTE: This check is INFORMATIONAL — we log warnings for anything suspicious # but only fail the build on patterns we know are broken (the ones Checks 1-4 # cover). Future DC-NNN tickets can promote specific patterns from warnings # to hard failures as we discover more. # ──────────────────────────────────────────────────────────────────────────── TOTAL_CHECKS=$((TOTAL_CHECKS + 1)) WARN_COUNT=0 if [[ -d "$API_DIR/src" ]]; then while IFS= read -r -d '' src_file; do rel_dir=$(dirname "${src_file#$API_DIR/}") while IFS= read -r require_line; do mod_path=$(echo "$require_line" | grep -oE "require\(['\"]\./[a-zA-Z0-9_-]+['\"]\)" | head -1 | sed -E "s|require\(['\"]\./||; s|['\"]\)||") [[ -z "$mod_path" ]] && continue # Candidate 1: same dir, .js file candidate="$rel_dir/$mod_path.js" [[ -f "$candidate" ]] && continue # Candidate 2: same dir, dir/index.js [[ -d "$rel_dir/$mod_path" && -f "$rel_dir/$mod_path/index.js" ]] && continue # Walk up parents looking for the module found_path="" test_dir="$rel_dir" while true; do test_dir=$(dirname "$test_dir") if [[ -f "$test_dir/$mod_path.js" || ( -d "$test_dir/$mod_path" && -f "$test_dir/$mod_path/index.js" ) ]]; then found_path="$test_dir/$mod_path" break fi [[ "$test_dir" == "." || "$test_dir" == "/" ]] && break done if [[ -n "$found_path" ]]; then log " WARN: ${rel_dir}/$(basename "$src_file"): require('./${mod_path}') resolves to ${found_path} (possible stale path)" WARN_COUNT=$((WARN_COUNT + 1)) fi done < <(grep -E "require\(['\"]\./[a-zA-Z0-9_-]+['\"]\)" "$src_file" 2>/dev/null || true) done < <(find "$API_DIR/src" -type f -name "*.js" -not -path "*/node_modules/*" -not -path "*/__tests__/*" -print0 2>/dev/null) fi if (( WARN_COUNT == 0 )); then TOTAL_OK=$((TOTAL_OK + 1)) log "OK: src/ require paths — no suspicious same-dir-vs-root mismatches" else log "INFO: src/ require paths — ${WARN_COUNT} informational warning(s) (does NOT fail build)" TOTAL_OK=$((TOTAL_OK + 1)) # informational only fi # ──────────────────────────────────────────────────────────────────────────── # Summary # ──────────────────────────────────────────────────────────────────────────── log "=== Verify summary: ${TOTAL_OK}/${TOTAL_CHECKS} checks passed ===" if (( ${#FAILED_CHECKS[@]} > 0 )); then log "=== FAILED CHECKS ===" for check in "${FAILED_CHECKS[@]}"; do log " - $check" done log "=== Build should be ABORTED — fix the source tree first ===" exit 1 fi log "All checks passed. Safe to proceed with Docker build." exit 0