@nitra/cursor 1.8.71 → 1.8.74

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/mdc/abie.mdc CHANGED
@@ -1,14 +1,18 @@
1
1
  ---
2
2
  description: Правила для проєктів AbInBev Efes
3
3
  alwaysApply: true
4
- version: '1.4'
4
+ version: '1.7'
5
5
  ---
6
6
 
7
- Правило **abie** для споживачів **@nitra/cursor**: **k8s** (**Deployment** + **HealthCheckPolicy**), overlay **ua** / **ru** (**nodeSelector**, видалення **HealthCheckPolicy** у **ru**) і гілки в **clean-merged-branch**. **`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
7
+ Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**), видалення **HealthCheckPolicy** у **ru**), а також гілки **dev**, **ua**, **ru** у **clean-merged-branch**.
8
+
9
+ **`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
10
+
11
+ **Канон перевірки** — **`npm/scripts/check-abie.mjs`**: верхній JSDoc і реалізація задають точні умови, допустимі домени для hostnames, тексти помилок. Нижче — зміст правила й орієнтовні фрагменти YAML; не дублюй тут покроковий алгоритм зі скрипта.
8
12
 
9
13
  ## k8s: `hc.yaml` поруч із Deployment
10
14
 
11
- Якщо під **`k8s`** є **`kind: Deployment`**, у **тій самій директорії** має бути **`hc.yaml`** з **HealthCheckPolicy** (**`networking.gke.io/v1`**): коректний modeline **`$schema`**, **`/healthz`**, порт **8080**, **`targetRef.name`** = **`metadata.name`**. Деталі — **`validateAbieHcYaml`** у **`npm/scripts/check-abie.mjs`**.
15
+ Якщо під **`k8s`** є **Deployment**, у **тій самій директорії** має бути **`hc.yaml`** з **HealthCheckPolicy** (**`networking.gke.io/v1`**): коректний modeline **`$schema`**, **`/healthz`**, порт **8080**, **`targetRef.name`** = **`metadata.name`**.
12
16
 
13
17
  ```yaml title="hc.yaml"
14
18
  # yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
@@ -30,19 +34,19 @@ spec:
30
34
  name: СЕРВІС
31
35
  ```
32
36
 
33
- ## k8s: overlay **HTTPRoute** `nginx-run` (**ua** / **ru**)
37
+ ## k8s: overlay **HTTPRoute** (**ua** / **ru**)
34
38
 
35
- Якщо під **`k8s`** є **Deployment**, у **кожному** **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** має бути inline **JSON6902** у **`patches[].patch`** на **`target.kind: HTTPRoute`**, **`name: nginx-run`**: **replace** **`/spec/hostnames`** (допустимі домени — як у прикладах нижче) і **replace** **`/spec/parentRefs/0/namespace`** (**`ua`** або **`ru`**). Для **ru** додатково потрібна анотація **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`** (зазвичай **op: add**, **`/metadata/annotations`**). Критерії**`validateAbieNginxRunHttpRoutePatches`** / **`getCombinedNginxRunPatchTextFromKustomization`** у **`npm/scripts/check-abie.mjs`**.
39
+ За наявності **Deployment** під **k8s** у **кожному** **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** потрібні **inline JSON6902** у **`patches`**: **target** **`kind: HTTPRoute`**, **непорожній `name`** (як у маніфесті маршруту). Мають бути зміни **`/spec/hostnames`** (домени abie — у скрипті) та **`/spec/parentRefs/0/namespace`** (**`ua`** / **`ru`**). Для **ru** анотація **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**. Як обирати **`op`** (**add** / **replace** тощо) у patch **k8s.mdc** (розділ про JSON patch у kustomization).
36
40
 
37
41
  ```yaml title="…/ua/kustomization.yaml (фрагмент)"
38
42
  - target:
39
43
  kind: HTTPRoute
40
- name: nginx-run
44
+ name: my-httproute
41
45
  patch: |-
42
46
  - op: replace
43
47
  path: /spec/hostnames
44
48
  value:
45
- - "abie.app" # також допускається vybeerai.com.ua, *.vybeerai.com.ua, *.abie.app
49
+ - "abie.app" # зокрема vybeerai.com.ua, *.vybeerai.com.ua, *.abie.app
46
50
  - op: replace
47
51
  path: /spec/parentRefs/0/namespace
48
52
  value: ua
@@ -51,12 +55,12 @@ spec:
51
55
  ```yaml title="…/ru/kustomization.yaml (фрагмент)"
52
56
  - target:
53
57
  kind: HTTPRoute
54
- name: nginx-run
58
+ name: my-httproute
55
59
  patch: |-
56
60
  - op: replace
57
61
  path: /spec/hostnames
58
62
  value:
59
- - "napitkivmeste.tech" # також допускається выбирайонлайн.рф, *.napitkivmeste.tech, *.выбирайонлайн.рф
63
+ - "napitkivmeste.tech" # зокрема выбирайонлайн.рф, *.napitkivmeste.tech, *.выбирайонлайн.рф
60
64
  - op: replace
61
65
  path: /spec/parentRefs/0/namespace
62
66
  value: ru
@@ -65,7 +69,7 @@ spec:
65
69
  ```yaml title="…/ru/kustomization.yaml (фрагмент)"
66
70
  - target:
67
71
  kind: HTTPRoute
68
- name: nginx-run
72
+ name: my-httproute
69
73
  patch: |-
70
74
  - op: add
71
75
  path: /metadata/annotations
@@ -73,9 +77,9 @@ spec:
73
77
  gwin.yandex.cloud/rules.http.upgradeTypes: "websocket"
74
78
  ```
75
79
 
76
- ## k8s: overlay **`ru`** і HealthCheckPolicy
80
+ ## k8s: overlay **ru** і HealthCheckPolicy
77
81
 
78
- Якщо в дереві **k8s** є **HealthCheckPolicy**, **check abie** вимагає **`ru/kustomization.yaml`** з patch **`$patch: delete`** для політики (узгоджено з **k8s.mdc** / **check-k8s**, **`ruKustomizationHasHealthCheckDeletePatch`** у **`npm/scripts/check-k8s.mjs`**). Підстав реальне ім’я замість **`СЕРВІС`**:
82
+ Якщо в дереві **k8s** є **HealthCheckPolicy**, у **`ru/kustomization.yaml`** має бути patch **`$patch: delete`** для політики (узгоджено з **k8s.mdc**; перевірка в **`check-k8s.mjs`**, **`ruKustomizationHasHealthCheckDeletePatch`**). Підстав реальне ім’я замість **`СЕРВІС`**:
79
83
 
80
84
  ```yaml title="…/ru/kustomization.yaml (фрагмент)"
