Job, step, condizioni e dipendenze

Un workflow è un insieme di job, e ogni job è una sequenza di step. Il job è l’unità di isolamento: gira su un runner proprio, con un file system proprio, e per impostazione predefinita i job di uno stesso workflow partono in parallelo. Gli step invece condividono lo stesso runner e la stessa directory di lavoro, e girano sempre in ordine. Da questa distinzione discende quasi tutto il resto: due step si passano file semplicemente scrivendoli su disco, mentre due job devono passarsi dati con gli artifact o con gli outputs.

Ogni job ha un identificatore (la chiave sotto jobs) che deve iniziare con una lettera o un underscore e contenere solo caratteri alfanumerici, trattini o underscore; name è invece l’etichetta leggibile mostrata nell’interfaccia. Uno step può eseguire un comando con run oppure invocare un’action con uses, mai entrambi nello stesso step.

needs e il grafo delle dipendenze

needs è l’unico modo per imporre un ordine fra job. Accetta una stringa o un array: il job parte solo quando tutti i job elencati sono terminati con successo. Combinando più needs si costruisce un grafo diretto, non una semplice lista: tre job che dipendono dallo stesso build partono insieme appena build finisce, e un job finale che li elenca tutti fa da punto di ricongiungimento.

La regola da memorizzare è la propagazione: se un job fallisce o viene saltato, tutti i job che lo richiedono vengono saltati a loro volta, a cascata lungo il grafo, a meno che non usino una condizione che li faccia proseguire comunque. È il motivo per cui un job di notifica scritto senza condizione non parte mai proprio quando servirebbe.

needs governa anche la visibilità dei dati. Uno step scrive un valore usando un id e il file GITHUB_OUTPUT, il job lo promuove nella mappa outputs, e il job a valle lo legge dal contesto needs, indicizzato per identificatore di job e nome di output. Senza needs quel contesto è vuoto: la dipendenza dichiarata non è solo ordinamento.

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.meta.outputs.version }}
    steps:
      - id: meta
        run: echo "version=1.4.2" >> "$GITHUB_OUTPUT"
  notify:
    needs: build
    if: ${{ !cancelled() }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build ${{ needs.build.outputs.version }}"

if e le status check function

if esiste a due livelli: jobs.<job_id>.if decide se parte il job intero, steps[*].if decide il singolo step. Un job che non soddisfa la condizione risulta skipped, e attenzione: lo stato skipped viene riportato come successo, quindi non blocca il merge di una pull request nemmeno se quel controllo è marcato come obbligatorio. È una delle confusioni tipiche su cui l’esame insiste.

Ogni if porta con sé un controllo implicito: se non scrivi altro vale success(), cioè “prosegui solo se finora è andato tutto bene”. Le status check function servono a sovrascriverlo: success(), failure(), cancelled() e always(). failure() è vero se qualcosa a monte è fallito, incluso un job antenato nel grafo; se la combini con altre condizioni la funzione deve restare presente, altrimenti torna in gioco il success() implicito.

always() merita cautela. Fa girare il passo in qualunque circostanza, cancellazione compresa, e la documentazione sconsiglia di usarlo su operazioni che possono bloccarsi, perché il run resta appeso fino al timeout. Quando l’intento è “gira sia in caso di successo sia di fallimento, ma non se ho annullato”, la forma corretta è la negazione di cancelled(), come nell’esempio sopra. Da non confondere con continue-on-error, che non decide se qualcosa parte ma se il suo fallimento deve far fallire il resto.

Per capire perché un job è stato saltato, scarica l’archivio dei log del run e apri system.txt del job: trovi la condizione originale (Evaluating), la stessa con i valori sostituiti a runtime (Expanded) e il risultato.

Permissions, defaults e workflow command

permissions regola cosa può fare il GITHUB_TOKEN e si dichiara a livello di workflow oppure di singolo job, con il valore del job che prevale. Il comportamento chiave, molto amato dalle domande d’esame: se specifichi anche un solo ambito, tutti gli altri passano a none. Elencare contents: read quindi non “aggiunge” un permesso, ma azzera tutto il resto, ed è esattamente il principio del minimo privilegio. Esistono le scorciatoie read-all e write-all e la mappa vuota per revocare tutto; ricorda id-token: write quando ti serve OIDC.

defaults.run evita di ripetere shell e working-directory su ogni step. Vale a livello di workflow o di job, con la definizione più specifica che prevale, e non ammette contesti o espressioni al suo interno.

I workflow command sono il canale con cui uno step parla con il runner. Le forme moderne scrivono su file: GITHUB_OUTPUT per gli output, GITHUB_ENV per esportare una variabile agli step successivi, GITHUB_PATH per il PATH, GITHUB_STEP_SUMMARY per il riepilogo in Markdown. Le vecchie set-output, set-env e add-path sono deprecate. Restano nella forma con i due punti le annotazioni ::notice::, ::warning:: ed ::error::, il raggruppamento ::group:: con ::endgroup:: e ::add-mask:: per oscurare un valore nei log. Infine distingui env, le variabili d’ambiente definite nel workflow su tre livelli con precedenza al più specifico, dal contesto vars delle configuration variables definite su repository, organizzazione o environment.