@nitra/cursor 1.8.84 → 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/mdc/k8s.mdc CHANGED
@@ -15,6 +15,8 @@ alwaysApply: false
15
15
 
16
16
  Далі — вміст маніфесту. Зайвий порожній рядок між коментарем і YAML не додавай, якщо в проєкті не прийнято інше.
17
17
 
18
+ **Виняток — без modeline:** `apiVersion: alb.yc.io/v1alpha1`, `kind: HttpBackendGroup` (Yandex ALB) — рядка **`# yaml-language-server: $schema=…`** у файлі **не** має бути (ні в першому рядку, ні далі). Перший рядок — одразу YAML (`apiVersion:` тощо). Перевірка — **`check-k8s.mjs`**.
19
+
18
20
  **Розширення:** усі маніфести під **`k8s`**, включно з **`kustomization.yaml`**, — лише **`.yaml`** (розширення **`.yml`** не використовуй).
19
21
 
20
22
  **Dockerfile / hadolint** — окреме правило **`docker.mdc`** і **`npx @nitra/cursor check docker`**.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.84",
3
+ "version": "1.8.86",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -13,6 +13,7 @@
13
13
  * **k8s:** якщо під деревом із сегментом **`k8s`** є YAML з **`kind: Deployment`**, у тій самій директорії
14
14
  * має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
15
15
  * як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **Service** з тим самим **`metadata.name`**.
16
+ * Загальні вимоги до **`# yaml-language-server: $schema`** для інших YAML під **`k8s`** — у **check-k8s.mjs** / **k8s.mdc** (наприклад **HttpBackendGroup** `alb.yc.io/v1alpha1` — **без** modeline).
16
17
  * Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
17
18
  * (логіка вмісту — **`ruKustomizationHasHealthCheckDeletePatch`** у **check-k8s.mjs**, узгоджено з **k8s.mdc**).
18
19
  *
@@ -27,6 +28,8 @@
27
28
  * — тоді в **`ua`/`ru` kustomization** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
28
29
  * (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**,
29
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`** у пакеті).
30
33
  * Вибір **`op`** — **k8s.mdc**.
31
34
  */
32
35
  import { existsSync } from 'node:fs'
@@ -45,6 +48,14 @@ const CONFIG_FILE = '.n-cursor.json'
45
48
  /** Маркер у kustomization.yaml: якщо зустрічається у файлі — для overlay ru у patch HTTPRoute потрібна анотація gwin…websocket. */
46
49
  const HASURA_JWT_SECRET_IN_KUSTOMIZATION = 'HASURA_GRAPHQL_JWT_SECRET'
47
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
+
48
59
  /** Очікуваний URL **`$schema`** для **hc.yaml** (abie.mdc). */
49
60
  export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
50
61
 
@@ -527,6 +538,136 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
527
538
  return false
528
539
  }
529
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
+
530
671
  /** Домени **hostnames** для overlay **ua** (підрядки у JSON6902-тексті patch), abie.mdc. */
531
672
  const ABIE_UA_HTTPROUTE_HOST_MARKERS = ['abie.app', 'vybeerai.com.ua', '*.abie.app', '*.vybeerai.com.ua']
532
673
 
@@ -609,9 +750,15 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
609
750
  * @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
610
751
  * @param {'ua' | 'ru'} mode **ua** або **ru**
611
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
612
754
  * @returns {string | null} повідомлення про помилку або **null**
613
755
  */
614
- export function validateAbieNginxRunHttpRoutePatches(combined, mode, fullKustomizationRaw) {
756
+ export function validateAbieNginxRunHttpRoutePatches(
757
+ combined,
758
+ mode,
759
+ fullKustomizationRaw,
760
+ sharedCrossNsBackendRefCount = 0
761
+ ) {
615
762
  if (typeof combined !== 'string' || combined.trim() === '') {
616
763
  return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — gwin… websocket лише за наявності HASURA_GRAPHQL_JWT_SECRET у файлі) — abie.mdc`
617
764
  }
@@ -636,6 +783,16 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode, fullKustomi
636
783
  if (ruNeedsWebsocket && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
637
784
  return 'HTTPRoute (ru): за наявності HASURA_GRAPHQL_JWT_SECRET у kustomization потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
638
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
+ }
639
796
  return null
640
797
  }
641
798
 
@@ -911,6 +1068,21 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
911
1068
  * @returns {Promise<void>}
912
1069
  */
