Lezione 26 di 35

Git 26

🔀 Git & GitHub · Modulo 5 · Lezione 26

Workflow YAML in profondità 🔧

Job multipli · Dipendenze tra job · Variabili d’ambiente · Matrici di test · Condizioni if

⏱️ Durata stimata: 75–90 min 📊 Livello: Avanzato (Modulo 5) 🔧 Strumenti: Terminale · Editor di testo · GitHub 📋 Prerequisiti: Lezione 25 (primo workflow funzionante)

🔧 Nella lezione precedente hai creato un workflow semplice con un job e pochi step. I progetti reali richiedono di più: testare su più versioni, eseguire job in parallelo, passare risultati da un job all’altro, saltare step in base a condizioni. In questa lezione padroneggi la sintassi YAML per costruire workflow flessibili e potenti.

🎯 Obiettivi della lezione

  • Creare workflow con job multipli (paralleli e sequenziali)
  • Usare needs per creare dipendenze tra job
  • Definire variabili d’ambiente a livello di workflow, job e step
  • Usare la strategy matrix per testare su più configurazioni
  • Usare condizioni if per eseguire step solo in certi casi
  • Passare dati tra step con GITHUB_OUTPUT

📖 1. Teoria

1.1 — Job multipli: paralleli vs sequenziali

Un workflow può avere più job. Per default, i job girano in parallelo — ciascuno sul proprio runner. Se vuoi che un job aspetti un altro, usi needs.

Paralleli (default)
Job A: test
runner 1
Job B: lint
runner 2
Partono insieme → finiscono quando entrambi sono pronti
Sequenziali (con needs)
Job A: test
↓ needs: test
Job B: deploy
B parte solo dopo che A ha finito con successo
# Job paralleli: test e lint girano contemporaneamente
jobs:
test:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– run: echo “Eseguo i test”

lint:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– run: echo “Controllo lo stile del codice”

# Job sequenziale: deploy parte solo dopo test E lint
deploy:
needs: [test, lint] ← aspetta entrambi
runs-on: ubuntu-latest
steps:
– run: echo “Deploy in produzione”
Visualizzazione del flusso
test
lint
↘ ↙
deploy
test e lint in parallelo → deploy solo dopo entrambi

1.2 — Variabili d’ambiente

Le variabili d’ambiente passano configurazioni ai tuoi comandi. Puoi definirle a 3 livelli: tutto il workflow, un singolo job, o un singolo step. Il livello più specifico sovrascrive quello più generale.

name: Demo variabili
on: push

# Livello 1: variabili per TUTTO il workflow
env:
PROGETTO: “Sito Classe 4A”
VERSIONE: “1.0.0”

jobs:
build:
runs-on: ubuntu-latest
# Livello 2: variabili per QUESTO job
env:
NODE_ENV: production
steps:
– name: Info progetto
run: echo “Progetto: $PROGETTO v$VERSIONE (env: $NODE_ENV)”

– name: Step con variabile locale
# Livello 3: variabili per QUESTO step
env:
MESSAGGIO: “Build completata!”
run: echo “$MESSAGGIO”
Variabili predefinite di GitHub
Variabile Contiene Esempio
${{ github.repository }} Nome completo del repository mario-rossi/sito-classe
${{ github.actor }} Chi ha attivato il workflow mario-rossi
${{ github.ref_name }} Nome del branch o tag main
${{ github.sha }} Hash del commit che ha attivato il workflow a1b2c3d4e5f6…
${{ github.event_name }} Il tipo di evento (push, pull_request…) push

1.3 — Strategy Matrix: testare su più configurazioni

La matrix strategy è la funzione più potente di GitHub Actions per i test. Definisci una lista di configurazioni e GitHub crea automaticamente un job per ogni combinazione — senza duplicare codice.

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]

steps:
– uses: actions/checkout@v4
– name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
– run: node –version
– run: npm test
GitHub crea 3 job automaticamente
test (Node 18)
runner separato
test (Node 20)
runner separato
test (Node 22)
runner separato
Tutti in parallelo — scrivi il codice una volta, gira su 3 versioni

