Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/plans/EXECPLAN_ci_shell_resilience.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,28 @@ Queremos que los desarrolladores puedan ejecutar los scripts `scripts/ci/*.sh` s
- [x] (2025-11-16 11:45Z) Normalizar `scripts/ci/infrastructure/validate-config.sh` y los hooks de `scripts/git-hooks/*.sh` (permisos + sintaxis) para que pasen la validación de scripts.
- [x] (2025-11-16 11:55Z) Confirmar que `scripts/ci/run-all-checks.sh` termina con código 0 y reporta secciones con PASS/SKIP según corresponda.
- [x] (2025-11-16 12:05Z) Documentar hallazgos y resultados en las secciones vivas antes de cerrar la ExecPlan.
- [x] (2025-11-16 12:40Z) Revisar `bandit-scan.sh`, `npm-audit.sh` y `test-execution-time.sh` para eliminar dependencias implícitas en GitHub Actions (instalaciones automáticas, pipelines con `tee` sin `pipefail`).
- [x] (2025-11-16 12:42Z) Ampliar las pruebas de `scripts/tests/test_ci_shell_scripts.py` cubriendo los degradados esperados para Bandit, npm y el test pyramid en entornos sin dependencias.

## Surprises & Discoveries

- Observación: la especificación OpenAPI (`docs/api/openapi_permisos.yaml`) contenía descripciones sin comillas con dos puntos, lo que rompía el parser YAML. Se resolvió citando los literales problemáticos.
- Observación: `.devcontainer/devcontainer.json` incluye comentarios estilo JavaScript; el validador JSON se detuvo hasta que se agregó una lista de exclusión controlada.
- Observación: algunos workflows (`requirements_validate_traceability.yml`) se generan dinámicamente y no son YAML puro, de modo que se marcaron como `skip` para evitar falsos positivos.
- Observación: `npm audit` requiere `package-lock.json` y acceso al registro; en entornos locales sin dependencias instaladas o sin red es preferible degradar a `skip` para no bloquear desarrolladores.

## Decision Log

- Decisión: Tratar los chequeos que dependen de Django/Bandit como "skip" cuando las dependencias no estén presentes. Rationale: el entorno local y de CI desconectado no puede instalarlas, pero necesitamos que el pipeline continúe. Fecha: 2025-11-16 / Autor: Codex.
- Decisión: Excluir `.devcontainer/devcontainer.json` y `requirements_validate_traceability.yml` de la validación estricta (registrando advertencias). Rationale: ambos archivos usan sintaxis extendida intencional (comentarios y plantillas) y romperían la verificación en frío. Fecha: 2025-11-16 / Autor: Codex.
- Decisión: No intentar instalaciones automáticas (`pip install bandit`, `npm audit fix`) dentro de los scripts; en su lugar, degradar con mensajes accionables cuando la CLI o la red no estén disponibles. Rationale: evitar bloqueos en entornos air-gapped y mantener tiempos de ejecución acotados. Fecha: 2025-11-16 / Autor: Codex.

## Outcomes & Retrospective

- `scripts/ci/run-all-checks.sh` ahora finaliza con código 0 en entornos sin Django/Bandit, marcando seis chequeos como SKIP y manteniendo el reporte final completo.
- Los scripts de infraestructura y seguridad reportan advertencias claras ("Skipping Django checks", "Bandit installation failed - skipping") en lugar de stack traces, mejorando la depuración local.
- Las pruebas `pytest scripts/tests/test_ci_shell_scripts.py` verifican el nuevo flujo (3/3 en 26.9s) confirmando el comportamiento degradado.
- Los scripts de seguridad y validación del test pyramid ahora detectan la ausencia de `bandit`, `npm`, Django o la red antes de ejecutar comandos costosos, retornando `SKIP` en segundos y evitando depender de GitHub Actions para descubrir estos casos.

## Context and Orientation