913
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
+
914
1086
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
915
1087
  if (uaAbsList.length === 0) {
916
1088
  passFn(
@@ -920,6 +1092,16 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
920
1092
  for (const abs of uaAbsList) {
921
1093
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
922
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
+ }
923
1105
  let raw
924
1106
  try {
925
1107
  raw = await readFile(abs, 'utf8')
@@ -929,7 +1111,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
929
1111
  return
930
1112
  }
931
1113
  const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
932
- const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua')
1114
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua', raw, sharedAnalysis.refCount)
933
1115
  if (v !== null) {
934
1116
  fail(`${rel}: ${v}`)
935
1117
  return
@@ -949,6 +1131,16 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
949
1131
  for (const abs of ruAbsList) {
950
1132
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
951
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
+ }
952
1144
  let raw
953
1145
  try {
954
1146
  raw = await readFile(abs, 'utf8')
@@ -958,7 +1150,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
958
1150
  return
959
1151
  }
960
1152
  const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
961
- const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru', raw)
1153
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru', raw, sharedAnalysis.refCount)
962
1154
  if (v !== null) {
963
1155
  fail(`${rel}: ${v}`)
964
1156
  return
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Перший рядок `# yaml-language-server: $schema=…`, без дублікатів, розширення `.yaml`
5
5
  * (окрім `kustomization.yaml`); URL схеми за першим документом — kustomization / yannh / datree
6
+ * (**виняток:** `apiVersion: alb.yc.io/v1alpha1`, `kind: HttpBackendGroup` — рядка `# yaml-language-server:` у файлі бути не має).
6
7
  * (datree за замовчуванням: GitHub Pages `https://datreeio.github.io/CRDs-catalog/…`).
7
8
  *
8
9
  * Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
@@ -671,6 +672,18 @@ function extractApiVersionAndKind(doc) {
671
672
  }
672
673
  }
673
674
 
675
+ /**
676
+ * Чи перший YAML-документ (до `---`) — **HttpBackendGroup** з API **alb.yc.io/v1alpha1** (Yandex ALB).
677
+ * Для таких файлів **check-k8s** не вимагає modeline `# yaml-language-server: $schema=…` і забороняє його.
678
+ * @param {string} yamlBody вміст файлу або фрагмент після modeline
679
+ * @returns {boolean} true, якщо `apiVersion`/`kind` першого документа збігаються з винятком
680
+ */
681
+ export function k8sYamlFirstDocIsAlbYcHttpBackendGroup(yamlBody) {
682
+ const first = firstYamlDocument(yamlBody)
683
+ const { apiVersion, kind } = extractApiVersionAndKind(first)
684
+ return apiVersion === 'alb.yc.io/v1alpha1' && kind === 'HttpBackendGroup'
685
+ }
686
+
674
687
  /**
675
688
  * Чи вміст overlay **`ru/kustomization.yaml`** містить Kustomize patch видалення **HealthCheckPolicy**.
676
689
  * @param {string} raw повний текст файлу
@@ -1599,12 +1612,38 @@ async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
1599
1612
  return
1600
1613
  }
1601
1614
 
1602
- const m = lines[0].match(MODELINE_RE)
1603
- if (!m) {
1615
+ const firstLineIsModeline = MODELINE_RE.test(lines[0])
1616
+ const bodyForFirstDoc = k8sYamlBodyForDocumentParse(lines)
1617
+ const isAlbHttpBackendGroup = k8sYamlFirstDocIsAlbYcHttpBackendGroup(bodyForFirstDoc)
1618
+
1619
+ if (isAlbHttpBackendGroup) {
1620
+ if (firstLineIsModeline) {
1621
+ fail(
1622
+ `${rel}: для kind HttpBackendGroup (apiVersion alb.yc.io/v1alpha1) не задавай # yaml-language-server: $schema — прибери перший рядок modeline (k8s.mdc)`
1623
+ )
1624
+ return
1625
+ }
1626
+ if (countSchemaModelines(lines) > 0) {
1627
+ fail(
1628
+ `${rel}: для kind HttpBackendGroup (apiVersion alb.yc.io/v1alpha1) не використовуй # yaml-language-server: $schema у файлі (k8s.mdc)`
1629
+ )
1630
+ return
1631
+ }
1632
+ const body = lines.join('\n')
1633
+ scanIngressInYamlDocuments(rel, body, fail)
1634
+ pass(`${rel}: HttpBackendGroup (alb.yc.io/v1alpha1) — modeline $schema не застосовується (k8s.mdc)`)
1635
+ const kustomizeManaged = kustomizeManagedRel.has(rel)
1636
+ validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged)
1637
+ scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail)
1638
+ return
1639
+ }
1640
+
1641
+ if (!firstLineIsModeline) {
1604
1642
  fail(`${rel}: перший рядок має бути коментарем # yaml-language-server: $schema=<url> (без префіксів перед #)`)
1605
1643
  return
1606
1644
  }
1607
1645
 
1646
+ const m = /** @type {RegExpMatchArray} */ (lines[0].match(MODELINE_RE))
1608
1647
  const schemaUrl = m[1]
1609
1648
  if (countSchemaModelines(lines) > 1) {
1610
1649
  fail(`${rel}: кілька рядків yaml-language-server $schema — лиш один modeline на файл (див. k8s.mdc)`)