From 01cde16d47fb11528793f47950421ccb70b54f39 Mon Sep 17 00:00:00 2001 From: Rodin Date: Sun, 10 May 2026 14:02:06 -0700 Subject: [PATCH] fix: validate all deps and improve robustness Addresses GPT review feedback: 1. MAJOR - Test deps now validated: All direct module deps (from go.mod) are checked against the allowlist, whether used in prod or tests. 2. MINOR - Prefix match: Uses grep -E with word boundary (^pkg(/|$|$)) to avoid false positives on similarly-prefixed modules. 3. MINOR - Bash version check: Script now fails early with helpful message if Bash < 4 (macOS default). Added shebang: #!/usr/bin/env bash 4. NIT - Removed redundant grep -v '_test' (go list -deps already excludes test-only deps without -test flag). --- scripts/check-deps.sh | 45 ++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/scripts/check-deps.sh b/scripts/check-deps.sh index 1d2999c..f88efd2 100755 --- a/scripts/check-deps.sh +++ b/scripts/check-deps.sh @@ -1,12 +1,21 @@ -#!/bin/bash +#!/usr/bin/env bash # check-deps.sh - Enforces the strict dependency allowlist from CONVENTIONS.md # Exit 1 if any unapproved import is found. +# +# Requires: Bash 4+ (for associative arrays), Go toolchain # # The allowlist is parsed from CONVENTIONS.md to maintain a single source of truth. -# Also enforces Scope column: "test only" packages cannot appear in non-test code. +# Enforces Scope column: "test only" packages cannot appear in non-test code. set -euo pipefail +# Check bash version +if ((BASH_VERSINFO[0] < 4)); then + echo "❌ Bash 4+ required (found ${BASH_VERSION})" + echo " On macOS: brew install bash" + exit 1 +fi + CONVENTIONS_FILE="${1:-CONVENTIONS.md}" if [ ! -f "$CONVENTIONS_FILE" ]; then @@ -16,13 +25,11 @@ fi # Parse approved packages from CONVENTIONS.md table using awk (POSIX-compatible) # Format: | `package` | use case | scope | -# Output: package:scope (e.g., "gopkg.in/yaml.v3:production") declare -A ALLOWED_PROD=() declare -A ALLOWED_TEST=() while IFS= read -r line; do # Use awk to extract package and scope from table row - # Split on | and extract backtick-wrapped package from column 1, scope from column 3 pkg=$(echo "$line" | awk -F'|' '{gsub(/^[[:space:]]*`|`[[:space:]]*$/, "", $2); print $2}') scope=$(echo "$line" | awk -F'|' '{gsub(/^[[:space:]]+|[[:space:]]+$/, "", $4); print tolower($4)}') @@ -53,7 +60,7 @@ matches_allowlist() { if [ "$import" = "$allowed" ]; then return 0 fi - # Literal prefix match for subpackages (no glob interpretation) + # Literal prefix match for subpackages: must match "pkg/" exactly if [ "${import#"$allowed/"}" != "$import" ]; then return 0 fi @@ -61,22 +68,19 @@ matches_allowlist() { return 1 } -# Get DIRECT dependencies only (exclude indirect/transitive) -# Fail closed: if go list fails, we exit non-zero -IMPORTS=$(go list -m -f '{{if not .Indirect}}{{.Path}}{{end}}' all 2>&1) || { - echo "❌ Failed to list dependencies: $IMPORTS" +# Get direct module dependencies from go.mod +DIRECT_IMPORTS=$(go list -m -f '{{if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all 2>&1) || { + echo "❌ Failed to list dependencies: $DIRECT_IMPORTS" exit 1 } +DIRECT_IMPORTS=$(echo "$DIRECT_IMPORTS" | grep -v '^$' || true) -# Filter out the module itself (first line) and empty lines -IMPORTS=$(echo "$IMPORTS" | tail -n +2 | grep -v '^$' || true) - -if [ -z "$IMPORTS" ]; then +if [ -z "$DIRECT_IMPORTS" ]; then echo "✅ No external dependencies" exit 0 fi -# Check all direct dependencies are in the allowlist +# Check ALL direct dependencies are in some allowlist VIOLATIONS="" while IFS= read -r import; do [ -z "$import" ] && continue @@ -84,7 +88,7 @@ while IFS= read -r import; do if ! matches_allowlist "$import" ALLOWED_PROD && ! matches_allowlist "$import" ALLOWED_TEST; then VIOLATIONS="${VIOLATIONS} - ${import} (not in allowlist)"$'\n' fi -done <<< "$IMPORTS" +done <<< "$DIRECT_IMPORTS" if [ -n "$VIOLATIONS" ]; then echo "❌ UNAPPROVED DEPENDENCIES DETECTED" @@ -97,12 +101,13 @@ if [ -n "$VIOLATIONS" ]; then fi # Enforce Scope: test-only packages must not appear in non-test code -# Get imports used by non-test code only -PROD_IMPORTS=$(go list -deps -f '{{if not .Standard}}{{.ImportPath}}{{end}}' ./... 2>/dev/null | grep -v '_test' || true) +# Get imports used by non-test code only (go list -deps without -test excludes test deps) +PROD_IMPORTS=$(go list -deps -f '{{if not .Standard}}{{.ImportPath}}{{end}}' ./... 2>/dev/null || true) TEST_ONLY_IN_PROD="" for test_pkg in "${!ALLOWED_TEST[@]}"; do - if echo "$PROD_IMPORTS" | grep -q "^${test_pkg}"; then + # Use word-boundary matching: exact match or followed by / + if echo "$PROD_IMPORTS" | grep -qE "^${test_pkg}(/|\$|$)"; then TEST_ONLY_IN_PROD="${TEST_ONLY_IN_PROD} - ${test_pkg} (marked 'test only' but used in production code)"$'\n' fi done @@ -118,5 +123,5 @@ if [ -n "$TEST_ONLY_IN_PROD" ]; then fi echo "✅ All dependencies are approved" -echo " Direct deps: $(echo "$IMPORTS" | wc -l | tr -d ' ')" -echo " Production: ${#ALLOWED_PROD[@]}, Test-only: ${#ALLOWED_TEST[@]}" +echo " Direct module deps: $(echo "$DIRECT_IMPORTS" | wc -l | tr -d ' ')" +echo " Production allowlist: ${#ALLOWED_PROD[@]}, Test-only allowlist: ${#ALLOWED_TEST[@]}"