81
85
  patches:
@@ -89,9 +93,9 @@ patches:
89
93
  $patch: delete
90
94
  ```
91
95
 
92
- ## k8s: overlay **`ua`** / **`ru`** і nodeSelector
96
+ ## k8s: overlay **ua** / **ru** і nodeSelector
93
97
 
94
- Якщо під **`k8s`** є **Deployment**, у **кожному** **`…/ua/kustomization.yaml`** та **`…/ru/kustomization.yaml`** має бути inline **JSON6902** у **`patches[].patch`** на **`target.kind: Deployment`** з **nodeSelector** за політикою abie (**ua** — **add** + **preem** false; **ru** — **replace** + **yandex.cloud/preemptible** false). Критерії збігу **`kustomizationHasAbieDeploymentNodeSelectorPatch`** у **`npm/scripts/check-abie.mjs`**.
98
+ За наявності **Deployment** під **k8s** у **кожному** **`…/ua/kustomization.yaml`** та **`…/ru/kustomization.yaml`** patch на **`kind: Deployment`**: **ua** — **`spec.template.spec.nodeSelector`** з **`preem: false`**; **ru** — **`spec.template.spec.nodeSelector`** з **`yandex.cloud/preemptible: false`**. Форму **JSON6902** (шлях **`/spec/template/spec/nodeSelector`**, **`op`**) див. **k8s.mdc**.
95
99
 
96
100
  ```yaml title="…/ua/kustomization.yaml (фрагмент)"
97
101
  patches:
@@ -119,7 +123,7 @@ patches:
119
123
 
120
124
  ### Базовий Deployment (`…/base/`)
121
125
 
122
- Якщо **Deployment** у YAML під **`k8s`** лежить у шляху з сегментом **`base`** (наприклад **`…/k8s/base/deploy.yaml`**), у **`spec.template.spec.nodeSelector`** має бути **`preem`** зі значенням **істинно** (**`true`** або рядок **`'true'`**) overlay **ua** / **ru** підміняє селектор через kustomize. Деталі — **`deploymentDocumentHasAbieBasePreemNodeSelector`** / **`isAbieK8sBaseYamlPath`** у **`npm/scripts/check-abie.mjs`**.
126
+ Якщо **Deployment** у YAML під **`k8s`** лежить у шляху з сегментом **`base`**, у **`spec.template.spec.nodeSelector`** має бути **`preem`** зі значенням **істинно** (**`true`** або рядок **`'true'`**); overlay **ua** / **ru** підміняє селектор.
123
127
 
124
128
  ```yaml title="…/base/deploy.yaml (фрагмент)"
125
129
  spec:
@@ -131,7 +135,7 @@ spec:
131
135
 
132
136
  ## Git branches
133
137
 
134
- У **`.github/workflows/clean-merged-branch.yml`** у кроці **`phpdocker-io/github-actions-delete-abandoned-branches`** значення **`with.ignore_branches`** має містити **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно), наприклад:
138
+ У **`.github/workflows/clean-merged-branch.yml`** у кроці **`phpdocker-io/github-actions-delete-abandoned-branches`** значення **`with.ignore_branches`** має містити **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно):
135
139
 
136
140
  ```yaml title=".github/workflows/clean-merged-branch.yml (фрагмент)"
137
141
  with:
package/mdc/k8s.mdc CHANGED
@@ -206,6 +206,20 @@ patches:
206
206
  value: dev