Expand Down Expand Up @@ -67,6 +72,9 @@ Esto provoca que la suite de infraestructura registre múltiples FAIL en cascada
6. Cambiar `scripts/ci/security/bandit-scan.sh` de modo que si Bandit no está disponible y la instalación falla, se registre un skip en vez de un fail.
7. Revisar `scripts/ci/infrastructure/validate-config.sh` para que el chequeo de settings degrade a skip cuando falte Django, manteniendo las validaciones de JSON/YAML.
8. Ejecutar `scripts/ci/run-all-checks.sh` y las pruebas unitarias para verificar que el pipeline regresa exit 0 y que los mensajes de skip están presentes.
9. Ajustar `scripts/ci/security/npm-audit.sh` para detectar el frontend bajo `ui/`, degradar a skip cuando `npm` no esté disponible o la red falle y evitar `npm audit fix` en entornos locales.
10. Simplificar `scripts/ci/security/bandit-scan.sh` para que omita instalaciones automáticas y degrade inmediatamente si la CLI no está presente.
11. Incorporar una guardia en `scripts/ci/testing/test-execution-time.sh` (Django + pytest) y habilitar `set -o pipefail` para evitar falsos positivos al canalizar la salida.

## Concrete Steps

Expand Down
35 changes: 24 additions & 11 deletions scripts/ci/infrastructure/health-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,18 @@ fi
log_info "Checking database connectivity..."
cd "$PROJECT_ROOT/api/callcentersite"

if [ -f "manage.py" ]; then
DJANGO_READY=true
if ! python3 -c "import django" >/dev/null 2>&1; then
DJANGO_READY=false
log_warn "Skipping Django checks: Django is not installed in the current environment"
CHECKS_SKIPPED=$((CHECKS_SKIPPED + 1))
elif [ ! -f "manage.py" ]; then
DJANGO_READY=false
log_warn "Skipping Django checks: manage.py not found"
CHECKS_SKIPPED=$((CHECKS_SKIPPED + 1))
fi

if [ "$DJANGO_READY" = true ]; then
if DB_CHECK_OUTPUT=$(python3 manage.py check --database default 2>&1); then
log_info "Database connectivity: OK"
CHECKS_PASSED=$((CHECKS_PASSED + 1))
Expand All @@ -65,17 +76,19 @@ if [ -f "manage.py" ]; then
CHECKS_FAILED=$((CHECKS_FAILED + 1))
fi

# Check 4: Django configuration
log_info "Checking Django configuration..."
if DJANGO_CHECK_OUTPUT=$(python3 manage.py check 2>&1); then
log_info "Django configuration: OK"
CHECKS_PASSED=$((CHECKS_PASSED + 1))
log_info "Checking Django configuration..."
if DJANGO_CHECK_OUTPUT=$(python3 manage.py check 2>&1); then
log_info "Django configuration: OK"
CHECKS_PASSED=$((CHECKS_PASSED + 1))
else
log_error "Django configuration check failed"
echo "$DJANGO_CHECK_OUTPUT" | tail -n 20 | while IFS= read -r line; do
log_error " $line"
done
CHECKS_FAILED=$((CHECKS_FAILED + 1))
fi
else
log_error "Django configuration check failed"
echo "$DJANGO_CHECK_OUTPUT" | tail -n 20 | while IFS= read -r line; do
log_error " $line"
done
CHECKS_FAILED=$((CHECKS_FAILED + 1))
log_warn "Skipping Django checks due to missing prerequisites"
fi

# Check 5: Required directories exist
Expand Down
3 changes: 1 addition & 2 deletions scripts/ci/run-all-checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
# 0 - All checks passed
# 1 - One or more checks failed

set -u
set -o pipefail
set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
Expand Down
19 changes: 7 additions & 12 deletions scripts/ci/security/bandit-scan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
# Exit codes:
# 0 - No security issues
# 1 - Security issues found
# 2 - Scan skipped (bandit not available)

set -e
set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
Expand All @@ -26,23 +27,17 @@ log_info "Running Bandit security scan..."

# Activar entorno virtual si existe
if [ -f "$PROJECT_ROOT/venv/bin/activate" ]; then
# shellcheck source=/dev/null
source "$PROJECT_ROOT/venv/bin/activate"
elif [ -f "$PROJECT_ROOT/.venv/bin/activate" ]; then
# shellcheck source=/dev/null
source "$PROJECT_ROOT/.venv/bin/activate"
fi

# Check if bandit is installed
if ! command -v bandit &> /dev/null; then
log_warn "Bandit not installed, attempting installation..."
if ! pip install bandit >/tmp/bandit_install.log 2>&1; then
log_warn "Bandit installation failed - skipping scan"
log_warn "Installer output:\n$(tail -n 5 /tmp/bandit_install.log)"
exit 2
fi
fi