Puoi combinare più dimensioni nella matrice. GitHub crea un job per ogni combinazione:

strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [18, 20]
# → 4 job: ubuntu+18, ubuntu+20, windows+18, windows+20

📌 Per i progetti scolastici: la matrix è utile ma non indispensabile. Se il tuo progetto è un sito HTML/CSS/JS senza dipendenze da una versione specifica di Node, un singolo job basta. La matrix diventa importante per librerie che devono funzionare su più versioni o su più sistemi operativi.

1.4 — Condizioni: if

Con if puoi eseguire step o job solo quando una condizione è vera. Utile per: fare deploy solo da main, saltare step per PR di draft, eseguire azioni solo se un file è cambiato.

jobs:
test:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– run: echo “Test eseguiti”

deploy:
needs: test
runs-on: ubuntu-latest
# Deploy solo se il push è su main (non su feature branch)
if: github.ref == ‘refs/heads/main’
steps:
– run: echo “Deploy in produzione!”
Condizione Quando è vera Caso d’uso
github.ref == ‘refs/heads/main’ Il push è sul branch main Deploy solo da main
github.event_name == ‘pull_request’ L’evento è una pull request Step solo per le PR
success() Tutti gli step precedenti sono passati Step di pulizia dopo successo
failure() Almeno uno step precedente è fallito Notifica di errore, upload log
always() Sempre — anche dopo un fallimento Pulizia risorse, report finale
contains(github.event.head_commit.message, ‘[skip ci]’) Il messaggio del commit contiene “[skip ci]” Saltare il CI per commit documentazione
# if a livello di step: notifica solo se il job fallisce
steps:
– uses: actions/checkout@v4
– name: Esegui test
run: npm test

– name: Notifica errore
if: failure()
run: echo “⚠️ I test sono falliti! Controlla i log.”

1.5 — Passare dati tra step: GITHUB_OUTPUT

Ogni step gira nello stesso runner ma è isolato. Per passare un valore da uno step all’altro, usi GITHUB_OUTPUT:

steps:
– name: Calcola versione
id: version ← dà un ID a questo step
run: echo “numero=1.2.3” >> $GITHUB_OUTPUT

– name: Usa la versione
run: echo “La versione è ${{ steps.version.outputs.numero }}”
# Output: “La versione è 1.2.3”

1.6 — Riepilogo della sintassi YAML per Actions

Struttura
name:
on:
env:
jobs:
  nome-job:
    runs-on:
    needs:
    if:
    env:
    strategy:
    steps:
Step
– name: “descrizione”
  uses: action@v
  with:
    param: valore

– name: “descrizione”
  run: comando
  env:
    VAR: valore
  if: condizione
  id: mio-id
Espressioni
${{ github.* }}
${{ matrix.* }}
${{ steps.id.outputs.* }}
${{ env.VAR }}
${{ secrets.* }}

success()
failure()
always()
contains(a, b)

💻 2. Esempi guidati

Esempio A — Workflow completo: test + lint + deploy

# File: .github/workflows/ci-cd.yml

name: CI/CD Pipeline

on:
push:
branches: [main]
pull_request:
branches: [main]

env:
PROGETTO: “Sito Classe”

jobs:
# Job 1: verifica file (parallelo con job 2)
verifica:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– name: Controlla file essenziali
run: |
for file in index.html style.css README.md; do
test -f “$file” && echo “✅ $file” || (echo “❌ $file mancante” && exit 1)
done

# Job 2: controlla HTML (parallelo con job 1)
html-check:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– name: Verifica struttura HTML
run: |
for f in $(find . -name “*.html”); do
grep -q “<!DOCTYPE” “$f” && echo “✅ $f ha DOCTYPE” || echo “⚠️ $f senza DOCTYPE”
done

# Job 3: report — parte dopo i primi due
report:
needs: [verifica, html-check]
runs-on: ubuntu-latest
if: github.ref == ‘refs/heads/main’
steps:
– uses: actions/checkout@v4
– name: Report del progetto
run: |
echo “📊 Report — $PROGETTO”
echo “Branch: ${{ github.ref_name }}”
echo “Attivato da: ${{ github.actor }}”
echo “File HTML: $(find . -name ‘*.html’ | wc -l)”
echo “Ultimo commit: ${{ github.sha }}”

