# GitHub Actions

ghchronicle como GitHub Action: la Action compuesta, sus cuatro modos y las dos cosas que un runner alojado no conserva.

Source: https://docs.jmrp.io/ghchronicle/es/install/actions/

El repositorio incluye una Action compuesta, así que un workflow no necesita
cadena de herramientas de Go: descarga un binario de release y lo llama.

```yaml
- uses: jmrplens/ghchronicle@v2
  with:
    token: ${{ secrets.GHCHRONICLE_TOKEN }}
    mode: once
    config: .github/ghchronicle.yaml
```

## Entradas

| Entrada           | Por omisión              | Qué hace                                                                                                                                              |
| ----------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`           | obligatoria              | Un token personal. El `GITHUB_TOKEN` automático no basta                                                                                              |
| `config`          | `""`                     | Ruta a un fichero de configuración. Omítela para usar valores por defecto a partir de `user`, cuyo estado dura una ejecución y no se puede guardar en caché |
| `user`            | el dueño del repositorio | La cuenta que recoger cuando no se da fichero de configuración                                                                                        |
| `mode`            | `once`                   | `once`, `backfill`, `card` o `migrate`                                                                                                                |
| `backfill-since`  | `""`                     | Límite para `backfill`: una fecha, `90d`, `2y` o una duración de Go. Vacío es sin límite                                                              |
| `card`            | `""`                     | Ruta del SVG que escribir. Vacío significa sin tarjeta                                                                                                |
| `card-layout`     | `summary`                | Uno de los trece diseños registrados                                                                                                                   |
| `card-theme`      | `auto`                   | `dark`, `light`, `auto`, o `both` para una tarjeta clara y su gemela `_dark`                                                                          |
| `card-fields`     | `""`                     | Campos separados por comas. Vacío es el conjunto por defecto del diseño                                                                               |
| `card-motion`     | `once`                   | `once`, `loop` u `off`; `loop` solo cambia `terminal` y `ticker`                                                                                                     |
| `card-width`      | `""`                     | Ancho de la tarjeta en píxeles. Vacío dibuja el diseño con su propio ancho; cada uno dibuja entre dos extremos propios, que dice su [sección](https://docs.jmrp.io/ghchronicle/es/card/layouts/). Solo `activity-heatmap` se gasta el sitio en datos, un año entero del calendario en su extremo lejano |
| `card-speed`      | `""`                     | A qué velocidad se reproduce un diseño animado, como decimal de 0 a 1. Vacío significa 0.5, el ritmo con el que siempre se han dibujado las tarjetas; por debajo la tarjeta va más lenta y por encima más rápida, y todos los diseños animados se escalan juntos. 0 es la animación más lenta y no una tarjeta quieta, eso es `card-motion: off` |
| `include-private` | `false`                  | `true` cuenta los repositorios privados cuando no se da fichero de configuración. Ver el aviso más abajo |
| `version`         | `latest`                 | La release que instalar: `latest` para la más nueva, o una release con o sin su `v`, así que `2.6.5` y `v2.6.5` son la misma. La etiqueta mayor `v2` es lo que toma `uses:`, no una release, y se rechaza |

## Los cuatro modos

- **once**

  Una pasada contra un fichero de configuración que tú das, que es como un
  workflow alimenta una base de datos.

  ```yaml
  name: Collect
  on:
    schedule:
      - cron: "*/30 * * * *"
    workflow_dispatch:

  jobs:
    collect:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v7
        - uses: jmrplens/ghchronicle@v2
          with:
            token: ${{ secrets.GHCHRONICLE_TOKEN }}
            mode: once
            config: .github/ghchronicle.yaml
          env:
            INFLUX_TOKEN: ${{ secrets.INFLUX_TOKEN }}
  ```

  El fichero de configuración referencia las credenciales de la base de datos
  como `${VAR}`, así que entran como secretos y nunca al repositorio.

- **card**

  Una pasada y un SVG, y nada escrito en ninguna base de datos. Este es el
  único modo que no necesita almacén alguno configurado.

  ```yaml
  name: Profile card
  on:
    schedule:
      - cron: "17 6 * * *"
    workflow_dispatch:

  permissions:
    contents: write

  jobs:
    card:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v7
        - uses: jmrplens/ghchronicle@v2
          with:
            token: ${{ secrets.GHCHRONICLE_TOKEN }}
            mode: card
            card: generated/github-stats.svg
            card-layout: github-stats
            card-theme: both
        - name: Commit if it changed
          run: |
            git config user.name "github-actions[bot]"
            git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
            git add generated/
            git diff --staged --quiet || git commit -m "Update the profile card"
            git push
  ```

  El SVG es idéntico byte a byte para la misma entrada, así que un día sin
  cambios no produce ningún commit.

- **backfill**

  Llega tan atrás como GitHub permita y espera al límite de peticiones en vez
  de detenerse. Ejecútalo una vez, a mano.

  ```yaml
  name: Backfill
  on:
    workflow_dispatch:
      inputs:
        since:
          description: "A date, 90d, 2y, or empty for no bound"
          default: "2y"

  jobs:
    backfill:
      runs-on: ubuntu-latest
      timeout-minutes: 360
      steps:
        - uses: actions/checkout@v7
        - uses: jmrplens/ghchronicle@v2
          with:
            token: ${{ secrets.GHCHRONICLE_TOKEN }}
            mode: backfill
            backfill-since: ${{ inputs.since }}
            config: .github/ghchronicle.yaml
  ```

  Dale un `timeout-minutes` generoso: un relleno sin límite aparca en cada
  reinicio de la ventana, y un job que muere a medias ha gastado la cuota y se
  ha quedado con parte del beneficio.

- **migrate**

  Pone al día los almacenes en los que escribe la configuración después de
  una actualización: ejecuta `-migrate -yes`, que aplica cada cambio que [el
  plan](https://docs.jmrp.io/ghchronicle/es/install/upgrading/#migraciones) encuentra pendiente,
  también los que necesitan la palabra de alguien, y vuelve a leer lo que
  despejó. Ejecútalo a mano, una vez, después de leer lo que dice `-migrate`
  sobre la misma configuración.

  ```yaml
  name: Migrate
  on:
    workflow_dispatch:

  jobs:
    migrate:
      runs-on: ubuntu-latest
      timeout-minutes: 60
      steps:
        - uses: actions/checkout@v7
        - uses: jmrplens/ghchronicle@v2
          with:
            token: ${{ secrets.GHCHRONICLE_TOKEN }}
            mode: migrate
            config: .github/ghchronicle.yaml
  ```

  No admite tarjeta. Un almacén que guarda filas de cuentas que esta
  configuración no recoge, o cuyas filas no se pudieron comparar con ella, se
  deja sin aplicar y el paso falla, porque solo `-migrate-others` lo despeja,
  y esa es una decisión para tomar en una terminal, no en un workflow. Sin un
  fichero de estado [restaurado entre ejecuciones](https://docs.jmrp.io/ghchronicle/es/install/actions/#dos-cosas-que-un-runner-alojado-no-conserva),
  cada ejecución de la Action empieza con uno nuevo, así que un almacén al que
  no se le puede preguntar, un fichero SQL, un Graphite o lo que haya detrás
  de un Telegraf, no tiene historia aquí y nunca se da por necesitado de un
  cambio; con uno restaurado, esos siguen su registro como en una máquina. Un
  job que agotó su tiempo mientras volvía a leer la historia no deja nada que
  el siguiente pueda retomar, porque `actions/cache` no guarda nada de un job
  que no terminó bien: ejecuta después `mode: backfill` con la misma
  configuración.

En `once` y `backfill`, un arranque comprueba los almacenes igual y avisa de lo
pendiente. Sobre un fichero de estado nuevo, que es cada ejecución sin uno
restaurado, no aplica nada por su cuenta ni siquiera con `migrate: auto`: el
fichero de estado se va con el runner, y con él el registro de que aún se debe
una relectura, así que una relectura que falló o un job cancelado a medias
dejaría un almacén despejado y nada que lo dijera. Con un fichero de estado
restaurado aplica lo que es seguro aplicar sin supervisión, como una máquina.
La Action convierte cada línea que dice que un cambio está pendiente, que se
aplicó uno al arrancar, que uno falló o que se debe una relectura en una
anotación de la ejecución en cuanto se escribe la línea, así que se ve en la
página de resumen del workflow y no solo en su registro, también en un job
cancelado a medias.

## Una tarjeta en el README de tu perfil

GitHub muestra en lo alto de tu perfil el README del repositorio que se llama
como tu cuenta, `<tú>/<tú>`. La tarjeta vive en ese repositorio como un fichero,
el README apunta a ella una vez y un workflow programado sustituye el fichero.
El README no se reescribe nunca.

1. Crea un token personal (ver [el token](https://docs.jmrp.io/ghchronicle/es/start/token/)) y
   guárdalo en `<tú>/<tú>` como el secreto `GHCHRONICLE_TOKEN`. El
   `GITHUB_TOKEN` automático no sirve, ni siquiera para números públicos: no es
   un usuario, así que la Action no puede listar tus repositorios con él y la
   tarjeta sale vacía.
2. Añade `.github/workflows/card.yml`:

   ```yaml
   name: Profile card
   on:
     schedule:
       - cron: "17 6 * * *"
     workflow_dispatch:

   permissions:
     contents: write

   # One run at a time: two that overlap would race each other to push.
   concurrency:
     group: profile-card
     cancel-in-progress: false

   jobs:
     card:
       runs-on: ubuntu-latest
       steps:
         - uses: actions/checkout@v7
         - uses: jmrplens/ghchronicle@v2
           with:
             token: ${{ secrets.GHCHRONICLE_TOKEN }}
             mode: card
             card: generated/card.svg
             card-layout: animated-counters
             card-theme: both
             card-motion: once
         - name: Commit if it changed
           run: |
             git config user.name "github-actions[bot]"
             git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
             git add generated/
             git diff --staged --quiet || git commit -m "Update the profile card"
             git push
   ```

3. Pega esta línea en `README.md` donde deba aparecer la tarjeta, una sola vez:

   ```html
   <picture><source media="(prefers-color-scheme: dark)" srcset="generated/card_dark.svg"><img src="generated/card.svg" alt="Mis estadísticas de GitHub"></picture>
   ```

4. Ejecuta el workflow a mano desde la pestaña Actions la primera vez, para que
   los ficheros existan antes de la primera ejecución programada.

**Más de una tarjeta.** Repite el paso `uses: jmrplens/ghchronicle@v2` con otra
ruta en `card` y otro diseño, y pega un `<picture>` por tarjeta. Cada paso es una
pasada propia, y cada una vuelve a pedir todos los números: una pasada que
escribe una tarjeta recoge todas las familias diga lo que diga su cadencia, y
una en modo `card` no escribe nada en el fichero de estado. Así que varios pasos
de tarjeta pueden compartir un `config`, y por tanto un `state_file`, y cada
tarjeta es la cuenta entera. Eso es también lo que cuesta cada una: una tarjeta
es una pasada en frío de todas las familias, así que N tarjetas son N pasadas
[al precio por familia](https://docs.jmrp.io/ghchronicle/es/api/cost/).

**Un README que no está en la raíz.** Esto es para otros repositorios, porque
GitHub muestra el README de un perfil solo desde la raíz de `<tú>/<tú>`. Las
rutas del `<picture>` son relativas al README, así que un `docs/README.md`
apunta a `../generated/card.svg`.

> **Repositorios privados**
>
> Sin fichero de configuración, las estrellas, los forks, los lenguajes, el
> tráfico y los nombres de repositorio salen solo de los repositorios públicos,
> mientras que las cifras de contribuciones, commits y pull requests son totales
> de la cuenta, tal como GitHub los muestra en tu perfil. Con
> `include-private: true` la tarjeta suma las estrellas, forks, lenguajes y
> tráfico de tus repositorios privados, y los diseños que listan repositorios
> **publican sus nombres**: `repo-list` y `summary` (el diseño por defecto de la
> Action) los listan de entrada, y también cualquier diseño al que se le pida
> `top_repos` en `card-fields`. Para contarlos sin nombrarlos, elige tú los campos y
> deja fuera `top_repos`, por ejemplo
> `card-fields: stars,forks,followers,contributions`.

## Dos cosas que un runner alojado no conserva

> **El token tiene que ser un token personal**
>
> El tráfico necesita acceso de escritura a *cada* repositorio que se recoja y,
> en un token fine-grained, el permiso de repositorio Administration (lectura);
> el `GITHUB_TOKEN` automático no puede recibir Administration, así que recibe
> un 403 incluso en el repositorio en el que corre el workflow. Tampoco tiene
> `security_events` ni `read:packages`, y no es un usuario, así que nada de
> ámbito de cuenta funciona con él. Ver [el token](https://docs.jmrp.io/ghchronicle/es/start/token/).

**El fichero de estado no sobrevive entre ejecuciones.** Sin él cada ejecución
es una primera ejecución. Corren todas las familias, diga lo que diga su
cadencia, porque nada recuerda cuándo corrieron por última vez. Se vuelve a
recorrer entera la lista de estrellas, y se lee entero el historial diario de
estrellas de cada repositorio, una página por cada treinta semanas de su vida.
`achievements` recorre enteras las pull requests fusionadas de la cuenta para
su recuento de coautorías, que fueron 35 consultas y 23,7 MB en una cuenta con
2.315. Y sin [fichero de caché](https://docs.jmrp.io/ghchronicle/es/configuration/#la-caché-que-hay-a-su-lado)
junto al estado, nada se pregunta con un ETag, así que ninguna respuesta es un
304 gratis. En una cuenta pequeña son un puñado de llamadas; en una cuenta con
muchas estrellas, repositorios antiguos o una historia larga, guárdalo en
caché.

Guardarlo en caché necesita un fichero `config:`. Sin él la Action escribe una
configuración propia y pone el estado en un directorio temporal nuevo dentro de
`RUNNER_TEMP` en cada ejecución, que ningún paso de caché puede nombrar. Con
él, apunta `state_file` al directorio personal:

```yaml
state_file: ~/.ghchronicle/state.json
```

y restaura y guarda ese directorio con un paso anterior al de ghchronicle:

```yaml
- uses: actions/cache@v6
  with:
    path: ~/.ghchronicle
    key: ghchronicle-state-${{ github.run_id }}
    restore-keys: ghchronicle-state-
