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).
This commit is contained in:
@@ -1,20 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# DashCaddy Post-Deploy Patch Script
|
||||
# DashCaddy Post-Deploy Verifier
|
||||
# Runs AFTER the host-side update script copies staging files into the API source
|
||||
# directory, but BEFORE the Docker build. Fixes upstream bugs in the released
|
||||
# tarball so the build succeeds and the container starts cleanly.
|
||||
# directory, but BEFORE the Docker build.
|
||||
#
|
||||
# Why this exists:
|
||||
# v1.14.4 (commit d2a48b1) shipped with broken relative require paths:
|
||||
# - Root server.js: `require('../src/...')` instead of `require('./src/...')`
|
||||
# - Many src/**/*.js: `require('./module-name')` instead of
|
||||
# `require('../module-name')` (files were moved into src/ but requires
|
||||
# not updated to point at root-level modules)
|
||||
# - Missing license-keygen.js at root
|
||||
# Without these patches, every auto-update results in a crash-looping container.
|
||||
# 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.
|
||||
#
|
||||
# Idempotent: safe to run multiple times, only changes files that match the
|
||||
# broken pattern. Reports what was already OK so you can confirm health.
|
||||
# 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
|
||||
@@ -23,201 +26,161 @@ set -uo pipefail
|
||||
|
||||
API_DIR="${1:-/opt/dashcaddy/dashcaddy-api}"
|
||||
|
||||
log() { echo "[dashcaddy-patch] $(date '+%Y-%m-%d %H:%M:%S') $*"; }
|
||||
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_PATCHED=0
|
||||
TOTAL_ALREADY_OK=0
|
||||
TOTAL_CHECKS=0
|
||||
TOTAL_OK=0
|
||||
FAILED_CHECKS=()
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Patch 1: Root-level server.js — fix '../src/...' requires to './src/...'
|
||||
# v1.14.4 was tagged with broken relative paths. server.js sits at the API
|
||||
# root, so any `require('../src/...')` is one directory too high.
|
||||
# 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
|
||||
if grep -q "require('\.\./src/" "$SERVER_JS"; then
|
||||
BAD_COUNT=$(grep -c "require('\.\./src/" "$SERVER_JS" || true)
|
||||
sed -i "s|require('\.\./src/|require('./src/|g" "$SERVER_JS"
|
||||
if ! grep -q "require('\.\./src/" "$SERVER_JS"; then
|
||||
log "Patched server.js: rewrote ${BAD_COUNT} '../src/...' requires to './src/...'"
|
||||
TOTAL_PATCHED=$((TOTAL_PATCHED + BAD_COUNT))
|
||||
else
|
||||
log "WARNING: server.js sed did not remove all bad requires"
|
||||
fi
|
||||
else
|
||||
TOTAL_ALREADY_OK=$((TOTAL_ALREADY_OK + 1))
|
||||
log "server.js: already correct (no '../src/...' requires)"
|
||||
fi
|
||||
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
|
||||
log "WARNING: $SERVER_JS not found"
|
||||
TOTAL_OK=$((TOTAL_OK + 1))
|
||||
log "OK: server.js — uses './src/...' requires"
|
||||
fi
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Patch 2: src/managers/license-manager.js — fix './license-keygen' require
|
||||
# The license-keygen module lives at the API root, so from src/managers/
|
||||
# the correct relative path is '../../license-keygen'.
|
||||
# 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
|
||||
if grep -q "require('\./license-keygen')" "$LICENSE_MGR"; then
|
||||
sed -i "s|require('\./license-keygen')|require('../../license-keygen')|g" "$LICENSE_MGR"
|
||||
if grep -q "require('\.\./\.\./license-keygen')" "$LICENSE_MGR"; then
|
||||
log "Patched license-manager.js: './license-keygen' → '../../license-keygen'"
|
||||
TOTAL_PATCHED=$((TOTAL_PATCHED + 1))
|
||||
else
|
||||
log "WARNING: license-manager.js sed did not apply"
|
||||
fi
|
||||
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
|
||||
TOTAL_ALREADY_OK=$((TOTAL_ALREADY_OK + 1))
|
||||
log "license-manager.js: already correct"
|
||||
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
|
||||
log "WARNING: $LICENSE_MGR not found"
|
||||
TOTAL_OK=$((TOTAL_OK + 1))
|
||||
log "OK: license-manager.js — correct license-keygen path"
|
||||
fi
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Patch 3: Generic src/**/*.js require path fix
|
||||
# For every file in any src/ subdir, find `require('./module-name')` patterns
|
||||
# where module-name.js exists at API root but NOT in the same subdir, and
|
||||
# rewrite them to `require('../module-name')`.
|
||||
#
|
||||
# This catches the bulk of v1.14.4's broken paths that the upstream refactor
|
||||
# left behind (files moved into src/ but requires not updated).
|
||||
# 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.
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
log "Scanning src/ for broken root-level requires..."
|
||||
|
||||
GENERIC_PATCHED=0
|
||||
GENERIC_ALREADY_OK=0
|
||||
|
||||
# Build a list of all .js files in src/ (excluding tests)
|
||||
while IFS= read -r -d '' src_file; do
|
||||
# Get the directory containing this file relative to API_DIR
|
||||
rel_dir=$(dirname "${src_file#$API_DIR/}") # e.g. "src/docker"
|
||||
depth=$(echo "$rel_dir" | tr '/' '\n' | wc -l)
|
||||
# depth=1 means src/foo.js (parent is "src")
|
||||
# depth=2 means src/docker/foo.js (parent is "src/docker"), need ../
|
||||
|
||||
# Find all `require('./name')` patterns in this file
|
||||
while IFS= read -r require_line; do
|
||||
# Extract the module path from inside the quotes
|
||||
mod_path=$(echo "$require_line" | grep -oE "require\(['\"]\./[a-zA-Z0-9_-]+['\"]\)" | head -1 | sed -E "s|require\(['\"]\./||; s|['\"]\)||")
|
||||
|
||||
if [[ -z "$mod_path" ]]; then continue; fi
|
||||
|
||||
# Compute the absolute path Node would resolve `./mod_path` to from this file
|
||||
# Candidate 1: same dir, .js file
|
||||
candidate="$rel_dir/$mod_path.js"
|
||||
if [[ -f "$candidate" ]]; then
|
||||
# File exists in same subdir → require is correct as-is
|
||||
continue
|
||||
fi
|
||||
|
||||
# Candidate 2: same dir, directory with index.js
|
||||
if [[ -d "$rel_dir/$mod_path" && -f "$rel_dir/$mod_path/index.js" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if it exists at the root (one level above src/, or at the
|
||||
# appropriate depth for nested src/ subdirs)
|
||||
# For a file at $rel_dir/$file.js, './mod' resolves to $rel_dir/mod.js
|
||||
# We need to find where mod.js actually exists.
|
||||
|
||||
found_path=""
|
||||
# Walk up from the same-dir candidate, checking each parent dir.
|
||||
# Start by checking the file's own dir (already done above), then
|
||||
# dirname(rel_dir), dirname(dirname(rel_dir)), ..., until we hit ".".
|
||||
# rel_dir is relative to API_DIR, so when test_dir becomes ".", we
|
||||
# should check API_DIR/$mod_path.js (the root), THEN break.
|
||||
test_dir="$rel_dir"
|
||||
while true; do
|
||||
test_dir=$(dirname "$test_dir")
|
||||
# Check this directory for the module: either .js file or dir/index.js
|
||||
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
|
||||
# Stop when we've gone past root
|
||||
[[ "$test_dir" == "." || "$test_dir" == "/" ]] && break
|
||||
done
|
||||
|
||||
if [[ -z "$found_path" ]]; then
|
||||
# Module not found anywhere — leave it alone, would need investigation
|
||||
continue
|
||||
fi
|
||||
|
||||
# Found at root. Compute the correct relative path from this file to root.
|
||||
# For src/docker/self-updater.js requiring platform-paths (at root):
|
||||
# need: '../../platform-paths'
|
||||
file_dir=$(dirname "$src_file")
|
||||
file_dir_rel="${file_dir#$API_DIR/}" # e.g. "src/docker"
|
||||
|
||||
# Number of dirs to go up: count slashes + 1
|
||||
# "src/docker" → 2 dirs → go up 2: ../../platform-paths
|
||||
up_count=$(echo "$file_dir_rel" | awk -F'/' '{print NF}')
|
||||
|
||||
up_path=""
|
||||
for ((i=0; i<up_count; i++)); do
|
||||
up_path="../$up_path"
|
||||
done
|
||||
correct_require="${up_path}${mod_path}"
|
||||
|
||||
# Apply the fix: require('./X') → require('../X')
|
||||
sed -i "s|require('\./${mod_path}')|require('${correct_require}')|g" "$src_file"
|
||||
log " Patched ${rel_dir}/$(basename "$src_file"): './${mod_path}' → '${correct_require}'"
|
||||
GENERIC_PATCHED=$((GENERIC_PATCHED + 1))
|
||||
|
||||
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)
|
||||
|
||||
if [[ $GENERIC_PATCHED -gt 0 ]]; then
|
||||
log "Generic src/ require patches: ${GENERIC_PATCHED} fixed"
|
||||
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
|
||||
log "Generic src/ require patches: 0 needed (all correct)"
|
||||
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
|
||||
TOTAL_PATCHED=$((TOTAL_PATCHED + GENERIC_PATCHED))
|
||||
TOTAL_ALREADY_OK=$((TOTAL_ALREADY_OK + GENERIC_ALREADY_OK))
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Patch 4: Ensure license-keygen.js exists at API root
|
||||
# v1.14.4's tarball didn't ship the root-level license-keygen.js. If missing,
|
||||
# restore from src/managers/license-keygen.js or a backup.
|
||||
# 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"
|
||||
LICENSE_SRC="$API_DIR/src/managers/license-keygen.js"
|
||||
if [[ ! -f "$LICENSE_ROOT" ]]; then
|
||||
BACKUP_FILE=""
|
||||
# Prefer the v1.13.x backup if it exists (the version that had it at root)
|
||||
if [[ -d "$API_DIR/../updates/backups" ]]; then
|
||||
BACKUP_FILE=$(find "$API_DIR/../updates/backups" -name "license-keygen.js" 2>/dev/null | head -1)
|
||||
fi
|
||||
# Fall back to src/managers/ if newer refactor put it there
|
||||
if [[ -z "$BACKUP_FILE" && -f "$LICENSE_SRC" ]]; then
|
||||
BACKUP_FILE="$LICENSE_SRC"
|
||||
fi
|
||||
# Last resort: search elsewhere
|
||||
if [[ -z "$BACKUP_FILE" ]]; then
|
||||
BACKUP_FILE=$(find /opt/dashcaddy -name "license-keygen.js" -not -path "*/node_modules/*" -not -path "*/updates/*" -not -path "*/backups/staging-*" 2>/dev/null | head -1)
|
||||
fi
|
||||
if [[ -n "$BACKUP_FILE" && -f "$BACKUP_FILE" ]]; then
|
||||
cp -f "$BACKUP_FILE" "$LICENSE_ROOT"
|
||||
log "Restored missing license-keygen.js from $BACKUP_FILE"
|
||||
TOTAL_PATCHED=$((TOTAL_PATCHED + 1))
|
||||
else
|
||||
log "ERROR: license-keygen.js missing at root and no backup available — build may fail"
|
||||
fi
|
||||
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_ALREADY_OK=$((TOTAL_ALREADY_OK + 1))
|
||||
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 "=== Post-deploy patch summary: ${TOTAL_PATCHED} require fixes applied, ${TOTAL_ALREADY_OK} components already OK ==="
|
||||
exit 0
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user