#!/bin/bash # check-deps.sh - Enforces the strict dependency allowlist from CONVENTIONS.md # Exit 1 if any unapproved import is found. # # 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. set -euo pipefail CONVENTIONS_FILE="${1:-CONVENTIONS.md}" if [ ! -f "$CONVENTIONS_FILE" ]; then echo "❌ CONVENTIONS.md not found" exit 1 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)}') if [ -n "$pkg" ] && [ "$pkg" != "Package" ] && [[ "$pkg" =~ ^[a-zA-Z] ]]; then if [[ "$scope" == *"test"* ]]; then ALLOWED_TEST["$pkg"]=1 else ALLOWED_PROD["$pkg"]=1 fi fi done < <(grep '| `' "$CONVENTIONS_FILE" 2>/dev/null || true) ALL_ALLOWED=("${!ALLOWED_PROD[@]}" "${!ALLOWED_TEST[@]}") if [ ${#ALL_ALLOWED[@]} -eq 0 ]; then echo "⚠️ No approved packages found in $CONVENTIONS_FILE" echo " (This is fine if you want stdlib-only)" fi # Helper: check if import matches any package in an associative array (literal prefix, no glob) matches_allowlist() { local import="$1" shift local -n allowlist=$1 for allowed in "${!allowlist[@]}"; do # Exact match if [ "$import" = "$allowed" ]; then return 0 fi # Literal prefix match for subpackages (no glob interpretation) if [ "${import#"$allowed/"}" != "$import" ]; then return 0 fi done 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" exit 1 } # Filter out the module itself (first line) and empty lines IMPORTS=$(echo "$IMPORTS" | tail -n +2 | grep -v '^$' || true) if [ -z "$IMPORTS" ]; then echo "✅ No external dependencies" exit 0 fi # Check all direct dependencies are in the allowlist VIOLATIONS="" while IFS= read -r import; do [ -z "$import" ] && continue if ! matches_allowlist "$import" ALLOWED_PROD && ! matches_allowlist "$import" ALLOWED_TEST; then VIOLATIONS="${VIOLATIONS} - ${import} (not in allowlist)"$'\n' fi done <<< "$IMPORTS" if [ -n "$VIOLATIONS" ]; then echo "❌ UNAPPROVED DEPENDENCIES DETECTED" echo "" echo "The following imports are not in the allowlist:" printf "%s" "$VIOLATIONS" echo "" echo "To add a dependency, update CONVENTIONS.md (requires Aaron's approval)" exit 1 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) TEST_ONLY_IN_PROD="" for test_pkg in "${!ALLOWED_TEST[@]}"; do if echo "$PROD_IMPORTS" | grep -q "^${test_pkg}"; then TEST_ONLY_IN_PROD="${TEST_ONLY_IN_PROD} - ${test_pkg} (marked 'test only' but used in production code)"$'\n' fi done if [ -n "$TEST_ONLY_IN_PROD" ]; then echo "❌ TEST-ONLY DEPENDENCIES IN PRODUCTION CODE" echo "" printf "%s" "$TEST_ONLY_IN_PROD" echo "" echo "These packages are marked 'test only' in CONVENTIONS.md" echo "and must only be imported from *_test.go files." exit 1 fi echo "✅ All dependencies are approved" echo " Direct deps: $(echo "$IMPORTS" | wc -l | tr -d ' ')" echo " Production: ${#ALLOWED_PROD[@]}, Test-only: ${#ALLOWED_TEST[@]}"