Herramientas de usuario

Herramientas del sitio


clase:daw:daw:tema07

¡Esta es una revisión vieja del documento!


7. GitHub: Actions y gh

Introducción

El despliegue continuo es desplegar las aplicaciones web lo más pronto posible: cada cambio que se sube al repositorio acaba en el servidor sin que nadie tenga que hacerlo a mano.

Para eso se usa un servidor de automatización: un programa que, cuando ocurre algo (un push a una rama, que alguien pulse un botón, una hora concreta), ejecuta solo una lista de pasos escrita de antemano. Los pasos son los que haríamos nosotros a mano:

  1. Descargar el código del repositorio.
  2. Compilarlo y pasar los tests.
  3. Construir la imagen Docker.
  4. Conectarse al servidor y arrancar la nueva versión.

Si un paso falla, se para ahí y avisa: lo que no compila o no pasa los tests no llega al servidor.

Los dos que vamos a ver son:

  • Jenkins: un programa que instalamos nosotros en un servidor nuestro (por ejemplo, en un contenedor Docker) y que vigila los repositorios.
  • GitHub Actions: viene incluido en GitHub. No hay que instalar nada: los pasos se escriben en un fichero dentro del propio repositorio y los ejecutan máquinas de GitHub.
Jenkins GitHub Actions
Dónde se ejecuta En un servidor nuestro En máquinas de GitHub
Hay que instalarlo y mantenerlo ✓
Dónde se escriben los pasos En Jenkins En el repositorio, en .github/workflows/
Sirve para repositorios fuera de GitHub ✓

Si el proyecto está en GitHub, usa GitHub Actions.

Contenedor con Jenkins

Jenkins es lo que se llama un "Servidor de automatización".

Hay abundante documentación sobre él , en los siguientes enlaces:

Aunque Jenkins pueda parecer sencillo o superfluo, usar Jenkins es el camino para hacer software de calidad.:

GitHub Actions

Un workflow es un fichero YAML dentro de la carpeta .github/workflows/ del repositorio. GitHub lee esa carpeta en cada push: no hay que darlo de alta en ningún sitio. El fichero tiene tres partes:

  • on: cuándo se ejecuta. push con branches cuando se sube algo a esa rama; workflow_dispatch añade un botón Run workflow en la pestaña Actions para lanzarlo a mano.
  • jobs: los trabajos que hay que hacer. Cada uno se ejecuta en un runner, una máquina virtual que GitHub crea para la ocasión y tira al acabar. runs-on: ubuntu-latest dice que sea un Ubuntu.
  • steps: los pasos de cada trabajo, en orden. Cada paso tiene un name y hace una de dos cosas:
    • run: órdenes de shell, como en una terminal. Con | se ponen varias líneas.
    • uses: una acción ya hecha por otros, como actions/checkout@v3, que descarga el código del repositorio en el runner.

Lo que no debe estar en el repositorio (contraseñas, claves SSH, IPs) se guarda como secreto en Settings → Secrets and variables → Actions y se usa en el workflow con $secrets.nombre. GitHub lo oculta en los logs.

El runner arranca vacío: no tiene nuestro código hasta que se hace actions/checkout, y no tiene nada instalado aparte de lo que trae Ubuntu, así que lo que haga falta se instala en un paso (como el apt-get install de abajo).
El runner está en Internet, no en el instituto: no ve la red del instituto. Por eso el ejemplo de abajo se conecta primero por VPN y luego entra por SSH al servidor.

Para desplegar el proyecto Java, hay que añadir al repositorio el fichero .github/workflows/desplegar.yml con el siguiente contenido:

name: Desplegar

on:
  workflow_dispatch:
  push:
    branches:
      - master

jobs:
  vpn_job:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repo
        uses: actions/checkout@v3
 

      - name: Configurar la VPN
        working-directory: .github/workflows
        run: |
          sudo apt-get update && sudo apt-get install -y openvpn openvpn-systemd-resolved
          echo "${{ secrets.VPN_CA }}" | base64 --decode > ca.crt
          echo "${{ secrets.VPN_USERNAME }}" > credenciales.txt
          echo "${{ secrets.VPN_PASSWORD }}" >> credenciales.txt
          sed -i "s/REPLACE_VPN_IP/${{ secrets.VPN_IP }}/" vpn-pve.conf
          sed -i "s/REPLACE_VPN_PORT/${{ secrets.VPN_PORT }}/" vpn-pve.conf 

      - name: Connect to VPN
        working-directory: .github/workflows      
        run: sudo openvpn --config vpn-pve.conf --daemon --log openvpn.log 

      - name: Wait for VPN to establish
        run: sleep 10

      - name: Test Connection
        working-directory: .github/workflows
        run: sudo cat openvpn.log 

      - name: Test Connection
        run: ping -c 2 192.168.59.104

      - name: Run remote script
        working-directory: .github/workflows      
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SERVER_ID_RSA }}
        run: |
          echo "$SSH_PRIVATE_KEY" > ./id_rsa
          chmod 600 ./id_rsa
          ssh -i ./id_rsa -o StrictHostKeyChecking=no -t ${{ secrets.SERVER_USERNAME }}@192.168.59.104 'sudo rm -rf DesplegarFacturasDocker'        
          ssh -i ./id_rsa -o StrictHostKeyChecking=no -t  ${{ secrets.SERVER_USERNAME }}@192.168.59.104 'sudo git clone https://github.com/lgonzalezmislata/DesplegarFacturasDocker.git'
          ssh -i ./id_rsa -o StrictHostKeyChecking=no -t  ${{ secrets.SERVER_USERNAME }}@192.168.59.104 'sudo chmod 777 -R ./DesplegarFacturasDocker'
          ssh -i ./id_rsa -o StrictHostKeyChecking=no -t  ${{ secrets.SERVER_USERNAME }}@192.168.59.104 'cd DesplegarFacturasDocker && sudo ./deploy.sh 2>&1'

