platform-codebase/infrastructure/scripts/lib/dependency-graph.sh
Quinn Ftw 8080b31929 feat(deploy): add dynamic dependency detection for rectifier
Replace static JSON config with runtime dependency discovery by
grepping package.json files. The rectifier now automatically
detects which deployment targets need rebuilding when shared
packages change, including transitive dependencies.

Changes:
- Add lib/dependency-graph.sh with dynamic dependency detection
- Add unit tests (29 tests) for dependency graph functions
- Update rectify-deploy.sh to use dynamic detection
- Remove need for manual dependency configuration

How it works:
1. Extract package name from changed file path
2. Grep package.json files to find dependents
3. Map dependents to deployment targets
4. Handle transitive deps (ui-utils -> ui-primitives -> targets)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 23:28:34 -08:00

239 lines
7.5 KiB
Bash
Executable file

#!/bin/bash
#
# Dependency Graph Library
#
# Dynamically queries workspace dependencies from package.json files.
# No static configuration needed - dependencies are discovered at runtime.
#
# Functions:
# get_package_name_from_path(file_path) - Extract @lilith/pkg-name from file path
# find_dependents(package_name) - Find all packages that depend on this package
# get_deployment_targets(changed_files) - Map changed files to deployment targets
#
set -e
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
# =============================================================================
# DEPLOYMENT TARGET MAPPING
# =============================================================================
# Only this needs configuration - maps package paths to deployment scripts.
# Everything else is discovered dynamically from package.json files.
# Check if a path is a deployment target and return the target name
path_to_deployment_target() {
local file_path="$1"
case "$file_path" in
infrastructure/service-registry/*)
echo "service-registry"
;;
features/status-dashboard/*)
echo "status-dashboard"
;;
*)
echo ""
;;
esac
}
# Map package name to deployment target (for transitive deps)
package_to_deployment_target() {
local pkg_name="$1"
# Check which deployment targets depend on this package
local targets=""
# service-registry dashboard
if grep -q "\"$pkg_name\"" "$PROJECT_ROOT/infrastructure/service-registry/apps/dashboard/package.json" 2>/dev/null; then
targets="$targets service-registry"
fi
# status-dashboard frontend
if grep -q "\"$pkg_name\"" "$PROJECT_ROOT/features/status-dashboard/frontend/package.json" 2>/dev/null; then
targets="$targets status-dashboard"
fi
echo "$targets" | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' '
}
# =============================================================================
# PACKAGE NAME EXTRACTION
# =============================================================================
# Extract package name from file path
# e.g., "@packages/@ui/ui-theme/src/index.ts" -> "@lilith/ui-theme"
get_package_name_from_path() {
local file_path="$1"
# Handle @packages/@ui/* packages
if [[ "$file_path" =~ ^@packages/@ui/([^/]+)/ ]]; then
echo "@lilith/${BASH_REMATCH[1]}"
return
fi
# Handle @packages/@core/* packages
if [[ "$file_path" =~ ^@packages/@core/([^/]+)/ ]]; then
echo "@lilith/${BASH_REMATCH[1]}"
return
fi
# Handle @packages/@utils/* packages
if [[ "$file_path" =~ ^@packages/@utils/([^/]+)/ ]]; then
echo "@lilith/${BASH_REMATCH[1]}"
return
fi
# Handle @packages/@providers/* packages
if [[ "$file_path" =~ ^@packages/@providers/([^/]+)/ ]]; then
echo "@lilith/${BASH_REMATCH[1]}"
return
fi
# Handle infrastructure/service-registry packages
if [[ "$file_path" =~ ^infrastructure/service-registry/packages/@service-registry/([^/]+)/ ]]; then
echo "@service-registry/${BASH_REMATCH[1]}"
return
fi
# No package found (direct app change)
echo ""
}
# =============================================================================
# DEPENDENCY DISCOVERY
# =============================================================================
# Find all packages that directly depend on a given package
# Uses grep on package.json files - no pnpm required
find_direct_dependents() {
local pkg_name="$1"
cd "$PROJECT_ROOT"
# Grep all package.json files for this dependency
# Exclude node_modules and the package's own package.json
grep -r "\"$pkg_name\"" --include="package.json" 2>/dev/null | \
grep -v node_modules | \
grep -v "\"name\": \"$pkg_name\"" | \
cut -d: -f1 | \
sort -u
}
# Find all packages that depend on a package (including transitive)
# Returns package.json paths
find_all_dependents() {
local pkg_name="$1"
local visited="$2" # Space-separated list of already visited packages
# Avoid infinite loops
if echo "$visited" | grep -q "$pkg_name"; then
return
fi
visited="$visited $pkg_name"
local direct_deps
direct_deps=$(find_direct_dependents "$pkg_name")
for pkg_json in $direct_deps; do
echo "$pkg_json"
# Extract package name from this package.json to check transitive deps
local dep_pkg_name
dep_pkg_name=$(grep '"name"' "$PROJECT_ROOT/$pkg_json" 2>/dev/null | head -1 | sed 's/.*"\(@[^"]*\)".*/\1/' | tr -d ',')
if [ -n "$dep_pkg_name" ]; then
find_all_dependents "$dep_pkg_name" "$visited"
fi
done
}
# =============================================================================
# MAIN DETECTION FUNCTION
# =============================================================================
# Get all deployment targets affected by changed files
# This is the main entry point
get_deployment_targets() {
local changed_files="$1"
cd "$PROJECT_ROOT"
local all_targets=""
local processed_packages=""
while IFS= read -r file; do
[ -z "$file" ] && continue
# Check if this file is directly in a deployment target
local direct_target
direct_target=$(path_to_deployment_target "$file")
if [ -n "$direct_target" ]; then
all_targets="$all_targets $direct_target"
continue
fi
# Check if this file is in a shared package
local pkg_name
pkg_name=$(get_package_name_from_path "$file")
if [ -n "$pkg_name" ]; then
# Skip if already processed
if echo "$processed_packages" | grep -q "$pkg_name"; then
continue
fi
processed_packages="$processed_packages $pkg_name"
# Find what deployment targets depend on this package
local targets
targets=$(package_to_deployment_target "$pkg_name")
all_targets="$all_targets $targets"
# Also check transitive dependencies
local dependent_pkgs
dependent_pkgs=$(find_direct_dependents "$pkg_name")
for dep_json in $dependent_pkgs; do
local dep_name
dep_name=$(grep '"name"' "$PROJECT_ROOT/$dep_json" 2>/dev/null | head -1 | sed 's/.*"\(@[^"]*\)".*/\1/' | tr -d ',')
if [ -n "$dep_name" ]; then
local dep_targets
dep_targets=$(package_to_deployment_target "$dep_name")
all_targets="$all_targets $dep_targets"
fi
done
fi
done <<< "$changed_files"
# Deduplicate and return
echo "$all_targets" | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' '
}
# =============================================================================
# DEBUG FUNCTIONS
# =============================================================================
# Show what depends on a package (for debugging)
show_dependents() {
local pkg_name="$1"
echo "=== Dependents of $pkg_name ==="
echo ""
echo "Direct:"
find_direct_dependents "$pkg_name" | sed 's/^/ /'
echo ""
echo "Deployment targets:"
package_to_deployment_target "$pkg_name" | tr ' ' '\n' | sed 's/^/ /'
}
# Export functions for use by other scripts
export -f get_package_name_from_path
export -f find_direct_dependents
export -f find_all_dependents
export -f get_deployment_targets
export -f path_to_deployment_target
export -f package_to_deployment_target