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:
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 | 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.
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.
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).
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
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:
winget install GitHub.cli (y reabrir la terminal).sudo apt install gh.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:
GitHub ya trae la etiqueta bug en cada repositorio; feature se crea una vez:
gh label create feature
gh issue create --title "Alta de clientes" --body "Formulario de alta con nombre, NIF y email." --label feature
gh issue list
gh issue view 12
gh issue edit 12 --add-label bug
gh issue close 12
gh issue create falla y no crea el issue.
workflow_dispatch, como el desplegar.yml de arriba:
gh workflow run desplegar.yml
gh run watch
gh run list --limit 5
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.
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.
Uno por proyecto, desde el menú + de arriba a la derecha de GitHub → New project:
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.
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
#!/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
#!/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
Añade al repositorio de tu código fuente de Java una acción de GitHhub Actions que sea para desplegar.
Se ejecutará:
mastergh y autentícate. Comprueba con gh auth status que está el permiso project.feature y un issue «Desplegar con GitHub Actions» con la etiqueta feature. Comprueba con gh issue list que sale.gh workflow run y síguelo con gh run watch.scripts/ y dales permiso de ejecución.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.