Uso de Git Hooks para proyectos (Parte 2)

Publicado por Víctor Madrid el

Arquitectura de SolucionesGit HooksGitAceleradores de DesarrolloAutomatización

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.

cabecera - Enmilocalfunciona - git - 1400x400px.jpg

A modo de recordatorio pongo los enlaces a los artículos anteriores :

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:

  1. Definir las necesidades del Git Hook
  2. Implementar un fichero de script específico (opcional)
  3. Creación de un Git Hook
  4. 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:

  1. Crear si no existen el directorio "scripts/" y el directorio "git-hooks/" en el repositorio a nivel del proyecto
  2. 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:

  1. Definir la temática de cada script a implementar
  2. Verificar que existe el directorio scripts/ o similar en el proyecto
  3. Crear uno o varios scripts ejecutables en su interior -> Cuidado con el nombre elegido para cada uno de ellos
  4. Verificar que tienen los permisos de ejecución
  5. (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.

Screenshot-2023-08-18-at-12.55.20

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

Screenshot-2023-08-18-at-13.01.27

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

Screenshot-2023-08-18-at-13.20.43

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

Screenshot-2023-08-18-at-13.27.08

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.

Screenshot-2023-08-07-at-15.55.53

Screenshot-2023-08-07-at-15.52.45

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

Screenshot-2023-08-20-at-11.16.37

Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.

Screenshot-2023-08-20-at-11.18.33

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.

Screenshot-2023-08-20-at-11.21.26

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.

Screenshot-2023-08-20-at-11.24.41

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

Screenshot-2023-08-20-at-11.40.53

Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.

Screenshot-2023-08-20-at-11.40.32

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

Screenshot-2023-08-20-at-12.00.42

Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.

Screenshot-2023-08-20-at-11.40.32

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

Screenshot-2023-08-20-at-11.49.13

Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.

Screenshot-2023-08-20-at-11.50.02

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

Screenshot-2023-08-20-at-11.49.13

Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.

Screenshot-2023-08-20-at-11.50.02

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

Screenshot-2023-08-20-at-11.53.02

Tras la realización de una operativa de commit para tratar de subir código se puede ver que se ejecutan los aspectos considerados.

Screenshot-2023-08-20-at-11.53.58

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

Screenshot-2023-08-20-at-12.09.43

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"

Screenshot-2023-08-20-at-12.15.08

Recordar tener la adaptación de la lectura de este fichero de configuración

git config --local include.path ../.gitconfig

Recursos

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

Autor

Víctor Madrid

Líder Técnico de la Comunidad de Arquitectura de Soluciones en atSistemas. Aprendiz de mucho y maestro de nada. Técnico, artista y polifacético a partes iguales ;-)