Construye, testea y publica tu aplicación Java con Github Actions

0
2739
header picture from post

Índice

  1. Introducción
  2. Creando el Workflow
  3. Github Packages

Introducción

La idea de este post es crear un flujo de integración continua que ejecute los tests y haga un deploy de nuestro paquete en el registry de Github Packages.

Si estás empezando con Github Actions, te recomiendo leer antes el tutorial [Introducción a Github Actions. Sintaxis básica.]

El proyecto:

  • Spring boot con maven.
  • Base de datos postgres con una tabla users.
    postgres console
  • Un endpoint /users que devuelve una lista de usuarios. Actualmente solo tengo el usuario autentia con ID 1. Mi test de integración comprobará que el endpoint me devuelve una lista de usuarios de longitud 1.
    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        private final UserService userService;
    
        @Autowired
        public UserController(UserService userService) {
            this.userService = userService;
        }
    
        @GetMapping
        public List<User> findAll() {
            return userService.findAll();
        }
    }
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    public class UserEndpointIT {
        @Autowired
        private TestRestTemplate restTemplate;
    
        @Autowired
        protected DatabaseService databaseService;
    
        @LocalServerPort
        private int port;
    
        @BeforeAll
        public void initDatabase(){
            databaseService.fill();
        }
    
        @Test
        public void user_endpoint_should_return_all_users() {
            HttpHeaders headers = new HttpHeaders();
            HttpEntity entity = new HttpEntity(headers);
    
            ResponseEntity<List<User>> response = this.restTemplate.exchange(createUrlWith("/users"), HttpMethod.GET, entity, new ParameterizedTypeReference<List<User>>() {
            });
            int EXPECTED_USERS = 1;
    
            assertEquals(EXPECTED_USERS, response.getBody().size());
        }
    
        private String createUrlWith(String endpoint) {
            return "http://localhost:" + port + endpoint;
        }
    }
  • Flyway como herramienta de migración de base de datos.
  • El plugin de maven docker-maven-plugin en el pom.xml para levantar mi contenedor de postgres en los tests de integración.
...
<plugin>
   <groupId>io.fabric8</groupId>
   <artifactId>docker-maven-plugin</artifactId>
   <version>${docker-maven-plugin.version}</version>
   <configuration>
      <images>
         <image>
            <name>${it.postgresql.image}</name>
            <run>
               <ports>
                  <port>${it.postgresql.port}:5432</port>
               </ports>
               <env>
                  <POSTGRES_USER>${it.postgresql.username}</POSTGRES_USER>
                  <POSTGRES_PASSWORD>${it.postgresql.password}</POSTGRES_PASSWORD>
                  <POSTGRES_DB>${it.postgresql.db}</POSTGRES_DB>
               </env>
               <wait>
                  <log>database system is ready to accept connections</log>
               </wait>
            </run>
         </image>
      </images>
   </configuration>
   <executions>
      <execution>
         <id>start</id>
         <phase>pre-integration-test</phase>
         <goals>
            <goal>start</goal>
         </goals>
      </execution>
      <execution>
         <id>stop</id>
         <phase>post-integration-test</phase>
         <goals>
            <goal>stop</goal>
         </goals>
      </execution>
   </executions>
</plugin>
...

Github actions nos permite la Integración Continua (CI) y el Despliegue Continuo (CD) desde nuestros repositorios de Github.
¿Cómo funciona? un evento ejecuta un workflow, por ejemplo, un push a la rama master o un pull request, y ese workflow a su vez puede tener distintos jobs. Podemos tener un job para ejecutar nuestros tests y otro job para publicar la aplicación. Cada job utiliza steps que tiene diferentes tareas, más conocidas como actions. Podemos crear nuestras propias actions (por aquí te dejo este post) o utilizar las creadas por la comunidad. También podemos nombrar cada acción y dar una breve descripción de lo que hace usando la key name. Es importante saber que por defecto, los jobs se ejecutan en paralelo a menos que los configuremos para que se ejecuten de forma secuencial (cuando un job depende de otro).

workflow structure

Creando el workflow

En primer lugar, debemos crear nuestro workflow en .github/workflows.

folder structure

