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
🔧 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
needsper 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
ifper 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.
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”
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.
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”
| 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.
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
Puoi combinare più dimensioni nella matrice. GitHub crea un job per ogni combinazione:
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.
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 |
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:
– 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
on:
env:
jobs:
nome-job:
runs-on:
needs:
if:
env:
strategy:
steps:
uses: action@v
with:
param: valore
– name: “descrizione”
run: comando
env:
VAR: valore
if: condizione
id: mio-id
${{ matrix.* }}
${{ steps.id.outputs.* }}
${{ env.VAR }}
${{ secrets.* }}
success()
failure()
always()
contains(a, b)
💻 2. Esempi guidati
Esempio A — Workflow completo: test + lint + deploy
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
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
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)
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)
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)
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)
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
- 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