@nitra/cursor 1.8.64 → 1.8.69

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,20 +1,22 @@
1
1
  ---
2
- description: Правила для проєктів abinbevefes
2
+ description: Правила для проєктів AbInBev Efes
3
3
  alwaysApply: true
4
- version: '1.0'
4
+ version: '1.2'
5
5
  ---
6
6
 
7
- ## k8s
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** без зауважень.
8
8
 
9
- Якщо в проекті є k8s deployment, рядом з ним повинен бути:
9
+ ## k8s: `hc.yaml` поруч із Deployment
10
+
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`**.
10
12
 
11
13
  ```yaml title="hc.yaml"
12
14
  # yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
13
15
  apiVersion: networking.gke.io/v1
14
16
  kind: HealthCheckPolicy
15
17
  metadata:
16
- name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
17
- namespace: dev # буде замінено через kustomize
18
+ name: СЕРВІС
19
+ namespace: dev # kustomize overlay
18
20
  spec:
19
21
  default:
20
22
  config:
@@ -25,34 +27,62 @@ spec:
25
27
  targetRef:
26
28
  group: ''
27
29
  kind: Service
28
- name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
30
+ name: СЕРВІС
29
31
  ```
30
32
 
31
- **Overlay `ru`:** у **`…/ru/kustomization.yaml`** під **`k8s`** додай **видалення** ресурсу HealthCheckPolicy для середовища, де політика не потрібна (підстав **реальне ім’я** замість `НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ`):
33
+ ## k8s: overlay **`ru`** і HealthCheckPolicy
34
+
35
+ Якщо в дереві **k8s** є **HealthCheckPolicy**, **check abie** вимагає **`ru/kustomization.yaml`** з patch **`$patch: delete`** для політики (узгоджено з **k8s.mdc** / **check-k8s**, **`ruKustomizationHasHealthCheckDeletePatch`** у **`npm/scripts/check-k8s.mjs`**). Підстав реальне ім’я замість **`СЕРВІС`**:
32
36
 
33
- ```yaml title="kustomization.yaml"
37
+ ```yaml title="…/ru/kustomization.yaml (фрагмент)"
34
38
  patches:
35
39
  - target:
36
40
  kind: HealthCheckPolicy
41
+ name: СЕРВІС
37
42
  patch: |-
38
43
  kind: HealthCheckPolicy
39
44
  metadata:
40
- name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
45
+ name: СЕРВІС
41
46
  $patch: delete
42
47
  ```
43
48
 
44
- ## branch
49
+ ## k8s: overlay **`ua`** / **`ru`** і nodeSelector
45
50
 
46
- В lean-merged-branch.yml список гілок, які повинні бути:
51
+ Якщо під **`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`**.
47
52
 
