Files
dashcaddy/scripts/dashcaddy-post-deploy-patches.sh
T
Hermes cbe0c912fc
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
DC-040: repurpose post-deploy-patches.sh as a verifier (fail-loud, not patch-and-continue)
Empirically measured against all 4 release versions + origin/main: every
patch in the old script is a no-op against every current release. v1.14.4
(the version that originally needed patches) doesn't even ship src/ in the
tarball — the old script silently no-op'd on it because it couldn't find
files to patch, then the build crashed with MODULE_NOT_FOUND in production.

Repurposed as a verifier: 5 hard checks (server.js requires, license-manager
path, src/ tree presence, license-keygen.js at root, generic src/ require
path scan) + informational warnings. Exits 1 on ANY failure with a clear
'Build should be ABORTED' message naming the v1.14.4-class bug if relevant.
Old behaviour was 'patch and continue' (silently hid regressions); new
behaviour is 'fail loud' (every regression now produces a build abort).

Files changed:
- scripts/dashcaddy-post-deploy-patches.sh — rewritten as verifier (222→274
  lines, header explains the empirical evidence + behaviour change)
- dashcaddy-api/scripts/test-dashcaddy-post-deploy-verifier.sh — new
  regression test, 17 assertions across 10 scenarios (clean tree, missing
  files, broken requires, empty src/, missing app.js, absolute path, etc.)

Empirical measurements documented:
- origin/main: 5/5 checks pass
- v1.14.9 (latest): 5/5 checks pass (0 patches applied under old script)
- v1.14.8: 5/5 checks pass (0 patches applied under old script)
- v1.14.4: 2/5 checks FAIL under new verifier (src/ missing, license-manager
  in wrong location) — old script silently no-op'd on the same input

Tests: 1214/1214 Jest + 31 shell assertions. Lint: 150 warnings, all
pre-existing in untouched files (zero new warnings introduced).
2026-07-13 15:09:08 -07:00

187 lines
10 KiB
Bash
Executable File

#!/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>
# 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