407 lines
9.9 KiB
Bash
407 lines
9.9 KiB
Bash
#!/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
|
|
}
|