#!/usr/bin/env bash # ============================================================================= # CI COMMON FUNCTIONS # ============================================================================= # Shared utility functions for local CI testing scripts. # # Usage: # source "$SCRIPT_DIR/lib/ci-common.sh" # # ============================================================================= # Prevent multiple sourcing [[ -n "${_CI_COMMON_LOADED:-}" ]] && return _CI_COMMON_LOADED=1 # ============================================================================= # COLOR DEFINITIONS # ============================================================================= if [[ -t 1 ]] && [[ -n "${TERM:-}" ]] && [[ "${TERM}" != "dumb" ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[0;37m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' MAGENTA='' CYAN='' WHITE='' BOLD='' DIM='' RESET='' fi # ============================================================================= # LOGGING FUNCTIONS # ============================================================================= # Log an info message log_info() { echo -e "${BLUE}[INFO]${RESET} $*" } # Log a success message log_success() { echo -e "${GREEN}[OK]${RESET} $*" } # Log a warning message log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2 } # Log an error message log_error() { echo -e "${RED}[ERROR]${RESET} $*" >&2 } # Log a debug message (only if VERBOSE is true) log_debug() { if [[ "${VERBOSE:-false}" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*" fi } # Log a step in a process log_step() { local step_num="$1" local total_steps="$2" local message="$3" echo -e "${CYAN}[${step_num}/${total_steps}]${RESET} ${BOLD}${message}${RESET}" } # Log a section header log_section() { echo "" echo -e "${BOLD}${MAGENTA}=== $* ===${RESET}" echo "" } # Log a subsection header log_subsection() { echo -e "${CYAN}--- $* ---${RESET}" } # ============================================================================= # ERROR HANDLING # ============================================================================= # Exit with error message die() { log_error "$@" exit 1 } # Check if a command exists require_command() { local cmd="$1" local install_hint="${2:-}" if ! command -v "$cmd" &>/dev/null; then log_error "Required command not found: $cmd" if [[ -n "$install_hint" ]]; then log_info "Install with: $install_hint" fi return 1 fi return 0 } # Check if a file exists require_file() { local file="$1" if [[ ! -f "$file" ]]; then log_error "Required file not found: $file" return 1 fi return 0 } # Check if a directory exists require_dir() { local dir="$1" if [[ ! -d "$dir" ]]; then log_error "Required directory not found: $dir" return 1 fi return 0 } # ============================================================================= # TIMING FUNCTIONS # ============================================================================= # Get current timestamp in seconds get_timestamp() { date +%s } # Format duration in human-readable format format_duration() { local seconds="$1" local minutes=$((seconds / 60)) local remaining_seconds=$((seconds % 60)) if [[ $minutes -gt 0 ]]; then echo "${minutes}m ${remaining_seconds}s" else echo "${remaining_seconds}s" fi } # Start a timer and return the start time start_timer() { get_timestamp } # Stop a timer and print the duration stop_timer() { local start_time="$1" local label="${2:-Operation}" local end_time end_time=$(get_timestamp) local duration=$((end_time - start_time)) log_info "$label completed in $(format_duration $duration)" } # ============================================================================= # STRING FUNCTIONS # ============================================================================= # Convert string to lowercase to_lower() { echo "$1" | tr '[:upper:]' '[:lower:]' } # Convert string to uppercase to_upper() { echo "$1" | tr '[:lower:]' '[:upper:]' } # Trim whitespace from string trim() { local var="$*" var="${var#"${var%%[![:space:]]*}"}" var="${var%"${var##*[![:space:]]}"}" echo -n "$var" } # Join array elements with delimiter join_by() { local delimiter="$1" shift local first="$1" shift printf '%s' "$first" "${@/#/$delimiter}" } # ============================================================================= # ARRAY FUNCTIONS # ============================================================================= # Check if array contains element array_contains() { local needle="$1" shift local element for element in "$@"; do [[ "$element" == "$needle" ]] && return 0 done return 1 } # ============================================================================= # FILE FUNCTIONS # ============================================================================= # Create directory if it doesn't exist ensure_dir() { local dir="$1" if [[ ! -d "$dir" ]]; then mkdir -p "$dir" log_debug "Created directory: $dir" fi } # Get absolute path get_absolute_path() { local path="$1" if [[ -d "$path" ]]; then (cd "$path" && pwd) elif [[ -f "$path" ]]; then local dir dir=$(dirname "$path") echo "$(cd "$dir" && pwd)/$(basename "$path")" else echo "$path" fi } # ============================================================================= # GIT FUNCTIONS # ============================================================================= # Get the repository root directory get_repo_root() { git rev-parse --show-toplevel 2>/dev/null } # Get current branch name get_current_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null } # Get current commit SHA get_current_sha() { git rev-parse HEAD 2>/dev/null } # Get short commit SHA get_short_sha() { git rev-parse --short HEAD 2>/dev/null } # Check if working directory is clean is_git_clean() { [[ -z "$(git status --porcelain 2>/dev/null)" ]] } # Get list of changed files compared to main branch get_changed_files() { local base_branch="${1:-main}" git diff --name-only "$base_branch"...HEAD 2>/dev/null } # ============================================================================= # MODULE DETECTION # ============================================================================= # Map of module names to source paths declare -A MODULE_PATHS=( ["Scanner"]="src/Scanner src/BinaryIndex" ["Concelier"]="src/Concelier src/Excititor" ["Authority"]="src/Authority" ["Policy"]="src/Policy src/RiskEngine" ["Attestor"]="src/Attestor src/Provenance" ["EvidenceLocker"]="src/EvidenceLocker" ["ExportCenter"]="src/ExportCenter" ["Findings"]="src/Findings" ["SbomService"]="src/SbomService" ["Notify"]="src/Notify src/Notifier" ["Router"]="src/Router src/Gateway" ["Cryptography"]="src/Cryptography" ["AirGap"]="src/AirGap" ["Cli"]="src/Cli" ["AdvisoryAI"]="src/AdvisoryAI" ["ReachGraph"]="src/ReachGraph" ["Orchestrator"]="src/Orchestrator" ["PacksRegistry"]="src/PacksRegistry" ["Replay"]="src/Replay" ["Aoc"]="src/Aoc" ["IssuerDirectory"]="src/IssuerDirectory" ["Telemetry"]="src/Telemetry" ["Signals"]="src/Signals" ["Web"]="src/Web" ["DevPortal"]="src/DevPortal" ) # Modules that use Node.js/npm instead of .NET declare -a NODE_MODULES=("Web" "DevPortal") # Detect which modules have changed based on git diff detect_changed_modules() { local base_branch="${1:-main}" local changed_files changed_files=$(get_changed_files "$base_branch") local changed_modules=() local module local paths for module in "${!MODULE_PATHS[@]}"; do paths="${MODULE_PATHS[$module]}" for path in $paths; do if echo "$changed_files" | grep -q "^${path}/"; then if ! array_contains "$module" "${changed_modules[@]}"; then changed_modules+=("$module") fi break fi done done # Check for infrastructure changes that affect all modules if echo "$changed_files" | grep -qE "^(Directory\.Build\.props|Directory\.Packages\.props|nuget\.config)"; then echo "ALL" return fi # Check for shared library changes if echo "$changed_files" | grep -q "^src/__Libraries/"; then echo "ALL" return fi if [[ ${#changed_modules[@]} -eq 0 ]]; then echo "NONE" else echo "${changed_modules[*]}" fi } # ============================================================================= # RESULT REPORTING # ============================================================================= # Print a summary table row print_table_row() { local col1="$1" local col2="$2" local col3="${3:-}" printf " %-30s %-15s %s\n" "$col1" "$col2" "$col3" } # Print pass/fail status print_status() { local name="$1" local passed="$2" local duration="${3:-}" if [[ "$passed" == "true" ]]; then print_table_row "$name" "${GREEN}PASSED${RESET}" "$duration" else print_table_row "$name" "${RED}FAILED${RESET}" "$duration" fi } # ============================================================================= # ENVIRONMENT LOADING # ============================================================================= # Load environment file if it exists load_env_file() { local env_file="$1" if [[ -f "$env_file" ]]; then log_debug "Loading environment from: $env_file" set -a # shellcheck source=/dev/null source "$env_file" set +a return 0 fi return 1 }