if ! command -v bandit &> /dev/null; then
log_warn "Bandit still unavailable after installation attempt - skipping scan"
if ! command -v bandit >/dev/null 2>&1; then
log_warn "Bandit CLI not detected - skipping scan"
log_warn "Install bandit locally to run this check (pip install bandit)"
exit 2
fi

Expand Down
58 changes: 39 additions & 19 deletions scripts/ci/security/npm-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
# Exit codes:
# 0 - No vulnerabilities
# 1 - Vulnerabilities found
# 2 - NPM not used (skip)
# 2 - NPM not used or prerequisites missing (skip)

set -e
set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
Expand All @@ -25,39 +25,59 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

log_info "Running NPM security audit..."

# Check if package.json exists
PACKAGE_JSON_FOUND=false
# Locate package.json (support monorepos)
AUDIT_DIR=""

if [ -f "$PROJECT_ROOT/package.json" ]; then
PACKAGE_JSON_FOUND=true
AUDIT_DIR="$PROJECT_ROOT"
log_info "Found package.json in repository root"
elif [ -f "$PROJECT_ROOT/ui/package.json" ]; then
AUDIT_DIR="$PROJECT_ROOT/ui"
log_info "Found package.json in ui"
elif [ -f "$PROJECT_ROOT/frontend/package.json" ]; then
cd "$PROJECT_ROOT/frontend"
PACKAGE_JSON_FOUND=true
AUDIT_DIR="$PROJECT_ROOT/frontend"
log_info "Found package.json in frontend"
fi

if [ "$PACKAGE_JSON_FOUND" = false ]; then
if [ -z "$AUDIT_DIR" ]; then
log_warn "No package.json found - skipping NPM audit"
exit 2
fi

# Check if npm is installed
if ! command -v npm &> /dev/null; then
log_error "npm not installed"
exit 1
if ! command -v npm >/dev/null 2>&1; then
log_warn "npm CLI not available - skipping audit"
log_warn "Install Node.js/npm locally to run this check"
exit 2
fi

log_info "Running npm audit..."

# Run npm audit
if npm audit --audit-level=moderate 2>&1 | tee /tmp/npm_audit.log; then
cd "$AUDIT_DIR"

AUDIT_LOG="/tmp/npm_audit.log"

if npm audit --audit-level=moderate >"$AUDIT_LOG" 2>&1; then
log_info "NPM audit passed - no vulnerabilities found"
exit 0
else
log_error "NPM audit found vulnerabilities"
cat /tmp/npm_audit.log
fi

log_info "Attempting to fix vulnerabilities..."
npm audit fix
if grep -E "(ENOTFOUND|ECONN|EAI_AGAIN|ENETUNREACH|network request failed)" "$AUDIT_LOG" >/dev/null 2>&1; then
log_warn "npm audit could not reach the registry - skipping (offline environment)"
tail -n 5 "$AUDIT_LOG" | while IFS= read -r line; do
log_warn " $line"
done
exit 2
fi

exit 1
if grep -E "(ENOLOCK|requires a lockfile)" "$AUDIT_LOG" >/dev/null 2>&1; then
log_warn "npm audit requires dependencies installed (missing lockfile) - skipping"
tail -n 5 "$AUDIT_LOG" | while IFS= read -r line; do
log_warn " $line"
done
exit 2
fi

log_error "NPM audit found vulnerabilities or failed unexpectedly"
cat "$AUDIT_LOG"
exit 1
18 changes: 16 additions & 2 deletions scripts/ci/testing/test-execution-time.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
#
# Exit codes:
# 0 - Test execution time is acceptable
# 1 - Tests are too slow
# 1 - Tests are too slow or failed
# 2 - Prerequisites missing (skip)

set -e
set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
Expand All @@ -28,11 +29,24 @@ cd "$PROJECT_ROOT/api/callcentersite"

# Activar entorno virtual
if [ -f "$PROJECT_ROOT/venv/bin/activate" ]; then
# shellcheck source=/dev/null
source "$PROJECT_ROOT/venv/bin/activate"
elif [ -f "$PROJECT_ROOT/.venv/bin/activate" ]; then
# shellcheck source=/dev/null
source "$PROJECT_ROOT/.venv/bin/activate"
fi

