@nitra/cursor 1.8.69 → 1.8.73
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 +56 -1
- package/mdc/k8s.mdc +24 -0
- package/package.json +1 -1
- package/scripts/check-abie.mjs +316 -13
- package/scripts/check-bun.mjs +4 -7
- package/scripts/check-docker.mjs +5 -8
- package/scripts/check-ga.mjs +5 -8
- package/scripts/check-js-format.mjs +4 -7
- package/scripts/check-js-lint.mjs +4 -7
- package/scripts/check-js-pino.mjs +10 -12
- package/scripts/check-k8s.mjs +5 -8
- package/scripts/check-nginx-default-tpl.mjs +5 -8
- package/scripts/check-npm-module.mjs +4 -7
- package/scripts/check-style-lint.mjs +4 -7
- package/scripts/check-text.mjs +4 -7
- package/scripts/check-vue.mjs +17 -19
- package/scripts/run-docker.mjs +5 -8
- package/scripts/utils/check-reporter.mjs +27 -0
package/mdc/abie.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила для проєктів AbInBev Efes
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.4'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
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** без зауважень.
|
|
@@ -30,6 +30,49 @@ spec:
|
|
|
30
30
|
name: СЕРВІС
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## k8s: overlay **HTTPRoute** `nginx-run` (**ua** / **ru**)
|
|
34
|
+
|
|
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`**.
|
|
36
|
+
|
|
37
|
+
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
38
|
+
- target:
|
|
39
|
+
kind: HTTPRoute
|
|
40
|
+
name: nginx-run
|
|
41
|
+
patch: |-
|
|
42
|
+
- op: replace
|
|
43
|
+
path: /spec/hostnames
|
|
44
|
+
value:
|
|
45
|
+
- "abie.app" # також допускається vybeerai.com.ua, *.vybeerai.com.ua, *.abie.app
|
|
46
|
+
- op: replace
|
|
47
|
+
path: /spec/parentRefs/0/namespace
|
|
48
|
+
value: ua
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```yaml title="…/ru/kustomization.yaml (фрагмент)"
|
|
52
|
+
- target:
|
|
53
|
+
kind: HTTPRoute
|
|
54
|
+
name: nginx-run
|
|
55
|
+
patch: |-
|
|
56
|
+
- op: replace
|
|
57
|
+
path: /spec/hostnames
|
|
58
|
+
value:
|
|
59
|
+
- "napitkivmeste.tech" # також допускається выбирайонлайн.рф, *.napitkivmeste.tech, *.выбирайонлайн.рф
|
|
60
|
+
- op: replace
|
|
61
|
+
path: /spec/parentRefs/0/namespace
|
|
62
|
+
value: ru
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```yaml title="…/ru/kustomization.yaml (фрагмент)"
|
|
66
|
+
- target:
|
|
67
|
+
kind: HTTPRoute
|
|
68
|
+
name: nginx-run
|
|
69
|
+
patch: |-
|
|
70
|
+
- op: add
|
|
71
|
+
path: /metadata/annotations
|
|
72
|
+
value:
|
|
73
|
+
gwin.yandex.cloud/rules.http.upgradeTypes: "websocket"
|
|
74
|
+
```
|
|
75
|
+
|
|
33
76
|
## k8s: overlay **`ru`** і HealthCheckPolicy
|
|
34
77
|
|
|
35
78
|
Якщо в дереві **k8s** є **HealthCheckPolicy**, **check abie** вимагає **`ru/kustomization.yaml`** з patch **`$patch: delete`** для політики (узгоджено з **k8s.mdc** / **check-k8s**, **`ruKustomizationHasHealthCheckDeletePatch`** у **`npm/scripts/check-k8s.mjs`**). Підстав реальне ім’я замість **`СЕРВІС`**:
|
|
@@ -74,6 +117,18 @@ patches:
|
|
|
74
117
|
yandex.cloud/preemptible: "false"
|
|
75
118
|
```
|
|
76
119
|
|
|
120
|
+
### Базовий Deployment (`…/base/`)
|
|
121
|
+
|
|
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`**.
|
|
123
|
+
|
|
124
|
+
```yaml title="…/base/deploy.yaml (фрагмент)"
|
|
125
|
+
spec:
|
|
126
|
+
template:
|
|
127
|
+
spec:
|
|
128
|
+
nodeSelector:
|
|
129
|
+
preem: 'true' # буде замінено через kustomize
|
|
130
|
+
```
|
|
131
|
+
|
|
77
132
|
## Git branches
|
|
78
133
|
|
|
79
134
|
У **`.github/workflows/clean-merged-branch.yml`** у кроці **`phpdocker-io/github-actions-delete-abandoned-branches`** значення **`with.ignore_branches`** має містити **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно), наприклад:
|
package/mdc/k8s.mdc
CHANGED
|
@@ -206,6 +206,30 @@ patches:
|
|
|
206
206
|
value: dev
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
+
4. Якщо в kustomization.yaml є remove разом з add на однаковий path:
|
|
210
|
+
|
|
211
|
+
```yaml title="overlay/kustomization.yaml (фрагмент)"
|
|
212
|
+
- op: remove
|
|
213
|
+
path: /spec/template/spec/nodeSelector
|
|
214
|
+
- op: add
|
|
215
|
+
path: /spec/template/spec/nodeSelector
|
|
216
|
+
value:
|
|
217
|
+
preem: "false"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
заміняй на replace:
|
|
221
|
+
|
|
222
|
+
```yaml title="overlay/kustomization.yaml (фрагмент)"
|
|
223
|
+
- target:
|
|
224
|
+
kind: Deployment
|
|
225
|
+
name: x
|
|
226
|
+
patch: |-
|
|
227
|
+
- op: replace
|
|
228
|
+
path: /spec/template/spec/nodeSelector
|
|
229
|
+
value:
|
|
230
|
+
preem: "false"
|
|
231
|
+
```
|
|
232
|
+
|
|
209
233
|
## Перевірка
|
|
210
234
|
|
|
211
235
|
**`npx @nitra/cursor check k8s`** — програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -14,9 +14,16 @@
|
|
|
14
14
|
* Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
|
|
15
15
|
* (логіка вмісту — **`ruKustomizationHasHealthCheckDeletePatch`** у **check-k8s.mjs**, узгоджено з **k8s.mdc**).
|
|
16
16
|
*
|
|
17
|
-
* **nodeSelector:** якщо
|
|
17
|
+
* **nodeSelector (base):** якщо **Deployment** лежить у шляху з сегментом **`base`** (наприклад **`…/k8s/base/deploy.yaml`**),
|
|
18
|
+
* у **`spec.template.spec.nodeSelector`** має бути **`preem`** з булевим значенням **true** або рядком **`'true'`** — overlay **ua** та **ru** далі підміняють селектор.
|
|
19
|
+
*
|
|
20
|
+
* **nodeSelector (overlay):** якщо є **Deployment** під **k8s**, у кожному **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`**
|
|
18
21
|
* має бути inline **JSON6902** patch на **`kind: Deployment`**: для **ua** — **`op: add`**, **`path: /spec/template/spec/nodeSelector`**,
|
|
19
22
|
* **`preem: false`**; для **ru** — **`op: replace`**, той самий **path**, **`yandex.cloud/preemptible: false`** (див. abie.mdc).
|
|
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`**.
|
|
20
27
|
*/
|
|
21
28
|
import { existsSync } from 'node:fs'
|
|
22
29
|
import { readFile } from 'node:fs/promises'
|
|
@@ -25,7 +32,7 @@ import { dirname, join, relative } from 'node:path'
|
|
|
25
32
|
import { parseAllDocuments } from 'yaml'
|
|
26
33
|
|
|
27
34
|
import { pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
|
|
28
|
-
import {
|
|
35
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
29
36
|
import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
|
|
30
37
|
import { walkDir } from './utils/walkDir.mjs'
|
|
31
38
|
|
|
@@ -59,6 +66,60 @@ export function isUaKustomizationPath(rel) {
|
|
|
59
66
|
return /(^|\/)ua\/kustomization\.yaml$/u.test(norm)
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Чи відносний шлях до YAML під **k8s** вказує на файл у каталозі **`base`** (сегмент **`base`** у шляху), abie.mdc.
|
|
71
|
+
* @param {string} rel шлях від кореня репозиторію
|
|
72
|
+
* @returns {boolean} true, якщо в шляху є **`/base/`**
|
|
73
|
+
*/
|
|
74
|
+
export function isAbieK8sBaseYamlPath(rel) {
|
|
75
|
+
const norm = rel.replaceAll('\\', '/')
|
|
76
|
+
return /(^|\/)base\//u.test(norm)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Чи значення **`preem`** у base **Deployment** вважається «істинним» за abie.mdc (**true** або рядок **`true`** без урахування регістру).
|
|
81
|
+
* @param {unknown} v значення з YAML
|
|
82
|
+
* @returns {boolean} **true**, якщо значення вважається істинним за abie.mdc
|
|
83
|
+
*/
|
|
84
|
+
function isAbiePreemTruthy(v) {
|
|
85
|
+
if (v === true) {
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
if (typeof v === 'string' && v.trim().toLowerCase() === 'true') {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Чи документ **Deployment** у **`…/base/…`** містить **`spec.template.spec.nodeSelector.preem`** зі значенням **true** (abie.mdc).
|
|
96
|
+
* @param {unknown} obj корінь YAML-документа (**Deployment**)
|
|
97
|
+
* @returns {boolean} true, якщо критерії виконано
|
|
98
|
+
*/
|
|
99
|
+
export function deploymentDocumentHasAbieBasePreemNodeSelector(obj) {
|
|
100
|
+
if (!isDeploymentDoc(obj)) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
104
|
+
const spec = rec.spec
|
|
105
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
109
|
+
if (template === null || typeof template !== 'object' || Array.isArray(template)) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
113
|
+
if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
const nodeSelector = /** @type {Record<string, unknown>} */ (podSpec).nodeSelector
|
|
117
|
+
if (nodeSelector === null || typeof nodeSelector !== 'object' || Array.isArray(nodeSelector)) {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
return isAbiePreemTruthy(nodeSelector.preem)
|
|
121
|
+
}
|
|
122
|
+
|
|
62
123
|
/**
|
|
63
124
|
* Чи увімкнено правило **abie** у конфігу репозиторію.
|
|
64
125
|
* @param {string} root корінь репозиторію (cwd)
|
|
@@ -213,6 +274,65 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
|
213
274
|
return dirs
|
|
214
275
|
}
|
|
215
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Для кожного **Deployment** у YAML під **`k8s`** з шляхом **`…/base/…`** вимагає **`spec.template.spec.nodeSelector.preem: true`** (abie.mdc).
|
|
279
|
+
* @param {string} root корінь репозиторію
|
|
280
|
+
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
281
|
+
* @param {(msg: string) => void} fail callback
|
|
282
|
+
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
283
|
+
* @returns {Promise<void>}
|
|
284
|
+
*/
|
|
285
|
+
async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
|
|
286
|
+
const baseFiles = yamlFilesAbs.filter(abs => {
|
|
287
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
288
|
+
return isAbieK8sBaseYamlPath(rel)
|
|
289
|
+
})
|
|
290
|
+
let anyBaseDeployment = false
|
|
291
|
+
for (const abs of baseFiles) {
|
|
292
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
293
|
+
let raw
|
|
294
|
+
try {
|
|
295
|
+
raw = await readFile(abs, 'utf8')
|
|
296
|
+
} catch (error) {
|
|
297
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
298
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
const body = stripBom(raw)
|
|
302
|
+
const lines = body.split(/\r?\n/u)
|
|
303
|
+
const first = lines[0] ?? ''
|
|
304
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
305
|
+
/** @type {import('yaml').Document[]} */
|
|
306
|
+
let docs
|
|
307
|
+
try {
|
|
308
|
+
docs = parseAllDocuments(rest)
|
|
309
|
+
} catch (error) {
|
|
310
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
311
|
+
fail(`${rel}: YAML (${msg})`)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
for (const doc of docs) {
|
|
315
|
+
if (doc.errors.length === 0) {
|
|
316
|
+
const obj = doc.toJSON()
|
|
317
|
+
if (isDeploymentDoc(obj)) {
|
|
318
|
+
anyBaseDeployment = true
|
|
319
|
+
if (!deploymentDocumentHasAbieBasePreemNodeSelector(obj)) {
|
|
320
|
+
fail(
|
|
321
|
+
`${rel}: Deployment у base: потрібен spec.template.spec.nodeSelector.preem: true (або 'true') — abie.mdc`
|
|
322
|
+
)
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (anyBaseDeployment) {
|
|
330
|
+
passFn('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc')
|
|
331
|
+
} else {
|
|
332
|
+
passFn('Немає Deployment у шляхах …/base/… — перевірку preem у base пропущено')
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
216
336
|
/**
|
|
217
337
|
* Прибирає BOM на початку файлу.
|
|
218
338
|
* @param {string} s вміст
|
|
@@ -352,6 +472,125 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
|
352
472
|
return false
|
|
353
473
|
}
|
|
354
474
|
|
|
475
|
+
/** Домени **hostnames** для overlay **ua** (підрядки у JSON6902-тексті patch), abie.mdc. */
|
|
476
|
+
const ABIE_UA_HTTPROUTE_HOST_MARKERS = ['abie.app', 'vybeerai.com.ua', '*.abie.app', '*.vybeerai.com.ua']
|
|
477
|
+
|
|
478
|
+
/** Домени **hostnames** для overlay **ru** (підрядки), abie.mdc. */
|
|
479
|
+
const ABIE_RU_HTTPROUTE_HOST_MARKERS = [
|
|
480
|
+
'napitkivmeste.tech',
|
|
481
|
+
'выбирайонлайн.рф',
|
|
482
|
+
'*.napitkivmeste.tech',
|
|
483
|
+
'*.выбирайонлайн.рф'
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Збирає тексти inline **patch** для **HTTPRoute/nginx-run** з одного розібраного документа **Kustomization**.
|
|
488
|
+
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
489
|
+
* @returns {string[]} непорожні рядки **patch**
|
|
490
|
+
*/
|
|
491
|
+
function collectNginxRunPatchStringsFromKustomizationDoc(doc) {
|
|
492
|
+
if (doc.errors.length > 0) {
|
|
493
|
+
return []
|
|
494
|
+
}
|
|
495
|
+
const root = doc.toJSON()
|
|
496
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) {
|
|
497
|
+
return []
|
|
498
|
+
}
|
|
499
|
+
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
500
|
+
if (rec.kind !== 'Kustomization') {
|
|
501
|
+
return []
|
|
502
|
+
}
|
|
503
|
+
const patches = rec.patches
|
|
504
|
+
if (!Array.isArray(patches)) {
|
|
505
|
+
return []
|
|
506
|
+
}
|
|
507
|
+
/** @type {string[]} */
|
|
508
|
+
const out = []
|
|
509
|
+
for (const p of patches) {
|
|
510
|
+
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
511
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
512
|
+
const target = pr.target
|
|
513
|
+
if (target !== null && typeof target === 'object' && !Array.isArray(target)) {
|
|
514
|
+
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
515
|
+
if (tg.kind === 'HTTPRoute' && tg.name === 'nginx-run') {
|
|
516
|
+
const patchStr = pr.patch
|
|
517
|
+
if (typeof patchStr === 'string' && patchStr.trim() !== '') {
|
|
518
|
+
out.push(patchStr)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return out
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Збирає всі inline **JSON6902**-фрагменти для **HTTPRoute/nginx-run** у **kustomization.yaml** (усі документи у файлі).
|
|
529
|
+
* @param {string} raw повний текст файлу
|
|
530
|
+
* @returns {string} текст для **`validateAbieNginxRunHttpRoutePatches`** (може бути порожнім)
|
|
531
|
+
*/
|
|
532
|
+
export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
533
|
+
const body = stripBom(raw)
|
|
534
|
+
const lines = body.split(/\r?\n/u)
|
|
535
|
+
const first = lines[0] ?? ''
|
|
536
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
537
|
+
/** @type {import('yaml').Document[]} */
|
|
538
|
+
let docs
|
|
539
|
+
try {
|
|
540
|
+
docs = parseAllDocuments(rest)
|
|
541
|
+
} catch {
|
|
542
|
+
return ''
|
|
543
|
+
}
|
|
544
|
+
/** @type {string[]} */
|
|
545
|
+
const chunks = []
|
|
546
|
+
for (const doc of docs) {
|
|
547
|
+
chunks.push(...collectNginxRunPatchStringsFromKustomizationDoc(doc))
|
|
548
|
+
}
|
|
549
|
+
return chunks.join('\n')
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Перевіряє сукупний текст patch(ів) **HTTPRoute/nginx-run** на відповідність abie.mdc.
|
|
554
|
+
* @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
|
|
555
|
+
* @param {'ua' | 'ru'} mode **ua** або **ru**
|
|
556
|
+
* @returns {string | null} повідомлення про помилку або **null**
|
|
557
|
+
*/
|
|
558
|
+
export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
|
|
559
|
+
if (typeof combined !== 'string' || combined.trim() === '') {
|
|
560
|
+
return `очікується patch target kind HTTPRoute name nginx-run (replace hostnames, parentRefs namespace ${mode}; для ru — також gwin… upgradeTypes websocket) — abie.mdc`
|
|
561
|
+
}
|
|
562
|
+
const hasHostnamesReplace = /-\s*op:\s*replace\b[\s\S]{0,200}?path:\s*\/spec\/hostnames\b/m.test(combined)
|
|
563
|
+
if (!hasHostnamesReplace) {
|
|
564
|
+
return 'HTTPRoute nginx-run: потрібен блок op replace з path /spec/hostnames (abie.mdc)'
|
|
565
|
+
}
|
|
566
|
+
const markers = mode === 'ua' ? ABIE_UA_HTTPROUTE_HOST_MARKERS : ABIE_RU_HTTPROUTE_HOST_MARKERS
|
|
567
|
+
if (!markers.some(m => combined.includes(m))) {
|
|
568
|
+
return `HTTPRoute nginx-run: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
|
|
569
|
+
}
|
|
570
|
+
const namespaceOk =
|
|
571
|
+
mode === 'ua'
|
|
572
|
+
? /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu.test(combined)
|
|
573
|
+
: /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu.test(combined)
|
|
574
|
+
if (!namespaceOk) {
|
|
575
|
+
return `HTTPRoute nginx-run: потрібен replace path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
|
|
576
|
+
}
|
|
577
|
+
if (mode === 'ru' && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
|
|
578
|
+
return 'HTTPRoute nginx-run (ru): потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
|
|
579
|
+
}
|
|
580
|
+
return null
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Чи **kustomization** містить валідні для abie записи **patch** для **HTTPRoute/nginx-run** (**ua** або **ru**).
|
|
585
|
+
* @param {string} raw повний текст **kustomization.yaml**
|
|
586
|
+
* @param {'ua' | 'ru'} mode overlay
|
|
587
|
+
* @returns {boolean} true, якщо **`validateAbieNginxRunHttpRoutePatches`** повертає **null**
|
|
588
|
+
*/
|
|
589
|
+
export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
|
|
590
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
591
|
+
return validateAbieNginxRunHttpRoutePatches(combined, mode) === null
|
|
592
|
+
}
|
|
593
|
+
|
|
355
594
|
/**
|
|
356
595
|
* Перевіряє **hc.yaml** на відповідність abie.mdc.
|
|
357
596
|
* @param {string} raw повний текст файлу
|
|
@@ -536,9 +775,10 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
|
|
|
536
775
|
* @param {string} root корінь репозиторію
|
|
537
776
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
538
777
|
* @param {(msg: string) => void} fail callback
|
|
778
|
+
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
539
779
|
* @returns {Promise<void>}
|
|
540
780
|
*/
|
|
541
|
-
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
|
|
781
|
+
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passFn) {
|
|
542
782
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
543
783
|
if (uaAbsList.length === 0) {
|
|
544
784
|
fail(
|
|
@@ -562,7 +802,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
|
|
|
562
802
|
)
|
|
563
803
|
return
|
|
564
804
|
}
|
|
565
|
-
|
|
805
|
+
passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
566
806
|
}
|
|
567
807
|
|
|
568
808
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
@@ -588,7 +828,69 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
|
|
|
588
828
|
)
|
|
589
829
|
return
|
|
590
830
|
}
|
|
591
|
-
|
|
831
|
+
passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** patch **HTTPRoute/nginx-run** (abie.mdc).
|
|
837
|
+
* @param {string} root корінь репозиторію
|
|
838
|
+
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
839
|
+
* @param {(msg: string) => void} fail callback
|
|
840
|
+
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
841
|
+
* @returns {Promise<void>}
|
|
842
|
+
*/
|
|
843
|
+
async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
|
|
844
|
+
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
845
|
+
if (uaAbsList.length === 0) {
|
|
846
|
+
fail(
|
|
847
|
+
'Є Deployment у k8s — додай ua/kustomization.yaml з patch HTTPRoute nginx-run (hostnames, parentRefs namespace ua) — abie.mdc'
|
|
848
|
+
)
|
|
849
|
+
return
|
|
850
|
+
}
|
|
851
|
+
for (const abs of uaAbsList) {
|
|
852
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
853
|
+
let raw
|
|
854
|
+
try {
|
|
855
|
+
raw = await readFile(abs, 'utf8')
|
|
856
|
+
} catch (error) {
|
|
857
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
858
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
859
|
+
return
|
|
860
|
+
}
|
|
861
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
862
|
+
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua')
|
|
863
|
+
if (v !== null) {
|
|
864
|
+
fail(`${rel}: ${v}`)
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
passFn(`${rel}: HTTPRoute nginx-run (ua) відповідає abie.mdc`)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
871
|
+
if (ruAbsList.length === 0) {
|
|
872
|
+
fail(
|
|
873
|
+
'Є Deployment у k8s — додай ru/kustomization.yaml з patch HTTPRoute nginx-run (hostnames, namespace ru, gwin websocket) — abie.mdc'
|
|
874
|
+
)
|
|
875
|
+
return
|
|
876
|
+
}
|
|
877
|
+
for (const abs of ruAbsList) {
|
|
878
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
879
|
+
let raw
|
|
880
|
+
try {
|
|
881
|
+
raw = await readFile(abs, 'utf8')
|
|
882
|
+
} catch (error) {
|
|
883
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
884
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
885
|
+
return
|
|
886
|
+
}
|
|
887
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
888
|
+
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru')
|
|
889
|
+
if (v !== null) {
|
|
890
|
+
fail(`${rel}: ${v}`)
|
|
891
|
+
return
|
|
892
|
+
}
|
|
893
|
+
passFn(`${rel}: HTTPRoute nginx-run (ru) відповідає abie.mdc`)
|
|
592
894
|
}
|
|
593
895
|
}
|
|
594
896
|
|
|
@@ -597,17 +899,14 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
|
|
|
597
899
|
* @returns {Promise<number>} 0 — OK, 1 — є порушення
|
|
598
900
|
*/
|
|
599
901
|
export async function check() {
|
|
600
|
-
|
|
601
|
-
const fail =
|
|
602
|
-
console.log(` ❌ ${msg}`)
|
|
603
|
-
exitCode = 1
|
|
604
|
-
}
|
|
902
|
+
const reporter = createCheckReporter()
|
|
903
|
+
const { pass, fail } = reporter
|
|
605
904
|
|
|
606
905
|
const root = process.cwd()
|
|
607
906
|
const enabled = await isAbieRuleEnabled(root)
|
|
608
907
|
if (!enabled) {
|
|
609
908
|
pass(`Правило abie не увімкнено в ${CONFIG_FILE} (rules) — перевірку пропущено`)
|
|
610
|
-
return
|
|
909
|
+
return reporter.getExitCode()
|
|
611
910
|
}
|
|
612
911
|
|
|
613
912
|
pass('Правило abie увімкнено — виконуємо перевірки')
|
|
@@ -672,6 +971,8 @@ export async function check() {
|
|
|
672
971
|
)
|
|
673
972
|
}
|
|
674
973
|
}
|
|
974
|
+
pass('Є Deployment — перевіряємо base: spec.template.spec.nodeSelector.preem (abie.mdc)')
|
|
975
|
+
await ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFiles, fail, pass)
|
|
675
976
|
} else {
|
|
676
977
|
pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
|
|
677
978
|
}
|
|
@@ -681,8 +982,10 @@ export async function check() {
|
|
|
681
982
|
|
|
682
983
|
if (deploymentDirs.size > 0) {
|
|
683
984
|
pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
|
|
684
|
-
await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail)
|
|
985
|
+
await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail, pass)
|
|
986
|
+
pass('Є Deployment — перевіряємо HTTPRoute nginx-run у ua/ru kustomization (abie.mdc)')
|
|
987
|
+
await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
|
|
685
988
|
}
|
|
686
989
|
|
|
687
|
-
return
|
|
990
|
+
return reporter.getExitCode()
|
|
688
991
|
}
|
package/scripts/check-bun.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { existsSync } from 'node:fs'
|
|
18
18
|
import { readFile } from 'node:fs/promises'
|
|
19
19
|
|
|
20
|
-
import {
|
|
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
|
-
|
|
57
|
-
const fail =
|
|
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
|
|
164
|
+
return reporter.getExitCode()
|
|
168
165
|
}
|
package/scripts/check-docker.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
46
|
-
const fail =
|
|
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
|
|
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
|
|
69
|
+
return reporter.getExitCode()
|
|
73
70
|
}
|
package/scripts/check-ga.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
126
|
-
const fail =
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
17
|
-
const fail =
|
|
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
|
|
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 {
|
|
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
|
-
|
|
45
|
-
const fail =
|
|
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
|
|
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 {
|
|
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
|
-
|
|
40
|
+
passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
|
|
40
41
|
if (content.includes('service.name=') && content.includes('service.namespace=')) {
|
|
41
|
-
|
|
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
|
-
|
|
57
|
-
const fail =
|
|
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
|
|
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
|
|
72
|
+
return reporter.getExitCode()
|
|
75
73
|
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
1486
|
-
const fail =
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
268
|
-
const fail =
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
25
|
-
const fail =
|
|
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
|
|
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 {
|
|
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
|
-
|
|
20
|
-
const fail =
|
|
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
|
|
105
|
+
return reporter.getExitCode()
|
|
109
106
|
}
|
package/scripts/check-text.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { existsSync } from 'node:fs'
|
|
13
13
|
import { readFile } from 'node:fs/promises'
|
|
14
14
|
|
|
15
|
-
import {
|
|
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
|
-
|
|
51
|
-
const fail =
|
|
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
|
|
215
|
+
return reporter.getExitCode()
|
|
219
216
|
}
|
package/scripts/check-vue.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
+
passFn(`${prefix}${viteConfig} використовує VueMacros`)
|
|
116
117
|
} else {
|
|
117
118
|
fail(`${prefix}${viteConfig} не містить VueMacros`)
|
|
118
119
|
}
|
|
119
120
|
if (content.includes('AutoImport')) {
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
const fail =
|
|
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
|
|
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
|
|
196
|
+
return reporter.getExitCode()
|
|
199
197
|
}
|
package/scripts/run-docker.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
48
|
-
const fail =
|
|
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
|
|
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
|
|
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 }}
|
|
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
|
+
}
|