En este segundo artículo aprenderemos a utilizar de forma práctica los Git Hooks en nuestros proyectos y así aprovecharnos de todos sus beneficios.
A modo de recordatorio pongo los enlaces a los artículos anteriores :
- Uso de Git Hooks para proyectos : Artículo de introducción y explicación teórica sobre los Git Hooks
Este es el índice que se va a utilizar para estructurar este artículo
Ejemplos de uso
Para enseñar a utilizar los Git Hooks de diferentes maneras y así practicar se ha habilitado un repositorio.
Todos los escenarios de uso serán expplicados en este artículo.
Para este apartado seguiremos los pasos definidos en el apartado "Construir un Git Hook desde 0" del primer artículo
Recordatorio de los pasos:
- Definir las necesidades del Git Hook
- Implementar un fichero de script específico (opcional)
- Creación de un Git Hook
- Preparación de la configuración
1. Definir las necesidades del Git Hook
Contexto General
En nuestro ejemplo haremos uso de un proyecto desarrollado con Node.js, pero podríamos haber utilizado cualquier otro lenguaje.
Elegiremos algunos de sus frameworks más estándar para mostrar un caso de uso "real".
Contestación a las preguntas
¿Qué queremos que haga de forma general el Git Hook?
Principalmente queremos añadir una capa de validaciones previa a realizar un commit, con el objetivo por ejemplo de minimizar los errores en la subida de código de nuestro equipo junior y con unas comprobaciones de errores particulares de nuestra forma de trabajo.
Nos gustaría entonces incorporar un Git Hook para la fase de pre commit.
¿Qué aspectos van a ser utilizados dentro del Git Hook de evento?
Pondremos el foco únicamente en dos aspectos:
- El cumplimiento en la ejecución de test unitarios sin importarnos la cobertura del testing
- La validación de un contrato de API mediante lintado con la herramienta Spectral
- Para más información sobre esta herramienta ver la serie de artículos de Spectral
¿Qué comportamiento queremos que tenga cada aspecto anterior con respecto a que algo vaya mal durante su ejecución?
Para la operativa de testing haremos que sólo se considere valido si todos los tests se ejecutan correctamente
Para la operativa lintado con Spectral del contrato de API será lo mismo, sólo se considerará válido si todo es correcto
En ambos casos debería de ser bloqueantes ante la detección de fallo en su ejecución.
Por otro lado, el orden de ejecución será el indicado: primero se validará el testing y luego se validará el lintado.
¿Qué momento (evento) utilizaremos para que se dispare?
Claramente, parece que se va a usar el Git Hook "pre-commit" ya que cumple todo lo que necesitamos.
Detalle de la operativa de testing
Se va a definir sobre el proyecto de desarrollo el stack tecnológico de testing, un ciclo de desarrollo básico definido donde se incorpora la ejecución de tests como fase y una batería de diferentes test unitarios.
Ejemplo de fichero "package.json" utilizado
{
"name": "spectral-project-git-hook",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
...
"test": "jest",
...
},
...
}
Se hará uso del framework de testing de jest, su ejecución se realiza dentro de las operaciones de scripting bajo el nombre de "test", se ejecutará sin parámetros por lo que su ejecución será con los valores por defecto (búsqueda en directorios normalizados de tests, etc.).
Se podrá lanzar de forma específica y aislada desde la opción de ejecución de npm: npm run xxx.
Detalle de la operativa de lintado con Spectral
Se va a definir sobre el proyecto de desarrollo el stack tecnológico de lintado, un ciclo de desarrollo básico definido donde se incorpora la ejecución de diferentes tipos de lintado con Spectral según las necesidades.
Ejemplo de fichero "package.json" utilizado
{
"name": "spectral-project-git-hook",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
...
"spectral:oas:lint:one": "spectral lint ./examples/example1.yaml",
"spectral:oas:lint": "spectral lint ./examples/*",
"spectral:oas:lint-warning-as-errors": "spectral lint -F warn ./examples/*"
...
},
...
}
Se hará uso de la herramienta Spectral, su ejecución se realiza dentro de las operaciones de scripting bajo el prefijo de "spectral:oas", se proporcionará diferentes opciones de ejecución.
Se podrá lanzar de forma específica y aislada desde la opción de ejecución de npm: npm run xxx.
2. Implementar un fichero de script específico (opcional)
Contexto general
Recordar que un aspecto importante de este apartado es la creación de un directorio específico dentro del proyecto para que contenga estos scripts como pueden ser los directorios a nivel del proyecto: scripts/, bin/, git-hooks/ o hooks/*
Pasos a seguir:
- Crear si no existen el directorio "scripts/" y el directorio "git-hooks/" en el repositorio a nivel del proyecto
- Verificar que se han creado correctamente
Nota
Estos scripts se pueden implementar en otro lenguaje al elegido para el desarrollo. Por regla general suelen realizarse conShell Script o Power Shell.
Scripting previo
En algunas ocasiones puede resultar interesante crear uno o varios scripts previos para ejecutar algunas de las operativas del proyecto: ejecución de test, ejecución de un análisis sintético, etc.. Estas operativas pueden ser desde un desarrollo simple a un desarrollo muy complejo en función de lo que se quiera hacer.
Como estos scripts puede que no tengan nada que ver con los Git Hooks lo podremos crear sobre el directorio "scripts/", que sería como una localización genérica para almacenar todo esto.
Ayuda
Se han dejado varios ejemplos de este tipo de scripting en el directorio scripts/
Pasos a seguir para crear un script:
- Definir la temática de cada script a implementar
- Verificar que existe el directorio scripts/ o similar en el proyecto
- Crear uno o varios scripts ejecutables en su interior -> Cuidado con el nombre elegido para cada uno de ellos
- Verificar que tienen los permisos de ejecución
- (Opcional) Probar su ejecución de forma aislada
Ejemplo: Shell Script que lanza la operativa de testing
# File: scripts/run-tests.sh
#!/bin/bash
echo "Running Test"
npm run test
status=$?
exit $status
Shell Script que lanza los tests del proyecto desde la opción de built-in de NPM. Los posibles parámetros utilizados se encontrarán definidos en el interior de la sección del fichero package.json
La salida del script se corresponderá con la salida de ejecución del comando ejecutado.
Una forma de ejecutarlo puede ser invocando al script ejecutable directamente.
Otra forma de ejecutar podría ser disponer de un operativa de built-in que llame al script externo
...
"scripts": {
...
"test:script": "./scripts/run-tests.sh",
...
},
...
Se podrá lanzar con el comando:
npm run test:script
También puede ser interesante unificar la funcionalidad compleja en un único script de forma autocontenida o bien referencia por composición, para poder definir el orden de ejecución y la respuesta para cada uno de los casos.
Ejemplo: Shell Script que lanza la operativa de validación solicitada: testing + lintado con Spectral
# File: scripts/run-pre-commit.sh
#!/bin/sh
# ### Configuration ###
# ### Execution ###
echo "[*] Running pre-commit script (script directory)"
# ***************
# Testing
# ***************
# Test Configuration
# Test Execution
echo "- Executing tests before commit"
npm run test
RESULT=$?
if [ "$RESULT" -ne 0 ]; then
echo "Failed execution of tests"
exit 1
fi
# ********************
# Spectral Lint
# ********************
echo "- Spectral OAS Lint"
npm run spectral:oas:lint:one
RESULT=$?
[ $RESULT -ne 0 ] && exit 1
exit 0
Se ha decidido para esta propuesta de script los siguientes aspectos:
- Que aparezcan mensajes del tipo "echo" para indicar los pasos que va realizando
- Que la ejecución de ambos aspectos (test unitarios y uso de Spectral) se realicen desde la ejecución de comando mediante : npm run xxx donde xxx es la fase definida dentro del fichero package.json
- Como alternativa se podría haber indicado que ejecutará cada uno de ellos desde otro script
- Que el resultado de cada fase se almacene en una variable y en caso de detectar un error se pare la ejecución
- Que para que el script tenga una ejecución correcta en su totalidad, la salida tiene que ser 0. Para que sea con error la salida debe de ser 1
Importante
Hay que tener en cuenta si quieres tener disponible alguna variable de entorno previa definida durante su ejecución.
...
"scripts": {
...
"pre-commit:script": "./scripts/run-pre-commit.sh"
...
},
...
Se podrá lanzar con el comando:
npm run pre-commit:script
3. Creación de un Git Hook
Escenario: "Creación de un Git Hook: Script ad-hoc"
En este escenario se tratará de aplicar lo aprendido haciendo uso de los pasos definidos en "Creación de un Git Hook: Script ad-hoc" del apartado "Construir un Git Hook" del primer artículo.
En esta implementación se puede ver la estructura del proyecto, la estructura del proyecto .git/hooks/ y que en su interior se ha creado el fichero "pre-commit" con los permisos adecuados. El contenido de este fichero es el mismo que el disponible en scripts/run-pre-commit.sh.
Una alternativa a esto, podría ser la de copiar directamente el script "run-pre-commit.sh" del directorio scripts/ con el nombre "pre-commit" en el directorio .git/hooks/ y los permisos adecuados. El comportamiento debería de ser igual que en el caso anterior. O bien hacer lo mismo con el fichero scripts/git/pre-commit que permitiría acelerar unos cuantos pasos de antes.
Escenario: "Creación de un Git Hook: Script enlazado"
En este escenario se tratará de aplicar lo aprendido haciendo uso de los pasos definidos en "Creación de un Git Hook: Script enlazado" del apartado "Construir un Git Hook" del primer artículo.
En esta implementación se puede ver la estructura del proyecto, la estructura del proyecto .git/hooks/ y que en su interior se ha creado el fichero "pre-commit" como un alias. El contenido de este fichero es el mismo que el del origen de su enlace simbólico.
ln -s -f ../../git-hooks/pre-commit .git/hooks/pre-commit
Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.
Escenario: "Creación de un Git Hook: Script enlazado con core.hookspath "
En este escenario se tratará de aplicar lo aprendido haciendo uso de los pasos definidos en "Creación de un Git Hook: Script enlazado con core.hookspath" del apartado "Construir un Git Hook" del primer artículo.
En esta implementación se puede ver la estructura del proyecto, la estructura del proyecto .git/hooks/ y que en su interior NO se ha creado el fichero "pre-commit" como un alias.
Se va a realizar un cambio en la adaptación de lectura de Git Hooks desde la configuración.
git config --global core.hookspath git-hooks
Verificar que aparece esta referencia en la configuracion ".gitconfig" de tu usuario.
git config --list --show-origin
Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.
Con estos ejemplos ya se podría comenzar a trabajar con los Git Hooks y estarían disponibles para que la persona los pudiera empezar a utilizar.
4. Preparación de la configuración
Este apartado se va a encargar de trabajar en la distribución y automatización de la configuración de Git Hooks en los proyectos.
Para ello, nos basaremos en las maneras de crear Git Hooks explicadas en el punto anterior y trataremos de automatizarlas de alguna manera para los diferentes escenarios que nos podremos encontrar.
Para más información acceder al mismo apartado del primer artículo.
Se van a plantear diferentes escenarios
Escenario: Script específico de Git Hooks
Características:
- Implementación en script externo implementado o no en el mismo lenguaje del proyecto
- Automatización de uno de los procesos:
- Creación de un Git Hook: Script ad-hoc
- Creación de un Git Hook: Script enlazado
- Creación de un Git Hook: Script enlazado con core.hookspath
- Ejecución bajo demanda
- Requiere documentación explicativa
- Districución con el proyecto y/o arquetipo
Ejemplo de script específico para "pre-commit" para implementación ad-hoc
Se define un script externo que se encuentra en un directorio concreto y que se ejecutará bajo petición. En este caso es un automatismo del proceso "Creación de un Git Hook: Script ad-hoc"
# File: scripts/git/prepare-githooks-pre-commit.script.sh
#!/bin/bash
# Execution
echo "Prepare 'pre-commit' Git Hook With Script"
cp ./run-adv-pre-commit.sh ../../.git/hooks/pre-commit
chmod +x ../../.git/hooks/pre-commit
echo "'pre-commit' Git Hook preparation successfully!"
exit 0
Ejecutar el script anterior
Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.
Ejemplo de script específico para "pre-commit" para implementación enlazada
Se define un script externo que se encuentra en un directorio concreto y que se ejecutará bajo petición. En este caso es un automatismo del proceso "Creación de un Git Hook: Script ad-hoc"
# File: scripts/git/prepare-githooks-symlinks.sh
#!/bin/bash
# ### Configuration ###
DEBUG=1
BASE_DIR=$(git rev-parse --show-toplevel)
HOOK_DIR="$BASE_DIR/.git/hooks"
CUSTOM_HOOK_DIR="$BASE_DIR/git-hooks"
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ "$DEBUG" -eq "1" ]; then
echo "Configuration"
echo " - PWD:$PWD"
echo " - Base Directory: $BASE_DIR"
echo " - Git Hook Directory: $HOOK_DIR"
echo " - Custom Git Hook Directory: $CUSTOM_HOOK_DIR"
echo " - Directory: $DIR"
echo " "
fi
# ### Execution ###
echo "Executing prepare ALL Git Hooks By Symlinks With Script"
# Symlink git hooks
for filename in ${CUSTOM_HOOK_DIR}/*; do
filename=$(basename ${filename})
echo " [*] Directory: $filename"
ln -s "$CUSTOM_HOOK_DIR/${filename}" "$HOOK_DIR/${filename}"
done
Ejecutar el script anterior
Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.
Escenario: Script general
Este caso es similar al anterior pero pudiendo hacer más cosas en el script o bien configurando más Git Hooks
Configuración específica de Git Hooks en el ciclo de desarrollo
Características:
- Implementación mediante secuencia de comandos o bien ejecución de un script externo
- Automatización de uno de los procesos:
- Creación de un Git Hook: Script ad-hoc
- Creación de un Git Hook: Script enlazado
- Creación de un Git Hook: Script enlazado con core.hookspath
- Incluido dentro del ciclo de desarrollo
- Existe alguna herramienta que facilitará su gestión
- Ejecución bajo demanda
- Requiere documentación explicativa
- Districución con el proyecto y/o arquetipo
Ejemplo: Built-in script de Node.js con comandos ad-hoc
Operación de built-in que automatiza el proceso "Creación de un Git Hook: Script ad-hoc". Se presentan dos formas de hacerse
"scripts": {
...
"prepare:githooks:pre-commit:adhoc": "cp ./scripts/git/pre-commit .git/hooks/ && chmod +x .git/hooks/pre-commit && echo 'pre-commit hook copied!'",
"prepare:githooks:pre-commit:script": "./scripts/git/prepare-githooks-pre-commit.npm.sh",
...
},
Ejecutar el built-in anterior
Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.
Ejemplo: Built-in script de Node.js con comandos de enlace
Operación de built-in que automatiza el proceso "Creación de un Git Hook: Script enlace". Se presentan dos formas de hacerse
"scripts": {
...
"prepare:githooks:link:all:adhoc": "for file in `ls git-hooks`; do ln -s -f ../../git-hooks/${file} .git/hooks/${file}; done",
"prepare:githooks:link:all:script": "./scripts/git/prepare-githooks-symlinks.sh",
...
},
Ejecutar el built-in anterior
Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.
Ejemplo: Built-in script de Node.js con comandos ad-hoc de configuracion core.hooksPath
Operación de built-in que automatiza el proceso "Creación de un Git Hook: Script enlazado con core.hookspath"
"scripts": {
...
"prepare:githooks:config": "git config core.hooksPath git-hooks",
...
},
Ejecutar el built-in anterior
Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.
Configuración estándar de Git Hooks en el ciclo de desarrollo
Características:
- Implementación mediante secuencia de comandos o bien ejecución de un script externo
- Automatización de uno de los procesos:
- Creación de un Git Hook: Script ad-hoc
- Creación de un Git Hook: Script enlazado
- Creación de un Git Hook: Script enlazado con core.hookspath
- Incluido dentro del ciclo de desarrollo
- Existe alguna herramienta que facilitará su gestión
- Ejecución con las fases del proyecto
- Requiere documentación explicativa
- Districución con el proyecto y/o arquetipo
Ejemplo: Built-in script de Node.js con uso postinstall
Operación de built-in que automatiza el proceso "Creación de un Git Hook: Script ad-hoc" y que se ejecutará como fase posterior a install.
"scripts": {
...
"postinstall": "./scripts/git/prepare-githooks-pre-commit.npm.sh",
...
},
Ejecutar el built-in anterior
Con este caso ya lo tendriamos configurado desde el inicio del proyecto
Fichero de configuración ".gitconfig"
Características:
- Implementación mediante secuencia de comandos o bien ejecución de un script externo
- Automatización de uno de los procesos:
- Creación de un Git Hook: Script ad-hoc
- Creación de un Git Hook: Script enlazado
- Creación de un Git Hook: Script enlazado con core.hookspath
- Requiere documentación explicativa
- Districución con el proyecto y/o arquetipo
Se hará uso de un fichero de configuración de Git, hay que tener cuidado de no excluirlo de las subidas de código en el ".gitignore"
Recordar tener la adaptación de la lectura de este fichero de configuración
git config --local include.path ../.gitconfig
Recursos
- Documentación sobre Git Hooks
- Documentación sobre Customización de Git Hooks
- Documentación
- Templates de Git
- Propuestas de pre-commit
- Awesome Git Hooks
Conclusiones
Una vez vista la parte práctica la cosa cambia bastante, aquí si qe se puede observar la potencia que puede tener su uso y la gran cantidad de beneficios que nos puede aportar con muy poco esfuerzo.
Una de las cosas que nos hará pensar más es la parte de la distribución pero en este artículo os he dado un par de ideas, aunque existen muchísimas más y que dependerán de vuestras necesidades a cubrir.
Espero que os haya ayudado un poco :-)
Un saludo