48
- ```yaml title=".github/workflows/clean-merged-branch.yml"
49
- ignore_branches: dev,ua,ru
53
+ ```yaml title="…/ua/kustomization.yaml (фрагмент)"
54
+ patches:
55
+ - target:
56
+ kind: Deployment
57
+ name: my-app
58
+ patch: |-
59
+ - op: add
60
+ path: /spec/template/spec/nodeSelector
61
+ value:
62
+ preem: 'false'
50
63
  ```
51
64
 
52
- ## Перевірка
65
+ ```yaml title="…/ru/kustomization.yaml (фрагмент)"
66
+ patches:
67
+ - target:
68
+ kind: Deployment
69
+ name: my-app
70
+ patch: |-
71
+ - op: replace
72
+ path: /spec/template/spec/nodeSelector
73
+ value:
74
+ yandex.cloud/preemptible: "false"
75
+ ```
76
+
77
+ ## Git branches
53
78
 
54
- `npx @nitra/cursor check abie`
79
+ У **`.github/workflows/clean-merged-branch.yml`** у кроці **`phpdocker-io/github-actions-delete-abandoned-branches`** значення **`with.ignore_branches`** має містити **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно), наприклад:
55
80
 
56
- Повна матриця полів **`hc.yaml`**, **`ignore_branches`** і умов для **`ru/kustomization.yaml`** — у **`npm/scripts/check-abie.mjs`**.
81
+ ```yaml title=".github/workflows/clean-merged-branch.yml (фрагмент)"
82
+ with:
83
+ ignore_branches: main,dev,ua,ru
84
+ ```
85
+
86
+ ## Перевірка
57
87
 
58
- Програмна перевірка (**`check-abie.mjs`**) виконується лише якщо у **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень (щоб не вимагати **ua**/**ru** у репозиторіях без цього правила).
88
+ **`npx @nitra/cursor check abie`**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.64",
3
+ "version": "1.8.69",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Перевіряє відповідність проєкту правилу abie.mdc (проєкти abinbevefes).
2
+ * Перевіряє відповідність проєкту правилу abie.mdc (проєкти AbInBev Efes).
3
3
  *
4
4
  * Застосовується лише якщо у **`.n-cursor.json`** у масиві **`rules`** є **`abie`** — інакше вихід **0**
5
5
  * без перевірок (щоб не суперечити типовому **ga.mdc** з **`ignore_branches: main,dev`**).
@@ -12,7 +12,11 @@
12
12
  * має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
13
13
  * як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **Service** з тим самим **`metadata.name`**.
14
14
  * Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
15
- * (узгоджено з **k8s.mdc** / **check-k8s.mjs**).
15
+ * (логіка вмісту **`ruKustomizationHasHealthCheckDeletePatch`** у **check-k8s.mjs**, узгоджено з **k8s.mdc**).
16
+ *
17
+ * **nodeSelector:** якщо є **Deployment** під **k8s**, у кожному **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`**
18
+ * має бути inline **JSON6902** patch на **`kind: Deployment`**: для **ua** — **`op: add`**, **`path: /spec/template/spec/nodeSelector`**,
19
+ * **`preem: false`**; для **ru** — **`op: replace`**, той самий **path**, **`yandex.cloud/preemptible: false`** (див. abie.mdc).
16
20
  */
17
21
  import { existsSync } from 'node:fs'
18
22
  import { readFile } from 'node:fs/promises'
@@ -20,7 +24,7 @@ import { dirname, join, relative } from 'node:path'
20
24
 
21
25
  import { parseAllDocuments } from 'yaml'
22
26
 
