@nitra/cursor 1.8.98 → 1.8.99

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,7 +1,7 @@
1
1
  ---
2
2
  description: Правила для проєктів AbInBev Efes
3
3
  alwaysApply: true
4
- version: '1.14'
4
+ version: '1.15'
5
5
  ---
6
6
 
7
7
  Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**, для спільних сервісів **`auth-run-hl`** / **`filelint-hl`** — **`namespace: dev`** у base та patch **`…/backendRefs/…/namespace`** у **ua** / **ru**), у overlay **ru** — кожен **Service** (у т. ч. **headless** / **`-hl`**) → **`spec.type: NodePort`** через **JSON6902** у **`kustomization.yaml`**, видалення **HealthCheckPolicy** у **ru**), гілки **dev**, **ua**, **ru** у **clean-merged-branch**, а також заборона артефактів **Firebase Hosting** у корені репозиторію.
@@ -115,17 +115,21 @@ spec:
115
115
 
116
116
  ## k8s: overlay **ru** і **Service** (у т. ч. headless → NodePort)
117
117
 
118
- Для кожного **Service** в YAML під **`…/k8s/…`**, де шлях файлу **не** містить **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару; у т. ч. **headless** з **`spec.clusterIP: None`** і **`-hl`**), якщо ще не **`spec.type: NodePort`** / **`LoadBalancer`** / **`ExternalName`**, у **`k8s/ru/kustomization.yaml`** того ж пакета (overlay **ru**) додай **inline** **JSON6902** у **`patches`**: **`target.kind: Service`**, **`target.name`** як у маніфеста, **`path: /spec/type`**, **`value: NodePort`**. Деталі перевірки — **`check-abie.mjs`**.
118
+ Для кожного **Service** в YAML під **`…/k8s/…`**, де шлях файлу **не** містить **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару; у т. ч. **headless** з **`spec.clusterIP: None`** і **`-hl`**), якщо ще не **`spec.type: NodePort`** / **`LoadBalancer`** / **`ExternalName`**, у **`k8s/ru/kustomization.yaml`** того ж пакета (overlay **ru**) додай **inline** **JSON6902** у **`patches`**: **`target.kind: Service`**, **`target.name`** як у маніфеста, **`path: /spec/type`**, **`value: NodePort`**. Якщо в base було **`spec.clusterIP: None`** або **`spec.clusterIPs`** з **`None`** (типово **headless**), у тому ж **patch** додай **`op: remove`** для **`/spec/clusterIP`** та **`/spec/clusterIPs`** інакше API відхилить **NodePort** з помилкою на **`clusterIPs`**. Деталі — **`check-abie.mjs`**.
119
119
 
120
- ```yaml title="…/ru/kustomization.yaml (фрагмент)"
120
+ ```yaml title="…/ru/kustomization.yaml (фрагмент, headless → NodePort)"
121
121
  patches:
122
122
  - target:
123
123
  kind: Service
124
- name: my-app
124
+ name: user-site-hl
125
125
  patch: |-
126
126
  - op: replace
127
127
  path: /spec/type
128
128
  value: NodePort
