Skip to content
Open
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
53 changes: 53 additions & 0 deletions commands/host/_lib/check-drupal-safe-uninstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#ddev-generated
#annertech-ddev
# Catch modules removed from core.extension.yml and composer.lock in the same push.
if [[ -n "$EXT_FILE" ]] && git -C "$APPROOT" rev-parse --git-dir >/dev/null 2>&1; then
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
15 changes: 14 additions & 1 deletion commands/host/sanity-check
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ OFFLINE=false
[[ "$*" == *"-o"* || "$*" == *"--offline"* ]] && OFFLINE=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 @@ -31,6 +39,7 @@ CONFIG_READER_MIN=3

ERRORS=0
WARNINGS=0
CRITICAL_ERRORS=0
APPROOT="${DDEV_APPROOT:-.}"
BEHIND_CDN=false
CDN_NAME=""
Expand All @@ -54,6 +63,7 @@ group "DRUPAL"
. "$LIB_DIR/check-drupal-version.sh"
. "$LIB_DIR/check-drupal-performance.sh"
. "$LIB_DIR/check-drupal-extensions.sh"
. "$LIB_DIR/check-drupal-safe-uninstall.sh"
. "$LIB_DIR/check-drupal-files-view.sh"
. "$LIB_DIR/check-drupal-simplei.sh"
. "$LIB_DIR/check-drupal-gin.sh"
Expand Down Expand Up @@ -85,7 +95,10 @@ fi

# ── 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
31 changes: 29 additions & 2 deletions scripts/git-hooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,35 @@ fi

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

# Expose the push range so sanity-check can diff only the committed changes.
# pre-push stdin format: <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
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