Esempio B — Matrix: test su Node 18 e 20

# File: .github/workflows/test-matrix.yml

name: Test su più versioni Node

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]

steps:
– uses: actions/checkout@v4

– name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

– name: Installa dipendenze
run: npm ci

– name: Esegui test
run: npm test

– name: Info versione
run: echo “✅ Test passati su Node ${{ matrix.node-version }}”

Esempio C — Condizioni e output tra step

# File: .github/workflows/smart.yml

name: Workflow intelligente
on: push

jobs:
analizza:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4

– name: Conta file HTML
id: conta
run: |
NUMERO=$(find . -name “*.html” | wc -l)
echo “html=$NUMERO” >> $GITHUB_OUTPUT
echo “Trovati $NUMERO file HTML”

– name: Avviso se troppo pochi file
if: steps.conta.outputs.html < 3
run: echo “⚠️ Solo ${{ steps.conta.outputs.html }} file HTML — ne servirebbero almeno 3”

– name: Messaggio di successo
if: steps.conta.outputs.html >= 3
run: echo “✅ Progetto con ${{ steps.conta.outputs.html }} pagine HTML — buon lavoro!”

– name: Info branch
run: echo “Eseguito su ${{ github.ref_name }} da ${{ github.actor }}”

🏋️ 3. Esercizi

🟢 Esercizio 1 — Job paralleli (Facile)

a) Crea un workflow parallel.yml con 2 job paralleli: info-sistema (stampa uname -a e date) e info-progetto (checkout + ls -la + conta i file).
b) Committa e pusha. Nella tab Actions, i due job partono contemporaneamente?
c) Quanto tempo impiega il workflow totale? (Dovrebbe essere circa il tempo del job più lungo, non la somma.)

🔵 Esercizio 2 — Job con needs e variabili (Medio)

a) Crea un workflow con 3 job: test, build, deploy.
b) build deve aspettare test. deploy deve aspettare build.
c) Aggiungi una variabile d’ambiente a livello di workflow: PROGETTO: "Il mio sito".
d) Ogni job stampa il nome del progetto e il proprio nome (es. “Il mio sito — fase: test”).
e) Il job deploy deve partire solo se il branch è main (if: github.ref == 'refs/heads/main').
f) Pusha su main: tutti e 3 i job girano. Pusha su un feature branch: solo test e build girano.

🟠 Esercizio 3 — Matrix strategy (Medio)

a) Crea un workflow matrix.yml che esegue un comando su 3 sistemi operativi: ubuntu-latest, windows-latest, macos-latest.
b) Usa strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] e runs-on: ${{ matrix.os }}.
c) Ogni job stampa il nome del sistema operativo e la data.
d) Pusha e controlla la tab Actions: vedi 3 job paralleli? Quale OS è il più veloce?
e) Aggiungi una seconda dimensione: greeting: ["Ciao", "Hello", "Hola"]. Quanti job vengono creati? (3 × 3 = 9)

🟣 Esercizio 4 — Pipeline CI/CD completa (Difficile)

Crea un workflow pipeline.yml che simula una pipeline CI/CD reale:

a) Job 1 — lint: verifica che tutti i file HTML abbiano DOCTYPE. Fallisce se manca.
b) Job 2 — test: verifica che i file essenziali esistano. Stampa un conteggio per tipo di file. Usa un output per passare il numero totale di file.
c) Job 3 — build: needs: [lint, test]. Simula una build (es. echo "Build completata").
d) Job 4 — deploy: needs: build, if: github.ref == 'refs/heads/main'. Simula un deploy. Stampa il nome dell’utente che ha fatto il push.
e) Testa: pusha su main (4 job girano), pusha su un branch (solo lint, test, build).
f) Fai fallire intenzionalmente lint (es. rimuovi DOCTYPE da un file). Il deploy non parte?

❓ 4. Quiz di verifica

1. Per default, i job in un workflow girano in parallelo o in sequenza?

