====== 8. gh 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.
Más información:
* [[https://cli.github.com/manual/|GitHub CLI manual]]: todas las órdenes con sus opciones.
===== Instalación =====
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.
==== 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'' del [[tema07|tema 7]]:
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//.
{{: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}}
**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 ([[#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''.
#!/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 ====
* 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 [[tema07#ejercicio_1|ejercicio 1 del tema 7]] con ''gh workflow run'' y síguelo con ''gh run watch''.
==== Ejercicio 2 ====
* 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.