@nitra/cursor 1.8.85 → 1.8.86

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,10 +1,10 @@
1
1
  ---
2
2
  description: Правила для проєктів AbInBev Efes
3
3
  alwaysApply: true
4
- version: '1.9'
4
+ version: '1.10'
5
5
  ---
6
6
 
7
- Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**), видалення **HealthCheckPolicy** у **ru**), гілки **dev**, **ua**, **ru** у **clean-merged-branch**, а також заборона артефактів **Firebase Hosting** у корені репозиторію.
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**), видалення **HealthCheckPolicy** у **ru**), гілки **dev**, **ua**, **ru** у **clean-merged-branch**, а також заборона артефактів **Firebase Hosting** у корені репозиторію.
8
8
 
9
9
  **`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
10
10
 
@@ -38,6 +38,28 @@ spec:
38
38
 
39
39
  За наявності **Deployment** під **k8s** і наявності **Vite** (**`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`** у каталозі пакета) у **`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`** лише якщо в **тому ж** **`ru/kustomization.yaml`** є згадка **`HASURA_GRAPHQL_JWT_SECRET`** (типово patch на **ConfigMap** Hasura). Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
40
40
 
41
+ ### HTTPRoute: спільні сервіси **`auth-run-hl`**, **`filelint-hl`**
42
+
43
+ Ці **Service** (headless **`-hl`**) живуть у **базовому** неймспейсі **`dev`**. У маніфесті **HTTPRoute** під **`k8s`** (шар без **`ua/`** та **`ru/`** — наприклад **`…/k8s/base/hr.yaml`**) для кожного **`backendRefs`** до такого сервісу явно вкажи **`namespace: dev`** і порт **8080**:
44
+
45
+ ```yaml title="…/k8s/base/hr.yaml (фрагмент)"
46
+ spec:
47
+ rules:
48
+ - matches:
49
+ - path:
50
+ type: PathPrefix
51
+ value: /
52
+ backendRefs:
53
+ - name: auth-run-hl
54
+ namespace: dev
55
+ port: 8080
56
+ - name: filelint-hl
57
+ namespace: dev
58
+ port: 8080
59
+ ```
60
+
61
+ У **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** додай до того самого **inline** patch на **`HTTPRoute`** (той самий **`target.name`**) операції **JSON6902** з **`path`**: **`/spec/rules/<i>/backendRefs/<j>/namespace`**, де **`<i>`** / **`<j>`** — індекси відповідно до порядку **`spec.rules`** та **`backendRefs`** у base-файлі; **`value`**: **`ua`** або **`ru`**. Якщо кілька таких **`backendRefs`**, потрібна окрема операція для кожного.
62
+
41
63
  ```yaml title="…/ua/kustomization.yaml (фрагмент)"
42
64
  - target:
43
65
  kind: HTTPRoute
@@ -50,6 +72,12 @@ spec:
50
72
  - op: replace
51
73
  path: /spec/parentRefs/0/namespace
52
74
  value: ua
75
+ - op: replace
76
+ path: /spec/rules/0/backendRefs/0/namespace
77
+ value: ua
78
+ - op: replace
79
+ path: /spec/rules/0/backendRefs/1/namespace
80
+ value: ua
53
81
  ```
54
82
 
55
83
  ```yaml title="…/ru/kustomization.yaml (фрагмент)"
@@ -64,6 +92,12 @@ spec:
64
92
  - op: replace
65
93
  path: /spec/parentRefs/0/namespace
66
94
  value: ru