# Verificar dependencias mínimas
if ! python3 -c "import django" >/dev/null 2>&1; then
log_warn "Skipping test execution time validation: Django not installed"
exit 2
fi

if ! command -v pytest >/dev/null 2>&1; then
log_warn "Skipping test execution time validation: pytest CLI not available"
exit 2
fi

# Run tests with timing
log_info "Running tests with timing analysis..."

Expand Down
67 changes: 59 additions & 8 deletions scripts/tests/test_ci_shell_scripts.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,83 @@
import os
import subprocess
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[2]
RUN_ALL_CHECKS = REPO_ROOT / "scripts" / "ci" / "run-all-checks.sh"
HEALTH_CHECK = REPO_ROOT / "scripts" / "ci" / "infrastructure" / "health-check.sh"
BANDIT_SCAN = REPO_ROOT / "scripts" / "ci" / "security" / "bandit-scan.sh"
NPM_AUDIT = REPO_ROOT / "scripts" / "ci" / "security" / "npm-audit.sh"
TEST_EXECUTION_TIME = REPO_ROOT / "scripts" / "ci" / "testing" / "test-execution-time.sh"


def _run_script(script_path, *args):
def _run_script(script_path, *args, env=None, timeout=60):
return subprocess.run(
["bash", str(script_path), *args],
cwd=REPO_ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=env,
timeout=timeout,
)


def test_run_all_checks_reports_summary_even_on_failure():
def test_run_all_checks_reports_summary_with_skips():
result = _run_script(RUN_ALL_CHECKS)

assert result.returncode != 0, "Expected the aggregated checks to fail in the default dev environment"
combined_output = f"{result.stdout}\\n{result.stderr}"
assert result.returncode == 0, "Aggregated checks should degrade to success with skips locally"
combined_output = f"{result.stdout}\n{result.stderr}"
assert "FINAL CI/CD REPORT" in combined_output
assert "Skipped:" in combined_output
assert "[SKIP]" in combined_output or "Skipped: 0" in combined_output


def test_health_check_surfaces_underlying_error_details():
def test_health_check_degrades_gracefully_when_django_missing():
result = _run_script(HEALTH_CHECK)

assert result.returncode != 0, "The health check should fail when dependencies are missing"
combined_output = f"{result.stdout}\\n{result.stderr}"
assert "ModuleNotFoundError" in combined_output or "No module named" in combined_output
assert result.returncode in (0, 2), "Health check should pass or skip when Django is unavailable"
combined_output = f"{result.stdout}\n{result.stderr}"
assert "Skipping Django checks" in combined_output


def test_run_all_checks_sets_strict_shell_flags():
contents = RUN_ALL_CHECKS.read_text()

assert "set -euo pipefail" in contents, "run-all-checks.sh must opt into strict shell error handling"


def test_bandit_scan_skips_quickly_when_cli_missing():
result = _run_script(BANDIT_SCAN, timeout=5)

assert result.returncode == 2, "Bandit scan should degrade to SKIP without attempting long installations"
combined_output = f"{result.stdout}\n{result.stderr}"
assert "Bandit CLI not detected" in combined_output or "Bandit not installed" in combined_output


def test_npm_audit_detects_ui_workspace_and_skips_without_npm():
env = os.environ.copy()
env["PATH"] = ":".join(
[
"/usr/local/sbin",
"/usr/local/bin",
"/usr/sbin",
"/usr/bin",
"/sbin",
"/bin",
]
)

result = _run_script(NPM_AUDIT, env=env, timeout=30)

assert result.returncode == 2, "NPM audit should skip when npm CLI is not available"
combined_output = f"{result.stdout}\n{result.stderr}"
assert "Found package.json in ui" in combined_output
assert "npm CLI not available" in combined_output or "npm not installed" in combined_output


def test_test_execution_time_skips_without_django():
result = _run_script(TEST_EXECUTION_TIME, timeout=20)

assert result.returncode == 2, "Test execution validation should skip when Django is absent"
combined_output = f"{result.stdout}\n{result.stderr}"
assert "Skipping test execution time validation" in combined_output
Loading