207
207
  ```
208
208
 
209
+ 4. **JSON patch у kustomization:** де можливо, змінюй ресурс через **`op: replace`** (одна операція на `path`), а не пару **`remove` + `add`** на той самий шлях. **`add`** / **`remove`** лишай лише коли **`replace`** не підходить (наприклад додати новий ключ або прибрати поле без заміни).
210
+
211
+ ```yaml title="overlay/kustomization.yaml (фрагмент)"
212
+ patches:
213
+ - target:
214
+ kind: Deployment
215
+ name: x
216
+ patch: |-
217
+ - op: replace
218
+ path: /spec/template/spec/nodeSelector
219
+ value:
220
+ preem: "false"
221
+ ```
222
+
209
223
  ## Перевірка
210
224
 
211
225
  **`npx @nitra/cursor check k8s`** — програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.71",
3
+ "version": "1.8.74",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -21,9 +21,10 @@
21
21
  * має бути inline **JSON6902** patch на **`kind: Deployment`**: для **ua** — **`op: add`**, **`path: /spec/template/spec/nodeSelector`**,
22
22
  * **`preem: false`**; для **ru** — **`op: replace`**, той самий **path**, **`yandex.cloud/preemptible: false`** (див. abie.mdc).
23
23
  *
24
- * **HTTPRoute nginx-run:** за тієї ж умови (**Deployment** під **k8s**) у **кожному** **`ua`/`ru` kustomization** має бути
25
- * inline **JSON6902** на **`kind: HTTPRoute`**, **`name: nginx-run`**: **replace** **`/spec/hostnames`** (домени з abie.mdc),
26
- * **replace** **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** також **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
24
+ * **HTTPRoute (overlay):** за тієї ж умови (**Deployment** під **k8s**) у **кожному** **`ua`/`ru` kustomization** має бути
25
+ * inline **JSON6902** на **`kind: HTTPRoute`** з **непорожнім `target.name`** (будь-яке ім’я): **replace** **`/spec/hostnames`**
26
+ * (домени з abie.mdc), **replace** **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** також
27
+ * **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
27
28
  */
28
29
  import { existsSync } from 'node:fs'
29
30
  import { readFile } from 'node:fs/promises'
@@ -32,7 +33,7 @@ import { dirname, join, relative } from 'node:path'
32
33
  import { parseAllDocuments } from 'yaml'
33
34
 
34
35
  import { pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
35
- import { pass } from './utils/pass.mjs'
36
+ import { createCheckReporter } from './utils/check-reporter.mjs'
36
37
  import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
37
38
  import { walkDir } from './utils/walkDir.mjs'
38
39
 
@@ -79,9 +80,9 @@ export function isAbieK8sBaseYamlPath(rel) {
79
80
  /**
80
81
  * Чи значення **`preem`** у base **Deployment** вважається «істинним» за abie.mdc (**true** або рядок **`true`** без урахування регістру).
81
82
  * @param {unknown} v значення з YAML
82
- * @returns {boolean}
83
+ * @returns {boolean} **true**, якщо значення вважається істинним за abie.mdc
83
84
  */
84
- function isAbiePreemTrueish(v) {
85
+ function isAbiePreemTruthy(v) {
85
86
  if (v === true) {
86
87
  return true
87
88
  }
@@ -117,7 +118,7 @@ export function deploymentDocumentHasAbieBasePreemNodeSelector(obj) {
117
118
  if (nodeSelector === null || typeof nodeSelector !== 'object' || Array.isArray(nodeSelector)) {
118
119
  return false
119
120
  }
120
- return isAbiePreemTrueish(nodeSelector.preem)
121
+ return isAbiePreemTruthy(nodeSelector.preem)
121
122
  }
122
123
 
123
124
  /**
@@ -279,9 +280,10 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
279
280
  * @param {string} root корінь репозиторію
280
281
  * @param {string[]} yamlFilesAbs yaml під k8s
281
282
  * @param {(msg: string) => void} fail callback
283
+ * @param {(msg: string) => void} passFn успішне повідомлення
282
284
  * @returns {Promise<void>}
283
285
  */
284
- async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail) {
286
+ async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
285
287
  const baseFiles = yamlFilesAbs.filter(abs => {
286
288
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
287
289
  return isAbieK8sBaseYamlPath(rel)
@@ -311,24 +313,24 @@ async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fai
311
313
  return
312
314
  }
313
315
  for (const doc of docs) {
314
- if (doc.errors.length > 0) {
315
- continue
316
- }
317
- const obj = doc.toJSON()
318
- if (!isDeploymentDoc(obj)) {
319
- continue
320
- }
321
- anyBaseDeployment = true
322
- if (!deploymentDocumentHasAbieBasePreemNodeSelector(obj)) {
323
- fail(`${rel}: Deployment у base: потрібен spec.template.spec.nodeSelector.preem: true (або 'true') — abie.mdc`)
324
- return
316
+ if (doc.errors.length === 0) {
317
+ const obj = doc.toJSON()
318
+ if (isDeploymentDoc(obj)) {
319
+ anyBaseDeployment = true
320
+ if (!deploymentDocumentHasAbieBasePreemNodeSelector(obj)) {
321
+ fail(
322
+ `${rel}: Deployment у base: потрібен spec.template.spec.nodeSelector.preem: true (або 'true') — abie.mdc`
323
+ )
324
+ return
325
+ }
326
+ }
325
327
  }
326
328
  }
327
329
  }
328
330
  if (anyBaseDeployment) {
329
- pass('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc')
331
+ passFn('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc')
330
332
  } else {
331
- pass('Немає Deployment у шляхах …/base/… — перевірку preem у base пропущено')
333
+ passFn('Немає Deployment у шляхах …/base/… — перевірку preem у base пропущено')
332
334
  }
333
335
  }
334
336
 
@@ -483,11 +485,11 @@ const ABIE_RU_HTTPROUTE_HOST_MARKERS = [
483
485
  ]
484
486
 
485
487
  /**
486
- * Збирає тексти inline **patch** для **HTTPRoute/nginx-run** з одного розібраного документа **Kustomization**.
488
+ * Збирає тексти inline **patch** для **HTTPRoute** (будь-який непорожній **target.name**) з одного документа **Kustomization**.
487
489
  * @param {import('yaml').Document} doc документ після **parseAllDocuments**
488
490
  * @returns {string[]} непорожні рядки **patch**
489
491
  */
490
- function collectNginxRunPatchStringsFromKustomizationDoc(doc) {
492
+ function collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc) {
491
493
  if (doc.errors.length > 0) {
492
494
  return []
493
495
  }
@@ -506,28 +508,25 @@ function collectNginxRunPatchStringsFromKustomizationDoc(doc) {
506
508
  /** @type {string[]} */
507
509
  const out = []
508
510
  for (const p of patches) {
509
- if (p === null || typeof p !== 'object' || Array.isArray(p)) {
510
- continue
511
- }
512
- const pr = /** @type {Record<string, unknown>} */ (p)
513
- const target = pr.target
514
- if (target === null || typeof target !== 'object' || Array.isArray(target)) {
515
- continue
516
- }
517
- const tg = /** @type {Record<string, unknown>} */ (target)
518
- if (tg.kind !== 'HTTPRoute' || tg.name !== 'nginx-run') {
519
- continue
520
- }
521
- const patchStr = pr.patch
522
- if (typeof patchStr === 'string' && patchStr.trim() !== '') {
523
- out.push(patchStr)
511
+ if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
512
+ const pr = /** @type {Record<string, unknown>} */ (p)
513
+ const target = pr.target
514
+ if (target !== null && typeof target === 'object' && !Array.isArray(target)) {
515
+ const tg = /** @type {Record<string, unknown>} */ (target)
516
+ if (tg.kind === 'HTTPRoute' && typeof tg.name === 'string' && tg.name.trim() !== '') {
517
+ const patchStr = pr.patch
518
+ if (typeof patchStr === 'string' && patchStr.trim() !== '') {
519
+ out.push(patchStr)
520
+ }
521
+ }
522
+ }
524
523
  }
525
524
  }
526
525
  return out
527
526
  }
528
527
 
529
528
  /**
530
- * Об’єднує всі inline **JSON6902**-фрагменти для **HTTPRoute/nginx-run** у **kustomization.yaml** (усі документи у файлі).
529
+ * Збирає всі inline **JSON6902**-фрагменти для **HTTPRoute** (непорожній **target.name**) у **kustomization.yaml** (усі документи у файлі).
531
530
  * @param {string} raw повний текст файлу
532
531
  * @returns {string} текст для **`validateAbieNginxRunHttpRoutePatches`** (може бути порожнім)
533
532
  */
@@ -546,47 +545,44 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
546
545
  /** @type {string[]} */
547
546
  const chunks = []
548
547
  for (const doc of docs) {
549
- chunks.push(...collectNginxRunPatchStringsFromKustomizationDoc(doc))
548
+ chunks.push(...collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc))
550
549
  }
551
550
  return chunks.join('\n')
552
551
  }
553
552
 
554
553
  /**
555
- * Перевіряє об’єднаний текст patch(ів) **HTTPRoute/nginx-run** на відповідність abie.mdc.
556
- * @param {string} combined текст одного або кількох inline **patch** (з’єднаних перевідом рядка)
554
+ * Перевіряє сукупний текст patch(ів) **HTTPRoute** (будь-яке **target.name**) на відповідність abie.mdc.
555
+ * @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
557
556
  * @param {'ua' | 'ru'} mode **ua** або **ru**
558
557
  * @returns {string | null} повідомлення про помилку або **null**
559
558
  */
560
559
  export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
561
560
  if (typeof combined !== 'string' || combined.trim() === '') {
562
- return `очікується patch target kind HTTPRoute name nginx-run (replace hostnames, parentRefs namespace ${mode}; для ru — також gwin… upgradeTypes websocket) — abie.mdc`
561
+ return `очікується patch target kind HTTPRoute з непорожнім target.name (replace hostnames, parentRefs namespace ${mode}; для ru — також gwin… upgradeTypes websocket) — abie.mdc`
563
562
  }
564
563
  const hasHostnamesReplace = /-\s*op:\s*replace\b[\s\S]{0,200}?path:\s*\/spec\/hostnames\b/m.test(combined)
565
564
  if (!hasHostnamesReplace) {
566
- return 'HTTPRoute nginx-run: потрібен блок op replace з path /spec/hostnames (abie.mdc)'
565
+ return 'HTTPRoute: потрібен блок op replace з path /spec/hostnames (abie.mdc)'
567
566
  }
568
567
  const markers = mode === 'ua' ? ABIE_UA_HTTPROUTE_HOST_MARKERS : ABIE_RU_HTTPROUTE_HOST_MARKERS
569
568
  if (!markers.some(m => combined.includes(m))) {
570
- return `HTTPRoute nginx-run: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
569
+ return `HTTPRoute: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
571
570
  }
572
- const ns = mode === 'ua' ? 'ua' : 'ru'
573
- const nsRe = new RegExp(
574
- String.raw`path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?${ns}['"]?(?:\s|$)`,
575
- 'mu'
576
- )
577
- if (!nsRe.test(combined)) {
578
- return `HTTPRoute nginx-run: потрібен replace path /spec/parentRefs/0/namespace з value ${ns} (abie.mdc)`
571
+ const namespaceOk =
572
+ mode === 'ua'
573
+ ? /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu.test(combined)
574
+ : /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu.test(combined)
575
+ if (!namespaceOk) {
576
+ return `HTTPRoute: потрібен replace path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
579
577
  }
580
- if (mode === 'ru') {
581
- if (!/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
582
- return 'HTTPRoute nginx-run (ru): потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
583
- }
578
+ if (mode === 'ru' && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
579
+ return 'HTTPRoute (ru): потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
584
580
  }
585
581
  return null
586
582
  }
587
583
 
588
584
  /**
589
- * Чи **kustomization** містить валідні для abie патчі **HTTPRoute/nginx-run** (**ua** або **ru**).
585
+ * Чи **kustomization** містить валідні для abie **patch** для **HTTPRoute** з непорожнім **target.name** (**ua** або **ru**).
590
586
  * @param {string} raw повний текст **kustomization.yaml**
591
587
  * @param {'ua' | 'ru'} mode overlay
592
588
  * @returns {boolean} true, якщо **`validateAbieNginxRunHttpRoutePatches`** повертає **null**
@@ -780,9 +776,10 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
780
776
  * @param {string} root корінь репозиторію
781
777
  * @param {string[]} yamlFilesAbs yaml під k8s
782
778
  * @param {(msg: string) => void} fail callback
779
+ * @param {(msg: string) => void} passFn успішне повідомлення
783
780
  * @returns {Promise<void>}
784
781
  */
785
- async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
782
+ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passFn) {
786
783
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
787
784
  if (uaAbsList.length === 0) {
788
785
  fail(
@@ -806,7 +803,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
806
803
  )
807
804
  return
808
805
  }
809
- pass(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
806
+ passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
810
807
  }
811
808
 
812
809
  const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
@@ -832,22 +829,23 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
832
829
  )
833
830
  return
834
831
  }
835
- pass(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
832
+ passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
836
833
  }
837
834
  }
838
835
 
839
836
  /**
840
- * Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** patch **HTTPRoute/nginx-run** (abie.mdc).
837
+ * Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc.
841
838
  * @param {string} root корінь репозиторію
842
839
  * @param {string[]} yamlFilesAbs yaml під k8s
843
840
  * @param {(msg: string) => void} fail callback
841
+ * @param {(msg: string) => void} passFn успішне повідомлення
844
842
  * @returns {Promise<void>}
845
843
  */
846
- async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail) {
844
+ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
847
845
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
848
846
  if (uaAbsList.length === 0) {
849
847
  fail(
850
- 'Є Deployment у k8s — додай ua/kustomization.yaml з patch HTTPRoute nginx-run (hostnames, parentRefs namespace ua) — abie.mdc'
848
+ 'Є Deployment у k8s — додай ua/kustomization.yaml з patch HTTPRoute (будь-який target.name: hostnames, parentRefs namespace ua) — abie.mdc'
851
849
  )
852
850
  return
853
851
  }
@@ -867,13 +865,13 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail) {
867
865
  fail(`${rel}: ${v}`)
868
866
  return
869
867
  }
870
- pass(`${rel}: HTTPRoute nginx-run (ua) відповідає abie.mdc`)
868
+ passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
871
869
  }
872
870
 
873
871
  const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
874
872
  if (ruAbsList.length === 0) {
875
873
  fail(
876
- 'Є Deployment у k8s — додай ru/kustomization.yaml з patch HTTPRoute nginx-run (hostnames, namespace ru, gwin websocket) — abie.mdc'
874
+ 'Є Deployment у k8s — додай ru/kustomization.yaml з patch HTTPRoute (будь-який target.name: hostnames, namespace ru, gwin websocket) — abie.mdc'
877
875
  )
878
876
  return
879
877
  }
@@ -893,7 +891,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail) {
893
891
  fail(`${rel}: ${v}`)
894
892
  return
895
893
  }
896
- pass(`${rel}: HTTPRoute nginx-run (ru) відповідає abie.mdc`)
894
+ passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
897
895
  }
898
896
  }
899
897
 
@@ -902,17 +900,14 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail) {
902
900
  * @returns {Promise<number>} 0 — OK, 1 — є порушення
903
901
  */
904
902
  export async function check() {
905
- let exitCode = 0
906
- const fail = msg => {
907
- console.log(` ❌ ${msg}`)
908
- exitCode = 1
909
- }
903
+ const reporter = createCheckReporter()
904
+ const { pass, fail } = reporter
910
905
 
911
906
  const root = process.cwd()
912
907
  const enabled = await isAbieRuleEnabled(root)
913
908
  if (!enabled) {
914
909
  pass(`Правило abie не увімкнено в ${CONFIG_FILE} (rules) — перевірку пропущено`)
915
- return 0
910
+ return reporter.getExitCode()
916
911
  }
917
912
 
918
913
  pass('Правило abie увімкнено — виконуємо перевірки')
@@ -978,7 +973,7 @@ export async function check() {
978
973
  }
979
974
  }
980
975
  pass('Є Deployment — перевіряємо base: spec.template.spec.nodeSelector.preem (abie.mdc)')
981
- await ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFiles, fail)
976
+ await ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFiles, fail, pass)
982
977
  } else {
983
978
  pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
984
979
  }
@@ -988,10 +983,10 @@ export async function check() {
988
983
 
989
984
  if (deploymentDirs.size > 0) {
990
985
  pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
991
- await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail)
992
- pass('Є Deployment — перевіряємо HTTPRoute nginx-run у ua/ru kustomization (abie.mdc)')
993
- await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail)
986
+ await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail, pass)
987
+ pass('Є Deployment — перевіряємо HTTPRoute у ua/ru kustomization (abie.mdc)')
988
+ await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
994
989
  }
995
990
 
996
- return exitCode
991
+ return reporter.getExitCode()
997
992
  }
@@ -17,7 +17,7 @@
17
17
  import { existsSync } from 'node:fs'
18
18
  import { readFile } from 'node:fs/promises'
19
19
 
20
- import { pass } from './utils/pass.mjs'
20
+ import { createCheckReporter } from './utils/check-reporter.mjs'
21
21
 
22
22
  /**
23
23
  * Чи ім'я пакета дозволене в кореневих `devDependencies` за bun.mdc (лише `@cspell/*` та `@nitra/*`).
@@ -53,11 +53,8 @@ async function loadNCursorRules() {
53
53
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
54
54
  */
55
55
  export async function check() {
56
- let exitCode = 0
57
- const fail = msg => {
58
- console.log(` ❌ ${msg}`)
59
- exitCode = 1
60
- }
56
+ const reporter = createCheckReporter()
57
+ const { pass, fail } = reporter
61
58
 
62
59
  const forbidden = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', '.yarnrc.yml']
63
60
  for (const f of forbidden) {
@@ -164,5 +161,5 @@ export async function check() {
164
161
  }
165
162
  }
166
163
 
167
- return exitCode
164
+ return reporter.getExitCode()
168
165
  }
@@ -8,7 +8,7 @@
8
8
  import { basename } from 'node:path'
9
9
 
10
10
  import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mjs'
11
- import { pass } from './utils/pass.mjs'
11
+ import { createCheckReporter } from './utils/check-reporter.mjs'
12
12
  import { walkDir } from './utils/walkDir.mjs'
13
13
 
14
14
  /**
@@ -42,18 +42,15 @@ export async function findDockerfilePaths(root) {
42
42
  * @returns {Promise<number>} 0 — все OK, 1 — є зауваження або помилка запуску
43
43
  */
44
44
  export async function check() {
45
- let exitCode = 0
46
- const fail = msg => {
47
- console.log(` ❌ ${msg}`)
48
- exitCode = 1
49
- }
45
+ const reporter = createCheckReporter()
46
+ const { pass, fail } = reporter
50
47
 
51
48
  const root = process.cwd()
52
49
  const files = await findDockerfilePaths(root)
53
50
 
54
51
  if (files.length === 0) {
55
52
  pass('Немає Dockerfile / Containerfile — перевірку hadolint пропущено')
56
- return 0
53
+ return reporter.getExitCode()
57
54
  }
58
55
 
59
56
  pass(`Знайдено файлів для hadolint: ${files.length}`)
@@ -69,5 +66,5 @@ export async function check() {
69
66
  }
70
67
  }
71
68
 
72
- return exitCode
69
+ return reporter.getExitCode()
73
70
  }
@@ -14,7 +14,7 @@ import { existsSync } from 'node:fs'
14
14
  import { readdir, readFile } from 'node:fs/promises'
15
15
  import { join } from 'node:path'
16
16
 
17
- import { pass } from './utils/pass.mjs'
17
+ import { createCheckReporter } from './utils/check-reporter.mjs'
18
18
  import {
19
19
  anyRunStepIncludes,
20
20
  eventPathsIncludeExact,
@@ -122,17 +122,14 @@ function verifyNoDirectBunOrCache(relPath, content, failFn, passFn) {
122
122
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
123
123
  */
124
124
  export async function check() {
125
- let exitCode = 0
126
- const fail = msg => {
127
- console.log(` ❌ ${msg}`)
128
- exitCode = 1
129
- }
125
+ const reporter = createCheckReporter()
126
+ const { pass, fail } = reporter
130
127
 
131
128
  const wfDir = '.github/workflows'
132
129
 
133
130
  if (!existsSync(wfDir)) {
134
131
  fail(`Директорія ${wfDir} не існує`)
135
- return exitCode
132
+ return reporter.getExitCode()
136
133
  }
137
134
 
138
135
  const setupBunDepsAction = '.github/actions/setup-bun-deps/action.yml'
@@ -290,5 +287,5 @@ export async function check() {
290
287
  }
291
288
  }
292
289
 
293
- return exitCode
290
+ return reporter.getExitCode()
294
291
  }
@@ -6,18 +6,15 @@
6
6
  import { existsSync } from 'node:fs'
7
7
  import { readFile } from 'node:fs/promises'
8
8
 
9
- import { pass } from './utils/pass.mjs'
9
+ import { createCheckReporter } from './utils/check-reporter.mjs'
10
10
 
11
11
  /**
12
12
  * Перевіряє відповідність проєкту правилам js-format.mdc
13
13
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
14
14
  */
15
15
  export async function check() {
16
- let exitCode = 0
17
- const fail = msg => {
18
- console.log(` ❌ ${msg}`)
19
- exitCode = 1
20
- }
16
+ const reporter = createCheckReporter()
17
+ const { pass, fail } = reporter
21
18
 
22
19
  const expectedKeys = [
23
20
  'arrowParens',
@@ -95,5 +92,5 @@ export async function check() {
95
92
  if (pkg.prettier) fail('package.json містить поле "prettier" — видали його')
96
93
  }
97
94
 
98
- return exitCode
95
+ return reporter.getExitCode()
99
96
  }
@@ -10,7 +10,7 @@ import { existsSync } from 'node:fs'
10
10
  import { readFile } from 'node:fs/promises'
11
11
 
12
12
  import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-workflow.mjs'
13
- import { pass } from './utils/pass.mjs'
13
+ import { createCheckReporter } from './utils/check-reporter.mjs'
14
14
 
15
15
  /** Очікуваний локальний скрипт. */
16
16
  export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
@@ -41,11 +41,8 @@ export function isCanonicalLintJs(script) {
41
41
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
42
42
  */
43
43
  export async function check() {
44
- let exitCode = 0
45
- const fail = msg => {
46
- console.log(` ❌ ${msg}`)
47
- exitCode = 1
48
- }
44
+ const reporter = createCheckReporter()
45
+ const { pass, fail } = reporter
49
46
 
50
47
  let eslintPath = ''
51
48
  if (existsSync('eslint.config.js')) {
@@ -242,5 +239,5 @@ export async function check() {
242
239
  if (existsSync(dup)) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй flat config`)
243
240
  }
244
241
 
245
- return exitCode
242
+ return reporter.getExitCode()
246
243
  }
@@ -8,16 +8,17 @@ import { existsSync } from 'node:fs'
8
8
  import { readFile } from 'node:fs/promises'
9
9
  import { join } from 'node:path'
10
10
 
11
- import { pass } from './utils/pass.mjs'
11
+ import { createCheckReporter } from './utils/check-reporter.mjs'
12
12
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
13
13
 
14
14
  /**
15
15
  * Перевіряє відповідність правилам js-pino.mdc для одного workspace-пакета.
16
16
  * @param {string} rootDir відносний шлях workspace (не `'.'`)
17
17
  * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
18
+ * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
18
19
  * @returns {Promise<void>} завершується після перевірок цього пакета
19
20
  */
20
- async function checkWorkspacePackage(rootDir, fail) {
21
+ async function checkWorkspacePackage(rootDir, fail, passFn) {
21
22
  const label = `[${rootDir}] `
22
23
  const pkgPath = join(rootDir, 'package.json')
23
24
  if (existsSync(pkgPath)) {
@@ -36,9 +37,9 @@ async function checkWorkspacePackage(rootDir, fail) {
36
37
  if (existsSync(configmapPath)) {
37
38
  const content = await readFile(configmapPath, 'utf8')
38
39
  if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
39
- pass(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
40
+ passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
40
41
  if (content.includes('service.name=') && content.includes('service.namespace=')) {
41
- pass(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
42
+ passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
42
43
  } else {
43
44
  fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
44
45
  }
@@ -53,23 +54,20 @@ async function checkWorkspacePackage(rootDir, fail) {
53
54
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
54
55
  */
55
56
  export async function check() {
56
- let exitCode = 0
57
- const fail = msg => {
58
- console.log(` ❌ ${msg}`)
59
- exitCode = 1
60
- }
57
+ const reporter = createCheckReporter()
58
+ const { pass, fail } = reporter
61
59
 
62
60
  const roots = await getMonorepoPackageRootDirs()
63
61
  const workspaceRoots = roots.filter(r => r !== '.')
64
62
 
65
63
  if (workspaceRoots.length === 0) {
66
64
  pass('js-pino: немає workspace-пакетів у кореневому package.json — перевірку залежностей і k8s у пакетах пропущено')
67
- return exitCode
65
+ return reporter.getExitCode()
68
66
  }
69
67
 
70
68
  for (const r of workspaceRoots) {
71
- await checkWorkspacePackage(r, fail)
69
+ await checkWorkspacePackage(r, fail, pass)
72
70
  }
73
71
 
74
- return exitCode
72
+ return reporter.getExitCode()
75
73
  }
@@ -51,7 +51,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path'
51
51
 
52
52
  import { parseAllDocuments } from 'yaml'
53
53
 
54
- import { pass } from './utils/pass.mjs'
54
+ import { createCheckReporter } from './utils/check-reporter.mjs'
55
55
  import { walkDir } from './utils/walkDir.mjs'
56
56
 
57
57
  /** Версія набору схем yannh — узгоджено з k8s.mdc */
@@ -1482,11 +1482,8 @@ async function ensureBaseKustomizationHasNamespace(root, yamlFiles, fail) {
1482
1482
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
1483
1483
  */
1484
1484
  export async function check() {
1485
- let exitCode = 0
1486
- const fail = msg => {
1487
- console.log(` ❌ ${msg}`)
1488
- exitCode = 1
1489
- }
1485
+ const reporter = createCheckReporter()
1486
+ const { pass, fail } = reporter
1490
1487
 
1491
1488
  const root = process.cwd()
1492
1489
 
@@ -1496,7 +1493,7 @@ export async function check() {
1496
1493
 
1497
1494
  if (yamlFiles.length === 0) {
1498
1495
  pass('Немає *.yaml під k8s — перевірку $schema пропущено')
1499
- return 0
1496
+ return reporter.getExitCode()
1500
1497
  }
1501
1498
 
1502
1499
  pass(`YAML у k8s: ${yamlFiles.length} файл(ів)`)
@@ -1515,5 +1512,5 @@ export async function check() {
1515
1512
 
1516
1513
  await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
1517
1514
 
1518
- return exitCode
1515
+ return reporter.getExitCode()
1519
1516
  }
@@ -18,7 +18,7 @@ import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
18
18
  import { basename, dirname, join, relative } from 'node:path'
19
19
 
20
20
  import { findDockerfilePaths } from './check-docker.mjs'
21
- import { pass } from './utils/pass.mjs'
21
+ import { createCheckReporter } from './utils/check-reporter.mjs'
22
22
  import { walkDir } from './utils/walkDir.mjs'
23
23
 
24
24
  /**
@@ -264,11 +264,8 @@ function dockerfileHasEnvsSubstTemplate(dockerfileContent) {
264
264
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
265
265
  */
266
266
  export async function check() {
267
- let exitCode = 0
268
- const fail = msg => {
269
- console.log(` ❌ ${msg}`)
270
- exitCode = 1
271
- }
267
+ const reporter = createCheckReporter()
268
+ const { pass, fail } = reporter
272
269
 
273
270
  const root = process.cwd()
274
271
 
@@ -284,7 +281,7 @@ export async function check() {
284
281
 
285
282
  if (templates.length === 0) {
286
283
  pass('Немає default.conf.template — перевірку nginx-default-tpl пропущено')
287
- return 0
284
+ return reporter.getExitCode()
288
285
  }
289
286
 
290
287
  pass(`Знайдено default.conf.template: ${templates.length}`)
@@ -383,5 +380,5 @@ export async function check() {
383
380
  fail('Очікується .vscode/settings.json з форматером nginx і formatOnSave (див. nginx-default-tpl.mdc)')
384
381
  }
385
382
 
386
- return exitCode
383
+ return reporter.getExitCode()
387
384
  }
@@ -7,7 +7,7 @@
7
7
  import { existsSync } from 'node:fs'
8
8
  import { readFile, stat } from 'node:fs/promises'
9
9
 
10
- import { pass } from './utils/pass.mjs'
10
+ import { createCheckReporter } from './utils/check-reporter.mjs'
11
11
  import {
12
12
  hasIdTokenWritePermission,
13
13
  hasNpmPublishStepWithPackage,
@@ -21,11 +21,8 @@ import {
21
21
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
22
22
  */
23
23
  export async function check() {
24
- let exitCode = 0
25
- const fail = msg => {
26
- console.log(` ❌ ${msg}`)
27
- exitCode = 1
28
- }
24
+ const reporter = createCheckReporter()
25
+ const { pass, fail } = reporter
29
26
 
30
27
  if (existsSync('package.json')) {
31
28
  pass('package.json існує')
@@ -114,5 +111,5 @@ export async function check() {
114
111
  fail(`Відсутній ${publishWf} (npm-module.mdc: npm publish)`)
115
112
  }
116
113
 
117
- return exitCode
114
+ return reporter.getExitCode()
118
115
  }
@@ -8,7 +8,7 @@
8
8
  import { existsSync } from 'node:fs'
9
9
  import { readFile } from 'node:fs/promises'
10
10
 
11
- import { pass } from './utils/pass.mjs'
11
+ import { createCheckReporter } from './utils/check-reporter.mjs'
12
12
  import { anyRunStepIncludesStylelint, parseWorkflowYaml } from './utils/gha-workflow.mjs'
13
13
 
14
14
  /**
@@ -16,11 +16,8 @@ import { anyRunStepIncludesStylelint, parseWorkflowYaml } from './utils/gha-work
16
16
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
17
17
  */
18
18
  export async function check() {
19
- let exitCode = 0
20
- const fail = msg => {
21
- console.log(` ❌ ${msg}`)
22
- exitCode = 1
23
- }
19
+ const reporter = createCheckReporter()
20
+ const { pass, fail } = reporter
24
21
 
25
22
  if (existsSync('package.json')) {
26
23
  const pkg = JSON.parse(await readFile('package.json', 'utf8'))
@@ -105,5 +102,5 @@ export async function check() {
105
102
  }
106
103
  }
107
104
 
108
- return exitCode
105
+ return reporter.getExitCode()
109
106
  }
@@ -12,7 +12,7 @@
12
12
  import { existsSync } from 'node:fs'
13
13
  import { readFile } from 'node:fs/promises'
14
14
 
15
- import { pass } from './utils/pass.mjs'
15
+ import { createCheckReporter } from './utils/check-reporter.mjs'
16
16
  import { anyRunStepIncludes, parseWorkflowYaml } from './utils/gha-workflow.mjs'
17
17
 
18
18
  /** Заголовок абзацу про апостроф у text.mdc / n-text.mdc. */
@@ -47,11 +47,8 @@ function verifyUkApostropheRuleParagraph(filePath, body, failFn, passFn) {
47
47
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
48
48
  */
49
49
  export async function check() {
50
- let exitCode = 0
51
- const fail = msg => {
52
- console.log(` ❌ ${msg}`)
53
- exitCode = 1
54
- }
50
+ const reporter = createCheckReporter()
51
+ const { pass, fail } = reporter
55
52
 
56
53
  const v8rIgnoreRequired = ['.vscode/extensions.json', '.vscode/settings.json']
57
54
  if (existsSync('.v8rignore')) {
@@ -215,5 +212,5 @@ export async function check() {
215
212
  }
216
213
  }
217
214
 
218
- return exitCode
215
+ return reporter.getExitCode()
219
216
  }
@@ -11,7 +11,7 @@ import { existsSync } from 'node:fs'
11
11
  import { readFile } from 'node:fs/promises'
12
12
  import { join, relative } from 'node:path'
13
13
 
14
- import { pass } from './utils/pass.mjs'
14
+ import { createCheckReporter } from './utils/check-reporter.mjs'
15
15
  import {
16
16
  findForbiddenVueImportsInSourceFile,
17
17
  isVueImportScanSourceFile,
@@ -53,9 +53,10 @@ function ukFilesCountPhrase(n) {
53
53
  * Перевіряє залежності та vite.config одного Vue-пакета.
54
54
  * @param {string} rootDir відносний шлях до пакета
55
55
  * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
56
+ * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
56
57
  * @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
57
58
  */
58
- async function checkVuePackage(rootDir, fail) {
59
+ async function checkVuePackage(rootDir, fail, passFn) {
59
60
  const label = packageLabel(rootDir)
60
61
  const prefix = `[${label}] `
61
62
 
@@ -66,7 +67,7 @@ async function checkVuePackage(rootDir, fail) {
66
67
  const allDeps = { ...deps, ...devDeps }
67
68
 
68
69
  if (deps.vue) {
69
- pass(`${prefix}vue в dependencies: ${deps.vue}`)
70
+ passFn(`${prefix}vue в dependencies: ${deps.vue}`)
70
71
  } else {
71
72
  fail(`${prefix}vue відсутній в dependencies`)
72
73
  }
@@ -74,7 +75,7 @@ async function checkVuePackage(rootDir, fail) {
74
75
  if (devDeps.vite) {
75
76
  const match = devDeps.vite.match(/(\d+)/)
76
77
  if (match && Number(match[1]) >= 8) {
77
- pass(`${prefix}vite >= 8: ${devDeps.vite}`)
78
+ passFn(`${prefix}vite >= 8: ${devDeps.vite}`)
78
79
  } else {
79
80
  fail(`${prefix}vite має бути >= 8, знайдено: ${devDeps.vite}`)
80
81
  }
@@ -83,25 +84,25 @@ async function checkVuePackage(rootDir, fail) {
83
84
  }
84
85
 
85
86
  if (devDeps['@vitejs/plugin-vue']) {
86
- pass(`${prefix}@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
87
+ passFn(`${prefix}@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
87
88
  } else {
88
89
  fail(`${prefix}@vitejs/plugin-vue відсутній в devDependencies`)
89
90
  }
90
91
 
91
92
  if (allDeps['vue-macros']) {
92
- pass(`${prefix}vue-macros: ${allDeps['vue-macros']}`)
93
+ passFn(`${prefix}vue-macros: ${allDeps['vue-macros']}`)
93
94
  } else {
94
95
  fail(`${prefix}vue-macros відсутній — bun add -d vue-macros`)
95
96
  }
96
97
 
97
98
  if (allDeps['unplugin-auto-import']) {
98
- pass(`${prefix}unplugin-auto-import присутній`)
99
+ passFn(`${prefix}unplugin-auto-import присутній`)
99
100
  } else {
100
101
  fail(`${prefix}unplugin-auto-import відсутній — bun add -d unplugin-auto-import`)
101
102
  }
102
103
 
103
104
  if (allDeps['vite-plugin-vue-layouts-next']) {
104
- pass(`${prefix}vite-plugin-vue-layouts-next присутній`)
105
+ passFn(`${prefix}vite-plugin-vue-layouts-next присутній`)
105
106
  } else {
106
107
  fail(`${prefix}vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next`)
107
108
  }
@@ -112,12 +113,12 @@ async function checkVuePackage(rootDir, fail) {
112
113
  const relConfig = join(rootDir, viteConfig)
113
114
  const content = await readFile(relConfig, 'utf8')
114
115
  if (content.includes('VueMacros')) {
115
- pass(`${prefix}${viteConfig} використовує VueMacros`)
116
+ passFn(`${prefix}${viteConfig} використовує VueMacros`)
116
117
  } else {
117
118
  fail(`${prefix}${viteConfig} не містить VueMacros`)
118
119
  }
119
120
  if (content.includes('AutoImport')) {
120
- pass(`${prefix}${viteConfig} використовує AutoImport`)
121
+ passFn(`${prefix}${viteConfig} використовує AutoImport`)
121
122
  } else {
122
123
  fail(`${prefix}${viteConfig} не містить AutoImport`)
123
124
  }
@@ -147,7 +148,7 @@ async function checkVuePackage(rootDir, fail) {
147
148
  }
148
149
  }
149
150
  if (importViolations === 0) {
150
- pass(
151
+ passFn(
151
152
  `${prefix}немає заборонених value-імпортів з 'vue' у джерелах (проскановано ${ukFilesCountPhrase(sourcePaths.length)})`
152
153
  )
153
154
  }
@@ -158,11 +159,8 @@ async function checkVuePackage(rootDir, fail) {
158
159
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
159
160
  */
160
161
  export async function check() {
161
- let exitCode = 0
162
- const fail = msg => {
163
- console.log(` ❌ ${msg}`)
164
- exitCode = 1
165
- }
162
+ const reporter = createCheckReporter()
163
+ const { pass, fail } = reporter
166
164
 
167
165
  if (existsSync('.vscode/extensions.json')) {
168
166
  const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
@@ -188,12 +186,12 @@ export async function check() {
188
186
 
189
187
  if (vueRoots.length === 0) {
190
188
  fail('vue не знайдено в dependencies жодного пакета (корінь репо та каталоги з кореневого workspaces)')
191
- return exitCode
189
+ return reporter.getExitCode()
192
190
  }
193
191
 
194
192
  for (const r of vueRoots) {
195
- await checkVuePackage(r, fail)
193
+ await checkVuePackage(r, fail, pass)
196
194
  }
197
195
 
198
- return exitCode
196
+ return reporter.getExitCode()
199
197
  }
@@ -11,7 +11,7 @@ import { basename } from 'node:path'
11
11
 
12
12
  import { isRunAsCli } from './cli-entry.mjs'
13
13
  import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mjs'
14
- import { pass } from './utils/pass.mjs'
14
+ import { createCheckReporter } from './utils/check-reporter.mjs'
15
15
  import { walkDir } from './utils/walkDir.mjs'
16
16
 
17
17
  /**
@@ -44,18 +44,15 @@ export async function findLintDockerfilePaths(root) {
44
44
  * @returns {Promise<number>} 0 — OK, 1 — зауваження або помилка
45
45
  */
46
46
  async function main() {
47
- let exitCode = 0
48
- const fail = msg => {
49
- console.log(` ❌ ${msg}`)
50
- exitCode = 1
51
- }
47
+ const reporter = createCheckReporter()
48
+ const { pass, fail } = reporter
52
49
 
53
50
  const root = process.cwd()
54
51
  const files = await findLintDockerfilePaths(root)
55
52
 
56
53
  if (files.length === 0) {
57
54
  pass('lint-docker: немає Dockerfile / *.Dockerfile — hadolint пропущено')
58
- return 0
55
+ return reporter.getExitCode()
59
56
  }
60
57
 
61
58
  pass(`lint-docker: файлів для hadolint: ${files.length}`)
@@ -71,7 +68,7 @@ async function main() {
71
68
  }
72
69
  }
73
70
 
74
- return exitCode
71
+ return reporter.getExitCode()
75
72
  }
76
73
 
77
74
  if (isRunAsCli()) {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Спільний репортер для check-скриптів і lint-docker.
3
+ *
4
+ * Об’єднує вивід успіхів (`pass` з `pass.mjs`) і помилок з префіксом ❌; накопичує код виходу **1**,
5
+ * якщо хоча б раз викликано `fail`.
6
+ *
7
+ * Використовуй `getExitCode()` у `return`, а не деструктуризацію `exitCode` — геттер «знімається» один раз.
8
+ */
9
+ import { pass } from './pass.mjs'
10
+
11
+ /**
12
+ * Створює пару `pass` / `fail` з накопиченням ненульового коду виходу.
13
+ * @returns {{ pass: typeof pass, fail: (msg: string) => void, getExitCode: () => number }} об’єкт з `pass`, `fail` і `getExitCode()` (0 або 1 після будь-якого `fail`)
14
+ */
15
+ export function createCheckReporter() {
16
+ let exitCode = 0
17
+ return {
18
+ pass,
19
+ fail(msg) {
20
+ console.log(` ❌ ${msg}`)
21
+ exitCode = 1
22
+ },
23
+ getExitCode() {
24
+ return exitCode
25
+ }
26
+ }
27
+ }