95
+ - op: replace
96
+ path: /spec/rules/0/backendRefs/0/namespace
97
+ value: ru
98
+ - op: replace
99
+ path: /spec/rules/0/backendRefs/1/namespace
100
+ value: ru
67
101
  ```
68
102
 
69
103
  Якщо в цьому ж файлі є **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura з JWT), додай окремий patch на **HTTPRoute** з анотацією для WebSocket:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.85",
3
+ "version": "1.8.86",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -28,6 +28,8 @@
28
28
  * — тоді в **`ua`/`ru` kustomization** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
29
29
  * (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**,
30
30
  * якщо в тому ж **`kustomization.yaml`** згадується **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura + JWT).
31
+ * **Спільні бекенди (`auth-run-hl`, `filelint-hl`):** у **HTTPRoute** під **`k8s`** поза overlay **ua** та **ru** (шлях не містить **`k8s/ua/`** чи **`k8s/ru/`**) кожен такий **`backendRefs`** має **`namespace: dev`** і порт **8080**;
32
+ * у patch overlay **ua** та **ru** — по одному **JSON6902** на **`/spec/rules/…/backendRefs/…/namespace`** з **`value`**: **ua** або **ru** (кількість patch-ів = кількість таких **`backendRefs`** у пакеті).
31
33
  * Вибір **`op`** — **k8s.mdc**.
32
34
  */
33
35
  import { existsSync } from 'node:fs'
@@ -46,6 +48,14 @@ const CONFIG_FILE = '.n-cursor.json'
46
48
  /** Маркер у kustomization.yaml: якщо зустрічається у файлі — для overlay ru у patch HTTPRoute потрібна анотація gwin…websocket. */
47
49
  const HASURA_JWT_SECRET_IN_KUSTOMIZATION = 'HASURA_GRAPHQL_JWT_SECRET'
48
50
 
51
+ /**
52
+ * Спільні **Service** (**`-hl`**) у **dev**: у base-**HTTPRoute** обов’язково **`namespace: dev`**, у overlay — patch **`…/backendRefs/…/namespace`** (abie.mdc).
53
+ * Експорт для споживачів / тестів.
54
+ */
55
+ export const ABIE_SHARED_CROSS_NS_BACKEND_NAMES = Object.freeze(['auth-run-hl', 'filelint-hl'])
56
+
57
+ const ABIE_SHARED_CROSS_NS_BACKEND_SET = new Set(ABIE_SHARED_CROSS_NS_BACKEND_NAMES)
58
+
49
59
  /** Очікуваний URL **`$schema`** для **hc.yaml** (abie.mdc). */
50
60
  export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
51
61
 
@@ -528,6 +538,136 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
528
538
  return false
529
539
  }
530
540
 
541
+ /**
542
+ * Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза піддеревами **`ua/`** та **`ru/`** (base-шар abie).
543
+ * @param {string} relFromRoot відносний шлях від кореня
544
+ * @param {string} pkgRelFromRoot каталог пакета відносно кореня (без завершального слеша після імені пакета)
545
+ * @returns {boolean}
546
+ */
547
+ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelFromRoot) {
548
+ const normRel = relFromRoot.replaceAll('\\', '/')
549
+ const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(/\/$/u, '')
550
+ const prefix = `${pkg}/k8s/`
551
+ if (!normRel.startsWith(prefix)) {
552
+ return false
553
+ }
554
+ const after = normRel.slice(prefix.length)
555
+ return !after.startsWith('ua/') && !after.startsWith('ru/')
556
+ }
557
+
558
+ /**
559
+ * З HTTPRoute-документа рахує **`backendRefs`** до **`auth-run-hl`** / **`filelint-hl`** і порушення **`namespace: dev`**.
560
+ * @param {unknown} obj корінь YAML
561
+ * @param {string} rel відносний шлях (повідомлення)
562
+ * @returns {{ refCount: number, errors: string[] }}
563
+ */
564
+ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
565
+ /** @type {string[]} */
566
+ const errors = []
567
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
568
+ return { refCount: 0, errors }
569
+ }
570
+ const rec = /** @type {Record<string, unknown>} */ (obj)
571
+ if (rec.kind !== 'HTTPRoute') {
572
+ return { refCount: 0, errors }
573
+ }
574
+ const spec = rec.spec
575
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
576
+ return { refCount: 0, errors }
577
+ }
578
+ const rules = /** @type {Record<string, unknown>} */ (spec).rules
579
+ if (!Array.isArray(rules)) {
580
+ return { refCount: 0, errors }
581
+ }
582
+ let refCount = 0
583
+ for (const rule of rules) {
584
+ if (rule === null || typeof rule !== 'object' || Array.isArray(rule)) {
585
+ continue
586
+ }
587
+ const brs = /** @type {Record<string, unknown>} */ (rule).backendRefs
588
+ if (!Array.isArray(brs)) {
589
+ continue
590
+ }
591
+ for (const br of brs) {
592
+ if (br === null || typeof br !== 'object' || Array.isArray(br)) {
593
+ continue
594
+ }
595
+ const brRec = /** @type {Record<string, unknown>} */ (br)
596
+ const name = brRec.name
597
+ if (typeof name !== 'string' || !ABIE_SHARED_CROSS_NS_BACKEND_SET.has(name)) {
598
+ continue
599
+ }
600
+ refCount++
601
+ const ns = brRec.namespace
602
+ if (typeof ns !== 'string' || ns !== 'dev') {
603
+ errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
604
+ }
605
+ }
606
+ }
607
+ return { refCount, errors }
608
+ }
609
+
610
+ /**
611
+ * З YAML під **k8s** пакета (без overlay **ua** та **ru**) збирає кількість **`backendRefs`** до **`auth-run-hl`** і **`filelint-hl`** і порушення **`namespace: dev`**.
612
+ * @param {string} root корінь репозиторію
613
+ * @param {string} pkgAbs абсолютний шлях до каталогу пакета
614
+ * @param {string[]} yamlFilesAbs усі **yaml** під **k8s** (як **findK8sYamlFiles**)
615
+ * @returns {Promise<{ refCount: number, baseErrors: string[] }>}
616
+ */
617
+ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs) {
618
+ const pkgRel = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
619
+ let refCount = 0
620
+ /** @type {string[]} */
621
+ const baseErrors = []
622
+ for (const abs of yamlFilesAbs) {
623
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
624
+ if (!isK8sYamlInAbiePackageExcludingUaRuOverlays(rel, pkgRel)) {
625
+ continue
626
+ }
627
+ let raw
628
+ try {
629
+ raw = await readFile(abs, 'utf8')
630
+ } catch {
631
+ continue
632
+ }
633
+ const body = stripBom(raw)
634
+ const lines = body.split(/\r?\n/u)
635
+ const first = lines[0] ?? ''
636
+ const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
637
+ /** @type {import('yaml').Document[]} */
638
+ let docs
639
+ try {
640
+ docs = parseAllDocuments(rest)
641
+ } catch {
642
+ continue
643
+ }
644
+ for (const doc of docs) {
645
+ if (doc.errors.length > 0) {
646
+ continue
647
+ }
648
+ const obj = doc.toJSON()
649
+ const st = httpRouteDocSharedCrossNsBackendStats(obj, rel)
650
+ refCount += st.refCount
651
+ baseErrors.push(...st.errors)
652
+ }
653
+ }
654
+ return { refCount, baseErrors }
655
+ }
656
+
657
+ /**
658
+ * Рахує операції JSON6902 з **`path`**: **`/spec/rules/…/backendRefs/…/namespace`** та **`value`** overlay.
659
+ * @param {string} combined сукупний текст patch **HTTPRoute**
660
+ * @param {'ua' | 'ru'} mode overlay
661
+ * @returns {number}
662
+ */
663
+ function countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode) {
664
+ const re =
665
+ mode === 'ua'
666
+ ? /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/gmu
667
+ : /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/gmu
668
+ return [...combined.matchAll(re)].length
669
+ }
670
+
531
671
  /** Домени **hostnames** для overlay **ua** (підрядки у JSON6902-тексті patch), abie.mdc. */
532
672
  const ABIE_UA_HTTPROUTE_HOST_MARKERS = ['abie.app', 'vybeerai.com.ua', '*.abie.app', '*.vybeerai.com.ua']
533
673
 
@@ -610,9 +750,15 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
610
750
  * @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
611
751
  * @param {'ua' | 'ru'} mode **ua** або **ru**
612
752
  * @param {string} [fullKustomizationRaw] повний текст **kustomization.yaml** — для **ru** визначає, чи потрібна анотація **gwin…websocket** (лише якщо є **`HASURA_GRAPHQL_JWT_SECRET`**)
753
+ * @param {number} [sharedCrossNsBackendRefCount] скільки **`backendRefs`** до **`auth-run-hl`** і **`filelint-hl`** у base **HTTPRoute** пакета — стільки ж patch-ів **`…/backendRefs/…/namespace`** з **`value`** overlay
613
754
  * @returns {string | null} повідомлення про помилку або **null**
614
755
  */
615
- export function validateAbieNginxRunHttpRoutePatches(combined, mode, fullKustomizationRaw) {
756
+ export function validateAbieNginxRunHttpRoutePatches(
757
+ combined,
758
+ mode,
759
+ fullKustomizationRaw,
760
+ sharedCrossNsBackendRefCount = 0
761
+ ) {
616
762
  if (typeof combined !== 'string' || combined.trim() === '') {
617
763
  return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — gwin… websocket лише за наявності HASURA_GRAPHQL_JWT_SECRET у файлі) — abie.mdc`
