designwithai-mcp 0.13.0 → 0.14.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog — designwithai-mcp
2
+
3
+ Les versions antérieures à 0.14.1 sont décrites dans l'historique git du dépôt
4
+ (`git log -- mcp/`). À partir d'ici, chaque version publiée a son entrée, tirée
5
+ des titres de PR (voir `CONTRIBUTING.md`).
6
+
7
+ ## 0.14.1
8
+
9
+ ### Sécurité
10
+
11
+ - **Le template d'Action `templates/designwithai-status.yml` ne prête plus son
12
+ shell aux noms de branche** (#2). Il interpolait `${{ github.head_ref }}` dans
13
+ `run:` avec `contents: write` sur tout le workflow : une PR ouverte depuis une
14
+ branche au nom piégé pouvait exécuter une commande avec le droit d'écrire dans
15
+ ton dépôt. Les refs passent désormais par `env:`, chaque job n'a que le droit
16
+ dont il a besoin, et la version de l'outil est figée.
17
+
18
+ **Si tu as copié ce fichier dans `.github/workflows/`, remplace-le** par celui de
19
+ cette version. Le copier depuis le paquet ne suffit pas : la copie dans ton dépôt
20
+ ne se met pas à jour toute seule.
21
+
22
+ Aucun changement de comportement des outils MCP ni de la CLI : le template
23
+ installe toujours `designwithai-mcp@0.14.0`, qui fait exactement le même travail.
@@ -174,8 +174,30 @@ export const LARGEUR_DEFAUT_REM = {
174
174
  * `h-auto`, `p-[7px]` ne se résolvent pas ici, et c'est très bien — ils ont
175
175
  * chacun leur chemin.
176
176
  */
177
- export function resoudrePasNomme(famille, pas, racine, theme = {}) {
177
+ export function resoudrePasNomme(famille, pas, racine, theme = {},
178
+ /**
179
+ * L'échelle typographique DÉCLARÉE, quand elle est énumérée.
180
+ *
181
+ * `--font-size-4` ne veut rien dire dans l'absolu : c'est le quatrième pas de
182
+ * l'échelle que l'équipe a choisie. Sans cette liste, la mesure ne peut que
183
+ * l'ignorer — et une échelle ignorée fait tomber le compte à zéro, ce qui
184
+ * ressemble à une victoire.
185
+ */
186
+ echelleDeclaree,
187
+ /** L'échelle de rayons déclarée. Même rôle, même raison. */
188
+ rayonsDeclares) {
178
189
  const cle = pas.toLowerCase();
190
+ /* Un pas du kit : `font-size-1` est le PREMIER de la liste, pas l'index 1. */
191
+ const kitTexte = cle.match(/^font-size-(\d+)$/);
192
+ if (kitTexte && echelleDeclaree?.length) {
193
+ const px = echelleDeclaree[Number(kitTexte[1]) - 1];
194
+ return px !== undefined ? { px, provenance: "theme" } : null;
195
+ }
196
+ const kitRayon = cle.match(/^radius-(\d+)$/);
197
+ if (kitRayon && rayonsDeclares?.length) {
198
+ const px = rayonsDeclares[Number(kitRayon[1]) - 1];
199
+ return px !== undefined ? { px, provenance: "theme" } : null;
200
+ }
179
201
  if (famille === "espacement") {
180
202
  if (cle in PAS_LITTERAUX) {
181
203
  const v = PAS_LITTERAUX[cle];
@@ -242,9 +264,76 @@ function lireTheme(items) {
242
264
  * C'est ce qui permet d'ajouter une unité ou une échelle sans rouvrir les huit
243
265
  * fonctions de mesure — et ce qui garantit que toutes convertissent pareil.
244
266
  */
245
- export function resoudreItems(brut) {
267
+ /**
268
+ * LA TABLE DES ALIAS — `--radius-carte` vaut `var(--radius-4)` vaut 8 pixels.
269
+ *
270
+ * Un dépôt qui a fini sa migration n'écrit plus de valeurs : il écrit des noms
271
+ * qui pointent sur des noms. Sans suivre la chaîne, la mesure voit
272
+ * `var(--radius-carte)`, n'en tire rien, et rend « 0 rayon distinct » sur un
273
+ * produit qui en emploie six.
274
+ *
275
+ * ELLE NE REND JAMAIS « CONFORME » CE QU'ELLE NE SAIT PAS RÉSOUDRE. Une chaîne
276
+ * qui n'aboutit pas ressort en illisible AVEC la chaîne citée — c'est ce qui
277
+ * distingue « 0 divergent sur 13 résolus » de « 0 divergent parce que 13
278
+ * invisibles ». La deuxième phrase est un mensonge qui a l'air d'un succès.
279
+ */
280
+ function tableDesAlias(brut) {
281
+ const t = new Map();
282
+ for (const it of brut) {
283
+ if (it.kind !== "token")
284
+ continue;
285
+ const v = (it.value ?? "").trim();
286
+ if (!v)
287
+ continue;
288
+ /* L'inventaire range les variables CSS SANS leurs deux tirets — `radius-sm`
289
+ et non `--radius-sm`. Chercher la forme écrite dans le code ne trouvait
290
+ donc jamais rien, et toute la table restait vide : la chaîne d'alias
291
+ échouait au premier maillon sans qu'on voie pourquoi. On indexe les deux
292
+ formes plutôt que d'en imposer une : les deux existent dans le dépôt. */
293
+ if (it.name.startsWith("--"))
294
+ t.set(it.name, v);
295
+ else if ((it.type ?? "") === "css-variable")
296
+ t.set(`--${it.name}`, v);
297
+ }
298
+ return t;
299
+ }
300
+ /** Le maillon final d'une chaîne, ou la chaîne parcourue si elle n'aboutit pas. */
301
+ export function suivreAlias(valeur, alias, pasKit, profondeurMax = 8) {
302
+ const chaine = [valeur];
303
+ let courant = valeur.trim();
304
+ for (let i = 0; i < profondeurMax; i++) {
305
+ const m = courant.match(/^var\(\s*(--[a-zA-Z0-9-]+)\s*(?:,[^)]*)?\)$/);
306
+ if (!m)
307
+ break;
308
+ const nom = m[1];
309
+ /* Un pas du kit ne se trouve pas dans le fichier : il vient de l'échelle
310
+ DÉCLARÉE. C'est le bout de la chaîne, et le seul endroit où elle peut
311
+ aboutir sur un nombre sans que ce nombre soit écrit quelque part. */
312
+ const kit = pasKit(nom);
313
+ if (kit !== null)
314
+ return { px: kit };
315
+ const suivant = alias.get(nom);
316
+ if (suivant === undefined)
317
+ return { chaine };
318
+ courant = suivant.trim();
319
+ chaine.push(courant);
320
+ }
321
+ const px = brutEnPixels(courant, RACINE_PAR_DEFAUT);
322
+ return px === null ? { chaine } : { px };
323
+ }
324
+ export function resoudreItems(brut, echelleDeclaree, rayonsDeclares) {
246
325
  const racine = resoudreRacine(brut);
247
326
  const theme = lireTheme(brut);
327
+ const alias = tableDesAlias(brut);
328
+ const pasKit = (nom) => {
329
+ const r = nom.match(/^--radius-(\d+)$/);
330
+ if (r && rayonsDeclares?.length)
331
+ return rayonsDeclares[Number(r[1]) - 1] ?? null;
332
+ const t = nom.match(/^--font-size-(\d+)$/);
333
+ if (t && echelleDeclaree?.length)
334
+ return echelleDeclaree[Number(t[1]) - 1] ?? null;
335
+ return null;
336
+ };
248
337
  const items = [];
249
338
  const illisibles = [];
250
339
  const em = [];
@@ -276,7 +365,7 @@ export function resoudreItems(brut) {
276
365
  /* Un pas nommé : sa valeur ne se lit pas, elle se résout. */
277
366
  if (it.attrs?.provenance === "nomme") {
278
367
  const famille = (it.attrs.famille ?? type);
279
- const r = resoudrePasNomme(famille, it.attrs.pas ?? "", racine.px, theme);
368
+ const r = resoudrePasNomme(famille, it.attrs.pas ?? "", racine.px, theme, echelleDeclaree, rayonsDeclares);
280
369
  if (!r) {
281
370
  illisibles.push(`${it.name}${it.location ? ` (${it.location})` : ""} — pas nommé inconnu de l'échelle`);
282
371
  continue;
@@ -290,7 +379,19 @@ export function resoudreItems(brut) {
290
379
  em.push(`${it.name} = ${v}${it.location ? ` (${it.location})` : ""}`);
291
380
  continue;
292
381
  }
293
- const px = brutEnPixels(v, racine.px);
382
+ let px = brutEnPixels(v, racine.px);
383
+ if (px === null && v.startsWith("var(")) {
384
+ const suivi = suivreAlias(v, alias, pasKit);
385
+ if ("px" in suivi) {
386
+ px = suivi.px;
387
+ }
388
+ else {
389
+ /* La chaîne est CITÉE : sans elle, « illisible » n'aide personne à
390
+ réparer, et le compte des non-résolues resterait un chiffre nu. */
391
+ illisibles.push(`${it.name}${it.location ? ` (${it.location})` : ""} — chaîne non résolue : ${suivi.chaine.join(" → ")}`);
392
+ continue;
393
+ }
394
+ }
294
395
  if (px === null) {
295
396
  illisibles.push(`${it.name} = « ${v || "(vide)"} »${it.location ? ` (${it.location})` : ""}`);
296
397
  continue;
@@ -31,7 +31,7 @@ export function craftReport(brut, opts = {}) {
31
31
  /* UNE SEULE PASSE DE RÉSOLUTION, avant tout. Les mesures qui suivent ne
32
32
  voient plus que des pixels : la racine, le thème et l'échelle du cadre ont
33
33
  déjà fait leur travail, et de la même façon pour toutes. */
34
- const resolution = resoudreItems(brut);
34
+ const resolution = resoudreItems(brut, opts.systeme?.taillesTypo, opts.systeme?.rayons);
35
35
  const items = resolution.items;
36
36
  const reference = referenceEspacement(items, opts.systeme);
37
37
  const refCibles = referenceCible(opts.systeme);
@@ -42,7 +42,19 @@ export function craftReport(brut, opts = {}) {
42
42
  const tailles = distinctes(items, estTailleTypo);
43
43
  const doublons = quasiDoublonsCouleur(items);
44
44
  const couleurs = new Set(items.filter(estCouleur).map((i) => (i.value ?? "").trim().toLowerCase()));
45
- const rayons = distinctes(items, estRayon);
45
+ /* L'EXCLUSION S'APPLIQUE ICI, ET ELLE SE COMPTE.
46
+ On ne retire pas des éléments en silence : on les met de côté, on compte
47
+ ce qu'on a mis de côté, et le rapport le dit fichier par fichier. Une
48
+ mesure qui rétrécit sans le dire ment plus efficacement qu'une mesure
49
+ fausse — elle a l'air juste. */
50
+ const exclusions = opts.systeme?.exclusions ?? [];
51
+ const estExclu = (it) => exclusions.some((e) => (it.location ?? "").includes(e.fichier));
52
+ const ecartes = exclusions.map((e) => ({
53
+ fichier: e.fichier,
54
+ raison: e.raison,
55
+ valeurs: items.filter((it) => estRayon(it) && (it.location ?? "").includes(e.fichier)).length,
56
+ }));
57
+ const rayons = distinctes(items.filter((it) => !estExclu(it)), estRayon);
46
58
  const bp = bascules(items);
47
59
  const plafonds = largeursMax(items);
48
60
  const cibles = opts.annotations ? ciblesTactiles(opts.annotations, refCibles.base) : null;
@@ -82,7 +94,12 @@ export function craftReport(brut, opts = {}) {
82
94
  paires: doublons.detail.paires.map((p) => ({ a: p.a, b: p.b, ecart: p.ecart })),
83
95
  nonMesures: doublons.completude.nonMesures,
84
96
  },
85
- rayons: { distincts: rayons.valeur, liste: rayons.detail, nonMesures: rayons.completude.nonMesures },
97
+ rayons: {
98
+ distincts: rayons.valeur,
99
+ liste: rayons.detail,
100
+ nonMesures: rayons.completude.nonMesures,
101
+ ecartes: ecartes.filter((e) => e.valeurs > 0),
102
+ },
86
103
  grille: {
87
104
  bascules: bp.detail.trouvees,
88
105
  horsPlages: bp.detail.horsPlages,
@@ -240,6 +257,13 @@ export function rendreCraftReport(r, opts = {}) {
240
257
  l.push(" Aucune paire trop proche pour être distinguée.");
241
258
  }
242
259
  l.push(` ${r.rayons.distincts} rayon(s) distinct(s)${r.rayons.liste.length ? ` : ${r.rayons.liste.join(", ")}` : ""}.` + cite("RADIUS-01"));
260
+ /* CE BLOC N'EST PAS FACULTATIF. Un chiffre obtenu en écartant des valeurs ne
261
+ vaut que si le lecteur sait lesquelles, combien, et pourquoi — sinon il
262
+ compare une mesure rétrécie à une mesure pleine sans le savoir. */
263
+ for (const e of r.rayons.ecartes) {
264
+ l.push(` 1 fichier écarté de ce compte : ${e.fichier}, ${e.valeurs} valeur(s).`);
265
+ l.push(` Raison déclarée : ${e.raison}`);
266
+ }
243
267
  l.push("");
244
268
  /* ── Grilles ── */
245
269
  l.push("LES BASCULES ET LES PLAFONDS");
@@ -191,6 +191,30 @@ function lireTaillesTexte(etat, contenu, fichier) {
191
191
  const vus = new Set();
192
192
  for (const m of contenu.matchAll(/font-size\s*:\s*([^;}]+)/gi)) {
193
193
  const brut = m[1].trim();
194
+ /* UN PAS DU KIT SE RÉSOUT, IL NE SE JETTE PAS.
195
+ `font-size: var(--font-size-4)` partait dans `unread` avec les `clamp()`,
196
+ et les deux n'ont rien à voir. Un clamp dépend de la largeur de la
197
+ fenêtre : prendre une de ses bornes ferait entrer dans l'échelle un
198
+ chiffre que personne n'a choisi. Un pas nommé, lui, désigne une valeur
199
+ précise — celle que l'équipe a déclarée dans `taillesTypo`.
200
+ Tant qu'on les traitait pareil, migrer les valeurs sauvages vers
201
+ l'échelle faisait tomber le compte à zéro : la mesure devenait aveugle,
202
+ et le zéro ressemblait à une victoire. */
203
+ const pas = brut.match(/^var\(\s*--font-size-(\d+)\s*\)$/i);
204
+ if (pas) {
205
+ const cle = `font-size-${pas[1]}`;
206
+ if (vus.has(cle))
207
+ continue;
208
+ vus.add(cle);
209
+ etat.items.push({
210
+ kind: "token",
211
+ name: cle,
212
+ type: "typographie",
213
+ location: fichier,
214
+ attrs: { provenance: "nomme", famille: "typographie", pas: cle },
215
+ });
216
+ continue;
217
+ }
194
218
  const px = brut.match(/^([\d.]+)px$/i);
195
219
  if (!px) {
196
220
  if (/clamp|calc|var\(/i.test(brut)) {
@@ -410,8 +434,47 @@ function lireValeursArbitraires(etat, contenu, fichier) {
410
434
  attrs: { provenance: "arbitraire" },
411
435
  });
412
436
  }
437
+ lireVariablesEnClasse(etat, contenu, fichier);
413
438
  lirePasNommes(etat, contenu, fichier);
414
439
  }
440
+ /**
441
+ * LES VARIABLES ÉCRITES DANS UNE CLASSE — `rounded-[var(--radius-carte)]`.
442
+ *
443
+ * C'est la forme que prend un dépôt qui a fini sa migration : plus une valeur
444
+ * en pixels dans le markup, rien que des noms. Tant qu'on ne la lisait pas, la
445
+ * mesure devenait AVEUGLE au moment précis où le travail était fait — elle
446
+ * rendait « 0 rayon distinct » sur un produit qui en emploie six, et le zéro
447
+ * ressemblait à une victoire.
448
+ *
449
+ * On note le nom ; la résolution se fait dans `craft-measures`, qui connaît la
450
+ * chaîne des alias et l'échelle déclarée. L'inventaire rapporte, il ne calcule
451
+ * pas.
452
+ */
453
+ function lireVariablesEnClasse(etat, contenu, fichier) {
454
+ const vus = new Set();
455
+ for (const m of contenu.matchAll(/\b([a-z]+(?:-[a-z]+)?)-\[var\(\s*(--[a-z0-9-]+)\s*\)\]/gi)) {
456
+ const type = PREFIXE_TYPE.find(([re]) => re.test(m[1]))?.[1];
457
+ if (!type)
458
+ continue;
459
+ /* `text-[var(--color-…)]` est une COULEUR, pas une taille. Le préfixe est
460
+ le même — Tailwind s'en sert pour les deux — et la ranger en typographie
461
+ remplissait la liste des illisibles de fausses tailles de texte. */
462
+ if (type === "typographie" && /color|ink|brand|surface|border|pilier/i.test(m[2]))
463
+ continue;
464
+ const cle = `${m[1]}:${m[2]}`;
465
+ if (vus.has(cle))
466
+ continue;
467
+ vus.add(cle);
468
+ etat.items.push({
469
+ kind: "token",
470
+ name: `${m[1]}-[var(${m[2]})]`,
471
+ type,
472
+ value: `var(${m[2]})`,
473
+ location: fichier,
474
+ attrs: { provenance: "variable" },
475
+ });
476
+ }
477
+ }
415
478
  /**
416
479
  * LES PAS NOMMÉS — la moitié du travail qui manquait.
417
480
  *
@@ -72,7 +72,11 @@ const MARQUE = {
72
72
  */
73
73
  const STYLE = `
74
74
  :root{
75
- --encre:#17151d; --encre-2:#5b5766; --encre-3:#8b8794;
75
+ /* L'encre 3 était #8b8794 : 3.17 à 3.51:1 selon la surface, sous le 4.5:1
76
+ d'AA — et c'est elle qui porte les dates, les numéros et les noms de
77
+ preuve, c'est-à-dire ce qu'on vient lire. Elle passe au pas 11 de l'échelle
78
+ mauve, le premier pas que le kit garantit lisible. */
79
+ --encre:#17151d; --encre-2:#5b5766; --encre-3:#6f6d78;
76
80
  --fond:#faf9fb; --carte:#ffffff; --trait:#e7e5ec;
77
81
  --coherent:${COULEUR_PILIER["Cohérent"]}; --soigne:${COULEUR_PILIER["Soigné"]}; --solide:${COULEUR_PILIER["Solide"]};
78
82
  --l:clamp(320px,92vw,880px);
@@ -47,6 +47,24 @@ export function lireSysteme() {
47
47
  const out = {
48
48
  ...(nombre(o.base) ? { base: nombre(o.base) } : {}),
49
49
  ...(nombre(o.ratioTypo) ? { ratioTypo: nombre(o.ratioTypo) } : {}),
50
+ ...(Array.isArray(o.rayons) && o.rayons.length > 0 &&
51
+ o.rayons.every((x) => typeof x === "number" && Number.isFinite(x) && x >= 0)
52
+ ? { rayons: [...new Set(o.rayons)].sort((a, b) => a - b) }
53
+ : {}),
54
+ ...(Array.isArray(o.exclusions) && o.exclusions.length > 0
55
+ ? {
56
+ exclusions: o.exclusions
57
+ .filter((x) => Boolean(x) &&
58
+ typeof x === "object" &&
59
+ typeof x.fichier === "string" &&
60
+ /* UNE EXCLUSION SANS RAISON N'EST PAS LUE. C'est la seule
61
+ façon d'empêcher que ce champ devienne une liste où l'on
62
+ range ce qu'on ne veut plus voir. */
63
+ typeof x.raison === "string" &&
64
+ x.raison.trim().length > 0)
65
+ .map((x) => ({ fichier: x.fichier.trim(), raison: x.raison.trim() })),
66
+ }
67
+ : {}),
50
68
  ...(Array.isArray(o.taillesTypo) && o.taillesTypo.length > 0 &&
51
69
  o.taillesTypo.every((x) => typeof x === "number" && Number.isFinite(x) && x > 0)
52
70
  ? { taillesTypo: [...new Set(o.taillesTypo)].sort((a, b) => a - b) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "designwithai-mcp",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "description": "Le serveur designwithai pour ton éditeur : audite ton produit — interface, navigation, performance, sécurité des données, hygiène Figma — trace les corrections, et fait remonter les résultats dans ton tableau de bord.",
@@ -23,7 +23,8 @@
23
23
  "templates",
24
24
  "skills",
25
25
  "handbook-embeddings.json",
26
- "README.md"
26
+ "README.md",
27
+ "CHANGELOG.md"
27
28
  ],
28
29
  "scripts": {
29
30
  "build": "tsc",
@@ -13,6 +13,12 @@
13
13
  # de la base à celui de la branche ; s'il est dans le .gitignore, il n'y a rien
14
14
  # à comparer et l'outil le dit au lieu d'inventer un résultat.
15
15
 
16
+ # SÉCURITÉ : les noms de branche viennent de l'auteur de la PR. Ils passent
17
+ # par `env:` et sont cités dans le shell, jamais interpolés dans `run:` — une
18
+ # branche nommée `x";curl …|sh;"` resterait une chaîne, pas une commande. Chaque
19
+ # job n'a que le droit dont il a besoin, et la version de l'outil est figée :
20
+ # la mettre à jour est un changement de ce fichier, relu comme les autres.
21
+
16
22
  name: État du produit
17
23
 
18
24
  on:
@@ -20,31 +26,29 @@ on:
20
26
  branches: [main]
21
27
  pull_request:
22
28
 
23
- permissions:
24
- contents: write
25
- pull-requests: write
29
+ permissions: {}
26
30
 
27
31
  jobs:
28
- etat:
32
+ page:
33
+ if: github.event_name == 'push'
29
34
  runs-on: ubuntu-latest
35
+ permissions:
36
+ contents: write
30
37
  steps:
31
38
  - uses: actions/checkout@v4
32
- with:
33
- # Le delta a besoin de la base : sans historique, `git show base:` échoue.
34
- fetch-depth: 0
35
39
 
36
40
  - uses: actions/setup-node@v4
37
41
  with:
38
42
  node-version: 20
39
43
 
40
- - run: npm install -g designwithai-mcp@^0.13.0
44
+ - run: npm install -g designwithai-mcp@0.14.0
41
45
 
42
46
  - name: La page d'état
43
- if: github.event_name == 'push'
44
- run: designwithai-mcp status-page --product "${{ github.event.repository.name }}"
47
+ env:
48
+ PRODUCT: ${{ github.event.repository.name }}
49
+ run: designwithai-mcp status-page --product "$PRODUCT"
45
50
 
46
51
  - name: La commiter si elle a bougé
47
- if: github.event_name == 'push'
48
52
  run: |
49
53
  if [ -n "$(git status --porcelain docs/etat-produit)" ]; then
50
54
  git config user.name "github-actions[bot]"
@@ -56,16 +60,37 @@ jobs:
56
60
  echo "La page n'a pas changé."
57
61
  fi
58
62
 
63
+ delta:
64
+ if: github.event_name == 'pull_request'
65
+ runs-on: ubuntu-latest
66
+ permissions:
67
+ contents: read
68
+ pull-requests: write
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+ with:
72
+ # Le delta a besoin de la base : sans historique, `git show base:` échoue.
73
+ fetch-depth: 0
74
+ persist-credentials: false
75
+
76
+ - uses: actions/setup-node@v4
77
+ with:
78
+ node-version: 20
79
+
80
+ - run: npm install -g designwithai-mcp@0.14.0
81
+
59
82
  - name: Le delta de la branche
60
- if: github.event_name == 'pull_request'
83
+ env:
84
+ BASE_REF: ${{ github.base_ref }}
85
+ HEAD_REF: ${{ github.head_ref }}
61
86
  run: |
62
87
  designwithai-mcp pr-delta \
63
- --base "origin/${{ github.base_ref }}" \
64
- --branch "${{ github.head_ref }}" > delta.md
88
+ --base "origin/$BASE_REF" \
89
+ --branch "$HEAD_REF" > delta.md
65
90
  cat delta.md
66
91
 
67
92
  - name: Le poster sur la PR
68
- if: github.event_name == 'pull_request'
69
93
  env:
70
94
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
71
- run: gh pr comment "${{ github.event.number }}" --body-file delta.md
95
+ PR_NUMBER: ${{ github.event.number }}
96
+ run: gh pr comment "$PR_NUMBER" --body-file delta.md