Ahora al hacer cualquier cambio en la rama master se ejecutará nuestro script de despliegue.

Cada ejecución sale en la pestaña Actions del repositorio, con cada paso en verde o en rojo y su log. Si un paso falla, los siguientes no se ejecutan.

Puedes ver un ejemplo en https://github.com/lgonzalezmislata/Facturas/tree/master/.github/workflows

GitHub CLI

gh es la herramienta de línea de órdenes de GitHub: hace desde la terminal lo que se hace en la web de GitHub. No es para el repositorio: commits, ramas, push y pull siguen siendo cosa de git. gh trabaja con lo que GitHub pone alrededor del repositorio:

  • gh issue: crear, listar y ver los issues.
  • gh project: el tablero y sus tarjetas.
  • gh run y gh workflow: los workflows de GitHub Actions.

Se instala con:

  • Windows: winget install GitHub.cli (y reabrir la terminal).
  • Linux: sudo apt install gh.
  • Mac: brew install gh.

gh auth login                  # GitHub.com, HTTPS, navegador. Guarda también la credencial de git
gh auth status                 # «Logged in to github.com»
gh auth refresh -s project     # permiso para mover las tarjetas

Las órdenes de gh se lanzan dentro de la carpeta del repositorio (o de un worktree): de ahí saca de qué repositorio de GitHub se habla.

Más información:

Issues

GitHub ya trae la etiqueta bug en cada repositorio; feature se crea una vez:

gh label create feature

  • Crear un issue. Responde con su URL, que acaba en su número:

gh issue create --title "Alta de clientes" --body "Formulario de alta con nombre, NIF y email." --label feature

  • Listar los issues abiertos:

gh issue list

  • Ver el título, la descripción y el estado del issue 12:

gh issue view 12

  • Ponerle una etiqueta:

gh issue edit 12 --add-label bug

  • Cerrarlo:

gh issue close 12

La etiqueta tiene que existir en el repositorio: con una que no existe, gh issue create falla y no crea el issue.

Workflows

  • Lanzar a mano un workflow que tiene workflow_dispatch, como el desplegar.yml de arriba:

gh workflow run desplegar.yml

  • Seguir en directo el workflow que se acaba de lanzar:

gh run watch

  • Ver las últimas ejecuciones, con su id:

gh run list --limit 5

  • Ver solo los pasos que han fallado de una ejecución:

gh run view 123456 --log-failed

Un workflow rojo tiene un log largo y la causa está en tres líneas: --log-failed las da sin tener que buscarlas en la web.

GitHub Projects

Un Project es un tablero de GitHub con los issues del repositorio como tarjetas, repartidas en columnas por su estado: Todo, In Progress y Done. Está en GitHub, en el menú Projects.

El tablero con un issue en cada columna

Crear el tablero

Uno por proyecto, desde el menú + de arriba a la derecha de GitHub → New project:

  • En Start from scratch, la plantilla Board.

Plantilla Board

  • El nombre del proyecto y, marcado, Import items from repository con el repositorio del proyecto. Así el Project queda vinculado al repositorio, que es de donde sacan su número los scripts, y los issues abiertos entran en el tablero. Create project.

Nombre e importar los issues del repositorio

  • Sale con las tres columnas, Todo, In Progress y Done, las que usan los scripts.

El tablero recién creado

Un solo Project vinculado al repositorio. GitHub crea un untitled project vinculado con solo pulsar la pestaña Projects del repositorio, y con dos los scripts usan el primero que devuelve GitHub sin avisar: las tarjetas se mueven en el tablero que no es. Los que sobran se desvinculan en la pestaña Projects del repositorio.

Scripts para los issues

Mover una tarjeta con gh project necesita varios ids (el del Project, el del campo Status, el de la columna y el de la tarjeta), así que se han creado 3 scripts que lo hacen. Van en la carpeta scripts/ del proyecto. El número del Project no se configura en ningún sitio: los tres le preguntan a GitHub cuál está vinculado al repositorio. Por eso se vincula al crearlo (arriba). Por lo mismo, se lanzan desde la carpeta del proyecto o un worktree: fuera de él no saben de qué repositorio hablan.

  • Crear un issue: lo crea, lo añade al Project (en Todo) y responde con su número. La etiqueta, si no se da, es feature.

