#!/bin/bash # detect-affected.sh - Dependency-aware change detection for Forgejo Actions # Uses Turborepo to trace workspace dependencies and determine what needs deployment # # Usage: ./detect-affected.sh [base_ref] [output_file] # base_ref: Git ref to compare against (default: HEAD~1) # output_file: Where to write results (default: .forgejo.env) # # Output: Writes to output file with DEPLOY_* variables # # Exit codes: # 0: Success # 1: Error (missing dependencies, git issues, etc.) set -euo pipefail BASE_REF="${1:-HEAD~1}" OUTPUT_FILE="${2:-.forgejo.env}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CODEBASE_DIR="$(cd "${SCRIPT_DIR}/../../../codebase" && pwd)" # Colors for local output (stripped in CI) RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2 echo "::info::$1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2 echo "::warning::$1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2 echo "::error::$1" } # Deployable features registry # Format: FEATURE_NAME:PACKAGE_PATTERNS (comma-separated glob patterns) # Note: Some features use @conversation-assistant/* instead of @lilith/* declare -A DEPLOYABLE_FEATURES=( ["STATUS_DASHBOARD"]="@lilith/status-dashboard-*,@lilith/host-status-monitor,@lilith/health-*" ["CONVERSATION_ASSISTANT"]="@conversation-assistant/*" ["LANDING"]="@lilith/landing,@lilith/landing-*" ["MARKETPLACE"]="@lilith/marketplace-*" ["PLATFORM_ADMIN"]="@lilith/platform-admin" ["PAYMENTS"]="@lilith/payments,@lilith/payments-*" ["SSO"]="@lilith/sso-*" ["WEBMAP"]="@lilith/webmap-*" ["EMAIL"]="@lilith/email-*" ["ANALYTICS"]="@lilith/analytics-*" ["PROFILE"]="@lilith/profile,@lilith/profile-*" ["FEATURE_FLAGS"]="@lilith/feature-flags,@lilith/feature-flags-*" ["I18N"]="@lilith/i18n,@lilith/i18n-*" ["SEO"]="@lilith/seo-*" ["KNOWLEDGE_VERIFICATION"]="@lilith/knowledge-verification-*" ["PLATFORM_USER"]="@lilith/platform-user" ["DATING_AUTOPILOT"]="@lilith/dating-autopilot" ) # Check prerequisites check_prerequisites() { if ! command -v pnpm &> /dev/null; then log_error "pnpm is required but not installed" exit 1 fi if ! command -v jq &> /dev/null; then log_error "jq is required but not installed" exit 1 fi if ! git rev-parse --git-dir &> /dev/null; then log_error "Not in a git repository" exit 1 fi # Verify base ref exists if ! git rev-parse "${BASE_REF}" &> /dev/null; then log_warn "Base ref '${BASE_REF}' not found, using empty tree (full build)" BASE_REF="4b825dc642cb6eb9a060e54bf8d69288fbee4904" # Git empty tree SHA fi } # Get affected packages using Turborepo get_affected_packages() { cd "${CODEBASE_DIR}" log_info "Detecting changes since ${BASE_REF}..." # Use turbo's filter to find affected packages # --dry-run=json gives us the execution plan without running # Note: turbo outputs warnings before JSON, so we extract JSON with sed local raw_output raw_output=$(pnpm turbo build --filter="...[${BASE_REF}]" --dry-run=json 2>/dev/null || echo '') # Extract JSON portion (everything from first { onwards) local turbo_output turbo_output=$(echo "${raw_output}" | awk '/^{/{found=1} found{print}') if [[ -z "${turbo_output}" ]]; then log_warn "No turbo output, falling back to git diff" # Fallback: use git diff to detect changed packages git diff --name-only "${BASE_REF}" 2>/dev/null | \ grep -E '^(features|@packages)/' | \ sed -E 's|^(features/[^/]+).*|\1|; s|^(@packages/[^/]+).*|\1|' | \ sort -u return fi # Extract package names from turbo output echo "${turbo_output}" | jq -r '.packages[]? // empty' | grep -v '^//' | sort -u } # Check if any package matches the feature patterns matches_feature() { local package="$1" local patterns="$2" # Split patterns by comma and check each IFS=',' read -ra PATTERN_ARRAY <<< "${patterns}" for pattern in "${PATTERN_ARRAY[@]}"; do # Convert glob pattern to regex (simple conversion) local regex="${pattern//\*/.*}" if [[ "${package}" =~ ^${regex}$ ]]; then return 0 fi done return 1 } # Determine which features are affected detect_affected_features() { local affected_packages="$1" declare -A affected_features while IFS= read -r package; do [[ -z "${package}" ]] && continue for feature in "${!DEPLOYABLE_FEATURES[@]}"; do if matches_feature "${package}" "${DEPLOYABLE_FEATURES[${feature}]}"; then affected_features["${feature}"]=1 log_info "Package '${package}' triggers DEPLOY_${feature}" fi done done <<< "${affected_packages}" # Also check for @packages changes (shared dependencies) # These trigger ALL features that depend on them local shared_changed=false while IFS= read -r package; do # Check if it's a shared package (in @packages/) if [[ "${package}" =~ ^@lilith/(types|core|utils|hooks|config|validation|infrastructure|providers|plugins|design-tokens|ui|utility)$ ]]; then shared_changed=true log_warn "Shared package '${package}' changed - may affect multiple features" fi done <<< "${affected_packages}" # Output environment variables echo "# Generated by detect-affected.sh at $(date -Iseconds)" > "${OUTPUT_FILE}" echo "# Base ref: ${BASE_REF}" >> "${OUTPUT_FILE}" echo "" >> "${OUTPUT_FILE}" local any_deploy=false for feature in "${!DEPLOYABLE_FEATURES[@]}"; do if [[ -v "affected_features[${feature}]" ]]; then echo "DEPLOY_${feature}=true" >> "${OUTPUT_FILE}" log_info "DEPLOY_${feature}=true" any_deploy=true else echo "DEPLOY_${feature}=" >> "${OUTPUT_FILE}" fi done # Set flag for shared package changes if [[ "${shared_changed}" == "true" ]]; then echo "SHARED_PACKAGES_CHANGED=true" >> "${OUTPUT_FILE}" log_warn "SHARED_PACKAGES_CHANGED=true - review dependent features" fi if [[ "${any_deploy}" == "false" ]]; then log_info "No deployable features affected" fi } # Also detect raw file changes for non-turbo awareness detect_file_changes() { local changed_files changed_files=$(git diff --name-only "${BASE_REF}" 2>/dev/null || echo "") # Infrastructure changes if echo "${changed_files}" | grep -q "^infrastructure/"; then echo "INFRA_CHANGED=true" >> "${OUTPUT_FILE}" log_info "Infrastructure files changed" fi # Root config changes (might affect everything) if echo "${changed_files}" | grep -qE "^(turbo\.json|pnpm-workspace\.yaml|tsconfig\.base\.json)$"; then echo "ROOT_CONFIG_CHANGED=true" >> "${OUTPUT_FILE}" log_warn "Root configuration changed - may require full rebuild" fi # Forgejo Actions pipeline changes if echo "${changed_files}" | grep -q "^\.forgejo"; then echo "PIPELINE_CHANGED=true" >> "${OUTPUT_FILE}" log_info "Pipeline configuration changed" fi } main() { log_info "Starting dependency-aware change detection" check_prerequisites local affected_packages affected_packages=$(get_affected_packages) if [[ -z "${affected_packages}" ]]; then log_info "No affected packages detected" # Still create output file with empty values echo "# No changes detected" > "${OUTPUT_FILE}" for feature in "${!DEPLOYABLE_FEATURES[@]}"; do echo "DEPLOY_${feature}=" >> "${OUTPUT_FILE}" done else log_info "Affected packages:" echo "${affected_packages}" | while read -r pkg; do [[ -n "${pkg}" ]] && echo " - ${pkg}" >&2 done detect_affected_features "${affected_packages}" fi detect_file_changes log_info "Output written to ${OUTPUT_FILE}" cat "${OUTPUT_FILE}" >&2 } main "$@"