@elia97/officina 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,7 +76,7 @@ Per Lighthouse un sito SSR si misura con `check lighthouse <url>`, sulla produzi
76
76
 
77
77
  Lo smoke legge gli header di sicurezza sulla pagina a cui arriva la radice. Se `/` è un redirect sulla stessa origine — il 302 di lingua che Astro emette con `i18n.routing.redirectToDefaultLocale`, per esempio — li cerca sulla destinazione, perché il redirect non porta la CSP delle pagine renderizzate, e il nome del controllo dice dove li ha letti: `header content-security-policy su /it`. Un redirect verso un'altra origine, o senza `location`, è un fallimento. Anche l'attesa che l'alias di produzione punti al deployment nuovo conta un 3xx come una risposta.
78
78
 
79
- Lo smoke visita anche le pagine rese a richiesta, quelle di `src/pages` con `export const prerender = false`. Ognuna risponde 200, oppure con un redirect sulla stessa origine, come una pagina protetta che rimanda all'accesso; un 200 è HTML e porta il meta `Content-Security-Policy` con la direttiva `script-src`, quello che il middleware dei template scrive nelle pagine rese a richiesta. Una pagina dinamica si visita col suo rappresentante di `routes.representatives`, altrimenti si salta dicendolo. Queste pagine non entrano nel controllo della barra finale né in Lighthouse. Nella 0.12.0 e nella 0.13.0 un'anomalia è un avviso, che lo smoke stampa con `·` senza fallire; dalla 0.14.0 sarà un fallimento. officina riconosce una pagina resa a richiesta dalla dichiarazione: con `output: 'server'`, una pagina senza `prerender = false` resta per tutti i gate una pagina prerenderizzata, e lo smoke la visita come le altre, senza cercarne la CSP.
79
+ Lo smoke visita anche le pagine rese a richiesta, quelle di `src/pages` con `export const prerender = false`. Ognuna risponde 200, oppure con un redirect sulla stessa origine, come una pagina protetta che rimanda all'accesso; un 200 è HTML e porta il meta `Content-Security-Policy` con la direttiva `script-src`, quello che il middleware dei template scrive nelle pagine rese a richiesta. Una pagina dinamica si visita col suo rappresentante di `routes.representatives`, altrimenti si salta dicendolo. Queste pagine non entrano nel controllo della barra finale né in Lighthouse. Un'anomalia fa fallire lo smoke. officina riconosce una pagina resa a richiesta dalla dichiarazione: con `output: 'server'`, una pagina senza `prerender = false` resta per tutti i gate una pagina prerenderizzata, e lo smoke la visita come le altre, senza cercarne la CSP.
80
80
 
81
81
  Quando fallisce, lo smoke dice che la produzione è online e rotta, e consiglia di tornare indietro dalla dashboard di Vercel promuovendo l'ultimo deployment di produzione sano. Con `features.database` accesa aggiunge che lo schema non torna indietro: le migrazioni già applicate restano, e il deployment promosso regge solo se sono compatibili con il suo codice, la regola di «Migrazioni compatibili con la produzione» in § Action per i workflow.
82
82
 
@@ -152,7 +152,7 @@ I passi dei workflow stanno in `actions/` di questo repository, come composite a
152
152
 
153
153
  | Action | Passi | Cosa resta nel workflow del progetto |
154
154
  |---|---|---|
155
- | `actions/ci` | checkout, node, install, `astro sync`, con `migrate` le migrazioni del database della corsa, `pnpm run ci`, i canary di `check secrets`, build, `perf:bundle`, `check:links`, `check:secrets`; con `e2e: 'true'` anche Playwright | trigger, permessi, il job `ci`; con un database, il branch Neon della corsa, creato e cancellato intorno all'action, e lo script delle migrazioni e il suo indirizzo passati in `with:` |
155
+ | `actions/ci` | checkout, node, install, `astro sync`, con `migrate` le migrazioni del database della corsa, `pnpm run ci`, i canary di `check secrets`, build, `perf:bundle`, `check:links`, `check:secrets`; con `e2e: 'true'` anche Playwright; se la PR o il push toccano solo documenti, dopo l'install soltanto `check:language`, `check:routes`, `check:roadmap` e `doctor` | trigger, permessi, il job `ci`; con un database, il branch Neon della corsa, creato e cancellato intorno all'action, e lo script delle migrazioni e il suo indirizzo passati in `with:` |
156
156
  | `actions/review` | `astro sync`, poi `fallow review` sul diff contro il merge-base | il job informativo |
157
157
  | `actions/deploy` | risoluzione del tag, stessa versione di officina, gate, `vercel pull`, con `migrate` le migrazioni, `build`, controllo della build, `deploy`, smoke; espone `url` | trigger, `environment`, i tre segreti Vercel passati in `with:`, il job che controlla se i segreti ci sono; con un database, lo script delle migrazioni e il segreto `PRODUCTION_DATABASE_URL` passati in `with:` |
158
158
  | `actions/lighthouse` | build equivalente alla produzione e `pnpm run lhci` | trigger, etichetta, `continue-on-error` |
@@ -183,6 +183,8 @@ jobs:
183
183
 
184
184
  I tre segreti sono input obbligatori e l'action li mette nell'`env` dei soli tre passi che chiamano `vercel`, come `database-url` in quello delle migrazioni. Nell'`env` del job li vedrebbero anche `pnpm install` e gli script di installazione di ogni dipendenza, che girano codice di terzi con in mano un token di deploy o il database di produzione.
185
185
 
186
+ **Il percorso veloce dei documenti.** Dopo l'install `actions/ci` lancia `officina scope`, che confronta l'`HEAD` del checkout con la base dell'evento: per una PR la sua base, mentre l'`HEAD` è il merge commit di GitHub; per un push il commit `before`. Se ogni file cambiato è un documento, cioè un `.md` alla radice, sotto `docs/`, sotto `.github/` o sotto `.claude/`, l'action lancia soltanto `check:language`, `check:routes`, `check:roadmap` e `doctor`, e salta il resto: `astro sync`, le migrazioni, `pnpm run ci`, la build, `perf:bundle`, `check:links`, `check:secrets` e gli e2e. `doctor` resta perché alcuni Markdown, come gli agenti in `.claude/agents/` o `docs/DECISIONS.md`, sono residui che vieta. Un'immagine o uno snippet in `docs/`, e `src/content/` anche quando è Markdown, fanno il percorso completo. Il job parte comunque, perché `ci` è un check obbligatorio e un `paths-ignore` sulla PR lo lascerebbe appeso. Nel dubbio il percorso è completo: un evento senza base, come il primo push di un branch, un diff che non riesce, nessun file cambiato, un file che non è un documento, che il log del passo nomina, o `officina scope` che fallisce, e allora il passo lo dice con un avviso. Il confronto è fra due alberi e scarica la sola base, senza la storia: il percorso veloce vuol dire che, fuori dai documenti, l'albero da provare è identico a quello della base, e una base andata avanti nel frattempo aggiunge file al diff e riporta il percorso completo. Uno spostamento conta come cancellazione più aggiunta, quindi un file di codice spostato in `docs/` non passa per documento. Su un push il percorso veloce non riprova il codice di `before`, come un `paths-ignore`.
187
+
186
188
  **Il deploy lanciato a mano.** Con `workflow_dispatch` il workflow viene dal ramo scelto in «Use workflow from», e il pacchetto dal lockfile del tag che si deploya: le due metà possono avere due versioni di officina diverse, e un passo dell'action più nuova può chiamare un'opzione che gli script del tag non conoscono. `actions/deploy` confronta le due versioni subito dopo l'install, prima di ogni script del progetto e di `vercel pull`, e si ferma dicendole se non coincidono o se il tag non installa officina. Un tag più vecchio si deploya lanciando Deploy da quel tag: il suo `deploy.yml` dichiara l'action della sua versione, con i gate di allora. Perché si possa, l'environment `production` del progetto deve ammettere i tag fra i ref da cui si deploya.
187
189
 
188
190
  **Il database di test.** Ogni corsa della CI migra un database suo: un branch Neon che il `ci.yml` del progetto crea prima di `actions/ci`, figlio di un branch vuoto, e cancella alla fine. Così due corse in parallelo, come la CI di una PR e quella di `main`, non vedono l'una le migrazioni dell'altra. Il genitore resta vuoto perché il migratore di drizzle-orm 0.45 applica una migrazione solo se è più recente dell'ultima applicata: ogni corsa le applica tutte, da zero, e nessuna si salta.
@@ -217,7 +219,7 @@ I tre segreti sono input obbligatori e l'action li mette nell'`env` dei soli tre
217
219
  api_key: ${{ secrets.NEON_API_KEY }}
218
220
  ```
219
221
 
220
- Il genitore, `test` nell'esempio, è il branch di test di prima. Si svuota una volta con `DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP SCHEMA IF EXISTS drizzle CASCADE;`, e da lì nessuno lo migra. Il primo passo legge la chiave dal proprio `env`, non da quello del job, dove la vedrebbero anche le dipendenze. `expires_at` cancella il branch anche quando il passo finale non arriva. La chiave dell'API di Neon crea e cancella i branch del progetto, quindi va limitata al progetto dove Neon lo permette, e non va data a Dependabot, che la passerebbe al codice di ogni aggiornamento. `doctor` avvisa quando `ci.yml` passa ancora un branch condiviso, `TEST_DATABASE_URL`.
222
+ Il genitore, `test` nell'esempio, è il branch di test di prima. Si svuota una volta con `DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP SCHEMA IF EXISTS drizzle CASCADE;`, e da lì nessuno lo migra. Il primo passo legge la chiave dal proprio `env`, non da quello del job, dove la vedrebbero anche le dipendenze. `expires_at` cancella il branch anche quando il passo finale non arriva. La chiave dell'API di Neon crea e cancella i branch del progetto, quindi va limitata al progetto dove Neon lo permette, e non va data a Dependabot, che la passerebbe al codice di ogni aggiornamento. `doctor` fallisce quando `ci.yml` passa ancora un branch condiviso, `TEST_DATABASE_URL`.
221
223
 
222
224
  Prima di `pnpm run ci` l'action lancia `officina migrate --script <migrate>`, che legge `database.migrationUrlKey` da `officina.config.ts` e dà l'indirizzo allo script sotto quella chiave, e solo sotto quella. `pnpm run ci` lo riceve come `TEST_DATABASE_URL`, per i test di integrazione. La build e `check:secrets` lo ricevono come `DATABASE_URL`, al posto del canary: una pagina prerenderizzata dal database lo legge davvero, e `check secrets` cerca il valore che la build ha ricevuto. Gli altri passi non lo vedono, e senza `database-url` i comandi ricevono l'ambiente del job com'è. Le PR di Dependabot e quelle da un fork non ricevono `NEON_API_KEY`: il branch non nasce, `database-url` arriva vuoto, e con `migrate` l'action non migra, lo dice con un `::notice::` e prosegue. I test di integrazione si saltano da soli, e la build riceve il canary, quindi una pagina prerenderizzata dal database non ha un database da leggere, e la build si ferma.
223
225
 
@@ -230,7 +232,7 @@ Prima di `pnpm run ci` l'action lancia `officina migrate --script <migrate>`, ch
230
232
  database-url: ${{ secrets.PRODUCTION_DATABASE_URL }}
231
233
  ```
232
234
 
233
- Dopo `vercel pull` e `check:placeholders --env`, subito prima di `vercel build`, l'action lancia `officina migrate --production --script <migrate>`, che dà l'indirizzo allo script sotto `database.migrationUrlKey`, e solo sotto quella. `PRODUCTION_DATABASE_URL` è un segreto dell'environment `production` del repository, che il job di deploy dichiara: lo leggono solo i job di quell'environment, e solo dai ref che le sue regole ammettono, da restringere a `main` e ai tag di release, perché senza regole lo legge un job su qualunque branch. È l'indirizzo diretto al database, senza pooler, come `DATABASE_URL_UNPOOLED` su Neon. Senza `database-url` il passo delle migrazioni si ferma, e il deploy con lui. `doctor` avvisa anche quando il deploy riceve `TEST_DATABASE_URL`, o la CI `PRODUCTION_DATABASE_URL`: le migrazioni andrebbero sul database sbagliato, uscendo 0. Il runner maschera il segreto intero, ma non la password da sola: il comando registra con `::add-mask::` l'indirizzo e la sua password, com'è e decodificata, su stdout e su stderr, e lancia lo script quando le maschere sono scritte.
235
+ Dopo `vercel pull` e `check:placeholders --env`, subito prima di `vercel build`, l'action lancia `officina migrate --production --script <migrate>`, che dà l'indirizzo allo script sotto `database.migrationUrlKey`, e solo sotto quella. `PRODUCTION_DATABASE_URL` è un segreto dell'environment `production` del repository, che il job di deploy dichiara: lo leggono solo i job di quell'environment, e solo dai ref che le sue regole ammettono, da restringere a `main` e ai tag di release, perché senza regole lo legge un job su qualunque branch. È l'indirizzo diretto al database, senza pooler, come `DATABASE_URL_UNPOOLED` su Neon. Senza `database-url` il passo delle migrazioni si ferma, e il deploy con lui. `doctor` fallisce anche quando il deploy riceve `TEST_DATABASE_URL`, o la CI `PRODUCTION_DATABASE_URL`: le migrazioni andrebbero sul database sbagliato, uscendo 0. Il runner maschera il segreto intero, ma non la password da sola: il comando registra con `::add-mask::` l'indirizzo e la sua password, com'è e decodificata, su stdout e su stderr, e lancia lo script quando le maschere sono scritte.
234
236
 
235
237
  **Il segreto delle migrazioni.** Su Vercel la chiave delle migrazioni non serve, e se l'applicazione la usa a runtime può restare Secret. Una risorsa del Marketplace impostata su «Production only», che protegge le sue credenziali come Secret, non ferma più le migrazioni. Rende però Secret anche `DATABASE_URL`, e una build che legge il database la riceve come `[SENSITIVE]` e si ferma, a migrazioni già applicate. Il prezzo è un secondo posto da tenere allineato: quando cambia l'indirizzo, che sia la password, l'host o il database, il segreto va aggiornato anche su GitHub. Con una password vecchia il deploy si ferma al passo delle migrazioni. Se invece il database vecchio risponde ancora, le migrazioni vanno lì, e il deploy non se ne accorge.
236
238
 
package/dist/bin.js CHANGED
@@ -22,6 +22,7 @@ const USAGE = `Uso:
22
22
  officina gen icons le icone del manifest da public/favicon.svg
23
23
  officina doctor cosa manca al progetto perché i generatori funzionino
24
24
  officina migrate --script <nome> le migrazioni sotto database.migrationUrlKey: da TEST_DATABASE_URL, o con --production da PRODUCTION_DATABASE_URL
25
+ officina scope in actions/ci, docs-only=true se la PR o il push toccano solo documenti
25
26
 
26
27
  Opzioni dei gate sui sorgenti: --diff, --base <ref>, --head <ref>, --strict, --format text|github
27
28
  bundle, links e secrets leggono dist/client; smoke [url] e analytics [GTM-…] vanno in rete; lighthouse [--local]
@@ -35,6 +36,7 @@ const GROUPS = {
35
36
  : { load: () => import('./gen/run.js'), args },
36
37
  doctor: (args) => ({ load: () => import('./checks/doctor.js'), args }),
37
38
  migrate: (args) => ({ load: () => import('./checks/migrate.js'), args }),
39
+ scope: (args) => ({ load: () => import('./checks/scope.js'), args }),
38
40
  };
39
41
  export async function run(argv) {
40
42
  const [group = '', ...rest] = argv;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(): number;
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ // actions/ci/action.yml appende lo stdout a $GITHUB_OUTPUT: lì va solo `docs-only=…`, la ragione va su stderr.
3
+ import process from 'node:process';
4
+ import { ciScope } from '../lib/ci-scope.js';
5
+ import { changedSince, eventBase } from '../lib/git.js';
6
+ export function main() {
7
+ const { docsOnly, reason } = ciScope(eventBase(), changedSince);
8
+ console.error(reason);
9
+ console.log(`docs-only=${docsOnly}`);
10
+ return 0;
11
+ }
12
+ if (import.meta.main)
13
+ process.exit(main());
@@ -69,10 +69,6 @@ export async function main(args = []) {
69
69
  printFailures(failures, isOn(config, readManifest({ read: projectFile }), 'database'));
70
70
  return 1;
71
71
  }
72
- const warnings = results.filter(({ status }) => status === 'warn').length;
73
- // TODO: dalla 0.14.0 le anomalie a richiesta sono `fail` (smoke-on-demand.ts): togli il log degli avvisi qui sotto.
74
- if (warnings > 0)
75
- console.log(`\n· ${warnings} avviso/i: dalla 0.14.0 fanno fallire lo smoke.`);
76
72
  console.log(`\n✓ Tutti i controlli passati su ${baseUrl}.\n`);
77
73
  return 0;
78
74
  }
@@ -28,17 +28,14 @@ export const CI_STEPS = [
28
28
  'doctor',
29
29
  ];
30
30
  const MANIFEST = 'package.json';
31
- // TODO: dalla 0.14.0 `check:secrets` è un errore di doctor: toglilo da NEW_SCRIPTS.
32
- const NEW_SCRIPTS = new Set(['check:secrets']);
33
31
  export function scriptGaps({ scripts = {} }) {
34
32
  const gaps = [];
35
33
  for (const [name, expected] of Object.entries(EXPECTED_SCRIPTS)) {
36
34
  const actual = scripts[name];
37
- const severity = NEW_SCRIPTS.has(name) ? { severity: 'warning' } : {};
38
35
  if (actual === undefined)
39
- gaps.push({ path: MANIFEST, message: `script \`${name}\` assente: atteso \`${expected}\``, ...severity });
36
+ gaps.push({ path: MANIFEST, message: `script \`${name}\` assente: atteso \`${expected}\`` });
40
37
  else if (!actual.startsWith(expected))
41
- gaps.push({ path: MANIFEST, message: `script \`${name}\` è \`${actual}\`: atteso \`${expected}\``, ...severity });
38
+ gaps.push({ path: MANIFEST, message: `script \`${name}\` è \`${actual}\`: atteso \`${expected}\`` });
42
39
  }
43
40
  const ci = scripts.ci ?? '';
44
41
  for (const step of CI_STEPS) {
@@ -0,0 +1,6 @@
1
+ export type CiScope = {
2
+ docsOnly: boolean;
3
+ reason: string;
4
+ };
5
+ export declare const isDocument: (path: string) => boolean;
6
+ export declare function ciScope(base: string | undefined, changed: (base: string) => string[]): CiScope;
@@ -0,0 +1,20 @@
1
+ const DOCUMENTS = [/^[^/]+\.md$/, /^(?:docs|\.github|\.claude)\/.+\.md$/];
2
+ export const isDocument = (path) => DOCUMENTS.some((pattern) => pattern.test(path));
3
+ const fullRun = (reason) => ({ docsOnly: false, reason: `percorso completo: ${reason}` });
4
+ export function ciScope(base, changed) {
5
+ if (base === undefined)
6
+ return fullRun("l'evento non ha una base con cui confrontarsi");
7
+ let paths;
8
+ try {
9
+ paths = changed(base);
10
+ }
11
+ catch (error) {
12
+ return fullRun(`il diff da ${base} non è riuscito: ${error.message}`);
13
+ }
14
+ if (paths.length === 0)
15
+ return fullRun(`nessun file cambiato da ${base}`);
16
+ const code = paths.find((path) => !isDocument(path));
17
+ if (code !== undefined)
18
+ return fullRun(`${code} non è un documento`);
19
+ return { docsOnly: true, reason: `percorso veloce: solo documenti (${paths.length} file)` };
20
+ }
@@ -72,10 +72,8 @@ function secretGaps(source, keys) {
72
72
  return message === undefined ? [] : [{ path: ASTRO_CONFIG, message }];
73
73
  });
74
74
  }
75
- // TODO: dalla 0.14.0 i requisiti del database e dell'autenticazione sono errori di doctor: togli `severity`.
76
- const asWarning = (gap) => ({ ...gap, severity: 'warning' });
77
75
  export function databaseAuthGaps(files, manifest, on) {
78
76
  const database = on.includes('database') ? databaseGaps(files, manifest) : [];
79
77
  const secrets = secretGaps(files.read(ASTRO_CONFIG), on.map((name) => DEPENDENCY_FEATURES[name].secret));
80
- return [...database, ...secrets].map(asWarning);
78
+ return [...database, ...secrets];
81
79
  }
package/dist/lib/git.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export declare const trackedAndUntracked: () => string[];
2
+ export declare const eventBase: () => string | undefined;
3
+ export declare function changedSince(base: string): string[];
2
4
  export type DiffScope = {
3
5
  base?: string | undefined;
4
6
  head?: string | undefined;
package/dist/lib/git.js CHANGED
@@ -19,14 +19,14 @@ export const trackedAndUntracked = () => [
19
19
  ...gitPaths('ls-files', []),
20
20
  ...gitPaths('ls-files', ['--others', '--exclude-standard']),
21
21
  ];
22
- function ensureCommit(ref) {
22
+ function ensureCommit(ref, { shallow = false } = {}) {
23
23
  const exists = spawnSync('git', ['cat-file', '-e', `${ref}^{commit}`], {
24
24
  stdio: 'ignore',
25
25
  });
26
26
  if (exists.status === 0)
27
27
  return;
28
28
  // Checkout shallow in CI: la base della PR spesso non c'è in locale.
29
- git(['fetch', '--no-tags', 'origin', ref]);
29
+ git(['fetch', '--no-tags', ...(shallow ? ['--depth=1'] : []), 'origin', ref]);
30
30
  }
31
31
  function defaultBase() {
32
32
  const originHead = git(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], {
@@ -42,14 +42,29 @@ function defaultBase() {
42
42
  }
43
43
  throw new Error('nessuna base trovata: passa --base <ref>');
44
44
  }
45
- // Sulle PR GitHub il diff va fatto tra gli SHA dell'evento, non tra branch locali.
46
- function pullRequestShas() {
45
+ const eventPayload = (name) => {
47
46
  const { GITHUB_EVENT_NAME, GITHUB_EVENT_PATH } = process.env;
48
- if (GITHUB_EVENT_NAME !== 'pull_request' || !GITHUB_EVENT_PATH || !existsSync(GITHUB_EVENT_PATH))
47
+ if (GITHUB_EVENT_NAME !== name || !GITHUB_EVENT_PATH || !existsSync(GITHUB_EVENT_PATH))
49
48
  return undefined;
50
- const { pull_request: pr } = JSON.parse(readFileSync(GITHUB_EVENT_PATH, 'utf8'));
49
+ return JSON.parse(readFileSync(GITHUB_EVENT_PATH, 'utf8'));
50
+ };
51
+ // Sulle PR GitHub il diff va fatto tra gli SHA dell'evento, non tra branch locali.
52
+ function pullRequestShas() {
53
+ const pr = eventPayload('pull_request')?.pull_request;
51
54
  return pr?.base?.sha && pr?.head?.sha ? { base: pr.base.sha, head: pr.head.sha } : undefined;
52
55
  }
56
+ // Il primo push di un branch porta come `before` uno SHA tutto zeri, che non è un commit.
57
+ function pushBefore() {
58
+ const before = eventPayload('push')?.before;
59
+ return typeof before === 'string' && !/^0+$/.test(before) ? before : undefined;
60
+ }
61
+ export const eventBase = () => pullRequestShas()?.base ?? pushBefore();
62
+ export function changedSince(base) {
63
+ ensureCommit(base, { shallow: true });
64
+ // Con il rilevamento delle rinomine, attivo di default, `--name-only` elenca solo il percorso nuovo;
65
+ // `submodule.<nome>.ignore = all` in .gitmodules toglie dal diff anche un sottomodulo aggiornato.
66
+ return gitPaths('diff', ['--name-only', '--no-renames', '--ignore-submodules=none', base, 'HEAD']);
67
+ }
53
68
  export function changedFiles({ base, head }) {
54
69
  const pr = base ? undefined : pullRequestShas();
55
70
  const from = base ?? pr?.base ?? defaultBase();
@@ -18,16 +18,15 @@ const attributeValue = (tag, attribute) => {
18
18
  };
19
19
  const hasCspMeta = (html) => [...html.matchAll(META)].some(([tag]) => attributeValue(tag, HTTP_EQUIV).toLowerCase() === 'content-security-policy' &&
20
20
  SCRIPT_SRC.test(attributeValue(tag, CONTENT)));
21
- // TODO: dalla 0.14.0 un'anomalia di una pagina resa a richiesta fa fallire lo smoke: qui `warn` diventa `fail`.
22
- const warn = (check, detail) => ({ check, status: 'warn', detail });
21
+ const fail = (check, detail) => ({ check, status: 'fail', detail });
23
22
  function redirect(check, response, url) {
24
23
  const location = response.headers.get('location');
25
24
  if (location === null)
26
- return warn(check, `risponde ${response.status} senza location`);
25
+ return fail(check, `risponde ${response.status} senza location`);
27
26
  const target = new URL(location, url);
28
27
  const { origin } = new URL(url);
29
28
  if (target.origin !== origin)
30
- return warn(check, `rimanda a ${target.href}, fuori da ${origin}`);
29
+ return fail(check, `rimanda a ${target.href}, fuori da ${origin}`);
31
30
  return { check, status: 'pass' };
32
31
  }
33
32
  async function visit({ get, baseUrl }, path) {
@@ -38,17 +37,17 @@ async function visit({ get, baseUrl }, path) {
38
37
  if (response.status >= 300 && response.status < 400)
39
38
  return redirect(check, response, url);
40
39
  if (response.status !== 200) {
41
- return warn(check, `atteso 200 o un redirect sulla stessa origine, ricevuto ${response.status}`);
40
+ return fail(check, `atteso 200 o un redirect sulla stessa origine, ricevuto ${response.status}`);
42
41
  }
43
42
  const contentType = response.headers.get('content-type') ?? '';
44
43
  if (!contentType.includes('text/html'))
45
- return warn(check, `atteso un content-type text/html, ricevuto "${contentType}"`);
44
+ return fail(check, `atteso un content-type text/html, ricevuto "${contentType}"`);
46
45
  if (!hasCspMeta((await response.text?.()) ?? ''))
47
- return warn(check, 'manca il meta Content-Security-Policy con script-src');
46
+ return fail(check, 'manca il meta Content-Security-Policy con script-src');
48
47
  return { check, status: 'pass' };
49
48
  }
50
49
  catch (error) {
51
- return warn(check, error instanceof Error ? error.message : String(error));
50
+ return fail(check, error instanceof Error ? error.message : String(error));
52
51
  }
53
52
  }
54
53
  export async function checkOnDemandPages(context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elia97/officina",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Gli strumenti dei progetti nati dai template di Elia: gate di qualità, generatori e verifiche di build e di produzione.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -55,7 +55,7 @@
55
55
  "test:pack": "bash scripts/pack-test.sh"
56
56
  },
57
57
  "devDependencies": {
58
- "@biomejs/biome": "2.5.14",
58
+ "@biomejs/biome": "2.5.15",
59
59
  "@types/node": "^26.5.1",
60
60
  "@vitest/coverage-v8": "^4.1.11",
61
61
  "fallow": "^3.26.0",