Mostra risposta

In parallelo. Ogni job gira sul proprio runner separato e parte appena il workflow si attiva. Se vuoi che un job aspetti un altro, usi la chiave needs: per creare una dipendenza. Ad esempio needs: test fa sì che il job attenda il completamento con successo del job “test” prima di partire. Puoi anche indicare più dipendenze con una lista: needs: [test, lint].


2. Cosa fa la strategy matrix?

Mostra risposta

Crea automaticamente un job per ogni combinazione di valori nella matrice. Ad esempio, con matrix: { os: [ubuntu, windows], node: [18, 20] }, GitHub crea 4 job (ubuntu+18, ubuntu+20, windows+18, windows+20), tutti con lo stesso codice ma configurazioni diverse. Ogni job gira in parallelo su un runner separato. Usi ${{ matrix.nome }} per accedere al valore corrente della matrice nello step. È il modo più efficiente per testare su più configurazioni senza duplicare il codice del workflow.


3. A cosa serve if: failure() in uno step?

Mostra risposta

Esegue quello step solo se uno step precedente è fallito. Normalmente, quando uno step fallisce, tutti gli step successivi vengono saltati. Ma con if: failure() lo step viene eseguito proprio in caso di fallimento. È perfetto per azioni di notifica (“⚠️ I test sono falliti!”), upload di log di errore, o pulizia di risorse. In modo simile, if: always() esegue lo step sempre, sia in caso di successo che di fallimento — utile per step di pulizia che devono girare in ogni caso.


4. Come passi un valore da uno step all’altro?

Mostra risposta

Nello step che produce il valore: assegna un id: allo step e scrivi echo "chiave=valore" >> $GITHUB_OUTPUT. Nello step che consuma il valore: accedi con ${{ steps.ID.outputs.chiave }}. Esempio: lo step con id: conta scrive echo "totale=42" >> $GITHUB_OUTPUT. Lo step successivo legge con ${{ steps.conta.outputs.totale }} e ottiene “42”. Questo funziona solo tra step dello stesso job — per passare dati tra job diversi servono meccanismi più avanzati (output del job o artifacts).


5. A quali 3 livelli puoi definire variabili d’ambiente (env)?

Mostra risposta

A livello di workflow (disponibili in tutti i job e step), di job (disponibili in tutti gli step di quel job), e di step (disponibili solo in quello step). Se la stessa variabile è definita a più livelli, il livello più specifico ha la precedenza: una variabile di step sovrascrive quella di job, che a sua volta sovrascrive quella di workflow. Le variabili si accedono con $NOME nei comandi shell (run:) o con ${{ env.NOME }} nelle espressioni YAML.


6. Cos’è ${{ github.actor }}?

Mostra risposta

È una variabile di contesto predefinita che contiene il nome utente GitHub della persona che ha attivato il workflow — chi ha fatto il push, aperto la PR, o cliccato “Run workflow”. Fa parte del contesto github.* che include informazioni sull’evento: github.repository (nome repo), github.ref_name (nome branch), github.sha (hash del commit), github.event_name (tipo di evento). Queste variabili sono disponibili in ogni step e sono utili per log, condizioni e messaggi personalizzati.


📚 Riepilogo della lezione

Cosa hai imparato oggi:
  • Job paralleli (default) vs sequenziali (con needs:)
  • env: a 3 livelli — workflow, job, step. Il più specifico vince.
  • Variabili predefinite: ${{ github.actor }}, ${{ github.ref_name }}, ${{ github.sha }}
  • Strategy matrix: testa su N configurazioni con un solo blocco di codice
  • if: condizioni su job e step — github.ref, success(), failure(), always()
  • GITHUB_OUTPUT: passa valori tra step con id: + ${{ steps.id.outputs.chiave }}
  • Pipeline tipica: lint + test (paralleli) → build → deploy (solo main)

Prossima lezione →

Lezione 27 — CI: test automatici
Scrivere test per il tuo progetto. Integrarli nel workflow.
Far fallire la PR se i test non passano.

Corso Git & GitHub · Per studenti e insegnanti · Lezione 26 di 45