Herramientas de usuario

Herramientas del sitio


clase:daw:daw:tema07

Diferencias

Muestra las diferencias entre dos versiones de la página.

Enlace a la vista de comparación

Ambos lados, revisión anteriorRevisión previa
Próxima revisión
Revisión previa
clase:daw:daw:tema07 [2026/10/28 09:57] – borrado - editor externo (Fecha desconocida) 127.0.0.1clase:daw:daw:tema07 [2026/10/28 10:18] (actual) – Quitar la sección «Contenedor con Jenkins»: el concepto ya está en la introducción claude
Línea 1: Línea 1:
 +====== 7. GitHub: Actions y gh ======
 +
 +
 +===== Introducción =====
 +
 +  * [[https://blog.develat.io/pipelines-ci-cd/|Integración continua y despliegue continuo]]
 +  * [[https://devopsti.wordpress.com/2014/09/26/integracion-continua-ci-entrega-continua-cd-y-despliegue-continuo-cd/|Integración continua (CI), Entrega continua (CD) y Despliegue Continuo (CD)]]
 +  * [[https://geeks.ms/jorge/2019/02/25/integracion-entrega-y-despliegue-continuo-diferencias-y-similitudes/|Integración, entrega y despliegue continuo. Diferencias y similitudes]]
 +
 +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:
 +
 +  - Descargar el código del repositorio.
 +  - Compilarlo y pasar los tests.
 +  - Construir la imagen Docker.
 +  - 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:
 +  * [[https://www.jenkins.io/|Jenkins]]: un programa que instalamos nosotros en un servidor nuestro (por ejemplo, en un contenedor Docker) y que vigila los repositorios.
 +  * [[https://github.com/features/actions|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.
 +
 +
 +===== 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.
 +
 +<note important>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).</note>
 +
 +<note important>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.</note>
 +
 +Para desplegar el proyecto Java, hay que añadir al repositorio el fichero ''.github/workflows/desplegar.yml'' con el siguiente contenido:
 +
 +<sxh yml>
 +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'
 +</sxh>
 +
 +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''.
 +
 +<sxh bash>
 +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
 +</sxh>
 +
 +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:
 +  * [[https://cli.github.com/manual/|GitHub CLI manual]]: todas las órdenes con sus opciones.
 +
 +==== Issues ====
 +GitHub ya trae la etiqueta ''bug'' en cada repositorio; ''feature'' se crea una vez:
 +
 +<sxh bash>
 +gh label create feature
 +</sxh>
 +
 +  * Crear un issue. Responde con su URL, que acaba en su número:
 +<sxh bash>
 +gh issue create --title "Alta de clientes" --body "Formulario de alta con nombre, NIF y email." --label feature
 +</sxh>
 +  * Listar los issues abiertos:
 +<sxh bash>
 +gh issue list
 +</sxh>
 +  * Ver el título, la descripción y el estado del issue 12:
 +<sxh bash>
 +gh issue view 12
 +</sxh>
 +  * Ponerle una etiqueta:
 +<sxh bash>
 +gh issue edit 12 --add-label bug
 +</sxh>
 +  * Cerrarlo:
 +<sxh bash>
 +gh issue close 12
 +</sxh>
 +
 +<note important>La etiqueta tiene que existir en el repositorio: con una que no existe, ''gh issue create'' falla y **no crea el issue**.</note>
 +
 +==== Workflows ====
 +  * Lanzar a mano un workflow que tiene ''workflow_dispatch'', como el ''desplegar.yml'' de arriba:
 +<sxh bash>
 +gh workflow run desplegar.yml
 +</sxh>
 +  * Seguir en directo el workflow que se acaba de lanzar:
 +<sxh bash>
 +gh run watch
 +</sxh>
 +  * Ver las últimas ejecuciones, con su id:
 +<sxh bash>
 +gh run list --limit 5
 +</sxh>
 +  * Ver solo los pasos que han fallado de una ejecución:
 +<sxh bash>
 +gh run view 123456 --log-failed
 +</sxh>
 +
 +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//.
 +
 +{{:clase:daw:daw:tablero-kanban.png?800|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**.
 +
 +{{:clase:daw:daw:tablero-crear-1-board.png?600|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**.
 +
 +{{:clase:daw:daw:tablero-crear-2-nombre.png?600|Nombre e importar los issues del repositorio}}
 +
 +  * Sale con las tres columnas, //Todo//, //In Progress// y //Done//, las que usan los scripts.
 +
 +{{:clase:daw:daw:tablero-crear-3-vacio.png?600|El tablero recién creado}}
 +
 +<note important>**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.</note>
 +
 +==== 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 ([[#crear_el_tablero|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''.
 +
 +<sxh bash;title:issue_create.sh>
 +#!/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]*$'
 +</sxh>
 +
 +<sxh bash>
 +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
 +</sxh>
 +
 +\\
 +
 +  * **Empezar un issue**: pasa su tarjeta a //In Progress//.
 +
 +<sxh bash;title:issue_in_progress.sh>
 +#!/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"
 +</sxh>
 +
 +<sxh bash>
 +scripts/issue_in_progress.sh 12
 +</sxh>
 +
 +\\
 +
 +  * **Cerrar un issue**: lo cierra y pasa su tarjeta a //Done//.
 +
 +<sxh bash;title:issue_close.sh>
 +#!/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"
 +</sxh>
 +
 +<sxh bash>
 +scripts/issue_close.sh 12
 +</sxh>
 +
 +===== 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|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.