Files
git.stella-ops.org/devops/scripts/lib/ci-common.sh

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
}