Tre livelli di scope e una precedenza da ricordare

GitHub Actions distingue nettamente due oggetti che a prima vista sembrano intercambiabili. I secret sono valori cifrati, scritti una volta e mai più leggibili dall’interfaccia; le variables (variabili di configurazione) sono valori in chiaro, pensati per dati non sensibili come una region, un nome di cluster o una label di runner. Entrambi si definiscono agli stessi tre livelli: organization, repository ed environment.

La regola di precedenza è identica per i due oggetti e vale la pena impararla nella formulazione della documentazione: quando lo stesso nome esiste a più livelli, vince quello definito al livello più basso. Quindi l’environment batte il repository, e il repository batte l’organizzazione. Non c’è merge e non c’è errore: il valore del livello inferiore semplicemente oscura gli altri, in silenzio. È il meccanismo che rende praticabile il pattern tipico su scala di organizzazione, cioè definire il default una volta sola al centro e lasciare che i singoli repository o i singoli environment lo sovrascrivano quando serve.

A livello di organizzazione entra in gioco un secondo asse, la visibility: un secret o una variabile può essere esposto a tutti i repository, ai soli repository privati, oppure a un elenco selezionato. Un secret di organizzazione che il repository non “vede” per visibility non è semplicemente sovrascrivibile, non esiste affatto per quel workflow.

Un dettaglio operativo che spiega parecchi comportamenti apparentemente strani: i secret di organizzazione e di repository vengono letti quando la run viene messa in coda, mentre quelli di environment vengono letti quando parte il job che referenzia quell’environment. Se un job resta in attesa di un’approvazione manuale e nel frattempo il secret dell’environment viene ruotato, il job userà il valore nuovo.

Leggere secret e variabili: i contesti secrets e vars

Nel workflow si accede ai due oggetti tramite due contesti distinti: il contesto secrets e il contesto vars. I nomi seguono le stesse regole per entrambi — solo caratteri alfanumerici e underscore, non possono iniziare con un numero né con il prefisso riservato GITHUB_, e sono case insensitive quando li si referenzia. Nel contesto secrets è sempre presente anche GITHUB_TOKEN, generato automaticamente per ogni run.

jobs:
  deploy:
    runs-on: ${{ vars.RUNNER_LABEL }}
    environment: production
    steps:
      - run: ./deploy.sh
        env:
          API_TOKEN: ${{ secrets.API_TOKEN }}
          REGION: ${{ vars.AWS_REGION }}

I due contesti non sono utilizzabili negli stessi punti del file, ed è qui che l’esame ama insistere. Il contesto vars è ammesso praticamente ovunque, incluse le chiavi runs-on, environment, concurrency e le condizioni if a livello di job: si può quindi pilotare la scelta del runner con una variabile di organizzazione. Il contesto secrets, al contrario, è ammesso solo nelle chiavi dove un valore serve davvero a runtime — env, with, run, le credenziali di container e service — e non è utilizzabile in una condizione if, né a livello di job né di step. Non è nemmeno disponibile dentro una composite action, per scelta di sicurezza: se una composite action ha bisogno di un segreto, glielo si passa come input.

Sui log vale la redazione automatica: GitHub oscura dai log i valori registrati come secret. Ma la protezione si applica al valore esatto, non alle sue trasformazioni. Se lo script codifica il segreto in base64, lo spezza o lo incapsula in un JSON, la stringa risultante viene stampata in chiaro. Per i valori derivati esiste il workflow command add-mask, che registra a runtime un ulteriore valore da oscurare. La confusione tipica da evitare: la redazione è una rete di sicurezza contro le stampe accidentali, non un controllo di accesso, e non va mai usata come giustificazione per mettere un dato sensibile dentro una variabile invece che dentro un secret.

Che cosa non attraversa i confini

Due confini vanno conosciuti a memoria. Il primo è il fork: con la sola eccezione di GITHUB_TOKEN, i secret non vengono passati al runner quando il workflow è innescato da un repository forkato. È il motivo per cui le pipeline che richiedono credenziali su pull request esterne vanno ripensate, non “sbloccate”.

Il secondo è il reusable workflow. Il workflow chiamato deve dichiarare i secret che accetta sotto on.workflow_call, e il chiamante li passa esplicitamente oppure usa secrets: inherit. Attenzione a due limiti reali: inherit funziona solo verso workflow della stessa organizzazione o enterprise, e i secret arrivano soltanto al workflow chiamato direttamente — in una catena annidata ogni livello deve ripassarli, non cascano da soli. Quello che invece non attraversa mai il confine è il contesto env: le variabili definite a livello di workflow nel chiamante non vengono propagate al chiamato, e quelle definite nel chiamato non sono visibili al chiamante. Per riportare indietro un valore si usano gli outputs del reusable workflow. Molti candidati sbagliano proprio qui, convinti che secrets: inherit porti con sé anche env.

Gestire secret e variabili via REST API

Su scala di organizzazione la console non basta: rotazioni e onboarding di nuovi repository si fanno via REST API, e le due famiglie di endpoint funzionano in modo deliberatamente diverso.

Per i secret il flusso è in due passi. Prima si recupera la chiave pubblica dello scope con GET /orgs/ORG/actions/secrets/public-key (o l’equivalente su repository ed environment), poi si cifra il valore con LibSodium e si invia PUT sull’endpoint del secret passando encrypted_value e key_id. Il PUT è un upsert: la rotazione di un secret esistente è esattamente la stessa chiamata di creazione, il che rende lo script idempotente. Il GET su un secret restituisce solo metadati, mai il valore. Per i secret di organizzazione l’elenco dei repository abilitati si governa con gli endpoint dedicati .../repositories.

Per le variabili non c’è cifratura, e cambia anche il verbo: si crea con POST /repos/OWNER/REPO/actions/variables, si aggiorna con PATCH sul nome e si rimuove con DELETE. A livello di organizzazione il parametro visibilityall, private o selected — è obbligatorio. Ricordare l’asimmetria PUT-con-cifratura per i secret contro POST/PATCH in chiaro per le variabili è uno dei modi più rapidi per riconoscere la risposta corretta in una domanda sull’automazione della gestione credenziali.