Tabla de Contenidos

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:

Más información:

Instalación

Se instala con:

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

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

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

Workflows

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.

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:

Plantilla Board

Nombre e importar los issues del repositorio

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.

#!/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

Ejercicios

Ejercicio 1

Ejercicio 2