Alejandro Hernández
Scala & Tooling

15 de enero de 2022 ·

Actualizando múltiples repositorios con Scala Steward y GitHub Actions

Si trabajas habitualmente con Scala, probablemente conozcas Scala Steward. Si no sabes lo que es, este extracto del propio repositorio lo resume bastante bien:

"Scala Steward is a bot that helps you keep your library dependencies, sbt plugins, and Scala and sbt versions up-to-date."

Scala Steward es un bot que te ayuda a mantener las dependencias de tus librerías, los plugins de SBT y las versiones de Scala y SBT actualizadas.

Se puede utilizar en cualquier proyecto público de Scala hospedado en GitHub, GitLab o BitBucket que utilice SBT o Mill simplemente añadiendo dicho repositorio a este archivo. Poco tiempo después empezarás a recibir Pull Requests.

Hay ya más de 1500 repositorios empleando esta instancia pública de Scala Steward que Frank Thomas (el creador de Scala Steward) tiene desplegado como un servicio gratuito para todo el ecosistema Open-Source de Scala.

Pero las posibilidades no se quedan ahí, también se puede lanzar una instancia propia usando GitHub Actions, Docker o incluso Coursier:

cs launch --contrib scala-steward

El infierno de las actualizaciones ™️

Seguramente, si trabajas en una organización con múltiples repositorios y con Scala Steward a cargo de mantener todo actualizado, tu equipo habrá descubierto el conocido como "infierno de las actualizaciones". ¿Y qué es esto? Pues no es otra cosa que empezar tu jornada laboral teniendo que revisar, aprobar y mergear cientos de Pull Request de actualizaciones creadas por Scala Steward.

Proporcionado por GIPHY

Si ya has echado un vistazo a las FAQ de Scala Steward, habrás visto que una opción para gestionar esto es mergear automáticamente dichas actualizaciones usando apps como Mergify, GitHub Actions como esta o habilitando el auto-merge en dichas PR.

Aunque el auto-mergeo puede ser una opción muy válida, es probable que, como yo, te hayas encontrado que no lo es tanto para tu caso. Bien porque tu organización no permita el mergeo automático de PR, o quizá porque tus PR tienen que seguir un flujo concreto que impide que lleguen a la rama por defecto de tu repositorio (comúnmente main o master).

Para estos casos, te traigo una solución que (al menos en mi equipo) está funcionando muy bien. Cómo resumen te diré que consiste en instruir a Scala Steward a que actualice una rama distinta a la de por defecto de forma automática (la típica rama develop, si sigues GitFlow), y crear una PR desde esa rama a tu rama por defecto cada cierto tiempo. Ahora bien, ¿cómo hacemos eso? pues con GitHub Actions.

Scala Steward, olvídate del main

El primer paso para huir del infierno de las actualizaciones es decirle a Scala Steward que en vez de actualizar la rama por defecto de nuestro repositorio, actualice una rama distinta. Para ello, tenemos dos opciones, dependiendo de como estemos lanzando Scala Steward.

Proporcionado por GIPHY
Si estás usando repos.json

Localiza la línea correspondiente a tu repositorio y añade :rama detrás.

- miorganizacion/mirepo:develop
Si estás usando la GitHub Action de Scala Steward

Añade un nuevo parámetro branches a la acción con el nombre de la rama a actualizar.

- name: Launch Scala Steward
  uses: scala-steward-org/scala-steward-action@v2
  with:
    github-token: ${{ github.token }}
    branches: develop

¡Y ya está! No necesitas hacer nada más para este paso.

Colega, ¿dónde está mi rama?

Una vez terminado el paso anterior, Scala Steward empezará a enviar PR actualizando esa rama que le hemos indicado, en vez de la rama por defecto del repositorio. El problema está en sí, como es lógico, dicha rama no existe. Para que todo esto funcione, necesitamos asegurarnos de dos cosas:

Proporcionado por GIPHY

Pues venga, manos a la obra. Vamos a crear un workflow de GitHub Actions que se encargue de hacer el upsert de la rama develop.

Si no sabes como funciona la sintaxis de GitHub Actions, aquí tienes toda la documentación necesaria para aprender a utilizarla (en inglés).

Para empezar, creamos un archivo upsert-develop-branch.yml en la carpeta .github/workflows de nuestro proyecto. Dentro escribiremos el esqueleto de un workflow que reaccione a actualizaciones de la rama main (o a como se llame tu rama por defecto).

name: Upsert `develop` branch

on:
  push:
    branches: main

jobs:
  upsert-develop-branch:
    runs-on: ubuntu-latest
    name: Create `develop` branch or rebase it
    steps:
      - run: echo "Upsert develop branch"

Ahora añadiremos los pasos.

El primer paso consistirá en asegurarnos que la rama develop existe. Para ello vamos a usar la CLI de GitHub (gh).

- name: Create `develop` branch (if it does not exists)
  env:
    GITHUB_TOKEN: ${{ github.token }}
  run: |
      gh api --silent \
          /repos/${{ github.repository }}/git/refs \
          -f ref="refs/heads/develop" \
          -f sha="${{ github.sha}}" ||
          echo '`develop` branch already exists on ${{ github.repository }}'

Creamos una referencia a la rama develop en el mismo punto (SHA) en el que esté la rama main (que vendrá en el contexto github.sha). Si el comando fallase, significa que la rama develop ya existe, por lo que avisamos al usuario por pantalla para evitar que el workflow entero falle.

gh nos permite hacer múltiples operaciones en GitHub desde la consola, de una forma sencilla y concisa. Si quieres saber más sobre los distintos comandos, te animo a que consultes su documentación (en inglés).