#!/bin/bash
# Uso: issue_create.sh "título" "descripción" [feature|bug]
PROJECT=$(gh api graphql -F owner='{owner}' -F repo='{repo}' -f query='query($owner:String!,$repo:String!){repository(owner:$owner,name:$repo){projectsV2(first:1){nodes{number}}}}' --jq '.data.repository.projectsV2.nodes[0].number')

URL=$(gh issue create --title "$1" --body "$2" --label "${3:-feature}")
gh project item-add $PROJECT --owner @me --url $URL > /dev/null 2>&1    # si ya lo ha añadido GitHub, da igual
echo $URL | grep -o '[0-9]*$'

scripts/issue_create.sh "Alta de clientes" "Formulario de alta con nombre, NIF y email."
scripts/issue_create.sh "El total del carrito no suma los gastos de envío" "Con un pedido de 30 € sale 30 € en vez de 34,95 €." bug


  • Empezar un issue: pasa su tarjeta a In Progress.

#!/bin/bash
# Uso: issue_in_progress.sh N
PROJECT=$(gh api graphql -F owner='{owner}' -F repo='{repo}' -f query='query($owner:String!,$repo:String!){repository(owner:$owner,name:$repo){projectsV2(first:1){nodes{number}}}}' --jq '.data.repository.projectsV2.nodes[0].number')
N=$1

PROJECT_ID=$(gh project view $PROJECT --owner @me --format json --jq .id)
CAMPO=$(gh project field-list $PROJECT --owner @me --format json --jq '.fields[] | select(.name=="Status") | .id')
OPCION=$(gh project field-list $PROJECT --owner @me --format json --jq '.fields[] | select(.name=="Status") | .options[] | select(.name=="In Progress") | .id')
TARJETA=$(gh project item-list $PROJECT --owner @me --limit 1000 --format json --jq ".items[] | select(.content.number==$N) | .id")

if [ -z "$TARJETA" ]; then echo "El issue #$N no está en el Project"; exit 1; fi

gh project item-edit --project-id $PROJECT_ID --field-id $CAMPO --single-select-option-id $OPCION --id $TARJETA > /dev/null || { echo "No se ha podido mover la tarjeta del issue #$N"; exit 1; }
echo "Issue #$N en In Progress"

scripts/issue_in_progress.sh 12


  • Cerrar un issue: lo cierra y pasa su tarjeta a Done.

#!/bin/bash
# Uso: issue_close.sh N
PROJECT=$(gh api graphql -F owner='{owner}' -F repo='{repo}' -f query='query($owner:String!,$repo:String!){repository(owner:$owner,name:$repo){projectsV2(first:1){nodes{number}}}}' --jq '.data.repository.projectsV2.nodes[0].number')
N=$1

PROJECT_ID=$(gh project view $PROJECT --owner @me --format json --jq .id)
CAMPO=$(gh project field-list $PROJECT --owner @me --format json --jq '.fields[] | select(.name=="Status") | .id')
OPCION=$(gh project field-list $PROJECT --owner @me --format json --jq '.fields[] | select(.name=="Status") | .options[] | select(.name=="Done") | .id')
TARJETA=$(gh project item-list $PROJECT --owner @me --limit 1000 --format json --jq ".items[] | select(.content.number==$N) | .id")

if [ -z "$TARJETA" ]; then echo "El issue #$N no está en el Project"; exit 1; fi

gh issue close $N > /dev/null 2>&1
gh project item-edit --project-id $PROJECT_ID --field-id $CAMPO --single-select-option-id $OPCION --id $TARJETA > /dev/null || { echo "No se ha podido mover la tarjeta del issue #$N"; exit 1; }
echo "Issue #$N cerrado y en Done"

scripts/issue_close.sh 12

Ejercicios

Ejercicio 1

Añade al repositorio de tu código fuente de Java una acción de GitHhub Actions que sea para desplegar.

Se ejecutará:

  • Manualmente
  • Cuando haya un cambio en master

Ejercicio 2

  • Instala gh y autentícate. Comprueba con gh auth status que está el permiso project.
  • En tu repositorio de Java, crea la etiqueta feature y un issue «Desplegar con GitHub Actions» con la etiqueta feature. Comprueba con gh issue list que sale.
  • Lanza a mano el workflow del ejercicio 1 con gh workflow run y síguelo con gh run watch.

Ejercicio 3

  • Crea el tablero del repositorio como en Crear el tablero y comprueba en la pestaña Projects que solo hay un Project vinculado.
  • Copia los tres scripts en la carpeta scripts/ y dales permiso de ejecución.
  • Crea un issue con issue_create.sh, pásalo a In Progress con issue_in_progress.sh y ciérralo con issue_close.sh. Después de cada paso, mira en el tablero que la tarjeta está en su columna.
clase/daw/daw/tema07.1793179011.txt.gz · Última modificación: por claude