23
- import { isRuKustomizationPath, pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
27
+ import { pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
24
28
  import { pass } from './utils/pass.mjs'
25
29
  import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
26
30
  import { walkDir } from './utils/walkDir.mjs'
@@ -35,6 +39,26 @@ const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
35
39
  /** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
36
40
  export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
37
41
 
42
+ /**
43
+ * Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім’ям файлу) — специфіка abie overlay.
44
+ * @param {string} rel шлях від кореня репозиторію
45
+ * @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
46
+ */
47
+ export function isRuKustomizationPath(rel) {
48
+ const norm = rel.replaceAll('\\', '/')
49
+ return /(^|\/)ru\/kustomization\.yaml$/u.test(norm)
50
+ }
51
+
52
+ /**
53
+ * Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед ім’ям файлу) — специфіка abie overlay.
54
+ * @param {string} rel шлях від кореня репозиторію
55
+ * @returns {boolean} true, якщо це `…/ua/kustomization.yaml`
56
+ */
57
+ export function isUaKustomizationPath(rel) {
58
+ const norm = rel.replaceAll('\\', '/')
59
+ return /(^|\/)ua\/kustomization\.yaml$/u.test(norm)
60
+ }
61
+
38
62
  /**
39
63
  * Чи увімкнено правило **abie** у конфігу репозиторію.
40
64
  * @param {string} root корінь репозиторію (cwd)
@@ -198,6 +222,136 @@ function stripBom(s) {
198
222
  return s.startsWith('\uFEFF') ? s.slice(1) : s
199
223
  }
200
224
 
225
+ /**
226
+ * Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**op: add**, **preem: false**).
227
+ * @param {string} patchText поле **patch** у kustomization
228
+ * @returns {boolean} true, якщо критерії abie.mdc виконано
229
+ */
230
+ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
231
+ if (typeof patchText !== 'string' || patchText.trim() === '') {
232
+ return false
233
+ }
234
+ if (!/op:\s*add\b/u.test(patchText)) {
235
+ return false
236
+ }
237
+ if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
238
+ return false
239
+ }
240
+ if (!/\bpreem:\s*['"]?false['"]?\b/u.test(patchText)) {
241
+ return false
242
+ }
243
+ return true
244
+ }
245
+
246
+ /**
247
+ * Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**op: replace**, **yandex.cloud/preemptible: false**).
248
+ * @param {string} patchText поле **patch** у kustomization
249
+ * @returns {boolean} true, якщо критерії abie.mdc виконано
250
+ */
251
+ function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
252
+ if (typeof patchText !== 'string' || patchText.trim() === '') {
253
+ return false
254
+ }
255
+ if (!/op:\s*replace\b/u.test(patchText)) {
256
+ return false
257
+ }
258
+ if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
259
+ return false
260
+ }
261
+ if (!/yandex\.cloud\/preemptible:\s*['"]?false['"]?/u.test(patchText)) {
262
+ return false
263
+ }
264
+ return true
265
+ }
266
+
267
+ /**
268
+ * Чи один елемент **patches** у kustomization відповідає abie nodeSelector для заданого **mode**.
269
+ * @param {unknown} p елемент масиву **patches**
270
+ * @param {'ua' | 'ru'} mode який overlay перевіряти
271
+ * @returns {boolean} true, якщо patch відповідає abie для **mode**
272
+ */
273
+ function inlineKustomizationPatchMatchesAbieMode(p, mode) {
274
+ if (p === null || typeof p !== 'object' || Array.isArray(p)) {
275
+ return false
276
+ }
277
+ const pr = /** @type {Record<string, unknown>} */ (p)
278
+ const target = pr.target
279
+ if (target === null || typeof target !== 'object' || Array.isArray(target)) {
280
+ return false
281
+ }
282
+ const tg = /** @type {Record<string, unknown>} */ (target)
283
+ if (tg.kind !== 'Deployment') {
284
+ return false
285
+ }
286
+ const patchStr = pr.patch
287
+ if (typeof patchStr !== 'string') {
288
+ return false
289
+ }
290
+ if (mode === 'ua' && jsonPatchTextHasUaDeploymentNodeSelector(patchStr)) {
291
+ return true
292
+ }
293
+ if (mode === 'ru' && jsonPatchTextHasRuDeploymentNodeSelector(patchStr)) {
294
+ return true
295
+ }
296
+ return false
297
+ }
298
+
299
+ /**
300
+ * Чи один YAML-документ kustomization містить відповідний inline patch на Deployment.
301
+ * @param {import('yaml').Document} doc документ після **parseAllDocuments**
302
+ * @param {'ua' | 'ru'} mode який overlay перевіряти
303
+ * @returns {boolean} true, якщо знайдено відповідний patch
304
+ */
305
+ function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
306
+ if (doc.errors.length > 0) {
307
+ return false
308
+ }
309
+ const root = doc.toJSON()
310
+ if (root === null || typeof root !== 'object' || Array.isArray(root)) {
311
+ return false
312
+ }
313
+ const rec = /** @type {Record<string, unknown>} */ (root)
314
+ if (rec.kind !== 'Kustomization') {
315
+ return false
316
+ }
317
+ const patches = rec.patches
318
+ if (!Array.isArray(patches)) {
319
+ return false
320
+ }
321
+ for (const p of patches) {
322
+ if (inlineKustomizationPatchMatchesAbieMode(p, mode)) {
323
+ return true
324
+ }
325
+ }
326
+ return false
327
+ }
328
+
329
+ /**
330
+ * Чи **kustomization.yaml** містить inline **patches** на **Deployment** з nodeSelector за abie.mdc (**ua** або **ru**).
331
+ * @param {string} raw повний текст файлу
332
+ * @param {'ua' | 'ru'} mode який overlay перевіряти
333
+ * @returns {boolean} true, якщо знайдено відповідний patch
334
+ */
335
+ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
336
+ const body = stripBom(raw)
337
+ const lines = body.split(/\r?\n/u)
338
+ const first = lines[0] ?? ''
339
+ const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
340
+ /** @type {import('yaml').Document[]} */
341
+ let docs
342
+ try {
343
+ docs = parseAllDocuments(rest)
344
+ } catch {
345
+ return false
346
+ }
347
+ for (const doc of docs) {
348
+ if (kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode)) {
349
+ return true
350
+ }
351
+ }
352
+ return false
353
+ }
354
+
201
355
  /**
202
356
  * Перевіряє **hc.yaml** на відповідність abie.mdc.
203
357
  * @param {string} raw повний текст файлу
@@ -341,7 +495,7 @@ async function collectHealthCheckPolicyRelPaths(root, yamlAbs) {
341
495
  }
342
496
 
343
497
  /**
344
- * Якщо є **HealthCheckPolicy**, вимагає **ru/kustomization.yaml** з patch видалення (як **check-k8s**).
498
+ * Якщо є **HealthCheckPolicy**, вимагає **ru/kustomization.yaml** з patch видалення (**ruKustomizationHasHealthCheckDeletePatch** у **check-k8s**).
345
499
  * @param {string} root корінь
346
500
  * @param {string[]} yamlFilesAbs абсолютні шляхи yaml k8s
347
501
  * @param {string[]} healthCheckPolicyRelativePaths відносні шляхи
@@ -377,6 +531,67 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
377
531
  )
378
532
  }
379
533
 
534
+ /**
535
+ * Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** (**kustomization.yaml**) JSON6902 patch nodeSelector (abie.mdc).
536
+ * @param {string} root корінь репозиторію
537
+ * @param {string[]} yamlFilesAbs yaml під k8s
538
+ * @param {(msg: string) => void} fail callback
539
+ * @returns {Promise<void>}
540
+ */
541
+ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
542
+ const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
543
+ if (uaAbsList.length === 0) {
544
+ fail(
545
+ 'Є Deployment у k8s — додай ua/kustomization.yaml з inline patch на Deployment: op add, path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
546
+ )
547
+ return
548
+ }
549
+ for (const abs of uaAbsList) {
550
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
551
+ let raw
552
+ try {
553
+ raw = await readFile(abs, 'utf8')
554
+ } catch (error) {
555
+ const msg = error instanceof Error ? error.message : String(error)
556
+ fail(`${rel}: не вдалося прочитати (${msg})`)
557
+ return
558
+ }
559
+ if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
560
+ fail(
561
+ `${rel}: потрібен patch target kind Deployment з op: add, path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
562
+ )
563
+ return
564
+ }
565
+ pass(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
566
+ }
567
+
568
+ const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
569
+ if (ruAbsList.length === 0) {
570
+ fail(
571
+ 'Є Deployment у k8s — додай ru/kustomization.yaml з inline patch на Deployment: op replace, path /spec/template/spec/nodeSelector, yandex.cloud/preemptible false (abie.mdc)'
572
+ )
573
+ return
574
+ }
575
+ for (const abs of ruAbsList) {
576
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
577
+ let raw
578
+ try {
579
+ raw = await readFile(abs, 'utf8')
580
+ } catch (error) {
581
+ const msg = error instanceof Error ? error.message : String(error)
582
+ fail(`${rel}: не вдалося прочитати (${msg})`)
583
+ return
584
+ }
585
+ if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
586
+ fail(
587
+ `${rel}: потрібен patch target kind Deployment з op: replace, path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
588
+ )
589
+ return
590
+ }
591
+ pass(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
592
+ }
593
+ }
594
+
380
595
  /**
381
596
  * Перевіряє відповідність проєкту правилам abie.mdc.
382
597
  * @returns {Promise<number>} 0 — OK, 1 — є порушення
@@ -464,5 +679,10 @@ export async function check() {
464
679
  const healthCheckPolicyRelativePaths = await collectHealthCheckPolicyRelPaths(root, yamlFiles)
465
680
  await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyRelativePaths, fail)
466
681
 
682
+ if (deploymentDirs.size > 0) {
683
+ pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
684
+ await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail)
685
+ }
686
+
467
687
  return exitCode
468
688
  }
@@ -666,16 +666,6 @@ function extractApiVersionAndKind(doc) {
666
666
  }
667
667
  }
668
668
 
669
- /**
670
- * Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім’ям файлу).
671
- * @param {string} rel шлях від кореня репозиторію
672
- * @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
673
- */
674
- export function isRuKustomizationPath(rel) {
675
- const norm = rel.replaceAll('\\', '/')
676
- return /(^|\/)ru\/kustomization\.yaml$/u.test(norm)
677
- }
678
-
679
669
  /**
680
670
  * Чи вміст overlay **`ru/kustomization.yaml`** містить Kustomize patch видалення **HealthCheckPolicy**.
681
671
  * @param {string} raw повний текст файлу