#!/bin/bash ## # Care API - Quality Gates Validator # # Valida métricas de qualidade e coverage para deployment # # @package Care_API # @author Descomplicar® Crescimento Digital # @version 1.0.0 # @since 2025-09-14 ## set -euo pipefail # Configurações de Quality Gates readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # Thresholds de qualidade readonly COVERAGE_MIN=70 # Coverage mínimo global readonly COVERAGE_CRITICAL=80 # Coverage para classes críticas readonly COMPLEXITY_MAX=10 # Complexidade ciclomática máxima readonly DUPLICATION_MAX=5 # Percentagem máxima de duplicação readonly MAINTAINABILITY_MIN=70 # Índice de manutenibilidade mínimo # Cores readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly NC='\033[0m' log() { local level="$1" shift local message="$*" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') case "$level" in "INFO") echo -e "${GREEN}[INFO]${NC} [$timestamp] $message" ;; "WARN") echo -e "${YELLOW}[WARN]${NC} [$timestamp] $message" ;; "ERROR") echo -e "${RED}[ERROR]${NC} [$timestamp] $message" >&2 ;; "DEBUG") echo -e "${BLUE}[DEBUG]${NC} [$timestamp] $message" ;; "PASS") echo -e "${GREEN}[PASS]${NC} [$timestamp] $message" ;; "FAIL") echo -e "${RED}[FAIL]${NC} [$timestamp] $message" ;; esac } # Estrutura para armazenar resultados declare -A gate_results declare -A gate_scores declare -i total_gates=0 declare -i passed_gates=0 # Função para registar resultado de gate register_gate() { local gate_name="$1" local status="$2" # pass|fail|warn local score="$3" # 0-100 local threshold="$4" # valor mínimo local actual="$5" # valor actual local message="$6" # mensagem descriptiva gate_results["$gate_name"]="$status" gate_scores["$gate_name"]="$score" ((total_gates++)) case "$status" in "pass") ((passed_gates++)) log "PASS" "✅ $gate_name: $actual (≥ $threshold) - $message" ;; "warn") log "WARN" "⚠️ $gate_name: $actual (< $threshold) - $message" ;; "fail") log "FAIL" "❌ $gate_name: $actual (< $threshold) - $message" ;; esac } # Gate 1: Coverage Global validate_global_coverage() { log "INFO" "Validando coverage global..." local clover_file="$PROJECT_DIR/coverage-reports/clover.xml" if [[ ! -f "$clover_file" ]]; then register_gate "coverage_global" "fail" 0 "$COVERAGE_MIN" "0" "Arquivo clover.xml não encontrado" return fi local coverage=$(php -r " \$xml = simplexml_load_file('$clover_file'); if (\$xml === false) { echo '0'; exit; } \$elements = (int) \$xml->project->metrics['elements']; \$covered = (int) \$xml->project->metrics['coveredelements']; echo \$elements > 0 ? round((\$covered / \$elements) * 100, 2) : 0; ") local status="pass" if (( $(echo "$coverage < $COVERAGE_MIN" | bc -l) )); then status="fail" elif (( $(echo "$coverage < $(($COVERAGE_MIN + 10))" | bc -l) )); then status="warn" fi register_gate "coverage_global" "$status" "$coverage" "$COVERAGE_MIN" "${coverage}%" "Coverage global do projecto" } # Gate 2: Coverage de Classes Críticas validate_critical_class_coverage() { log "INFO" "Validando coverage de classes críticas..." local clover_file="$PROJECT_DIR/coverage-reports/clover.xml" if [[ ! -f "$clover_file" ]]; then register_gate "coverage_critical" "fail" 0 "$COVERAGE_CRITICAL" "0" "Dados não disponíveis" return fi # Lista de classes críticas (endpoints e serviços principais) local critical_classes=( "class-auth-endpoints.php" "class-patient-endpoints.php" "class-appointment-endpoints.php" "class-security-manager.php" ) local total_critical=0 local covered_critical=0 local low_coverage_classes=() for class_file in "${critical_classes[@]}"; do local class_coverage=$(php << PHP xpath('//file') as \$file) { \$filename = (string) \$file['name']; if (strpos(\$filename, \$targetClass) !== false) { \$metrics = \$file->metrics; if (\$metrics) { \$elements = (int) \$metrics['elements']; \$covered = (int) \$metrics['coveredelements']; echo \$elements > 0 ? round((\$covered / \$elements) * 100, 2) : 0; exit; } } } echo '0'; ?> PHP ) ((total_critical++)) if (( $(echo "$class_coverage >= $COVERAGE_CRITICAL" | bc -l) )); then ((covered_critical++)) else low_coverage_classes+=("$class_file:${class_coverage}%") fi done local critical_score=$(( (covered_critical * 100) / total_critical )) local status="pass" if [[ $covered_critical -lt $((total_critical * 8 / 10)) ]]; then status="fail" elif [[ $covered_critical -lt $total_critical ]]; then status="warn" fi local message="$covered_critical/$total_critical classes críticas" if [[ ${#low_coverage_classes[@]} -gt 0 ]]; then message="$message (baixa: ${low_coverage_classes[*]})" fi register_gate "coverage_critical" "$status" "$critical_score" "80" "${critical_score}%" "$message" } # Gate 3: Complexidade Ciclomática validate_cyclomatic_complexity() { log "INFO" "Validando complexidade ciclomática..." # Usar PHPLOC se disponível if ! command -v phploc >/dev/null 2>&1; then register_gate "complexity" "warn" 50 "$COMPLEXITY_MAX" "N/A" "PHPLOC não disponível" return fi local complexity_report="$PROJECT_DIR/coverage-reports/metrics/phploc.txt" if [[ ! -f "$complexity_report" ]]; then phploc "$PROJECT_DIR/src" > "$complexity_report" 2>/dev/null || { register_gate "complexity" "warn" 50 "$COMPLEXITY_MAX" "N/A" "Erro ao gerar relatório" return } fi # Extrair complexidade média local avg_complexity=$(grep "Average Complexity" "$complexity_report" | awk '{print $4}' | head -1) avg_complexity=${avg_complexity:-10} local status="pass" local score=100 if (( $(echo "$avg_complexity > $COMPLEXITY_MAX" | bc -l) )); then status="fail" score=$((100 - ($(echo "($avg_complexity - $COMPLEXITY_MAX) * 10" | bc -l | cut -d'.' -f1)))) [[ $score -lt 0 ]] && score=0 elif (( $(echo "$avg_complexity > $(($COMPLEXITY_MAX - 2))" | bc -l) )); then status="warn" score=75 fi register_gate "complexity" "$status" "$score" "$COMPLEXITY_MAX" "$avg_complexity" "Complexidade ciclomática média" } # Gate 4: Duplicação de Código validate_code_duplication() { log "INFO" "Validando duplicação de código..." if ! command -v phpcpd >/dev/null 2>&1; then register_gate "duplication" "warn" 50 "$DUPLICATION_MAX" "N/A" "PHPCPD não disponível" return fi local duplication_report="$PROJECT_DIR/coverage-reports/metrics/phpcpd.txt" if [[ ! -f "$duplication_report" ]]; then phpcpd "$PROJECT_DIR/src" > "$duplication_report" 2>/dev/null || { register_gate "duplication" "pass" 100 "$DUPLICATION_MAX" "0%" "Nenhuma duplicação encontrada" return } fi # Extrair percentagem de duplicação local duplication_percent=$(grep -oP '\d+\.\d+(?=% duplicated lines)' "$duplication_report" | head -1) duplication_percent=${duplication_percent:-0} local status="pass" local score=100 if (( $(echo "$duplication_percent > $DUPLICATION_MAX" | bc -l) )); then status="fail" score=$((100 - $(echo "$duplication_percent * 10" | bc -l | cut -d'.' -f1))) [[ $score -lt 0 ]] && score=0 elif (( $(echo "$duplication_percent > $((DUPLICATION_MAX - 2))" | bc -l) )); then status="warn" score=75 fi register_gate "duplication" "$status" "$score" "$DUPLICATION_MAX" "${duplication_percent}%" "Código duplicado" } # Gate 5: Sintaxe e Standards validate_code_standards() { log "INFO" "Validando padrões de código..." cd "$PROJECT_DIR" # Executar PHPCS local phpcs_output=$(composer run phpcs 2>&1 || true) local phpcs_errors=$(echo "$phpcs_output" | grep -oP '\d+(?= ERRORS?)' | head -1) local phpcs_warnings=$(echo "$phpcs_output" | grep -oP '\d+(?= WARNINGS?)' | head -1) phpcs_errors=${phpcs_errors:-0} phpcs_warnings=${phpcs_warnings:-0} local total_issues=$((phpcs_errors + phpcs_warnings)) local status="pass" local score=100 if [[ $phpcs_errors -gt 0 ]]; then status="fail" score=$((100 - (phpcs_errors * 10))) [[ $score -lt 0 ]] && score=0 elif [[ $phpcs_warnings -gt 5 ]]; then status="warn" score=$((100 - (phpcs_warnings * 2))) [[ $score -lt 60 ]] && score=60 fi register_gate "code_standards" "$status" "$score" "0" "${total_issues}" "${phpcs_errors} erros, ${phpcs_warnings} avisos" } # Gate 6: Cobertura de Testes por Suite validate_test_suites_coverage() { log "INFO" "Validando cobertura por suite de testes..." local suites=("unit" "integration" "contract") local suite_thresholds=(85 70 75) # Thresholds específicos por suite for i in "${!suites[@]}"; do local suite="${suites[i]}" local threshold="${suite_thresholds[i]}" local suite_clover="$PROJECT_DIR/coverage-${suite}.xml" if [[ -f "$suite_clover" ]]; then local coverage=$(php -r " \$xml = simplexml_load_file('$suite_clover'); if (\$xml === false) { echo '0'; exit; } \$elements = (int) \$xml->project->metrics['elements']; \$covered = (int) \$xml->project->metrics['coveredelements']; echo \$elements > 0 ? round((\$covered / \$elements) * 100, 2) : 0; ") local status="pass" if (( $(echo "$coverage < $threshold" | bc -l) )); then status=$( [[ $suite == "unit" ]] && echo "fail" || echo "warn" ) fi register_gate "coverage_${suite}" "$status" "$coverage" "$threshold" "${coverage}%" "Suite $suite" else register_gate "coverage_${suite}" "warn" 0 "$threshold" "N/A" "Suite $suite - dados não disponíveis" fi done } # Função para gerar relatório final generate_quality_report() { log "INFO" "📊 RELATÓRIO DE QUALITY GATES" local overall_score=$((passed_gates * 100 / total_gates)) local status_emoji if [[ $overall_score -ge 80 ]]; then status_emoji="🟢" elif [[ $overall_score -ge 60 ]]; then status_emoji="🟡" else status_emoji="🔴" fi echo "" echo "======================================" echo "$status_emoji QUALITY GATES SUMMARY" echo "======================================" echo "Passed Gates: $passed_gates/$total_gates" echo "Overall Score: $overall_score%" echo "" # Detalhar cada gate for gate in "${!gate_results[@]}"; do local status="${gate_results[$gate]}" local score="${gate_scores[$gate]}" case "$status" in "pass") echo "✅ $gate: $score%" ;; "warn") echo "⚠️ $gate: $score%" ;; "fail") echo "❌ $gate: $score%" ;; esac done echo "" # Determinar se deployment pode prosseguir local can_deploy=true local blocking_gates=() for gate in "${!gate_results[@]}"; do if [[ "${gate_results[$gate]}" == "fail" ]]; then # Gates críticos que bloqueiam deployment case "$gate" in "coverage_global"|"code_standards"|"coverage_critical") can_deploy=false blocking_gates+=("$gate") ;; esac fi done if [[ "$can_deploy" == "true" ]]; then log "PASS" "🚀 DEPLOYMENT AUTORIZADO - Todos os gates críticos passaram" return 0 else log "FAIL" "🚫 DEPLOYMENT BLOQUEADO - Gates críticos falharam: ${blocking_gates[*]}" return 1 fi } # Função principal main() { local mode="${1:-validate}" log "INFO" "🏥 Care API - Quality Gates Validator iniciado" cd "$PROJECT_DIR" case "$mode" in "validate") # Executar todos os quality gates validate_global_coverage validate_critical_class_coverage validate_cyclomatic_complexity validate_code_duplication validate_code_standards validate_test_suites_coverage # Gerar relatório final generate_quality_report ;; "coverage-only") validate_global_coverage validate_critical_class_coverage validate_test_suites_coverage generate_quality_report ;; "standards-only") validate_code_standards validate_cyclomatic_complexity validate_code_duplication generate_quality_report ;; *) echo "Uso: $0 {validate|coverage-only|standards-only}" echo "" echo " validate - Executar todos os quality gates" echo " coverage-only - Validar apenas coverage" echo " standards-only - Validar apenas padrões de código" echo "" exit 1 ;; esac } # Executar se chamado directamente if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi