@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 +6 -4
- package/dist/bin.js +2 -0
- package/dist/checks/scope.d.ts +2 -0
- package/dist/checks/scope.js +13 -0
- package/dist/checks/smoke.js +0 -4
- package/dist/lib/alignment.js +2 -5
- package/dist/lib/ci-scope.d.ts +6 -0
- package/dist/lib/ci-scope.js +20 -0
- package/dist/lib/database-auth.js +1 -3
- package/dist/lib/git.d.ts +2 -0
- package/dist/lib/git.js +21 -6
- package/dist/lib/smoke-on-demand.js +7 -8
- package/package.json +2 -2
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.
|
|
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`
|
|
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`
|
|
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,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());
|
package/dist/checks/smoke.js
CHANGED
|
@@ -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
|
}
|
package/dist/lib/alignment.js
CHANGED
|
@@ -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}
|
|
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}
|
|
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,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]
|
|
78
|
+
return [...database, ...secrets];
|
|
81
79
|
}
|
package/dist/lib/git.d.ts
CHANGED
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
|
-
|
|
46
|
-
function pullRequestShas() {
|
|
45
|
+
const eventPayload = (name) => {
|
|
47
46
|
const { GITHUB_EVENT_NAME, GITHUB_EVENT_PATH } = process.env;
|
|
48
|
-
if (GITHUB_EVENT_NAME !==
|
|
47
|
+
if (GITHUB_EVENT_NAME !== name || !GITHUB_EVENT_PATH || !existsSync(GITHUB_EVENT_PATH))
|
|
49
48
|
return undefined;
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
44
|
+
return fail(check, `atteso un content-type text/html, ricevuto "${contentType}"`);
|
|
46
45
|
if (!hasCspMeta((await response.text?.()) ?? ''))
|
|
47
|
-
return
|
|
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
|
|
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.
|
|
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.
|
|
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",
|