Tre tipi di action, tre compromessi diversi

Una custom action è un’unità di lavoro riutilizzabile che si richiama da uno step con uses. GitHub ne prevede tre tipi, e la scelta non è stilistica: ognuno porta con sé vincoli di piattaforma precisi.

Una JavaScript action viene eseguita direttamente dal runner, senza container: il codice gira sul Node.js già presente sulla macchina. È il tipo più veloce, perché non c’è nessuna immagine da costruire o scaricare, ed è l’unico che funziona senza modifiche su runner Linux, Windows e macOS. Il prezzo è che la logica deve essere JavaScript puro: se dipende da un binario esterno, quel binario deve già trovarsi sul runner.

Una Docker container action impacchetta insieme codice e ambiente. Serve quando occorrono una versione specifica di sistema operativo, un interprete assente dall’immagine del runner o un insieme di dipendenze rigidamente controllato. Il vincolo che l’esame chiede spesso di riconoscere: le Docker action girano solo su Linux. I runner GitHub-hosted Windows e macOS non le eseguono, e un self-hosted deve essere Linux con Docker installato. Sono inoltre più lente, perché l’immagine va costruita o recuperata a ogni esecuzione.

Una composite action non contiene codice compilato: raggruppa una sequenza di step — comandi run e altre action richiamate a loro volta con uses — in un’unica unità. È il modo corretto per togliere duplicazione dai workflow che ripetono sempre gli stessi cinque step, e funziona su tutti i sistemi operativi. Confusione tipica da evitare: una composite action non è una reusable workflow. La prima sostituisce uno step dentro un job, la seconda sostituisce un job intero.

Che cosa deve contenere action.yml

Ogni action, di qualunque tipo, richiede un file di metadata chiamato action.yml (o action.yaml) nella directory radice della action. name e description sono obbligatori; author e branding restano facoltativi.

Gli inputs dichiarano i parametri. Ogni input vuole una description e accetta required, default e deprecationMessage per segnalare i parametri in dismissione. Attenzione a un dettaglio che genera bug silenziosi: dichiarare required: true non produce automaticamente un errore se il chiamante omette il valore, quindi la validazione va scritta nel codice. Gli input raggiungono il processo come variabili d’ambiente nella forma INPUT_<NOME>, in maiuscolo e con gli spazi convertiti in underscore.

Gli outputs si comportano diversamente a seconda del tipo. Per JavaScript e Docker basta la description, perché il valore viene scritto a runtime sul file indicato da GITHUB_OUTPUT. Per una composite action serve in più la chiave value, che mappa esplicitamente l’output sul risultato di uno step interno: senza value l’output resta vuoto. È uno dei punti su cui l’esame propone frammenti quasi identici e chiede quale sia corretto.

La sezione runs è ciò che distingue davvero i tre tipi:

runs:
  using: node24        # in alternativa: node20
  main: dist/index.js
  pre: dist/setup.js
  post: dist/cleanup.js
  post-if: always()

Per Docker si usa using: docker con image — che può valere Dockerfile per costruire in loco oppure puntare a un registry — più entrypoint, args, env e gli opzionali pre-entrypoint e post-entrypoint. Per le composite si usa using: composite con la lista steps, dove ogni step run deve dichiarare shell: qui è obbligatorio, a differenza dei workflow normali. Ogni step supporta anche env, working-directory, if, id e continue-on-error.

branding serve alla scheda su GitHub Marketplace: accetta icon, un’icona Feather della versione 4.28.0, e color, da scegliere in un elenco chiuso (white, black, yellow, blue, green, orange, red, purple, gray-dark).

Struttura delle directory e come si referenzia

Se la action è destinata alla condivisione, conviene un repository dedicato: versionamento e issue restano separati dal codice applicativo, e il repository deve essere pubblico per pubblicarla sul Marketplace. Se serve solo internamente si colloca sotto .github/actions/nome-action/, e lo stesso repository può ospitarne più di una in sottodirectory distinte.

Per una JavaScript action la struttura tipica è action.yml, package.json, il sorgente in src/ e il bundle in dist/. La directory node_modules non va committata: si usa un bundler per produrre l’unico file distribuibile indicato in main. Per una Docker action servono action.yml, il Dockerfile e di norma un entrypoint.sh.

Nel workflow il riferimento è owner/repo@ref, dove la forma davvero immutabile è lo SHA completo del commit, non un tag. Una action in sottodirectory si richiama con owner/repo/percorso@ref, un’immagine con il prefisso docker://. Per una action nello stesso repository esistono due forme: ./percorso, che richiede un checkout precedente, e il riferimento self-repository $/percorso, che risolve allo stesso repository al commit in esecuzione e non richiede il checkout: è la forma oggi raccomandata.

Dockerfile: i dettagli che fanno fallire il job

Le Docker action hanno regole proprie, ed è qui che si concentrano gli errori di permessi. Non usare l’istruzione USER: la action deve girare come utente Docker predefinito, altrimenti perde l’accesso alla directory GITHUB_WORKSPACE montata da GitHub. Evita anche WORKDIR, perché GitHub monta il workspace e lo imposta come working directory, sovrascrivendo l’istruzione.

L’entrypoint dichiarato nel metadata sovrascrive l’ENTRYPOINT del Dockerfile, e gli args sovrascrivono CMD. Nella forma exec le variabili d’ambiente non vengono espanse: per ottenere la sostituzione occorre invocare esplicitamente una shell. Lo script entrypoint.sh deve avere il bit di esecuzione registrato in Git, altrimenti si perde nei cloni e nei fork. Sono disponibili solo le capability Linux predefinite: non se ne possono aggiungere né rimuovere.

Infine il segnale di esito, identico per tutti i tipi: exit code 0 significa successo, qualunque valore diverso da zero fa fallire lo step. In una JavaScript action si usa core.setFailed del toolkit; in una Docker o composite action basta terminare con exit 1.