```

Una `~` en `state_file` es el directorio personal a partir de la 2.6.1. Una
versión anterior la lee tal como está escrita, un directorio llamado `~` dentro
del checkout que el paso de caché nunca ve, así que con un `version` anterior a
la 2.6.1 escribe la ruta entera: `/home/runner/.ghchronicle/state.json` en un
runner Ubuntu alojado por GitHub.

El directorio guarda entonces también el fichero de caché, que un paso `once`
escribe y un paso `backfill` solo lee, así que una ejecución restaurada de él
pregunta además a GitHub con los validadores que guardó la anterior, y solo
paga lo que cambió. Una pasada en modo `card` lee el
estado restaurado, que es lo que le permite saltarse los dos recorridos, y nunca
lo vuelve a escribir, ni tampoco el fichero de caché que hay a su lado: entrega
sus puntos a la tarjeta y a ningún almacén, así que nada de lo que aprendió
puede decirle a la siguiente recogida que una familia ya está hecha. Quien llena
la entrada de `actions/cache` es un paso `once`, con los dos ficheros, o un paso
`backfill`, solo con el fichero de estado. Ningún paso deja ahí un registro de
escrituras: una ejecución que termina con su pasada no abre ninguno.

## Sin la Action

Lo mismo a mano, si prefieres no depender de ella:

```yaml
- uses: actions/setup-go@v7
  with:
    go-version: stable
- run: go install github.com/jmrplens/ghchronicle/v2/cmd/ghchronicle@latest
- run: ghchronicle -config .github/ghchronicle.yaml -card profile.svg -card-only
  env:
    GITHUB_TOKEN: ${{ secrets.GHCHRONICLE_TOKEN }}
```