name: my first workflow for a java project
on:
  push:
    branches: [ master ]
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-java@v1
        with:
          java-version: 11
          settings-path: ".m2/"
      - uses: actions/cache@v2
        with:
          path: ~/.m2/repository
          key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }}
      - name: Publish package
        run:  mvn $MAVEN_CLI_OPTS clean deploy
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
      - name: Copying target jar
        run: |
          mkdir myTarget
          cp target/*.jar myTarget
      - name: Uploading jar
        uses: actions/upload-artifact@v2
        with:
          name: myPackage
          path: myTarget

Comenzamos añadiendo un nombre al workflow y configurando el evento que lo lanzará. En este caso, el evento es un push a la rama master. Ahora necesitamos configurar los jobs para el workflow. El runs-on configura el job para que se ejecute en una máquina virtual de Github, en este caso, un runner de Ubuntu Linux (mas información aquí). En el ejemplo anterior, solo tengo un job publish que realiza los siguientes pasos:

  • La acción de checkout descarga una copia del repositorio en el runner.
  • La acción de setup configura el entorno Java. La key with nos permite especificar las variables de entrada de dicha ación. En el ejemplo especificamos la version del JDK y settings-path nos permite cambiar la ubicación del fichero settings.xml
  • Teniendo en cuenta que los jobs que se ejecutan en los runner se lanzan siempre en un entorno virtual nuevo, nos gustaría cachear ciertos ficheros o dependencias que no suelen cambiar con frecuencia para no tener que descargarlos cada vez que se lanza la acción. En el ejemplo anterior, estoy almacenando en caché todos las dependencias del repositorio m2. Es importante establecer una key para que la acción intente recuperar los archivos de caché en función de si el hash ha cambiado. Para crear la key, estoy usando la variable runner.os que me indica dónde se está ejecutando el job actual y la estoy concatenando con la función hashfile que devuelve un hash de los archivos especificados (más información aquí). He estado leyendo sobre cómo hacer un clean caché desde la interfaz sin tener que modificar la key del workflow directamente, y día de hoy, hay una issue abierta en github sobre esto. Hay un workaround y es usar secrets. Cada vez que actualicemos nuestro secret desde la interfaz, el workflow desechará la caché anterior y creará una nueva entrada. Otro punto a tener en cuenta es que GitHub eliminará cualquier entrada de caché a la que no se haya accedido en 7 días y el tamaño de caché está limitado a 5Gb.
...
- uses: actions/cache@v2
   with:
       path: ~/.m2/repository
       key: ${{ secrets.CACHE_VERSION }}
...

secret tab from github

  • Voy a publicar el proyecto en Github Packages y para ello ejecuto mvn clean deployEs importante saber que la acción actions/setup-java@v1 crea automáticamente el fichero settings.xml con la autenticación del servidor como se ve a continuación. Para poder usar GITHUB_TOKEN, necesitamos referenciarlo en nuestro workflow, por eso lo vemos como variable de entrada en la acción Publish package. En caso de no hacerlo, obtendremos un 401 Unauthorized.
<servers>
    <server>
        <id>github</id>
        <username>${{ secrets.GITHUB_ACTOR }}</username>
        <password>${{ secrets.GITHUB_TOKEN }}</password>
    </server>
</servers>

El servidor creado tendrá un id github, la variable de entorno GITHUB_ACTOR es nuestro nombre de usuario y la variable de entorno GITHUB_TOKEN es nuestra contraseña (estas variables de entorno existen en nuestro repositorio de github por defecto). En el pom.xml, solo necesito añadir la configuración del repositorio donde se va a subir.

...
<distributionManagement>
    <repository>
        <id>github</id>
        <name>github packages</name>
        <url>https://maven.pkg.github.com//[your_github_username]/[your_repository_name]</url>
    </repository>
</distributionManagement>
...

Si hacemos un push, en la parte derecha de nuestro repositorio podemos encontrar el paquete subido.

package button github

Si hacemos click, podemos ver la dependencia.

dependency in github

  • Uno de los últimos pasos es subir el artefacto. Recordemos que un artefacto es un archivo o una colección de archivos generados durante la ejecución de un workflow. Lo primero es crear la carpeta myTarget y copiar el jar. La última acción upload-artifact@v2 recibe dos parámetros de entrada, el nombre del artefacto y los archivos que se subirán.

generated artifact

Si descargamos myPackage, y descomprimimos el zip, vemos el jar.

artifact file

Cuando hacemos el push a master, en la pestaña Actions podremos ver el workflow en ejecución. Si todo va bien y no hay ningún error, nos saldrá un tick en verde.

ran workflow

Cuando hacemos click en el workflow ejecutado, podemos ver el listado de jobs junto con las acciones que ha ejecutado. En mi ejemplo solo vemos un job que es publish.

listed actions

Si comprobamos los logs de la acción publish package, podemos ver cómo el contenedor de postgres se levanta correctamente al igual que el contexto de spring. Flyway realiza las migraciones pertinentes en base de datos y posteriormente se lanzan los tests.

postgres container started log

postgres docker container stop logspring context up and test passed

Github Packages

Github packages es un servicio de hosting que nos permite alojar nuestros paquetes de forma privada o pública y usarlos como dependencias en otros proyectos. Como vimos en la sección anterior, hemos subido el proyecto como dependencia a Github Packages. Pero, ¿cómo puedo instalar una dependencia de maven almacenada en el registry de github packages?. Además de añadir la dependencia en el pom.xml a <dependencies> como es lógico, debemos seguir los siguientes pasos:

  • En nuestro pom.xml, necesitamos apuntar al repositorio de github donde está la dependencia que queremos instalar.
...
<repositories>
    <repository>
        <id>github</id>
        <url>https://maven.pkg.github.com/[github_username]/[repository_name]</url>
    </repository>
</repositories>
...
  • El registry de github packages está disponible a través de la api de GitHub y necesita autorización, por lo que debemos añadir nuestras credenciales de github en el settings.xml. En mi caso las he añadido en mi settings global (~/.m2/settings.xml)
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                          https://maven.apache.org/xsd/settings-1.0.0.xsd">

    <servers>
        <server>
            <id>github</id>
            <username>[your_github_username]</username>
            <password>[your_github_token]</password>
        </server>
    </servers>

</settings>

Para generar un token, podemos hacerlo desde Settings > Developer Settings > Personal access tokens y activar el permiso read:packages.

Una vez realizados estos pasos y si todo ha ido bien, la dependencia se instalará sin ningún problema 😃.

 

 

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad