Automatizzare un repo con GitHub Actions

GitHub Actions è la piattaforma di CI/CD integrata in GitHub: definisce workflow dichiarativi in YAML, salvati in .github/workflows/*.yml, che GitHub esegue automaticamente in risposta a eventi del repository (push, pull request, schedule, ecc.) su macchine virtuali temporanee (runner) messe a disposizione da GitHub stesso o auto-ospitate. A differenza di un tema grafico o di una libreria condivisa, un workflow non viene "incluso": vive interamente dentro il repository che lo usa, un file per ogni pipeline.

Mappa: cosa serve per cosa

1. Anatomia di un workflow

Un workflow minimo che gira su ogni push:

name: CI

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest
Chiave Livello Significato
on workflow eventi che innescano l'esecuzione
jobs workflow insieme dei job, ciascuno su un runner proprio
runs-on job immagine della VM/container (es. ubuntu-latest)
needs job dipendenze da altri job — un arco nel DAG, vedi §3
steps job sequenza ordinata di operazioni sullo stesso runner
uses step richiama un'action riutilizzabile (owner/repo@versione)
run step esegue un comando di shell direttamente
with step parametri passati a un'action
env workflow/job/step variabili d'ambiente (i livelli più interni sovrascrivono quelli esterni)
if job/step condizione booleana di esecuzione

uses e run sono le due facce di uno step: uses richiama codice pubblicato da altri (versionato con un tag, un branch o uno SHA — fissarlo a un tag come @v4 è la norma, uno SHA dà garanzie più forti contro modifiche silenziose all'action), run esegue comandi shell arbitrari nell'ambiente del runner.

1.1 Runner e isolamento

Ogni job parte da una macchina pulita: nessuno stato sopravvive da un job al successivo se non viene esplicitamente propagato con actions/cache o actions/upload-artifact / download-artifact (vedi §5). Questo è l'opposto del modello a memoria condivisa: due job nello stesso workflow non hanno più in comune di due processi su macchine diverse in rete.

2. Eventi e trigger

La chiave on accetta uno o più eventi:

on:
  push:
    branches: [main]
  pull_request:
  schedule:
    - cron: "0 3 * * 1-5"
  workflow_dispatch:
Campo cron Valori Esempio
minuto 0–59 0
ora 0–23 3
giorno del mese 1–31 * (ogni giorno)
mese 1–12 * (ogni mese)
giorno della settimana 0–6 (0 = domenica) 1-5 (lun–ven)

cron: "0 3 * * 1-5" si legge quindi "ogni giorno feriale alle 3:00 UTC" — utile per job periodici (rigenerare un report, invalidare una cache) che non dipendono da un push.

3. Il grafo dei job: needs e parallelismo

Senza needs, tutti i job di un workflow partono in parallelo, ciascuno sul proprio runner. needs introduce una dipendenza esplicita: il job a valle aspetta che quello a monte finisca (e di norma che abbia successo).

flowchart LR
    lint["lint"] --> deploy["deploy"]
    test["test"] --> deploy

lint e test non hanno needs tra loro: girano in parallelo, su due runner distinti; deploy dichiara needs: [lint, test] e parte solo quando entrambi sono terminati con successo. L'insieme dei job con le loro dipendenze forma un DAG, esattamente come i grafi di dipendenza visti per le riduzioni tra problemi in teoria_riduzioni.md: qui i nodi sono job invece di problemi, e gli archi sono "deve finire prima di" invece di "si riduce a".

È un parallelismo diverso da quello delle goroutine di fondamenti_go.md §9: le goroutine condividono lo stesso processo e comunicano su canali in-memory; i job di Actions girano su runner isolati e comunicano solo scambiandosi artifact (file), mai stato in memoria — il modello CSP di Go ("condividi la memoria comunicando" su un canale) non si applica qui, perché non c'è memoria da condividere in primo luogo.

4. Strategia a matrice

strategy.matrix ripete lo stesso job una volta per ogni combinazione di parametri:

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
        python-version: ["3.10", "3.11", "3.12"]
    runs-on: $
    steps:
      - uses: actions/setup-python@v5
        with:
          python-version: $
      - run: pytest

Se la matrice ha $k$ assi $V_1, \dots, V_k$ (qui $k=2$: os e python-version), il numero di job generati è il prodotto cartesiano delle cardinalità di ciascun asse:

\[|J| = \prod_{i=1}^{k} |V_i|\]

nell'esempio sopra, $|J| = 2 \times 3 = 6$ job indipendenti (GitHub impone un limite pratico di 256 job per matrice). exclude e include permettono di togliere o aggiungere combinazioni specifiche senza dover elencare tutto il prodotto cartesiano.

5. Cache e artifact: la memoization applicata alla CI

actions/cache salva una directory tra run diversi dello stesso workflow, indicizzata da una chiave:

- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-$

Concettualmente è la stessa idea della memoization top-down vista in problema_fibonacci.md §2: lì si evita di ricalcolare fib(k) se il risultato per quel k è già in tabella; qui si evita di reinstallare le dipendenze se il loro hash (hashFiles('requirements.txt'), che gioca il ruolo di chiave della tabella dei risultati) non è cambiato rispetto a un run precedente. In entrambi i casi la chiave è ciò che rende sicuro saltare il lavoro: se l'input cambia, cambia la chiave, e il "ricalcolo" (qui: pip install) riparte da zero.

La stessa idea si legge anche in chiave di costo ammortizzato (teoria_costo.md §5.1): il primo run su una cache fredda paga il costo pieno dell'installazione (l'analogo di un ridimensionamento del vettore dinamico), i run successivi con cache calda pagano solo il costo di un lookup — su una sequenza lunga di run, il costo medio per run tende a un valore molto più basso del caso pessimo di un singolo run a freddo.

Gli artifact (actions/upload-artifact / download-artifact) risolvono un problema diverso: non la ripetizione nel tempo, ma il trasferimento di file tra job dello stesso run (es. il job build produce un eseguibile, il job deploy lo scarica) — è il meccanismo usato anche per pubblicare pagine statiche, vedi §7.

6. Permessi, secrets e OIDC

Ogni workflow riceve automaticamente un GITHUB_TOKEN con permessi di default piuttosto ampi; il blocco permissions li restringe esplicitamente al minimo necessario (principio del privilegio minimo):

permissions:
  contents: read
  pages: write
  id-token: write

I secrets ($) sono invece valori cifrati e persistenti configurati nelle impostazioni del repository/organizzazione — usati quando serve davvero un credenziale di lungo periodo che l'OIDC non può sostituire (es. una API key di terze parti).

concurrency evita esecuzioni sovrapposte dello stesso gruppo logico:

concurrency:
  group: pages
  cancel-in-progress: false

con cancel-in-progress: false un nuovo run entra in coda invece di cancellare quello in corso — importante per un deploy (non si vuole un sito a metà pubblicato), mentre per build di test su pull request si usa spesso true, per non sprecare minuti di CI su commit ormai superati da un push successivo.

7. Caso di studio: pubblicare Sphinx con GitHub Actions

Il repository nn-option-pricing — una rete neurale feed-forward addestrata ad approssimare la formula di Black-Scholes per il pricing di opzioni call europee, usata qui come caso d'uso reale — pubblica la propria documentazione (generata con Sphinx a partire dai docstring del pacchetto Python nn_option_pricing) su GitHub Pages tramite .github/workflows/docs.yml:

name: Build and publish documentation

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install -r requirements-docs.txt
          pip install -e . --no-build-isolation

      - name: Build Sphinx documentation
        run: sphinx-build -W -b html docs/source docs/build/html

      - name: Configure GitHub Pages
        uses: actions/configure-pages@v5

      - name: Upload GitHub Pages artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: docs/build/html

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: $

    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

Due job, un solo arco needs:

flowchart LR
    build["build<br/>sphinx-build + upload-pages-artifact"] --> deploy["deploy<br/>environment: github-pages"]

Punti da notare, richiamando le sezioni precedenti:

Il risultato è pubblicato all'indirizzo https://matteogiorgi.github.io/nn-option-pricing, rigenerato automaticamente a ogni push su main (o su richiesta manuale, grazie a workflow_dispatch) senza intervento umano oltre al push stesso.

8. Jekyll vs Sphinx: quando serve davvero un workflow

Questo stesso repository (geonote) pubblica le proprie pagine su GitHub Pages senza alcun workflow: come descritto in tema_geoteo.md §2.4, basta Settings $\to$ Pages $\to$ Deploy from a branch, perché Jekyll è generato nativamente dalla pipeline di build di GitHub Pages. nn-option-pricing, che usa Sphinx invece di Jekyll, non ha questa scorciatoia: Sphinx non è tra i generatori supportati nativamente da GitHub Pages, quindi la build va eseguita a mano (o, meglio, automatizzata) e solo l'HTML risultante va caricato come artifact di Pages.

  geonote (Jekyll) nn-option-pricing (Sphinx)
Generatore Jekyll, supportato nativamente Sphinx, non supportato nativamente
Configurazione Pages Deploy from a branch GitHub Actions (workflow personalizzato)
Chi builda l'infrastruttura di GitHub Pages un job del workflow (build), su un runner qualsiasi
File coinvolti _layouts/, _config.yml (vedi tema_geoteo.md §2) .github/workflows/docs.yml, docs/source/
Trigger push su main (gestito da GitHub) push su main o workflow_dispatch (dichiarati esplicitamente)

La regola generale: serve un workflow di Actions per pubblicare su Pages ogni volta che il generatore del sito non è Jekyll (Sphinx, Hugo, Docusaurus, un semplice script che produce HTML statico…) — in tutti questi casi tocca a un job build fare esplicitamente ciò che, per Jekyll, GitHub fa da solo dietro le quinte.

9. Documentazione e risorse

Nota sulla versione: le action esterne (actions/checkout, actions/setup-python, …) sono versionate indipendentemente dalla piattaforma. Fissare un tag maggiore (es. @v4) è la norma; verifica sempre changelog e breaking change sulla pagina del Marketplace dell'action prima di aggiornarlo.