platform-tooling/scripts/ci/detect-affected.sh
2026-02-27 15:20:12 -08:00

246 lines
8 KiB
Bash
Executable file

#!/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 "$@"