618
764
  }
@@ -637,6 +783,16 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode, fullKustomi
637
783
  if (ruNeedsWebsocket && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
638
784
  return 'HTTPRoute (ru): за наявності HASURA_GRAPHQL_JWT_SECRET у kustomization потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
639
785
  }
786
+ const sharedCount =
787
+ typeof sharedCrossNsBackendRefCount === 'number' && Number.isFinite(sharedCrossNsBackendRefCount)
788
+ ? Math.max(0, Math.floor(sharedCrossNsBackendRefCount))
789
+ : 0
790
+ if (sharedCount > 0) {
791
+ const patchHits = countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode)
792
+ if (patchHits < sharedCount) {
793
+ return `HTTPRoute: для backendRefs до спільних сервісів auth-run-hl, filelint-hl очікується ${sharedCount} JSON6902 patch(ів) з path /spec/rules/…/backendRefs/…/namespace та value ${mode} (зараз ${patchHits}) — abie.mdc`
794
+ }
795
+ }
640
796
  return null
641
797
  }
642
798
 
@@ -912,6 +1068,21 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
912
1068
  * @returns {Promise<void>}
913
1069
  */
914
1070
  async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
1071
+ /** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
1072
+ const sharedBackendAnalysisByPkg = new Map()
1073
+ /**
1074
+ * @param {string} pkgAbs
1075
+ * @returns {Promise<{ refCount: number, baseErrors: string[] }>}
1076
+ */
1077
+ const getSharedBackendAnalysis = pkgAbs => {
1078
+ let p = sharedBackendAnalysisByPkg.get(pkgAbs)
1079
+ if (!p) {
1080
+ p = analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs)
1081
+ sharedBackendAnalysisByPkg.set(pkgAbs, p)
1082
+ }
1083
+ return p
1084
+ }
1085
+
915
1086
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
916
1087
  if (uaAbsList.length === 0) {
917
1088
  passFn(
@@ -921,6 +1092,16 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
921
1092
  for (const abs of uaAbsList) {
922
1093
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
923
1094
  if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
1095
+ const pkgAbs = abiePackageDirFromK8sOverlay(root, abs)
1096
+ if (!pkgAbs) {
1097
+ fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
1098
+ return
1099
+ }
1100
+ const sharedAnalysis = await getSharedBackendAnalysis(pkgAbs)
1101
+ for (const err of sharedAnalysis.baseErrors) {
1102
+ fail(err)
1103
+ return
1104
+ }
924
1105
  let raw
925
1106
  try {
926
1107
  raw = await readFile(abs, 'utf8')
@@ -930,7 +1111,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
930
1111
  return
931
1112
  }
932
1113
  const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
933
- const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua')
1114
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua', raw, sharedAnalysis.refCount)
934
1115
  if (v !== null) {
935
1116
  fail(`${rel}: ${v}`)
936
1117
  return
@@ -950,6 +1131,16 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
950
1131
  for (const abs of ruAbsList) {
951
1132
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
952
1133
  if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
1134
+ const pkgAbs = abiePackageDirFromK8sOverlay(root, abs)
1135
+ if (!pkgAbs) {
1136
+ fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
1137
+ return
1138
+ }
1139
+ const sharedAnalysis = await getSharedBackendAnalysis(pkgAbs)
1140
+ for (const err of sharedAnalysis.baseErrors) {
1141
+ fail(err)
1142
+ return
1143
+ }
953
1144
  let raw
954
1145
  try {
955
1146
  raw = await readFile(abs, 'utf8')
@@ -959,7 +1150,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
959
1150
  return
960
1151
  }
961
1152
  const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
962
- const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru', raw)
1153
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru', raw, sharedAnalysis.refCount)
963
1154
  if (v !== null) {
964
1155
  fail(`${rel}: ${v}`)
965
1156
  return