El siguiente paso será hacer checkout de la rama develop. Debemos añadir fetch-depth: 0 para evitar que el workflow haga un shallow-clone.

- name: Checkout develop branch
  uses: actions/checkout@v2
  with:
    ref: develop
    fetch-depth: 0

Por último, añadimos el paso que se encarga de hacer el rebase de la rama develop. Si el primer paso creó una rama nueva, este paso no hará nada; si ya existía previamente, este paso la rebaseará a la última posición de main.

- name: Rebase `develop` branch to latest `origin/main`
  run: |
    git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
    git config user.name "github-actions[bot]"
    git rebase origin/main
    git push -f -u origin develop
Necesitamos establecer las opciones de user.email y user.name de Git puesto que estas no se inicializan cuando se usa la acción de checkout.

¡Y ya está! Ya tenemos nuestro primer workflow terminado. Aquí tienes el código completo:

.github/workflows/upsert-develop-branch.yml
name: Upsert `develop` branch

on:
  push:
    branches: main

jobs:
  upsert-develop-branch:
    runs-on: ubuntu-latest
    name: Create `develop` branch or rebase it to latest `main`
    steps:
      - name: Create `develop` branch (if it does not exists)
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: |
            gh api --silent \
                /repos/${{ github.repository }}/git/refs \
                -f ref="refs/heads/develop" \
                -f sha="${{ github.sha}}" ||
                echo '`develop` branch already exists on ${{ github.repository }}'
      - name: Checkout develop branch
        uses: actions/checkout@v2
        with:
          ref: develop
          fetch-depth: 0

      - name: Rebase `develop` branch to latest `origin/main`
        run: |
            git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
            git config user.name "github-actions[bot]"
            git rebase origin/main
            git push -f -u origin develop

Una vez añadamos este archivo a nuestro repositorio cualquier cambio en la rama main provocará que la rama develop se cree o actualice.

A partir de este punto, podemos empezar a recibir PR de Scala Steward a la rama develop. Únicamente necesitaremos que dichas PR se mergeen de forma automática. Para ello, como ya se mencionó en la primera sección, podemos usar apps como Mergify, GitHub Actions como esta o habilitar el auto-merge en dichas PR usando un nuevo workflow, lo dejo a tu elección.

Proporcionado por Giphy

El día de la actualización

Llegados a esté punto, tendremos nuestra rama develop cargada de actualizaciones. Así que lo único que falta es asegurarnos de que cada cierto tiempo, se cree una PR a nuestra rama por defecto.

¿Y cómo podemos hacer eso? ¡Pues claro! Otra vez usando GitHub Actions. ¡Al lío!

Primero de todo, igual que antes, crearemos un archivo scheduled-updates-pr.yml en la carpeta .github/workflows de nuestro proyecto. La diferencia con el workflow que escribimos en el paso anterior estará en que en vez de reaccionar a actualizaciones de main haremos que este workflow se lance una y otra vez, en los intervalos que designemos usando el evento schedule.

En el ejemplo se establece que el workflow se lance semanalmente. Si quieres cambiar esta programación puedes servirte de esta página para calcular tu comando CRON.
name: Create PR from `develop` to `main`

on:
  schedule:
    - cron: "0 0 * * MON"

jobs:
  create-develop-pr:
    runs-on: ubuntu-latest
    name: Create PR from `develop` to `main`
    steps:
      - run: echo "Create develop PR"

Este workflow arrancará todos los lunes a las 00:00.

Proporcionado por Giphy

Y ahora vamos a por la implementación. Consistirá en un único paso que hará lo siguiente:

- name: Create Pull Request
  run: |
    develop=$(gh api /repos/$GITHUB_REPOSITORY/commits/refs/heads/develop -q '.sha')
    main=$(gh api /repos/$GITHUB_REPOSITORY/commits/refs/heads/main -q '.sha')

    if [[ $develop != $main ]]; then
        gh api /repos/$GITHUB_REPOSITORY/pulls \
          -f title="Scala Steward Updates" \
          -f base=main \
          -f head=develop
    else
        echo "There are no updates"
    fi
  env:
    GITHUB_TOKEN: ${{ github.token }}

¡Hecho! Nuestro nuevo workflow no necesita nada más. Aquí tienes la versión completa:

.github/workflows/scheduled-updates-pr.yml
name: Create PR from `develop` to `main`

on:
  schedule:
    - cron: "0 0 * * MON"

jobs:
  create-develop-pr:
    runs-on: ubuntu-latest
    name: Create PR from `develop` to `main`
    steps:
      - name: Create Pull Request
        run: |
          develop=$(gh api /repos/$GITHUB_REPOSITORY/commits/refs/heads/develop -q '.sha')
          main=$(gh api /repos/$GITHUB_REPOSITORY/commits/refs/heads/main -q '.sha')

          if [[ $develop != $main ]]; then
              gh api /repos/$GITHUB_REPOSITORY/pulls \
                -f title="Scala Steward Updates" \
                -f base=main \
                -f head=develop
          else
              echo "There are no updates"
          fi
        env:
          GITHUB_TOKEN: ${{ github.token }}

Y... ¡listo! Con estos dos workflows que acabamos de crear ya tenemos todo lo necesario para evitar "El infierno de las actualizaciones ™️". Podemos replicar esto mismo en todos los repositorios Scala de nuestra organización y mantenerlos actualizados sin demasiado esfuerzo.

Te dejo aquí algunas mejoras que puedes implementar por si te apetece cacharrear con GitHub Actions:

¡Gracias y hasta la próxima!

¿Te ha gustado el artículo?
Si quieres, puedes invitarme a un café

¿Has encontrado una errata? ¡Envíame una PR!