129
+ - op: remove
130
+ path: /spec/clusterIP
131
+ - op: remove
132
+ path: /spec/clusterIPs
129
133
  ```
130
134
 
131
135
  ## k8s: overlay **ru** і HealthCheckPolicy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.98",
3
+ "version": "1.8.99",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -34,7 +34,7 @@
34
34
  * Вибір **`op`** — **k8s.mdc**.
35
35
  *
36
36
  * **Service (overlay ru):** для кожного **Service**, оголошеного в YAML під **`…/k8s/…`**, де шлях **не** проходить через **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару, у т. ч. **headless** з **`clusterIP: None`** і **`-hl`**), якщо ще не **NodePort** / **LoadBalancer** / **ExternalName**,
37
- * у файлі **`k8s/ru/kustomization.yaml`** того ж пакета (overlay середовища **ru**) — inline **JSON6902** на **`kind: Service`** з тим самим **`target.name`**: **`path: /spec/type`**, **`value: NodePort`**.
37
+ * у файлі **`k8s/ru/kustomization.yaml`** того ж пакета (overlay середовища **ru**) — inline **JSON6902** на **`kind: Service`** з тим самим **`target.name`**: **`path: /spec/type`**, **`value: NodePort`**; якщо в base було **`spec.clusterIP: None`** або **`spec.clusterIPs`** з **`None`** — у тому ж patch додай **`op: remove`** для **`/spec/clusterIP`** та **`/spec/clusterIPs`** (інакше **NodePort** з **`None`** відхиляє API).
38
38
  */
39
39
  import { existsSync } from 'node:fs'
40
40
  import { readFile } from 'node:fs/promises'
@@ -369,6 +369,61 @@ export function serviceDocumentRequiresAbieRuNodePortOverlay(obj) {
369
369
  return true
370
370
  }
371
371
 
372
+ /**
373
+ * Чи в base-**Service** є **headless** **`None`**, який треба прибрати в **ru** перед **NodePort** (**clusterIP** / **clusterIPs**).
374
+ * @param {unknown} obj корінь YAML (**Service**)
375
+ * @returns {boolean} **true**, якщо **`spec.clusterIP === 'None'`** або **`spec.clusterIPs`** містить **`'None'`**
376
+ */
377
+ export function serviceDocumentRequiresRuClusterIPNoneRemoval(obj) {
378
+ if (!isServiceDoc(obj)) {
379
+ return false
380
+ }
381
+ const rec = /** @type {Record<string, unknown>} */ (obj)
382
+ const spec = rec.spec
383
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
384
+ return false
385
+ }
386
+ const sp = /** @type {Record<string, unknown>} */ (spec)
387
+ if (sp.clusterIP === 'None') {
388
+ return true
389
+ }
390
+ const cips = sp.clusterIPs
391
+ if (Array.isArray(cips) && cips.includes('None')) {
392
+ return true
393
+ }
394
+ return false
395
+ }
396
+
397
+ /**
398
+ * Чи **JSON6902**-текст містить **`op: remove`** для заданого **`path`** (порядок ключів **op** / **path** неважливий).
399
+ * @param {string} patchText поле **patch** у kustomization
400
+ * @param {string} posixPath наприклад **`/spec/clusterIP`**
401
+ * @returns {boolean} true, якщо знайдено пару **remove** + **path**
402
+ */
403
+ export function jsonPatchRemovesPath(patchText, posixPath) {
404
+ if (typeof patchText !== 'string' || patchText.trim() === '') {
405
+ return false
406
+ }
407
+ if (posixPath !== '/spec/clusterIP' && posixPath !== '/spec/clusterIPs') {
408
+ return false
409
+ }
410
+ const pathRe =
411
+ posixPath === '/spec/clusterIP'
412
+ ? String.raw`path:\s*\/spec\/clusterIP\b`
413
+ : String.raw`path:\s*\/spec\/clusterIPs\b`
414
+ const opRe = String.raw`op:\s*remove\b`
415
+ return new RegExp(`${opRe}[\\s\\S]{0,200}?${pathRe}`, 'mu').test(patchText) || new RegExp(`${pathRe}[\\s\\S]{0,200}?${opRe}`, 'mu').test(patchText)
416
+ }
417
+
418
+ /**
419
+ * Чи patch прибирає **headless** поля **`clusterIP`** / **`clusterIPs`**, щоб **NodePort** пройшов валідацію API.
420
+ * @param {string} patchText поле **patch** у kustomization
421
+ * @returns {boolean} true, якщо є **remove** і для **`/spec/clusterIP`**, і для **`/spec/clusterIPs`**
422
+ */
423
+ export function jsonPatchTextClearsHeadlessServiceClusterIPNone(patchText) {
424
+ return jsonPatchRemovesPath(patchText, '/spec/clusterIP') && jsonPatchRemovesPath(patchText, '/spec/clusterIPs')
425
+ }
426
+
372
427
  /**
373
428
  * Чи фрагмент **JSON6902** у **`patch`** задає **`/spec/type`** зі значенням **NodePort** (abie overlay **ru**).
374
429
  * @param {string} patchText поле **patch** у kustomization
@@ -430,31 +485,24 @@ function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
430
485
  }
431
486
 
432
487
  /**
433
- * Імена **Service**, для яких у **ru/kustomization.yaml** немає очікуваного patch **`/spec/type` → NodePort** (abie.mdc).
488
+ * Збирає тексти **patch** на **Service** з **kustomization.yaml** (усі документи).
434
489
  * @param {string} raw повний текст **kustomization.yaml**
435
- * @param {Iterable<string>} serviceNames імена **metadata.name** з base-шару
436
- * @returns {string[]} відсортовані імена без коректного patch
490
+ * @returns {Map<string, string>} **target.name** об’єднаний текст **patch**
437
491
  */
438
- export function getMissingAbieRuServiceNodePortPatchServiceNames(raw, serviceNames) {
439
- const req = [...new Set([...serviceNames].filter(n => typeof n === 'string' && n.trim() !== ''))].toSorted((a, b) =>
440
- a.localeCompare(b)
441
- )
442
- if (req.length === 0) {
443
- return []
444
- }
492
+ function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
445
493
  const body = stripBom(raw)
446
494
  const lines = body.split(/\r?\n/u)
447
495
  const first = lines[0] ?? ''
448
496
  const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
497
+ /** @type {Map<string, string>} */
498
+ const byName = new Map()
449
499
  /** @type {import('yaml').Document[]} */
450
500
  let docs
451
501
  try {
452
502
  docs = parseAllDocuments(rest)
453
503
  } catch {
454
- return req
504
+ return byName
455
505
  }
456
- /** @type {Map<string, string>} */
457
- const byName = new Map()
458
506
  for (const doc of docs) {
459
507
  const chunk = collectAbieServicePatchTextsByNameFromKustomizationDoc(doc)
460
508
  for (const [k, v] of chunk) {
@@ -462,21 +510,51 @@ export function getMissingAbieRuServiceNodePortPatchServiceNames(raw, serviceNam
462
510
  byName.set(k, prev === undefined ? v : `${prev}\n${v}`)
463
511
  }
464
512
  }
465
- return req.filter(n => {
466
- const pt = byName.get(n)
467
- return pt === undefined || !jsonPatchTextSetsServiceTypeNodePort(pt)
468
- })
513
+ return byName
469
514
  }
470
515
 
471
516
  /**
472
- * Для кожного пакета збирає імена **Service**, які в overlay **ru** мають стати **NodePort** (abie.mdc).
517
+ * Повідомлення про порушення patch **Service** у **ru/kustomization.yaml** (abie.mdc).
518
+ * @param {string} raw повний текст **kustomization.yaml**
519
+ * @param {Map<string, { requiresClusterIPNoneClear: boolean }>} targetsByName ім’я **Service** → чи треба прибрати **None**
520
+ * @returns {string[]} порожньо, якщо все OK
521
+ */
522
+ export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
523
+ if (targetsByName.size === 0) {
524
+ return []
525
+ }
526
+ const byName = collectAbieRuServicePatchTextByTargetNameFromRaw(raw)
527
+ /** @type {string[]} */
528
+ const errors = []
529
+ for (const name of [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))) {
530
+ const flags = targetsByName.get(name)
531
+ const requiresClear = flags?.requiresClusterIPNoneClear === true
532
+ const pt = byName.get(name)
533
+ if (pt === undefined || String(pt).trim() === '') {
534
+ errors.push(`${name}: немає inline patch для kind: Service`)
535
+ } else {
536
+ if (!jsonPatchTextSetsServiceTypeNodePort(pt)) {
537
+ errors.push(`${name}: потрібен JSON6902 path /spec/type та value NodePort`)
538
+ }
539
+ if (requiresClear && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
540
+ errors.push(
541
+ `${name}: для spec.clusterIP/spec.clusterIPs: None додай у той самий patch op: remove для path /spec/clusterIP та /spec/clusterIPs (abie.mdc)`
542
+ )
543
+ }
544
+ }
545
+ }
546
+ return errors
547
+ }
548
+
549
+ /**
550
+ * Для кожного пакета збирає **Service**, які в overlay **ru** мають стати **NodePort** (abie.mdc).
473
551
  * @param {string} root корінь репозиторію
474
552
  * @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
475
553
  * @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
476
- * @returns {Promise<Map<string, Set<string>>>} **pkgAbs** → множина імен **Service**
554
+ * @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean }>>>} **pkgAbs** → (**ім’я** прапорці)
477
555
  */
478
- async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
479
- /** @type {Map<string, Set<string>>} */
556
+ async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail) {
557
+ /** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean }>>} */
480
558
  const map = new Map()
481
559
  for (const abs of yamlAbs) {
482
560
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
@@ -516,12 +594,16 @@ async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
516
594
  const meta = /** @type {Record<string, unknown>} */ (rec.metadata)
517
595
  const n = meta.name
518
596
  if (typeof n === 'string' && n.trim() !== '') {
519
- let s = map.get(pkgAbs)
520
- if (!s) {
521
- s = new Set()
522
- map.set(pkgAbs, s)
597
+ let inner = map.get(pkgAbs)
598
+ if (!inner) {
599
+ inner = new Map()
600
+ map.set(pkgAbs, inner)
523
601
  }
524
- s.add(n)
602
+ const needClear = serviceDocumentRequiresRuClusterIPNoneRemoval(obj)
603
+ const prev = inner.get(n)
604
+ inner.set(n, {
605
+ requiresClusterIPNoneClear: (prev?.requiresClusterIPNoneClear === true) || needClear
606
+ })
525
607
  }
526
608
  }
527
609
  }
@@ -535,7 +617,7 @@ async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
535
617
  }
536
618
 
537
619
  /**
538
- * У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort**, якщо ще не **NodePort** / **LoadBalancer** / **ExternalName** (abie.mdc).
620
+ * У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort**; якщо в base було **`clusterIP: None`** / **`clusterIPs: None`** — також **`op: remove`** на **`/spec/clusterIP`** та **`/spec/clusterIPs`** (abie.mdc).
539
621
  * @param {string} root корінь
540
622
  * @param {string[]} yamlFilesAbs yaml під **k8s**
541
623
  * @param {(msg: string) => void} fail callback
@@ -543,18 +625,19 @@ async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
543
625
  * @returns {Promise<void>}
544
626
  */
545
627
  async function ensureRuAbieServiceNodePortPatches(root, yamlFilesAbs, fail, passFn) {
546
- const byPkg = await collectAbieRuNodePortServiceNamesByPackage(root, yamlFilesAbs, fail)
547
- const entries = [...byPkg.entries()].filter(([, names]) => names.size > 0)
628
+ const byPkg = await collectAbieRuNodePortServiceTargetsByPackage(root, yamlFilesAbs, fail)
629
+ const entries = [...byPkg.entries()].filter(([, m]) => m.size > 0)
548
630
  if (entries.length === 0) {
549
631
  passFn('Немає Service у шарі k8s без k8s/ua/ та k8s/ru/ — patch NodePort у k8s/ru/ не вимагається (abie.mdc)')
550
632
  return
551
633
  }
552
- for (const [pkgAbs, names] of entries.toSorted((a, b) => a[0].localeCompare(b[0]))) {
634
+ for (const [pkgAbs, targetsByName] of entries.toSorted((a, b) => a[0].localeCompare(b[0]))) {
553
635
  const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
554
636
  const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
637
+ const nameList = [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))
555
638
  if (!existsSync(ruAbs)) {
556
639
  fail(
557
- `${relPkg}/k8s: є Service (у т. ч. headless), для overlay ru потрібен patch /spec/type NodePort: ${[...names].toSorted((a, b) => a.localeCompare(b)).join(', ')} — додай ru/kustomization.yaml (abie.mdc)`
640
+ `${relPkg}/k8s: є Service, для overlay ru потрібен patch Service (NodePort; для headless ще remove clusterIP/clusterIPs): ${nameList.join(', ')} — додай ru/kustomization.yaml (abie.mdc)`
558
641
  )
559
642
  return
560
643
  }
@@ -567,11 +650,9 @@ async function ensureRuAbieServiceNodePortPatches(root, yamlFilesAbs, fail, pass
567
650
  fail(`${relRu}: не вдалося прочитати (${msg})`)
568
651
  return
569
652
  }
570
- const missing = getMissingAbieRuServiceNodePortPatchServiceNames(raw, names)
571
- if (missing.length > 0) {
572
- fail(
573
- `${relRu}: для kind: Service потрібен inline JSON6902 з path /spec/type та value NodePort (ім’я target: ${missing.join(', ')}) — abie.mdc`
574
- )
653
+ const patchErrors = getAbieRuServiceNodePortPatchErrors(raw, targetsByName)
654
+ if (patchErrors.length > 0) {
655
+ fail(`${relRu}: ${patchErrors.join('; ')}`)
575
656
  return
576
657
  }
577
658
  passFn(`${relRu}: patch Service → NodePort (ru) відповідає abie.mdc`)