Módulo 11: Laboratorio — hooks, seguridad y CI/CD
Unidad: GIT-13
Capa: laboratorio avanzado.
Público: alumnado de DAW, DAM, ASIR y grados de informática con el tronco completo.
Itinerario: posterior al trabajo diario con remotos, recuperación y proyecto final.
Prerrequisitos: módulos 5, 6 y 8; una cuenta de GitHub y un repositorio propio.
Objetivo observable: distinguir los tres niveles de control —validación local, política del servidor y automatización de la forja—, configurar un hook, explicar por qué no es una medida de seguridad y hacer que una comprobación automática impida integrar código roto.
Fuentes técnicas: githooks, gitignore, rulesets de GitHub y GitHub Actions.
Duración orientativa: 3 horas.
Idea clave: tres niveles, tres garantías distintas
| Nivel | Dónde vive | Quién lo puede saltar | Para qué sirve |
|---|---|---|---|
| Hook local | Tu .git/hooks |
Tú mismo, con --no-verify |
Ayudarte a no cometer un error |
| Política del servidor | Configuración del repositorio en GitHub | Nadie, salvo quien tenga bypass | Impedir que algo entre en main |
| CI/CD | Ejecución en los servidores de GitHub | Nadie | Comprobar de verdad que el código funciona |
La confusión que este laboratorio quiere eliminar: un hook local no protege el repositorio. Es una comodidad para quien lo tiene instalado. Lo que protege es la política del servidor. Todo lo del nivel 1 es Git; los niveles 2 y 3 no son Git, son GitHub.
Laboratorio A — Hooks locales
Un hook es un guion que Git ejecuta automáticamente en un momento del ciclo de trabajo. Si el guion devuelve un código de salida distinto de 0, Git aborta la operación.
Los que ya tienes
mkdir hooks-practica && cd hooks-practica
git init -b main
ls .git/hooksapplypatch-msg.sample
commit-msg.sample
fsmonitor-watchman.sample
post-update.sample
pre-applypatch.sample
…Git instala ejemplos con extensión .sample, inactivos precisamente por esa extensión. Un hook se activa cuando existe con el nombre exacto (pre-commit) y permiso de ejecución.
Un primer hook
printf '#!/bin/sh\nif git diff --cached --check; then exit 0; else exit 1; fi\n' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
printf 'línea con espacio \n' > prueba.txt
git add prueba.txt
git commit -m "test: prueba de hook"prueba.txt:1: trailing whitespace.
+línea con espacio El commit no se ha creado. El archivo sigue preparado y el historial no ha avanzado:
git log --oneline
git status --shortfatal: your current branch 'main' does not have any commits yet
A prueba.txtCorrige y repite:
printf 'línea sin espacio\n' > prueba.txt
git add prueba.txt
git commit -m "test: prueba de hook"[main (root-commit) 6a0b0e1] test: prueba de hook
1 file changed, 1 insertion(+)Aquí se reutiliza git diff --cached --check, que ya conoces del módulo 12: detecta espacios sobrantes y marcadores de conflicto.
Un hook útil: detectar credenciales
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
# Rechaza el commit si detecta algo que parezca una credencial.
if git diff --cached -U0 | grep -nE '^\+.*(AKIA[0-9A-Z]{16}|api[_-]?key\s*=|password\s*=|BEGIN [A-Z ]*PRIVATE KEY)'; then
echo ""
echo "pre-commit: posible credencial en los cambios preparados. Commit abortado."
exit 1
fi
exit 0
EOF
chmod +x .git/hooks/pre-commit
printf 'api_key = "AKIAIOSFODNN7EXAMPLE"\n' > config.py
git add config.py
git commit -m "feat: añade configuración"7:+api_key = "AKIAIOSFODNN7EXAMPLE"
pre-commit: posible credencial en los cambios preparados. Commit abortado.Solo inspecciona las líneas añadidas de lo preparado (-U0 y el filtro ^\+), que es lo que estás a punto de confirmar.
Y ahora la lección del laboratorio
git commit --no-verify -m "feat: añade configuración"
git log --oneline -1b8cb704 feat: añade configuraciónEl secreto ha entrado en el historial y el hook no ha dicho nada. --no-verify salta todos los hooks, y no hay forma de impedirlo: el hook se ejecuta en tu equipo, con tus permisos.
Y hay un segundo agujero:
cd .. && git clone hooks-practica clon
ls clon/.git/hooks/pre-commitls: clon/.git/hooks/pre-commit: No such file or directoryLos hooks no viajan al clonar. Están en .git/, que no se versiona. Quien clone tu proyecto no tendrá ninguna de tus protecciones.
De ahí la conclusión que hay que llevarse: un hook es un despertador, no una cerradura.
Compartir hooks con el equipo: core.hooksPath
Se pueden versionar en una carpeta normal del proyecto:
mkdir -p .githooks
cp .git/hooks/pre-commit .githooks/pre-commit
chmod +x .githooks/pre-commit
git add .githooks && git commit -m "chore: añade hooks del proyecto en .githooks"
git config core.hooksPath .githooksCompruébalo:
printf 'password = "hunter2"\n' > otro.py
git add otro.py
git commit -m "feat: otro"7:+password = "hunter2"
pre-commit: posible credencial en los cambios preparados. Commit abortado.Ahora el hook está en el repositorio y le llega a todo el mundo… pero cada persona debe ejecutar git config core.hooksPath .githooks una vez. Git no lo activa solo, y con razón: ejecutar automáticamente guiones que vienen en un repositorio clonado sería un problema de seguridad grave.
Es un buen sitio para documentarlo en el README, junto a las instrucciones de instalación.
Laboratorio B — Un secreto publicado por error
Es la emergencia más común del curso, y el orden de los pasos importa más que los comandos.
El protocolo
- Revocar o rotar la credencial en su proveedor. Este paso es el primero y no es negociable. Borrarla del repositorio no la invalida: si alguien la copió, sigue funcionando.
- Avisar al equipo y detener cualquier uso de esa credencial.
- Eliminarla del estado actual: quitar el archivo, añadir la regla a
.gitignore(módulo 4) y hacer un commit correctivo. - Solo entonces, valorar si hay que borrarla del historial.
Por qué el orden es ese
Recupera el repositorio del laboratorio A, donde el secreto entró con --no-verify:
git log -S "AKIAIOSFODNN7EXAMPLE" --oneline
git grep -n "AKIA" $(git rev-list --all) -- config.pyb8cb704 feat: añade configuración
73f2607…:config.py:1:api_key = "AKIAIOSFODNN7EXAMPLE"
b8cb704…:config.py:1:api_key = "AKIAIOSFODNN7EXAMPLE"Aunque borres el archivo hoy, el valor sigue siendo recuperable desde cualquier clon. Es lo mismo que demostró el módulo 6 con revert: corriges el estado, no el pasado.
Reescribir el historial para eliminarlo (con git filter-repo o git lfs migrate-style rewriting) cambia todos los hashes posteriores y obliga a que el equipo entero vuelva a clonar. Es una operación coordinada, con responsable y aviso previo — nunca un push --force improvisado.
Y aun haciéndolo bien, la credencial estuvo expuesta. Por eso el paso 1 es rotarla: es el único que realmente cierra el problema.
Prevención, en los tres niveles
| Nivel | Medida |
|---|---|
| Local | .gitignore desde el primer commit; hook pre-commit |
| Servidor | Protección de main; secret scanning de GitHub |
| CI | Un paso que busque patrones de credencial y falle |
Ninguno basta solo. El .gitignore es el más barato y el que más disgustos evita.
Laboratorio C — Política del servidor y CI en GitHub
Este laboratorio necesita un repositorio propio en GitHub. Todo lo que sigue está reproducido sobre uno real.
Proteger main
En Settings → Rules → New ruleset, con objetivo la rama por defecto y estas reglas:
- Require a pull request before merging — nada entra en
mainsin PR. - Block force pushes (
non_fast_forward) — impide reescribir lo publicado. - Restrict deletions — impide borrar la rama.
Un detalle que cuesta un rato descubrir: un ruleset recién creado puede quedar en Disabled, es decir, guardado pero sin aplicarse. Hay que ponerlo en Active. Compruébalo intentando saltártelo:
git switch main
printf "\nLinea de prueba.\n" >> README.md
git commit -am "test: comprueba la proteccion de main"
git push origin mainremote: error: GH013: Repository rule violations found for refs/heads/main.
remote:
remote: - Changes must be made through a pull request.
remote:
! [remote rejected] main -> main (push declined due to repository rule violations)
error: failed to push some refs to 'github.com:usuario/repo.git'Compara esta protección con el hook del laboratorio A: esto no se salta con --no-verify. Se aplica en el servidor, no en tu equipo. Ese es el salto de nivel.
Conviene además no añadirse a uno mismo como actor de bypass: si quien imparte el curso puede saltarse la regla, el alumnado aprende que la regla es opcional.
Un workflow de CI
Crea .github/workflows/ci.yml en una rama, nunca directamente en main:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
comprobar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Comprobaciones del proyecto
run: ./comprobar.sh
- name: Buscar credenciales en los cambios
run: |
if grep -rnE '(AKIA[0-9A-Z]{16}|BEGIN [A-Z ]*PRIVATE KEY)' --include='*' . ; then
echo "Se ha detectado una posible credencial."
exit 1
fi
echo "Sin credenciales detectadas."Con el comprobar.sh del módulo 12:
#!/bin/sh
test -f README.md || { echo "FALLO: falta README.md"; exit 1; }
echo "OK: comprobaciones superadas"Publica y abre la pull request:
git switch -c lab/ci-ejemplo
git add .github/workflows/ci.yml comprobar.sh
git commit -m "ci: añade workflow de comprobaciones"
git push -u origin lab/ci-ejemploGitHub ejecuta el workflow automáticamente al abrir la PR:
gh run list --limit 1completed success ci: añade workflow de comprobaciones CI lab/ci-ejemplo pull_request 7sFíjate en que es el mismo contrato de código de salida que usan el hook del laboratorio A, comprobar.sh del módulo 12 y git bisect run del módulo 9: 0 pasa, distinto de 0 falla. Aprendido una vez, sirve en los tres sitios.
Cuando falla
Añade a la rama un commit con una credencial simulada y observa:
./config.py:1:AWS_KEY = "AKIAIOSFODNN7EXAMPLE"
Se ha detectado una posible credencial.
##[error]Process completed with exit code 1.gh run list --limit 1completed failure feat: añade configuración CI lab/ci-ejemplo pull_request 8sEl paso que casi todo el mundo olvida
Con la CI en rojo, mira si la PR se puede integrar:
gh pr view 1 --json mergeable,mergeStateStatus{"mergeStateStatus":"UNSTABLE","mergeable":"MERGEABLE"}MERGEABLE. La CI ha fallado y aun así la pull request se puede fusionar. La protección exigía una PR, no exigía que las comprobaciones pasaran. Son dos reglas distintas, y sin la segunda la CI es solo un semáforo decorativo.
Añade al ruleset Require status checks to pass, indicando el nombre del job (comprobar), y repite:
{"mergeStateStatus":"BLOCKED","mergeable":"MERGEABLE"}gh pr merge 1 --mergeX Pull request #1 is not mergeable: the base branch policy prohibits the merge.Ahora sí. Corrige el problema, la CI pasa a verde y el estado cambia a CLEAN:
{"mergeStateStatus":"CLEAN","mergeable":"MERGEABLE","checks":[{"name":"comprobar","conclusion":"SUCCESS"}]}Esa secuencia UNSTABLE → BLOCKED → CLEAN es el resumen del laboratorio.
Lo que la CI no hace
Comprueba lo que le has pedido comprobar, nada más. Una CI en verde no significa que el código sea correcto, seguro ni legible: significa que pasó tus comprobaciones. No sustituye la revisión humana de la pull request (módulo 8), la complementa.
Errores frecuentes
- Confiar en un hook local como control de seguridad. Se salta con
--no-verifyy no llega a quien clona. Lo que protege es la política del servidor. - Versionar hooks en
.githooksy creer que ya están activos. Cada persona debe ejecutargit config core.hooksPath .githooksuna vez. Documéntalo en el README. - Crear el ruleset y dejarlo en
Disabled. Parece configurado y no aplica nada. Compruébalo siempre intentando un push directo amain. - Exigir pull request pero no exigir que la CI pase. La PR se puede fusionar en rojo. Son dos reglas separadas.
- Añadir
.enval.gitignoredespués de haberlo confirmado y creer que desaparece del historial. No desaparece (módulo 6). - Rotar la credencial al final, o no rotarla. Es el primer paso, no el último: el resto no la invalida.
- Copiar un token real en un ejemplo, una incidencia o un ejercicio. Usa valores de documentación, como
AKIAIOSFODNN7EXAMPLE. - Darse permisos de bypass sobre las propias reglas. Convierte la política en una sugerencia.
Ejercicio y criterios
Sobre un repositorio propio en GitHub:
- Escribe un hook
pre-commitque rechace credenciales. Demuestra el rechazo y el commit corregido. - Demuestra que el hook no es una protección: sáltalo con
--no-verifyy comprueba que el clon no lo hereda. - Versiónalo en
.githooks, actívalo concore.hooksPathy documenta el paso en el README. - Protege
maincon un ruleset activo y captura el mensajeGH013de un push directo rechazado. - Añade un workflow de CI con las comprobaciones del proyecto y ábrelo en una pull request.
- Provoca un fallo de CI y captura el estado de la PR antes y después de añadir «Require status checks to pass».
- Documenta el protocolo de respuesta ante una clave expuesta, justificando el orden de los pasos.
Criterios de revisión:
- Se distingue con evidencia propia qué es Git (hooks), qué es la política de GitHub y qué es la CI.
- Aparece el rechazo
GH013y se explica por qué--no-verifyno lo evita. - Se muestran los dos estados de la PR (
UNSTABLEyBLOCKED) y se explica la diferencia. - El protocolo de la credencial empieza por rotarla, y se justifica por qué.
- Ningún ejemplo contiene una credencial real.
- No se ha usado
push --forcesobremainni permisos de bypass.
Resumen
Al finalizar, el alumnado debe saber:
- Escribir y activar un hook, y explicar el contrato de código de salida.
- Argumentar por qué un hook local no es una medida de seguridad:
--no-verifyy la no distribución al clonar. - Compartir hooks con
core.hooksPath, sabiendo que requiere una activación manual por persona. - Proteger una rama en GitHub y reconocer el rechazo
GH013. - Escribir un workflow de CI sencillo y leer su resultado.
- Distinguir «exigir PR» de «exigir que la CI pase», y saber que hacen falta las dos.
- Aplicar el protocolo ante una credencial expuesta, empezando por rotarla.
Estado editorial: listo.
Registro de revisión técnica
Revisado el 5 de agosto de 2026 con Git 2.47.1, gh 2.97.0 y GitHub Actions. Los laboratorios A y B se han reproducido en local desde carpetas vacías; el laboratorio C, sobre un repositorio real de GitHub, con su ruleset y sus ejecuciones de Actions.
Laboratorio A. Verificado el rechazo del hook pre-commit con git diff --cached --check (trailing whitespace, historial sin avanzar, archivo aún preparado) y el commit correcto tras corregir. Se ha añadido, y verificado, lo que faltaba para que el laboratorio tenga sentido:
- Un hook de detección de credenciales que aborta con su mensaje propio.
- Que
git commit --no-verifylo salta por completo y el secreto entra en el historial. Sin esta demostración, el laboratorio daba a entender que un hook protege el repositorio. - Que un clon no hereda los hooks (
.git/hooks/pre-commitno existe en el clon). core.hooksPathcomo forma de versionarlos, comprobando que actúa desde.githooksy que exige una activación manual por persona.
Laboratorio B. Verificado con git log -S y git grep sobre git rev-list --all que la credencial confirmada sigue siendo recuperable desde cualquier clon aunque se borre el archivo. Se ha reordenado el protocolo para dejar la rotación de la credencial como paso 1 explícito y justificado.
Laboratorio C, reproducido sobre GitHub. Se ha comprobado que:
- Un ruleset creado queda en
enforcement: disabledhasta activarlo; en ese estado no impide nada. Documentado como error frecuente porque parece configurado. - Con el ruleset activo, un push directo a
mainse rechaza conGH013: Repository rule violations foundyChanges must be made through a pull request. - El workflow se ejecuta al abrir la PR (
success, 7 s) y falla cuando el paso de búsqueda de credenciales encuentraAKIAIOSFODNN7EXAMPLE, con##[error]Process completed with exit code 1. - El hallazgo principal del laboratorio: con la CI en rojo y solo la regla de pull request, la PR queda en
mergeStateStatus: UNSTABLEymergeable: MERGEABLE, es decir, se puede fusionar igualmente. Tras añadirrequired_status_checkscon el contextocomprobar, el estado pasa aBLOCKEDygh pr mergerespondethe base branch policy prohibits the merge. Al corregir,CLEAN. La secuenciaUNSTABLE → BLOCKED → CLEANse ha incorporado como columna vertebral de la sección.
Todos los valores de credencial usados son el ejemplo de documentación de AWS (AKIAIOSFODNN7EXAMPLE), nunca claves reales. Los hashes y tiempos de las salidas corresponden a esta ejecución.