name: Quality Gates on: workflow_call: inputs: working-directory: description: 'Cartella che contiene package.json, relativa alla root del repo. Default: la root.' type: string default: '.' node-version: type: string default: '24.16.0' npm-version: type: string default: '' owns-auth-roles: description: >- Solo per il repository proprietario di auth.ruoli (node-user-profiling): gli consente di dichiarare ruoli nelle proprie migrazioni. Ogni altro repo deve lasciarlo a false, e il gate qui sotto glielo impedisce. type: boolean default: false jobs: quality-gates: runs-on: ubuntu-latest defaults: run: working-directory: ${{ inputs.working-directory || '.' }} steps: - name: Checkout uses: https://gitea.com/actions/checkout@v6 - name: Setup Node.js uses: https://gitea.com/actions/setup-node@v4 with: node-version: ${{ inputs.node-version || '24.16.0' }} - name: Upgrade npm if: inputs.npm-version != '' run: npm install -g npm@${{ inputs.npm-version }} - name: Cache npm uses: https://github.com/actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Configure npm private registry run: | echo "@pzeta:registry=https://gitea.pzetatouch.it/api/packages/PZeta_Touch/npm/" >> ~/.npmrc echo "//gitea.pzetatouch.it/api/packages/PZeta_Touch/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc - name: Nessun ruolo dichiarato fuori da node-user-profiling shell: bash env: # La deroga stava nel campo `if:` dello step, e li' non arriva: nel # workflow_call di act_runner i valori `with:` del chiamante non # raggiungono il contesto `inputs` in quella posizione, quindi # `!inputs.owns-auth-roles` restava vero anche con l'input a true e il # gate falliva proprio in node-user-profiling, l'unico repository # autorizzato a dichiarare ruoli. Letta come variabile d'ambiente la # deroga arriva, e la si confronta come stringa. OWNS_AUTH_ROLES: ${{ inputs.owns-auth-roles }} run: | # `auth.ruoli` appartiene a node-user-profiling e il suo contratto (seed # 13, esteso dalla migrazione 19) vieta agli altri moduli di crearne. # Non e' una formalita': il claim `role` del JWT e' il primo ruolo # applicativo dell'utente e PostgREST ci fa un SET ROLE, ma i ruoli # PostgreSQL esistono solo per i tredici livelli di piattaforma. Un # ruolo per-modulo in quel claim e' una sessione che non parte. # Nove microfrontend lo avevano disatteso: 21 ruoli, 27 su 35 senza # omonimo PostgreSQL. Questo controllo esiste perche' non si ripeta. set -uo pipefail # Il nome del repository e' la rete di sicurezza della deroga: se # nemmeno l'env dovesse arrivare, il proprietario dello schema si # riconosce lo stesso e non resta bloccato dal proprio gate. Le due # condizioni dicono la stessa cosa, una dichiarata e una constatata. repo="${GITHUB_REPOSITORY:-}" if [ "${OWNS_AUTH_ROLES:-}" = "true" ] || [ "${repo##*/}" = "node-user-profiling" ]; then echo "Repository proprietario di auth.ruoli: controllo non applicabile." exit 0 fi dirs="" for d in migrations seeds sql database/migrations; do [ -d "$d" ] && dirs="$dirs $d" done if [ -z "$dirs" ]; then echo "Nessuna cartella di migrazioni: controllo non applicabile." exit 0 fi # Il boundary dopo `ruoli` evita che auth.ruolipermessi finisca fra i # match; il filtro sulle righe che iniziano per -- lascia passare i # commenti, che spiegano spesso proprio questa regola. # Fuori dal controllo le cartelle che non alimentano il catalogo delle # migrazioni: `una-tantum` raccoglie i delta generati per riallineare a # mano un tenant, `rollback` gli script di ritorno. Sono strumenti # operativi, spesso fotografie di uno stato passato, e riscriverli # significherebbe falsare la storia che documentano. hits=$(grep -rniE --include='*.sql' \ --exclude-dir=una-tantum --exclude-dir=rollback \ 'insert[[:space:]]+into[[:space:]]+auth\.ruoli([[:space:]]|\(|$)' \ $dirs 2>/dev/null \ | awk -F: '{ riga=$0; sub(/^[^:]*:[^:]*:/, "", riga); gsub(/^[[:space:]]+/, "", riga); if (riga !~ /^--/) print }' || true) if [ -n "$hits" ]; then echo "::error::Questo repository dichiara ruoli in auth.ruoli, che appartiene a node-user-profiling." echo "" echo "$hits" echo "" echo "Cosa fare invece: dichiarare un GRUPPO d'area con i permessi del modulo." echo " INSERT INTO auth.gruppi (nome, nomecompleto, descrizione, tipogruppo, idruololivello)" echo " ... poi INSERT INTO auth.gruppipermessi per collegarvi i propri permessi." echo "" echo "Il livello che accompagna il gruppo si sceglie fra i tredici di piattaforma" echo "(manager, contabile, auditor, cfo, dipendente, viewer, operatore, cliente):" echo "sono gli unici ad avere un ruolo PostgreSQL, senza il quale il SET ROLE fallisce." echo "Esempio: vue-timesheet/migrations/12_gruppi_area.sql" exit 1 fi echo "Nessun INSERT INTO auth.ruoli: contratto rispettato." - name: Dockerfile - registry @pzeta e ARG per stage shell: bash # BLOCCANTE dal 12/09/2026, chiusa la tappa 2 di PZeta_Touch/flux-repo#133. # Prima era solo un avviso, perche' 56 repo avevano ancora uno dei due # difetti e bloccare li avrebbe fatti diventare rossi tutti insieme. Ora # sono corretti: su 238 Dockerfile del portale ne restano difettosi due, # `vue-conservazione` e `telegram-media-bot`, che non sono repository git # e quindi non passano di qui. run: | # Due difetti che hanno rotto le release di node-xmlvalidation per due # mesi senza che nessuno se ne accorgesse (PZeta_Touch/flux-repo#129, # #133): (a) il registry @pzeta scritto in minuscolo nell'.npmrc # generato dal Dockerfile, mentre i lockfile risolvono su PZeta_Touch; # (b) una ARG usata in uno stage che non la dichiara, e che li' arriva # vuota. Insieme, `npm install -g npm@` installa npm 12, che rifiuta # l'URL scritto diverso (EALLOWREMOTE). Questo job non puo' accorgersene # da solo: qui l'immagine non si costruisce, e la release fallisce solo # al tag. set +e # La radice del repository e non working-directory: i Dockerfile stanno # spesso fuori dalla cartella del package. cd "${GITHUB_WORKSPACE:-.}" || exit 0 files=$(find . \( -name node_modules -o -name .git -o -name dist -o -name build \ -o -name .nuxt -o -name .output -o -name coverage \) -prune -o \ -type f \( -name 'Dockerfile' -o -name 'Dockerfile.*' -o -name '*.Dockerfile' \ -o -name '*.dockerfile' \) -print 2>/dev/null | sed 's|^\./||' | sort) if [ -z "$files" ]; then echo "Nessun Dockerfile: controllo non applicabile." exit 0 fi # (b) ARG fuori scope: una ARG vale SOLO nello stage che la dichiara. # Quelle dichiarate prima del primo FROM valgono solo nelle righe FROM, # e vanno ridichiarate dentro lo stage per essere viste dai RUN # (documentazione Docker, "Scoping" in build/building/variables). # Nessuna eredita' da `FROM `: le ENV si ereditano, le ARG no. # Una versione precedente di questa guardia presumeva il contrario e # lasciava passare proprio i casi come nuxt-vue-components-docs, dove # la ARG dichiarata in `base` veniva usata in `deps` (#133). arg_scope() { ${AWK:-awk} ' function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s } # Nomi dichiarati da ARG/ENV: "A", "A=1", "A=1 B=2", "A valore". function declared(rest, out, n, i, t) { rest = trim(rest) split("", out) if (rest !~ /=/) { n = split(rest, t, /[ \t]+/); if (n > 0) out[t[1]] = 1; return } n = split(rest, t, /[ \t]+/) for (i = 1; i <= n; i++) if (t[i] ~ /^[A-Za-z_][A-Za-z0-9_]*(=|$)/) { sub(/=.*/, "", t[i]); out[t[i]] = 1 } } function handle(kw, rest, ln, n, i, t, base, alias, k, p, s, v, d) { if (pass == 1) { if (kw == "ARG") { declared(rest, d); for (k in d) allargs[k] = 1 } return } if (kw == "FROM") { n = split(trim(rest), t, /[ \t]+/); i = 1 while (i <= n && t[i] ~ /^--/) i++ base = tolower(t[i]); alias = "" if (i + 2 <= n && toupper(t[i + 1]) == "AS") alias = tolower(t[i + 2]) stages++ stage = (alias != "" ? alias : "#" stages) # Scope nuovo e vuoto: niente viene ereditato dallo stage di base. # (Nessun apostrofo qui dentro: chiuderebbe la stringa awk.) split("", scope) return } if (stage == "") return if (kw == "ARG") { declared(rest, d); for (k in d) { scope[k] = 1; varof[stage, k] = 1 }; return } s = rest while (match(s, /[$][{]?[A-Za-z_][A-Za-z0-9_]*/)) { v = substr(s, RSTART, RLENGTH); sub(/^[$][{]?/, "", v) s = substr(s, RSTART + RLENGTH) # In un RUN espande la shell: una variabile assegnata nel comando # stesso (VERSIONE="$(...)" && ... ${VERSIONE}) non va scambiata per la ARG. if (kw == "RUN" && match(rest, "(^|[^A-Za-z0-9_$])" v "=")) continue if ((v in allargs) && !(v in scope) && !(v in predefined) && !((ln, v) in seen)) { seen[ln, v] = 1 printf "%d\t%s\t%s\n", ln, v, stage } } if (kw == "ENV") { declared(rest, d); for (k in d) { scope[k] = 1; varof[stage, k] = 1 } } } function flush() { if (buf != "" && match(buf, /^[ \t]*[A-Za-z]+/)) { kw = toupper(trim(substr(buf, RSTART, RLENGTH))) handle(kw, substr(buf, RSTART + RLENGTH), start) } buf = "" } BEGIN { np = split("TARGETPLATFORM TARGETOS TARGETARCH TARGETVARIANT BUILDPLATFORM BUILDOS BUILDARCH BUILDVARIANT HTTP_PROXY HTTPS_PROXY FTP_PROXY NO_PROXY ALL_PROXY http_proxy https_proxy ftp_proxy no_proxy all_proxy", pp, " ") for (i = 1; i <= np; i++) predefined[pp[i]] = 1 } FNR == 1 { pass++; buf = ""; stage = ""; stages = 0; split("", scope); split("", varof) } { line = $0; sub(/\r$/, "", line) if (line ~ /^[ \t]*#/) next # commenti, anche dentro una continuazione if (buf == "" && line ~ /^[ \t]*$/) next if (buf == "") start = FNR if (line ~ /\\[ \t]*$/) { sub(/\\[ \t]*$/, " ", line); buf = buf line; next } buf = buf line flush() } ' "$1" "$1" } # Caso di prova: se l'awk di questo runner non riconosce una ARG fuori # scope nota, il controllo (b) non e' affidabile e lo si dice, invece di # dichiarare in regola un Dockerfile che non si e' potuto leggere. argcheck=1 prova=$(mktemp) printf 'ARG X=1\nFROM scratch AS a\nRUN echo ${X}\n' > "$prova" if [ "$(arg_scope "$prova" 2>/dev/null | cut -f2)" != "X" ]; then argcheck=0 echo "::warning::Guardia Dockerfile: l'analisi delle ARG non funziona con l'awk di questo runner ($(${AWK:-awk} -W version 2>&1 | head -1)). Il controllo (b) e' saltato, il (a) resta." fi rm -f "$prova" findings="" for f in $files; do # (a) registry: npm confronta l'URL del lockfile come stringa, e i lockfile # risolvono tutti su PZeta_Touch. reg=$(grep -nE 'packages/(pzeta_touch|pzeta)/' "$f" 2>/dev/null | cut -d: -f1 | tr '\n' ' ' | sed 's/ *$//') if [ -n "$reg" ]; then findings="${findings}${f} ${reg%% *} registry $(echo "$reg" | sed 's/ /, /g') - " fi # (b) ARG fuori scope, con arg_scope definita sopra. args="" [ "$argcheck" = "1" ] && args=$(arg_scope "$f" 2>/dev/null) while IFS=' ' read -r n v st; do [ -n "$n" ] || continue findings="${findings}${f} ${n} arg ${v} ${st} " done <> "$GITHUB_STEP_SUMMARY" 2>/dev/null fi exit 1 - name: Check outdated packages run: npm outdated || true - name: Install dependencies run: npm ci - name: Security audit run: npm audit --audit-level=high || true - name: Lint run: npm run lint:check - name: Type check run: npm run typecheck - name: Format check run: npm run format:check - name: Build run: npm run build - name: Test run: npm run test