Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion commands/host/sanity-check
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ SILENT=false
[[ "$*" == *"-s"* ]] && SILENT=true
pass() { $SILENT || echo -e "${GREEN} ✓ $1${NC}"; }
fail() { echo -e "${RED} ✗ $1${NC}"; ERRORS=$((ERRORS + 1)); }
critical_fail() { echo -e "${RED}${BOLD} ✗ CRITICAL: $1${NC}"; ERRORS=$((ERRORS + 1)); CRITICAL_ERRORS=$((CRITICAL_ERRORS + 1)); }
warn() { echo -e "${YELLOW} ! $1${NC}"; WARNINGS=$((WARNINGS + 1)); }
bail_if_critical() {
if [[ $CRITICAL_ERRORS -gt 0 ]]; then
echo ""
echo -e "${RED}${BOLD}── $CRITICAL_ERRORS critical error(s) — subsequent checks skipped, push must be blocked ──${NC}"
exit 2
fi
}
title() { echo -e "\n── $1 ──"; }
group() { echo -e "\n${BOLD}$1${NC}"; }

Expand All @@ -26,6 +34,7 @@ CONFIG_READER_MIN=3

ERRORS=0
WARNINGS=0
CRITICAL_ERRORS=0
APPROOT="${DDEV_APPROOT:-.}"
BEHIND_CDN=false
LOCK_FILE="$APPROOT/composer.lock"
Expand Down Expand Up @@ -114,6 +123,60 @@ else
fi
fi

# ── Drupal: Safe module uninstall check ─────────────────────────────────────
if [[ -n "$EXT_FILE" ]] && git -C "$APPROOT" rev-parse --git-dir >/dev/null 2>&1; then
# Pre-push hook sets GIT_PUSH_RANGE_BASE/TIP so we diff the exact commits
# being pushed. Otherwise diff the working tree vs HEAD (uncommitted).
if [[ -n "$GIT_PUSH_RANGE_BASE" ]]; then
DIFF_FROM="$GIT_PUSH_RANGE_BASE"
DIFF_TO="${GIT_PUSH_RANGE_TIP:-HEAD}"
else
DIFF_FROM="HEAD"
DIFF_TO=""
fi

EXT_DIFF=$(git -C "$APPROOT" diff "$DIFF_FROM" $DIFF_TO -- "$EXT_FILE" 2>/dev/null)
REMOVED_MODULES=$(echo "$EXT_DIFF" | grep -E "^-[[:space:]]+[a-z_0-9]+:[[:space:]]*[0-9]+" | sed -E 's/^-[[:space:]]+([a-z_0-9]+):.*/\1/')

if [[ -n "$REMOVED_MODULES" ]]; then
LOCK_DIFF=""
[[ -f "$LOCK_FILE" ]] && LOCK_DIFF=$(git -C "$APPROOT" diff "$DIFF_FROM" $DIFF_TO -- "$LOCK_FILE" 2>/dev/null)

SEARCH_DIRS=()
for d in "$APPROOT/web/modules" "$APPROOT/web/profiles" "$APPROOT/modules"; do
[[ -d "$d" ]] && SEARCH_DIRS+=("$d")
done

UNSAFE=()
while IFS= read -r mod; do
[[ -z "$mod" ]] && continue

MOD_PATH=""
if [[ ${#SEARCH_DIRS[@]} -gt 0 ]]; then
MOD_PATH=$(find "${SEARCH_DIRS[@]}" -maxdepth 5 -type d -name "$mod" ! -path "*/.git/*" 2>/dev/null | head -1)
fi

COMPOSER_REMOVED=false
if [[ -n "$LOCK_DIFF" ]] && echo "$LOCK_DIFF" | grep -qE "^-[[:space:]]+\"name\":[[:space:]]+\"drupal/${mod}\""; then
COMPOSER_REMOVED=true
fi

if [[ -z "$MOD_PATH" ]] || $COMPOSER_REMOVED; then
UNSAFE+=("$mod")
fi
done <<< "$REMOVED_MODULES"

if [[ ${#UNSAFE[@]} -gt 0 ]]; then
for m in "${UNSAFE[@]}"; do
critical_fail "Module '$m' is being uninstalled AND its code/package removed in the same change — uninstall first (deploy), then remove code in a later change"
done
else
pass "Module uninstallations keep code/package in place (safe)"
fi
fi
fi
bail_if_critical

# ── Drupal: Files view with operations ──────────────────────────────────────
FILES_VIEW="$APPROOT/config/sync/views.view.files.yml"

Expand Down Expand Up @@ -416,7 +479,10 @@ fi # end Upsun checks

# ── Summary ─────────────────────────────────────────────────────────────────
echo ""
if [[ $ERRORS -gt 0 ]]; then
if [[ $CRITICAL_ERRORS -gt 0 ]]; then
echo -e "${RED}${BOLD}── $CRITICAL_ERRORS critical error(s), $ERRORS total error(s) — push must be blocked ──${NC}"
exit 2
elif [[ $ERRORS -gt 0 ]]; then
echo -e "${RED}── $ERRORS error(s) found ──${NC}"
exit 1
elif [[ $WARNINGS -gt 0 ]]; then
Expand Down
32 changes: 30 additions & 2 deletions scripts/git-hooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,36 @@ fi

# ── Sanity check ─────────────────────────────────────────────────────────────
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
ddev sanity-check -s
if [[ $? -ne 0 ]]; then

# Expose the range being pushed so sanity-check can diff committed changes.
# Pre-push stdin format (per line): <local_ref> <local_sha> <remote_ref> <remote_sha>
ZERO_SHA="0000000000000000000000000000000000000000"
PUSH_LINE=$(echo "$PUSH_REFS" | grep -v '^$' | head -1)
if [[ -n "$PUSH_LINE" ]]; then
read -r _ PUSH_LOCAL_SHA _ PUSH_REMOTE_SHA <<< "$PUSH_LINE"
if [[ -n "$PUSH_REMOTE_SHA" && "$PUSH_REMOTE_SHA" != "$ZERO_SHA" ]]; then
export GIT_PUSH_RANGE_BASE="$PUSH_REMOTE_SHA"
else
# New branch: base is the merge-base with the default remote branch.
for ref in "origin/HEAD" "origin/main" "origin/master"; do
if git rev-parse --verify --quiet "$ref" >/dev/null 2>&1; then
MERGE_BASE=$(git merge-base "$ref" "$PUSH_LOCAL_SHA" 2>/dev/null)
[[ -n "$MERGE_BASE" ]] && export GIT_PUSH_RANGE_BASE="$MERGE_BASE"
break
fi
done
fi
[[ -n "$PUSH_LOCAL_SHA" && "$PUSH_LOCAL_SHA" != "$ZERO_SHA" ]] && export GIT_PUSH_RANGE_TIP="$PUSH_LOCAL_SHA"
fi

SANITY_OUTPUT=$(ddev sanity-check -s 2>&1)
SANITY_EXIT=$?
echo "$SANITY_OUTPUT"
if echo "$SANITY_OUTPUT" | grep -q "CRITICAL:"; then
echo ""
echo "🚫 Push blocked: critical sanity-check failure. This cannot be bypassed — fix the issues above and try again."
exit 1
elif [[ $SANITY_EXIT -ne 0 ]]; then
echo ""
if [[ "$CURRENT_BRANCH" == "dev" ]]; then
echo "⚠️ Sanity-check failed on 'dev' branch — pushing anyway."
Expand Down