@nitra/cursor 1.9.18 → 1.9.20

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.
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * **Гілки:** у **`.github/workflows/clean-merged-branch.yml`** у кроці з
8
8
  * **`phpdocker-io/github-actions-delete-abandoned-branches`** у **`with.ignore_branches`** мають бути
9
- * **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно).
9
+ * **dev** та **ua** (разом з іншими гілками, якщо потрібно).
10
10
  *
11
11
  * **Firebase Hosting:** у **підкаталогах першого рівня** (безпосередні діти кореня репозиторію; `node_modules` / `.git` пропускаються) не має бути
12
12
  * **`.firebaserc`**, **`firebase.json`** та каталогу **`.firebase/`**; у **самому** корені репозиторію ці імена не перевіряються.
@@ -15,35 +15,29 @@
15
15
  * має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
16
16
  * як у abie.mdc, **`requestPath`** — непорожній шлях від кореня (рядок, що починається з **`/`**: **`/healthz`**, **`/IsAlive`**, **`/api/live`** тощо), порт **8080**, **`targetRef`** на **headless Service** (ім'я з суфіксом **`-hl`**):
17
17
  * якщо **`metadata.name`** уже закінчується на **`-hl`**, **`targetRef.name`** має збігатися з ним; інакше **`targetRef.name`** = **`${metadata.name}-hl`**.
18
- * Загальні вимоги до **`# yaml-language-server: $schema`** для інших YAML під **`k8s`** — у **check-k8s.mjs** / **k8s.mdc** (наприклад **HttpBackendGroup** `alb.yc.io/v1alpha1` — **без** modeline).
19
- * Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
20
- * (логіка вмісту — **`ruKustomizationHasHealthCheckDeletePatch`** у **check-k8s.mjs**, узгоджено з **k8s.mdc**).
18
+ * Загальні вимоги до **`# yaml-language-server: $schema`** для інших YAML під **`k8s`** — у **check-k8s.mjs** / **k8s.mdc**.
21
19
  *
22
20
  * **nodeSelector (base):** якщо **Deployment** лежить у шляху з сегментом **`base`** (наприклад **`…/k8s/base/deploy.yaml`**),
23
- * у **`spec.template.spec.nodeSelector`** має бути **`preem`** з булевим значенням **true** або рядком **`'true'`** — overlay **ua** та **ru** далі підміняють селектор.
21
+ * у **`spec.template.spec.nodeSelector`** має бути **`preem`** з булевим значенням **true** або рядком **`'true'`** — overlay **ua** далі підміняє селектор.
24
22
  *
25
- * **nodeSelector (overlay):** якщо в дереві **k8s** пакета є **Deployment**, у **`ua`/`ru` kustomization** цього пакета — inline patch на **`kind: Deployment`**
26
- * з **`path: /spec/template/spec/nodeSelector`**: **ua** **`preem: false`**; **ru** — **`yandex.cloud/preemptible: false`**.
23
+ * **nodeSelector (overlay ua):** якщо в дереві **k8s** пакета є **Deployment**, у **`ua/kustomization.yaml`** цього пакета — inline patch на **`kind: Deployment`**
24
+ * з **`path: /spec/template/spec/nodeSelector`** та **`preem: false`**.
27
25
  * Узагальнені вимоги **k8s.mdc** до JSON6902 (зокрема заборона **remove** + **add** на той самий **path**) перевіряє **check-k8s.mjs**; **check-abie** — лише abie-специфічний вміст (без дублювання цього правила).
28
26
  *
29
- * **HTTPRoute (overlay):** лише якщо в каталозі пакета (батько **`k8s`**) є **`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`**
30
- * — тоді в **`ua`/`ru` kustomization** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
31
- * (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**,
32
- * якщо в тому ж **`kustomization.yaml`** згадується **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura + JWT).
27
+ * **HTTPRoute (overlay ua):** лише якщо в каталозі пакета (батько **`k8s`**) є **`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`**
28
+ * — тоді в **`ua/kustomization.yaml`** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
29
+ * (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua**, також дозволені префікси **`ua-*`**).
33
30
  * **HTTPRoute (base / dev):** у маніфесті **HTTPRoute** у шляху з сегментом **`base`** (наприклад **`…/k8s/base/hr.yaml`**) у **`spec.hostnames`** дозволені лише **`aiml.live`**, **`*.aiml.live`** та інші піддомени **aiml.live** (канонічно порівняння без урахування регістру).
34
- * **Спільні бекенди (`auth-run-hl`, `file-link-hl`):** у **HTTPRoute** під **`k8s`** поза overlay **ua** та **ru** (шлях не містить **`k8s/ua/`** чи **`k8s/ru/`**) кожен такий **`backendRefs`** має **`namespace: dev`** і порт **8080**;
35
- * у patch overlay **ua** та **ru** — по одному **JSON6902** на **`/spec/rules/…/backendRefs/…/namespace`** з **`value`**: **ua** або **ru** (кількість patch-ів = кількість таких **`backendRefs`** у пакеті).
31
+ * **Спільні бекенди (`auth-run-hl`, `file-link-hl`):** у **HTTPRoute** під **`k8s`** поза overlay **ua** (шлях не містить **`k8s/ua/`**) кожен такий **`backendRefs`** має **`namespace: dev`** і порт **8080**;
32
+ * у patch overlay **ua** — по одному **JSON6902** на **`/spec/rules/…/backendRefs/…/namespace`** з **`value`**: **ua** (кількість patch-ів = кількість таких **`backendRefs`** у пакеті).
36
33
  * Вибір **`op`** — **k8s.mdc**.
37
34
  *
38
- * **Service (overlay ru):** для кожного **Service**, оголошеного в YAML під **`…/k8s/…`**, де шлях **не** проходить через **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару, у т. ч. **headless** з **`clusterIP: None`** і **`-hl`**), якщо ще не **NodePort** / **LoadBalancer** / **ExternalName**,
39
- * у файлі **`k8s/ru/kustomization.yaml`** того ж пакета (overlay середовища **ru**) — inline **JSON6902** на **`kind: Service`** з тим самим **`target.name`**: **`path: /spec/type`**, **`value: NodePort`**; якщо в base було **`spec.clusterIP: None`** — **`op: remove`** для **`/spec/clusterIP`**; якщо в base **явно** задано **`spec.clusterIPs`** — також **`remove`** для **`/spec/clusterIPs`** (інакше **API** може залишити **`None`** для **NodePort**; без ключа **`clusterIPs`** у base **`remove`** на **`/spec/clusterIPs`** ламає **`kubectl kustomize`**).
40
- *
41
- * **env→cluster DNS:** abie живе у трьох різних кластерах (GKE dev/ua + YC ru), тож DNS-суфікс і namespace-префікс у будь-якому
35
+ * **env→cluster DNS:** abie живе у двох GKE-кластерах (dev / ua), тож DNS-суфікс і namespace-префікс у будь-якому
42
36
  * **внутрішньокластерному** URL виду `http://<svc>.<ns>.svc.<dns>` мають відповідати імені env-файла. Скануються всі `*.env` файли,
43
- * basename яких збігається з `dev.env` / `ua.env` / `ru.env` (опційно з провідною крапкою — `.dev.env` тощо). Для кожного знайденого
37
+ * basename яких збігається з `dev.env` / `ua.env` (опційно з провідною крапкою — `.dev.env` тощо). Для кожного знайденого
44
38
  * internal URL у файлі (не лише `HASURA_GRAPHQL_ENDPOINT`, а й KVCMS, auth-run, file-link тощо) валідатор `validateAbieEnvInternalUrls`
45
- * вимагає: для `dev.env` — DNS `abie-dev.internal` і namespace починається з `dev-`; для `ua.env` — `abie-ua.internal` + `ua-`;
46
- * для `ru.env` — `cluster.local` + `ru-`. Файл `.env` без імені (локальний для розробника) виключено зі сканування — як і у `check-hasura.mjs`.
39
+ * вимагає: для `dev.env` — DNS `abie-dev.internal` і namespace починається з `dev-`; для `ua.env` — `abie-ua.internal` + `ua-`.
40
+ * Файл `.env` без імені (локальний для розробника) виключено зі сканування — як і у `check-hasura.mjs`.
47
41
  */
48
42
  import { existsSync } from 'node:fs'
49
43
  import { readdir, readFile } from 'node:fs/promises'
@@ -51,7 +45,7 @@ import { basename, dirname, join, relative } from 'node:path'
51
45
 
52
46
  import { parseAllDocuments } from 'yaml'
53
47
 
54
- import { pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
48
+ import { pathHasK8sSegment } from './check-k8s.mjs'
55
49
  import { createCheckReporter } from './utils/check-reporter.mjs'
56
50
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
57
51
  import { runConftestBatch } from './utils/run-conftest-batch.mjs'
@@ -62,9 +56,6 @@ const CONFIG_FILE = '.n-cursor.json'
62
56
  /** Каталоги-діти в корені, які пропускаються при скануванні на артефакти Firebase Hosting (abie). */
63
57
  const ABIE_FIREBASE_HOSTING_SCAN_SKIP_TOP_DIR_NAMES = new Set(['.git', 'node_modules'])
64
58
 
65
- /** Маркер у kustomization.yaml: якщо зустрічається у файлі — для overlay ru у patch HTTPRoute потрібна анотація gwin…websocket. */
66
- const HASURA_JWT_SECRET_IN_KUSTOMIZATION = 'HASURA_GRAPHQL_JWT_SECRET'
67
-
68
59
  /**
69
60
  * Спільні **Service** (**`-hl`**) у **dev**: у base-**HTTPRoute** обов'язково **`namespace: dev`**, у overlay — patch **`…/backendRefs/…/namespace`** (abie.mdc).
70
61
  * Експорт для споживачів / тестів.
@@ -81,83 +72,52 @@ export const ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT = 'aiml.live'
81
72
 
82
73
  const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
83
74
  const LINE_SPLIT_RE = /\r?\n/u
84
- const RU_KUSTOMIZATION_PATH_RE = /(^|\/)ru\/kustomization\.yaml$/u
85
75
  const UA_KUSTOMIZATION_PATH_RE = /(^|\/)ua\/kustomization\.yaml$/u
86
- const OVERLAY_PACKAGE_DIR_RE = /^(.+)\/k8s\/(?:ua|ru)\/kustomization\.yaml$/u
76
+ const OVERLAY_PACKAGE_DIR_RE = /^(.+)\/k8s\/ua\/kustomization\.yaml$/u
87
77
  const BASE_SEGMENT_RE = /(^|\/)base\//u
88
78
  const YAML_EXTENSION_RE = /\.ya?ml$/iu
89
- const K8S_PACKAGE_DIR_RE = /^(.+)\/k8s\//u
90
- const PATCH_PATH_TYPE_RE = /path:\s*\/spec\/type\b/u
91
- const PATCH_VALUE_NODE_PORT_RE = /value:\s*['"]?NodePort['"]?(?:\s|$)/iu
92
79
  const PATCH_NODE_SELECTOR_PATH_RE = /path:\s*\/spec\/template\/spec\/nodeSelector\b/u
93
80
  const PATCH_PREEM_FALSE_RE = /\bpreem:\s*['"]?false['"]?\b/u
94
- const PATCH_YANDEX_PREEMPTIBLE_FALSE_RE = /yandex\.cloud\/preemptible:\s*['"]?false['"]?/u
95
81
  const TRAILING_SLASH_RE = /\/$/u
96
82
  const PATCH_HOSTNAMES_PATH_RE = /path:\s*\/spec\/hostnames\b/mu
97
- // Overlay namespaces: allow ua/ru and ua-*/ru-* (e.g. ua-b2b, ru-b2b).
83
+ // Overlay namespaces: allow `ua` and `ua-*` (e.g. ua-b2b).
98
84
  const PATCH_PARENT_REF_NS_UA_RE =
99
85
  /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/imu
100
- const PATCH_PARENT_REF_NS_RU_RE =
101
- /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/imu
102
- const WEBSOCKET_ANNOTATION_RE = /gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/mu
103
- const REMOVE_CLUSTER_IP_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIP\b/mu
104
- const REMOVE_CLUSTER_IP_BEFORE_OP_RE = /path:\s*\/spec\/clusterIP\b[\s\S]{0,200}?op:\s*remove\b/mu
105
- const REMOVE_CLUSTER_IPS_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIPs\b/mu
106
- const REMOVE_CLUSTER_IPS_BEFORE_OP_RE = /path:\s*\/spec\/clusterIPs\b[\s\S]{0,200}?op:\s*remove\b/mu
107
-
108
- /** Підрядок образу Hasura у контейнері Deployment (abie.mdc nginx-sidecar). */
109
- const HASURA_IMAGE_MARKER = 'hasura/graphql-engine'
110
- /** Nginx-sidecar image (abie.mdc): nginx:*-alpine. */
111
- const NGINX_SIDECAR_IMAGE_RE = /image:\s*nginx:\S*-alpine/u
112
- /** containerPort: 8081 у patch Deployment (abie.mdc). */
113
- const NGINX_SIDECAR_CONTAINER_PORT_RE = /containerPort:\s*8081\b/u
114
- /** port: 8081 у patch Service -hl (proxy порт, abie.mdc). */
115
- const PATCH_PROXY_PORT_8081_RE = /\bport:\s*8081\b/u
116
- /** configmap-nginx.yaml у resources kustomization (abie.mdc). */
117
- const RESOURCES_CONFIGMAP_NGINX_RE = /configmap-nginx\.yaml/u
118
- /** path /spec/rules/{i}/backendRefs/{j}/port … value: 8081 у patch HTTPRoute (path→value, abie.mdc). */
119
- const HTTPROUTE_BACKENDREF_PORT_8081_RE =
120
- /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/port\b[\s\S]{0,200}?value:\s*8081\b/mu
121
- /** Те саме, value→path. */
122
- const HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE =
123
- /value:\s*8081\b[\s\S]{0,200}?path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/port\b/mu
124
86
 
125
87
  /**
126
- * Регекс basename env-файлу abie: `dev.env` / `ua.env` / `ru.env`, опційно з провідною крапкою (`.dev.env` тощо).
88
+ * Регекс basename env-файлу abie: `dev.env` / `ua.env`, опційно з провідною крапкою (`.dev.env` тощо).
127
89
  * Файл рівно `.env` (без імені) — виключення з правила: локальний файл розробника, `check-abie` його не сканує
128
90
  * (так само як `check-hasura`, див. `isEnvFile`).
129
91
  */
130
- const ABIE_ENV_FILE_BASENAME_RE = /^\.?(dev|ua|ru)\.env$/u
92
+ const ABIE_ENV_FILE_BASENAME_RE = /^\.?(dev|ua)\.env$/u
131
93
 
132
94
  /**
133
95
  * Глобальний регекс кластерного internal URL у тексті env-файлу.
134
96
  * Використовується з `String.prototype.matchAll`, тому має флаг `g`.
135
- * Дозволяє два DNS-формати: `<cluster>.internal` (GKE) і `cluster.local` (YC / стандартний k8s).
97
+ * Допустимий DNS-формат `<cluster>.internal` (GKE).
136
98
  * Порт необов'язковий — у KVCMS-конфігах інколи лежить URL без порту (8080 додається сервісом за замовчуванням).
137
99
  */
138
100
  const ABIE_INTERNAL_URL_GLOBAL_RE =
139
- /\bhttp:\/\/([a-z0-9][a-z0-9-]*)\.([a-z0-9][a-z0-9-]*)\.svc\.((?:[a-z0-9][a-z0-9-]*\.internal)|cluster\.local)(?::\d+)?(?:\/[^\s"'`]*)?/giu
101
+ /\bhttp:\/\/([a-z0-9][a-z0-9-]*)\.([a-z0-9][a-z0-9-]*)\.svc\.([a-z0-9][a-z0-9-]*\.internal)(?::\d+)?(?:\/[^\s"'`]*)?/giu
140
102
 
141
103
  /**
142
104
  * Очікуваний кластерний DNS-суфікс і namespace-префікс для кожного env-файлу abie.
143
- * `dev` / `ua` живуть у GKE з власним `<cluster>.internal`; `ru` — у YC, де DNS-суфікс
144
- * стандартний `cluster.local` (без імені кластера).
105
+ * `dev` / `ua` живуть у двох GKE-кластерах з власним `<cluster>.internal`.
145
106
  */
146
107
  const ABIE_ENV_CLUSTER_DNS_MAP = Object.freeze({
147
108
  dev: Object.freeze({ clusterDns: 'abie-dev.internal', namespacePrefix: 'dev-' }),
148
- ua: Object.freeze({ clusterDns: 'abie-ua.internal', namespacePrefix: 'ua-' }),
149
- ru: Object.freeze({ clusterDns: 'cluster.local', namespacePrefix: 'ru-' })
109
+ ua: Object.freeze({ clusterDns: 'abie-ua.internal', namespacePrefix: 'ua-' })
150
110
  })
151
111
 
152
112
  /**
153
- * Дістає ім'я env (`dev` / `ua` / `ru`) з basename env-файлу abie.
113
+ * Дістає ім'я env (`dev` / `ua`) з basename env-файлу abie.
154
114
  * Для не-abie env-файлів (наприклад `production.env`, `.env` без імені) повертає `null`.
155
115
  * @param {string} basenameOfEnvFile basename файла (без шляху)
156
- * @returns {('dev' | 'ua' | 'ru') | null} ім'я env або `null`
116
+ * @returns {('dev' | 'ua') | null} ім'я env або `null`
157
117
  */
158
118
  export function abieEnvNameFromBasename(basenameOfEnvFile) {
159
119
  const m = basenameOfEnvFile.match(ABIE_ENV_FILE_BASENAME_RE)
160
- return m ? /** @type {'dev' | 'ua' | 'ru'} */ (m[1]) : null
120
+ return m ? /** @type {'dev' | 'ua'} */ (m[1]) : null
161
121
  }
162
122
 
163
123
  /**
@@ -165,7 +125,7 @@ export function abieEnvNameFromBasename(basenameOfEnvFile) {
165
125
  * для кожного знайденого internal URL. URL шукається глобально (`matchAll`), тож одне й те саме
166
126
  * порушення в кількох змінних дасть стільки ж окремих помилок.
167
127
  * @param {string} content вміст env-файлу (UTF-8)
168
- * @param {'dev' | 'ua' | 'ru'} envName ім'я env, отримане з `abieEnvNameFromBasename`
128
+ * @param {'dev' | 'ua'} envName ім'я env, отримане з `abieEnvNameFromBasename`
169
129
  * @returns {string[]} порожній масив, якщо все OK; інакше — список повідомлень про порушення
170
130
  */
171
131
  export function validateAbieEnvInternalUrls(content, envName) {
@@ -189,16 +149,6 @@ export function validateAbieEnvInternalUrls(content, envName) {
189
149
  return errors
190
150
  }
191
151
 
192
- /**
193
- * Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім'ям файлу) — специфіка abie overlay.
194
- * @param {string} rel шлях від кореня репозиторію
195
- * @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
196
- */
197
- export function isRuKustomizationPath(rel) {
198
- const norm = rel.replaceAll('\\', '/')
199
- return RU_KUSTOMIZATION_PATH_RE.test(norm)
200
- }
201
-
202
152
  /**
203
153
  * Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед ім'ям файлу) — специфіка abie overlay.
204
154
  * @param {string} rel шлях від кореня репозиторію
@@ -210,10 +160,10 @@ export function isUaKustomizationPath(rel) {
210
160
  }
211
161
 
212
162
  /**
213
- * Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/(ua|ru)/kustomization.yaml`**.
163
+ * Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/ua/kustomization.yaml`**.
214
164
  * @param {string} root корінь репозиторію
215
- * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
216
- * @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua чи ru
165
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
166
+ * @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua
217
167
  */
218
168
  export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
219
169
  const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
@@ -224,7 +174,7 @@ export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
224
174
  /**
225
175
  * Чи для цього overlay застосовувати вимоги **HTTPRoute** (лише Vite-пакети).
226
176
  * @param {string} root корінь репозиторію
227
- * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
177
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
228
178
  * @returns {boolean} **true**, якщо поруч із **k8s** є **vite.config** (**js** / **mjs** / **ts**)
229
179
  */
230
180
  export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
@@ -240,10 +190,10 @@ export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
240
190
  }
241
191
 
242
192
  /**
243
- * Чи в дереві **k8s** того ж пакета, що й overlay **ua** або **ru**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
193
+ * Чи в дереві **k8s** того ж пакета, що й overlay **ua**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
244
194
  * @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
245
195
  * @param {string} root корінь репозиторію
246
- * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
196
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
247
197
  * @returns {boolean} **true**, якщо хоч один каталог із **deploymentDirs** лежить під **`…/k8s/`** цього пакета
248
198
  */
249
199
  export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizationAbs) {
@@ -307,7 +257,7 @@ export async function isAbieRuleEnabled(root) {
307
257
  }
308
258
 
309
259
  // Per-document валідація `clean-merged-branch.yml` (with.ignore_branches з
310
- // dev/ua/ru) делегована rego-пакету `abie.clean_merged_ignore_branches`
260
+ // dev/ua) делегована rego-пакету `abie.clean_merged_ignore_branches`
311
261
  // (`npm/policy/abie/clean_merged_ignore_branches/`). JS викликає
312
262
  // `runConftestBatch` у `checkCleanMergedBranch`.
313
263
 
@@ -355,383 +305,6 @@ function isDeploymentDoc(obj) {
355
305
  )
356
306
  }
357
307
 
358
- /**
359
- * Чи документ — **Service**.
360
- * @param {unknown} obj корінь YAML-документа
361
- * @returns {boolean} true, якщо **kind** — **Service**
362
- */
363
- function isServiceDoc(obj) {
364
- return (
365
- obj !== null &&
366
- typeof obj === 'object' &&
367
- !Array.isArray(obj) &&
368
- /** @type {Record<string, unknown>} */ (obj).kind === 'Service'
369
- )
370
- }
371
-
372
- /**
373
- * Чи відносний шлях до YAML під **`…/k8s/…`** не в каталозі overlay **`k8s/ua/`** чи **`k8s/ru/`** у репозиторії (після **`k8s/`** одразу не йде **`ua/`** чи **`ru/`**).
374
- * @param {string} relFromRoot шлях від кореня
375
- * @returns {boolean} true для base / спільних маніфестів; **false** для файлів усередині **`k8s/ua/…`** або **`k8s/ru/…`**
376
- */
377
- function k8sYamlRelOutsideUaRuOverlays(relFromRoot) {
378
- const norm = relFromRoot.replaceAll('\\', '/')
379
- const idx = norm.indexOf('/k8s/')
380
- if (idx === -1) {
381
- return false
382
- }
383
- const after = norm.slice(idx + '/k8s/'.length)
384
- return after.length > 0 && !after.startsWith('ua/') && !after.startsWith('ru/')
385
- }
386
-
387
- /**
388
- * Каталог пакета з відносного шляху **`…/k8s/…`** (частина до **`/k8s/`**).
389
- * @param {string} root корінь репозиторію
390
- * @param {string} relFromRoot відносний шлях
391
- * @returns {string | null} абсолютний шлях до каталогу пакета або **null**
392
- */
393
- function abiePackageDirFromK8sYamlRel(root, relFromRoot) {
394
- const norm = relFromRoot.replaceAll('\\', '/')
395
- const m = norm.match(K8S_PACKAGE_DIR_RE)
396
- return m ? join(root, m[1]) : null
397
- }
398
-
399
- /**
400
- * Чи **Service** у base-шарі abie потребує в **ru** patch **`spec.type: NodePort`** (у т. ч. **headless**; не вже **NodePort** / **LoadBalancer** / **ExternalName**).
401
- * @param {unknown} obj корінь YAML (**Service**)
402
- * @returns {boolean} true, якщо для overlay **ru** очікується **NodePort**
403
- */
404
- export function serviceDocumentRequiresAbieRuNodePortOverlay(obj) {
405
- if (!isServiceDoc(obj)) {
406
- return false
407
- }
408
- const rec = /** @type {Record<string, unknown>} */ (obj)
409
- const meta = rec.metadata
410
- if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
411
- return false
412
- }
413
- const name = /** @type {Record<string, unknown>} */ (meta).name
414
- if (typeof name !== 'string' || name.trim() === '') {
415
- return false
416
- }
417
- const spec = rec.spec
418
- if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
419
- return true
420
- }
421
- const sp = /** @type {Record<string, unknown>} */ (spec)
422
- const t = sp.type
423
- if (t === 'NodePort' || t === 'LoadBalancer' || t === 'ExternalName') {
424
- return false
425
- }
426
- return true
427
- }
428
-
429
- /**
430
- * Чи в base-**Service** задано **headless** через **`spec.clusterIP: None`**, який треба прибрати в **ru** перед **NodePort**.
431
- * @param {unknown} obj корінь YAML (**Service**)
432
- * @returns {boolean} **true**, якщо **`spec.clusterIP === 'None'`**
433
- */
434
- export function serviceDocumentRequiresRuClusterIPNoneRemoval(obj) {
435
- if (!isServiceDoc(obj)) {
436
- return false
437
- }
438
- const rec = /** @type {Record<string, unknown>} */ (obj)
439
- const spec = rec.spec
440
- if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
441
- return false
442
- }
443
- const sp = /** @type {Record<string, unknown>} */ (spec)
444
- return sp.clusterIP === 'None'
445
- }
446
-
447
- /**
448
- * Чи в base-**Service** у **`spec`** явно задано поле **`clusterIPs`** (тоді **`remove`** на **`/spec/clusterIPs`** безпечний для **`kubectl kustomize`**).
449
- * @param {unknown} obj корінь YAML (**Service**)
450
- * @returns {boolean} **true**, якщо **`Object.hasOwn(spec, 'clusterIPs')`**
451
- */
452
- export function serviceDocumentBaseDeclaresClusterIPsField(obj) {
453
- if (!isServiceDoc(obj)) {
454
- return false
455
- }
456
- const rec = /** @type {Record<string, unknown>} */ (obj)
457
- const spec = rec.spec
458
- if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
459
- return false
460
- }
461
- const sp = /** @type {Record<string, unknown>} */ (spec)
462
- return Object.hasOwn(sp, 'clusterIPs')
463
- }
464
-
465
- /**
466
- * Чи **JSON6902**-текст містить **`op: remove`** для заданого **`path`** (порядок ключів **op** / **path** неважливий).
467
- * @param {string} patchText поле **patch** у kustomization
468
- * @param {string} posixPath **`/spec/clusterIP`** або **`/spec/clusterIPs`**
469
- * @returns {boolean} true, якщо знайдено пару **remove** + **path**
470
- */
471
- export function jsonPatchRemovesPath(patchText, posixPath) {
472
- if (typeof patchText !== 'string' || patchText.trim() === '') {
473
- return false
474
- }
475
- if (posixPath !== '/spec/clusterIP' && posixPath !== '/spec/clusterIPs') {
476
- return false
477
- }
478
- if (posixPath === '/spec/clusterIP') {
479
- return REMOVE_CLUSTER_IP_AFTER_OP_RE.test(patchText) || REMOVE_CLUSTER_IP_BEFORE_OP_RE.test(patchText)
480
- }
481
- return REMOVE_CLUSTER_IPS_AFTER_OP_RE.test(patchText) || REMOVE_CLUSTER_IPS_BEFORE_OP_RE.test(patchText)
482
- }
483
-
484
- /**
485
- * Чи patch містить **`op: remove`** для **`/spec/clusterIP`**, щоб прибрати **headless** перед **NodePort**.
486
- * @param {string} patchText поле **patch** у kustomization
487
- * @returns {boolean} true, якщо є **remove** для **`/spec/clusterIP`**
488
- */
489
- export function jsonPatchTextClearsHeadlessServiceClusterIPNone(patchText) {
490
- return jsonPatchRemovesPath(patchText, '/spec/clusterIP')
491
- }
492
-
493
- /**
494
- * Чи фрагмент **JSON6902** у **`patch`** задає **`/spec/type`** зі значенням **NodePort** (abie overlay **ru**).
495
- * @param {string} patchText поле **patch** у kustomization
496
- * @returns {boolean} true, якщо знайдено **path** і **value**
497
- */
498
- export function jsonPatchTextSetsServiceTypeNodePort(patchText) {
499
- if (typeof patchText !== 'string' || patchText.trim() === '') {
500
- return false
501
- }
502
- if (!PATCH_PATH_TYPE_RE.test(patchText)) {
503
- return false
504
- }
505
- if (!PATCH_VALUE_NODE_PORT_RE.test(patchText)) {
506
- return false
507
- }
508
- return true
509
- }
510
-
511
- /**
512
- * Витягує ім'я та текст patch для Service з елемента patches.
513
- * @param {unknown} p елемент масиву patches
514
- * @returns {{ name: string, patchStr: string } | null} ім'я та текст patch або null
515
- */
516
- function extractServicePatchEntry(p) {
517
- if (p === null || typeof p !== 'object' || Array.isArray(p)) return null
518
- const pr = /** @type {Record<string, unknown>} */ (p)
519
- const target = pr.target
520
- if (target === null || typeof target !== 'object' || Array.isArray(target)) return null
521
- const tg = /** @type {Record<string, unknown>} */ (target)
522
- if (tg.kind !== 'Service' || typeof tg.name !== 'string' || tg.name.trim() === '') return null
523
- const patchStr = pr.patch
524
- if (typeof patchStr !== 'string' || patchStr.trim() === '') return null
525
- return { name: tg.name, patchStr }
526
- }
527
-
528
- /**
529
- * З одного документа **Kustomization** збирає пари **Service name → patch text** для **inline patches** з **target.kind: Service**.
530
- * @param {import('yaml').Document} doc документ після **parseAllDocuments**
531
- * @returns {Map<string, string>} ім'я сервісу → текст **patch**
532
- */
533
- function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
534
- /** @type {Map<string, string>} */
535
- const out = new Map()
536
- if (doc.errors.length > 0) return out
537
- const root = doc.toJSON()
538
- if (root === null || typeof root !== 'object' || Array.isArray(root)) return out
539
- const rec = /** @type {Record<string, unknown>} */ (root)
540
- if (rec.kind !== 'Kustomization' || !Array.isArray(rec.patches)) return out
541
- for (const p of rec.patches) {
542
- const entry = extractServicePatchEntry(p)
543
- if (entry) {
544
- const prev = out.get(entry.name)
545
- out.set(entry.name, prev === undefined ? entry.patchStr : `${prev}\n${entry.patchStr}`)
546
- }
547
- }
548
- return out
549
- }
550
-
551
- /**
552
- * Збирає тексти **patch** на **Service** з **kustomization.yaml** (усі документи).
553
- * @param {string} raw повний текст **kustomization.yaml**
554
- * @returns {Map<string, string>} **target.name** → об'єднаний текст **patch**
555
- */
556
- function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
557
- const body = stripBom(raw)
558
- const lines = body.split(LINE_SPLIT_RE)
559
- const first = lines[0] ?? ''
560
- const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
561
- /** @type {Map<string, string>} */
562
- const byName = new Map()
563
- /** @type {import('yaml').Document[]} */
564
- let docs
565
- try {
566
- docs = parseAllDocuments(rest)
567
- } catch {
568
- return byName
569
- }
570
- for (const doc of docs) {
571
- const chunk = collectAbieServicePatchTextsByNameFromKustomizationDoc(doc)
572
- for (const [k, v] of chunk) {
573
- const prev = byName.get(k)
574
- byName.set(k, prev === undefined ? v : `${prev}\n${v}`)
575
- }
576
- }
577
- return byName
578
- }
579
-
580
- /**
581
- * Збирає помилки patch для одного Service за ім'ям.
582
- * @param {string} name ім'я Service
583
- * @param {string | undefined} pt текст patch або undefined
584
- * @param {{ requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean } | undefined} flags прапорці
585
- * @param {string[]} errors масив для запису помилок
586
- */
587
- function collectServicePatchErrors(name, pt, flags, errors) {
588
- if (pt === undefined || String(pt).trim() === '') {
589
- errors.push(`${name}: немає inline patch для kind: Service`)
590
- return
591
- }
592
- if (!jsonPatchTextSetsServiceTypeNodePort(pt)) {
593
- errors.push(`${name}: потрібен JSON6902 path /spec/type та value NodePort`)
594
- }
595
- if (flags?.requiresClusterIPNoneClear === true && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
596
- errors.push(
597
- `${name}: для spec.clusterIP: None додай у той самий patch op: remove для path /spec/clusterIP (abie.mdc)`
598
- )
599
- }
600
- if (flags?.requiresClusterIPsRemove === true && !jsonPatchRemovesPath(pt, '/spec/clusterIPs')) {
601
- errors.push(
602
- `${name}: у base задано spec.clusterIPs — додай op: remove для path /spec/clusterIPs (інакше NodePort з None у clusterIPs; abie.mdc)`
603
- )
604
- }
605
- }
606
-
607
- /**
608
- * Повідомлення про порушення patch **Service** у **ru/kustomization.yaml** (abie.mdc).
609
- * @param {string} raw повний текст **kustomization.yaml**
610
- * @param {Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean }>} targetsByName ім'я **Service** → прапорці patch
611
- * @returns {string[]} порожньо, якщо все OK
612
- */
613
- export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
614
- if (targetsByName.size === 0) return []
615
- const byName = collectAbieRuServicePatchTextByTargetNameFromRaw(raw)
616
- /** @type {string[]} */
617
- const errors = []
618
- for (const name of [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))) {
619
- collectServicePatchErrors(name, byName.get(name), targetsByName.get(name), errors)
620
- }
621
- return errors
622
- }
623
-
624
- /**
625
- * Для кожного пакета збирає **Service**, які в overlay **ru** мають стати **NodePort** (abie.mdc).
626
- * @param {string} root корінь репозиторію
627
- * @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
628
- * @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
629
- * @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>>} **pkgAbs** → (**ім'я** → прапорці)
630
- */
631
- /**
632
- * Обробляє один Service-документ для збору NodePort-патч цілей.
633
- * @param {unknown} obj YAML-документ (toJSON)
634
- * @param {string} pkgAbs абсолютний шлях до пакета
635
- * @param {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} map результуючий Map
636
- */
637
- function processServiceDocForNodePortTargets(obj, pkgAbs, map) {
638
- if (!serviceDocumentRequiresAbieRuNodePortOverlay(obj)) return
639
- const rec = /** @type {Record<string, unknown>} */ (obj)
640
- const meta = /** @type {Record<string, unknown>} */ (rec.metadata)
641
- const n = meta.name
642
- if (typeof n !== 'string' || n.trim() === '') return
643
- let inner = map.get(pkgAbs)
644
- if (!inner) {
645
- inner = new Map()
646
- map.set(pkgAbs, inner)
647
- }
648
- const needClear = serviceDocumentRequiresRuClusterIPNoneRemoval(obj)
649
- const needClusterIPsRemove = serviceDocumentBaseDeclaresClusterIPsField(obj)
650
- const prev = inner.get(n)
651
- inner.set(n, {
652
- requiresClusterIPNoneClear: prev?.requiresClusterIPNoneClear === true || needClear,
653
- requiresClusterIPsRemove: prev?.requiresClusterIPsRemove === true || needClusterIPsRemove
654
- })
655
- }
656
-
657
- /**
658
- * Обробляє YAML-документи з одного файлу для збору NodePort-патч цілей.
659
- * @param {import('yaml').Document[]} docs документи з файлу
660
- * @param {string} pkgAbs абсолютний шлях пакета
661
- * @param {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} map результуючий Map
662
- */
663
- function collectNodePortTargetsFromDocs(docs, pkgAbs, map) {
664
- for (const doc of docs) {
665
- if (doc.errors.length === 0) {
666
- processServiceDocForNodePortTargets(doc.toJSON(), pkgAbs, map)
667
- }
668
- }
669
- }
670
-
671
- /**
672
- * Для кожного пакета збирає **Service**, які в overlay **ru** мають стати **NodePort** (abie.mdc).
673
- * @param {string} root корінь репозиторію
674
- * @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
675
- * @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
676
- * @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>>} пакет → назва сервісу → прапори NodePort
677
- */
678
- async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail) {
679
- /** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} */
680
- const map = new Map()
681
- for (const abs of yamlAbs) {
682
- const rel = relative(root, abs).replaceAll('\\', '/') || abs
683
- const pkgAbs = k8sYamlRelOutsideUaRuOverlays(rel) ? abiePackageDirFromK8sYamlRel(root, rel) : null
684
- if (pkgAbs) {
685
- const docs = await readAndParseYamlDocs(abs, rel, fail)
686
- if (docs) collectNodePortTargetsFromDocs(docs, pkgAbs, map)
687
- }
688
- }
689
- return map
690
- }
691
-
692
- /**
693
- * У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort**; при **`clusterIP: None`** — **`op: remove`** на **`/spec/clusterIP`**; якщо в base є **`spec.clusterIPs`** — ще **`remove`** на **`/spec/clusterIPs`** (abie.mdc).
694
- * @param {string} root корінь
695
- * @param {string[]} yamlFilesAbs yaml під **k8s**
696
- * @param {(msg: string) => void} fail callback
697
- * @param {(msg: string) => void} passFn успішне повідомлення
698
- * @returns {Promise<void>}
699
- */
700
- async function ensureRuAbieServiceNodePortPatches(root, yamlFilesAbs, fail, passFn) {
701
- const byPkg = await collectAbieRuNodePortServiceTargetsByPackage(root, yamlFilesAbs, fail)
702
- const entries = [...byPkg.entries()].filter(([, m]) => m.size > 0)
703
- if (entries.length === 0) {
704
- passFn('Немає Service у шарі k8s без k8s/ua/ та k8s/ru/ — patch NodePort у k8s/ru/ не вимагається (abie.mdc)')
705
- return
706
- }
707
- for (const [pkgAbs, targetsByName] of entries.toSorted((a, b) => a[0].localeCompare(b[0]))) {
708
- const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
709
- const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
710
- const nameList = [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))
711
- if (!existsSync(ruAbs)) {
712
- fail(
713
- `${relPkg}/k8s: є Service, для overlay ru потрібен patch Service (NodePort; для headless — ще remove /spec/clusterIP): ${nameList.join(', ')} — додай ru/kustomization.yaml (abie.mdc)`
714
- )
715
- return
716
- }
717
- const relRu = relative(root, ruAbs).replaceAll('\\', '/') || ruAbs
718
- let raw
719
- try {
720
- raw = await readFile(ruAbs, 'utf8')
721
- } catch (error) {
722
- const msg = error instanceof Error ? error.message : String(error)
723
- fail(`${relRu}: не вдалося прочитати (${msg})`)
724
- return
725
- }
726
- const patchErrors = getAbieRuServiceNodePortPatchErrors(raw, targetsByName)
727
- if (patchErrors.length > 0) {
728
- fail(`${relRu}: ${patchErrors.join('; ')}`)
729
- return
730
- }
731
- passFn(`${relRu}: patch Service → NodePort (ru) відповідає abie.mdc`)
732
- }
733
- }
734
-
735
308
  /**
736
309
  * Директорії, де є хоча б один **Deployment** у файлах **k8s**.
737
310
  * @param {string} root корінь cwd
@@ -793,7 +366,7 @@ function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, pas
793
366
  * @returns {string} той самий рядок без BOM (U+FEFF) на початку
794
367
  */
795
368
  function stripBom(s) {
796
- return s.startsWith('\uFEFF') ? s.slice(1) : s
369
+ return s.startsWith('') ? s.slice(1) : s
797
370
  }
798
371
 
799
372
  /**
@@ -855,29 +428,10 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
855
428
  return true
856
429
  }
857
430
 
858
- /**
859
- * Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**yandex.cloud/preemptible: false** на **`/spec/template/spec/nodeSelector`**).
860
- * Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
861
- * @param {string} patchText поле **patch** у kustomization
862
- * @returns {boolean} true, якщо критерії abie.mdc виконано
863
- */
864
- function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
865
- if (typeof patchText !== 'string' || patchText.trim() === '') {
866
- return false
867
- }
868
- if (!PATCH_NODE_SELECTOR_PATH_RE.test(patchText)) {
869
- return false
870
- }
871
- if (!PATCH_YANDEX_PREEMPTIBLE_FALSE_RE.test(patchText)) {
872
- return false
873
- }
874
- return true
875
- }
876
-
877
431
  /**
878
432
  * Чи один елемент **patches** у kustomization відповідає abie nodeSelector для заданого **mode**.
879
433
  * @param {unknown} p елемент масиву **patches**
880
- * @param {'ua' | 'ru'} mode який overlay перевіряти
434
+ * @param {'ua'} mode який overlay перевіряти
881
435
  * @returns {boolean} true, якщо patch відповідає abie для **mode**
882
436
  */
883
437
  function inlineKustomizationPatchMatchesAbieMode(p, mode) {
@@ -900,16 +454,13 @@ function inlineKustomizationPatchMatchesAbieMode(p, mode) {
900
454
  if (mode === 'ua' && jsonPatchTextHasUaDeploymentNodeSelector(patchStr)) {
901
455
  return true
902
456
  }
903
- if (mode === 'ru' && jsonPatchTextHasRuDeploymentNodeSelector(patchStr)) {
904
- return true
905
- }
906
457
  return false
907
458
  }
908
459
 
909
460
  /**
910
461
  * Чи один YAML-документ kustomization містить відповідний inline patch на Deployment.
911
462
  * @param {import('yaml').Document} doc документ після **parseAllDocuments**
912
- * @param {'ua' | 'ru'} mode який overlay перевіряти
463
+ * @param {'ua'} mode який overlay перевіряти
913
464
  * @returns {boolean} true, якщо знайдено відповідний patch
914
465
  */
915
466
  function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
@@ -937,9 +488,9 @@ function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
937
488
  }
938
489
 
939
490
  /**
940
- * Чи **kustomization.yaml** містить inline **patches** на **Deployment** з nodeSelector за abie.mdc (**ua** або **ru**).
491
+ * Чи **kustomization.yaml** містить inline **patches** на **Deployment** з nodeSelector за abie.mdc (overlay **ua**).
941
492
  * @param {string} raw повний текст файлу
942
- * @param {'ua' | 'ru'} mode який overlay перевіряти
493
+ * @param {'ua'} mode який overlay перевіряти
943
494
  * @returns {boolean} true, якщо знайдено відповідний patch
944
495
  */
945
496
  export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
@@ -963,12 +514,12 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
963
514
  }
964
515
 
965
516
  /**
966
- * Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза піддеревами **`ua/`** та **`ru/`** (base-шар abie).
517
+ * Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза піддеревом **`ua/`** (base-шар abie).
967
518
  * @param {string} relFromRoot відносний шлях від кореня
968
519
  * @param {string} pkgRelFromRoot каталог пакета відносно кореня (без завершального слеша після імені пакета)
969
520
  * @returns {boolean} `true`, якщо шлях належить до base-шару abie
970
521
  */
971
- export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelFromRoot) {
522
+ export function isK8sYamlInAbiePackageExcludingUaOverlay(relFromRoot, pkgRelFromRoot) {
972
523
  const normRel = relFromRoot.replaceAll('\\', '/')
973
524
  const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(TRAILING_SLASH_RE, '')
974
525
  const prefix = `${pkg}/k8s/`
@@ -976,7 +527,7 @@ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelF
976
527
  return false
977
528
  }
978
529
  const after = normRel.slice(prefix.length)
979
- return !after.startsWith('ua/') && !after.startsWith('ru/')
530
+ return !after.startsWith('ua/')
980
531
  }
981
532
 
982
533
  /**
@@ -1028,7 +579,7 @@ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
1028
579
  }
1029
580
 
1030
581
  /**
1031
- * З YAML під **k8s** пакета (без overlay **ua** та **ru**) збирає кількість **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** і порушення **`namespace: dev`**.
582
+ * З YAML під **k8s** пакета (без overlay **ua**) збирає кількість **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** і порушення **`namespace: dev`**.
1032
583
  * @param {string} root корінь репозиторію
1033
584
  * @param {string} pkgAbs абсолютний шлях до каталогу пакета
1034
585
  * @param {string[]} yamlFilesAbs усі **yaml** під **k8s** (як **findK8sYamlFiles**)
@@ -1041,7 +592,7 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
1041
592
  const baseErrors = []
1042
593
  for (const abs of yamlFilesAbs) {
1043
594
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
1044
- if (isK8sYamlInAbiePackageExcludingUaRuOverlays(rel, pkgRel)) {
595
+ if (isK8sYamlInAbiePackageExcludingUaOverlay(rel, pkgRel)) {
1045
596
  const docs = await readAndParseYamlDocs(abs, rel, silentFail)
1046
597
  if (docs) {
1047
598
  for (const doc of docs) {
@@ -1061,28 +612,19 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
1061
612
  /**
1062
613
  * Рахує операції JSON6902 з **`path`**: **`/spec/rules/…/backendRefs/…/namespace`** та **`value`** overlay.
1063
614
  * @param {string} combined сукупний текст patch **HTTPRoute**
1064
- * @param {'ua' | 'ru'} mode overlay
615
+ * @param {'ua'} mode overlay
1065
616
  * @returns {number} кількість знайдених патчів namespace
1066
617
  */
1067
618
  function countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode) {
619
+ if (mode !== 'ua') return 0
1068
620
  const re =
1069
- mode === 'ua'
1070
- ? /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/gimu
1071
- : /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/gimu
621
+ /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/gimu
1072
622
  return [...combined.matchAll(re)].length
1073
623
  }
1074
624
 
1075
625
  /** Домени **hostnames** для overlay **ua** (підрядки у JSON6902-тексті patch), abie.mdc. */
1076
626
  const ABIE_UA_HTTPROUTE_HOST_MARKERS = ['abie.app', 'vybeerai.com.ua', '*.abie.app', '*.vybeerai.com.ua']
1077
627
 
1078
- /** Домени **hostnames** для overlay **ru** (підрядки), abie.mdc. */
1079
- const ABIE_RU_HTTPROUTE_HOST_MARKERS = [
1080
- 'napitkivmeste.tech',
1081
- 'выбирайонлайн.рф',
1082
- '*.napitkivmeste.tech',
1083
- '*.выбирайонлайн.рф'
1084
- ]
1085
-
1086
628
  /**
1087
629
  * Витягує текст patch HTTPRoute з елемента patches.
1088
630
  * @param {unknown} p елемент масиву patches
@@ -1147,39 +689,30 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
1147
689
  /**
1148
690
  * Перевіряє сукупний текст patch(ів) **HTTPRoute** (будь-яке **target.name**) на відповідність abie.mdc.
1149
691
  * @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
1150
- * @param {'ua' | 'ru'} mode **ua** або **ru**
1151
- * @param {string} [fullKustomizationRaw] повний текст **kustomization.yaml** — для **ru** визначає, чи потрібна анотація **gwin…websocket** (лише якщо є **`HASURA_GRAPHQL_JWT_SECRET`**)
692
+ * @param {'ua'} mode overlay (наразі лише **ua**)
693
+ * @param {string} [_fullKustomizationRaw] збережено для зворотної сумісності API (не використовується)
1152
694
  * @param {number} [sharedCrossNsBackendRefCount] скільки **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** у base **HTTPRoute** пакета — стільки ж patch-ів **`…/backendRefs/…/namespace`** з **`value`** overlay
1153
695
  * @returns {string | null} повідомлення про помилку або **null**
1154
696
  */
1155
697
  export function validateAbieNginxRunHttpRoutePatches(
1156
698
  combined,
1157
699
  mode,
1158
- fullKustomizationRaw,
700
+ _fullKustomizationRaw,
1159
701
  sharedCrossNsBackendRefCount = 0
1160
702
  ) {
1161
703
  if (typeof combined !== 'string' || combined.trim() === '') {
1162
- return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — gwin… websocket лише за наявності HASURA_GRAPHQL_JWT_SECRET у файлі) — abie.mdc`
704
+ return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}) — abie.mdc`
1163
705
  }
1164
706
  if (!PATCH_HOSTNAMES_PATH_RE.test(combined)) {
1165
707
  return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
1166
708
  }
1167
- const markers = mode === 'ua' ? ABIE_UA_HTTPROUTE_HOST_MARKERS : ABIE_RU_HTTPROUTE_HOST_MARKERS
709
+ const markers = ABIE_UA_HTTPROUTE_HOST_MARKERS
1168
710
  if (!markers.some(m => combined.includes(m))) {
1169
711
  return `HTTPRoute: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
1170
712
  }
1171
- const namespaceOk =
1172
- mode === 'ua' ? PATCH_PARENT_REF_NS_UA_RE.test(combined) : PATCH_PARENT_REF_NS_RU_RE.test(combined)
1173
- if (!namespaceOk) {
713
+ if (!PATCH_PARENT_REF_NS_UA_RE.test(combined)) {
1174
714
  return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
1175
715
  }
1176
- const ruNeedsWebsocket =
1177
- mode === 'ru' &&
1178
- typeof fullKustomizationRaw === 'string' &&
1179
- fullKustomizationRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)
1180
- if (ruNeedsWebsocket && !WEBSOCKET_ANNOTATION_RE.test(combined)) {
1181
- return 'HTTPRoute (ru): за наявності HASURA_GRAPHQL_JWT_SECRET у kustomization потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
1182
- }
1183
716
  const sharedCount =
1184
717
  typeof sharedCrossNsBackendRefCount === 'number' && Number.isFinite(sharedCrossNsBackendRefCount)
1185
718
  ? Math.max(0, Math.floor(sharedCrossNsBackendRefCount))
@@ -1194,9 +727,9 @@ export function validateAbieNginxRunHttpRoutePatches(
1194
727
  }
1195
728
 
1196
729
  /**
1197
- * Чи **kustomization** містить валідні для abie **patch** для **HTTPRoute** з непорожнім **target.name** (**ua** або **ru**).
730
+ * Чи **kustomization** містить валідні для abie **patch** для **HTTPRoute** з непорожнім **target.name** (**ua**).
1198
731
  * @param {string} raw повний текст **kustomization.yaml**
1199
- * @param {'ua' | 'ru'} mode overlay
732
+ * @param {'ua'} mode overlay
1200
733
  * @returns {boolean} true, якщо **`validateAbieNginxRunHttpRoutePatches`** повертає **null**
1201
734
  */
1202
735
  export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
@@ -1231,85 +764,11 @@ export function validateAbieHcModeline(raw, relPath) {
1231
764
  return null
1232
765
  }
1233
766
 
1234
- /**
1235
- * Збирає відносний шлях із документів, що містять HealthCheckPolicy.
1236
- * @param {import('yaml').Document[]} docs документи з файлу
1237
- * @param {string} rel відносний шлях файлу
1238
- * @param {string[]} out масив для запису шляхів
1239
- */
1240
- function collectHealthCheckPolicyRelFromDocs(docs, rel, out) {
1241
- for (const doc of docs) {
1242
- if (doc.errors.length === 0) {
1243
- const obj = doc.toJSON()
1244
- if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
1245
- const rec = /** @type {Record<string, unknown>} */ (obj)
1246
- if (rec.kind === 'HealthCheckPolicy' && !out.includes(rel)) {
1247
- out.push(rel)
1248
- }
1249
- }
1250
- }
1251
- }
1252
- }
1253
-
1254
- /**
1255
- * Збирає відносні шляхи файлів із **HealthCheckPolicy** у дереві k8s.
1256
- * @param {string} root корінь
1257
- * @param {string[]} yamlAbs абсолютні шляхи
1258
- * @returns {Promise<string[]>} унікальні відносні шляхи yaml із **HealthCheckPolicy**
1259
- */
1260
- async function collectHealthCheckPolicyRelPaths(root, yamlAbs) {
1261
- /** @type {string[]} */
1262
- const out = []
1263
- for (const abs of yamlAbs) {
1264
- const rel = relative(root, abs).replaceAll('\\', '/') || abs
1265
- const docs = await readAndParseYamlDocs(abs, rel, silentFail)
1266
- if (docs) collectHealthCheckPolicyRelFromDocs(docs, rel, out)
1267
- }
1268
- return out
1269
- }
1270
-
1271
- /**
1272
- * Якщо є **HealthCheckPolicy**, вимагає **ru/kustomization.yaml** з patch видалення (**ruKustomizationHasHealthCheckDeletePatch** у **check-k8s**).
1273
- * @param {string} root корінь
1274
- * @param {string[]} yamlFilesAbs абсолютні шляхи yaml k8s
1275
- * @param {string[]} healthCheckPolicyRelativePaths відносні шляхи
1276
- * @param {(msg: string) => void} fail callback
1277
- * @returns {Promise<void>}
1278
- */
1279
- async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, healthCheckPolicyRelativePaths, fail) {
1280
- if (healthCheckPolicyRelativePaths.length === 0) {
1281
- return
1282
- }
1283
- const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
1284
- if (ruAbsList.length === 0) {
1285
- fail(
1286
- `Знайдено HealthCheckPolicy у ${healthCheckPolicyRelativePaths.join(', ')} — додай ru/kustomization.yaml з patch видалення (abie.mdc)`
1287
- )
1288
- return
1289
- }
1290
- for (const abs of ruAbsList) {
1291
- let raw
1292
- try {
1293
- raw = await readFile(abs, 'utf8')
1294
- } catch (error) {
1295
- const msg = error instanceof Error ? error.message : String(error)
1296
- fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
1297
- return
1298
- }
1299
- if (ruKustomizationHasHealthCheckDeletePatch(raw)) {
1300
- return
1301
- }
1302
- }
1303
- fail(
1304
- 'Є HealthCheckPolicy, але жоден ru/kustomization.yaml не містить очікуваного patch видалення (kind: HealthCheckPolicy, metadata.name, $patch: delete) — abie.mdc'
1305
- )
1306
- }
1307
-
1308
767
  /**
1309
768
  * Перевіряє одну kustomization.yaml на nodeSelector patch для заданого overlay.
1310
769
  * @param {string} abs абсолютний шлях до файлу
1311
770
  * @param {string} rel відносний шлях (для повідомлень)
1312
- * @param {'ua' | 'ru'} mode параметр mode
771
+ * @param {'ua'} mode параметр mode
1313
772
  * @param {Set<string>} deploymentDirs директорії з Deployment (Set)
1314
773
  * @param {string} root корінь репозиторію
1315
774
  * @param {(msg: string) => void} fail callback при помилці
@@ -1330,8 +789,7 @@ async function checkNodeSelectorKustomization(abs, rel, mode, deploymentDirs, ro
1330
789
  return false
1331
790
  }
1332
791
  if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode)) {
1333
- const detail = mode === 'ua' ? 'preem: false' : 'yandex.cloud/preemptible: false'
1334
- fail(`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та ${detail} (abie.mdc)`)
792
+ fail(`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`)
1335
793
  return false
1336
794
  }
1337
795
  passFn(`${rel}: nodeSelector patch (${mode}) відповідає abie.mdc`)
@@ -1339,14 +797,14 @@ async function checkNodeSelectorKustomization(abs, rel, mode, deploymentDirs, ro
1339
797
  }
1340
798
 
1341
799
  /**
1342
- * Перевіряє наявність патчів nodeSelector для ua/ru overlay у k8s.
800
+ * Перевіряє наявність патчів nodeSelector для ua overlay у k8s.
1343
801
  * @param {string} root корінь репозиторію
1344
802
  * @param {string[]} yamlFilesAbs абсолютні шляхи yaml-файлів під k8s
1345
803
  * @param {Set<string>} deploymentDirs директорії з Deployment (Set)
1346
804
  * @param {(msg: string) => void} fail callback при помилці
1347
805
  * @param {(msg: string) => void} passFn callback при успішній перевірці
1348
806
  */
1349
- async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
807
+ async function ensureUaAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
1350
808
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
1351
809
  if (uaAbsList.length === 0) {
1352
810
  fail(
@@ -1359,26 +817,13 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
1359
817
  const ok = await checkNodeSelectorKustomization(abs, rel, 'ua', deploymentDirs, root, fail, passFn)
1360
818
  if (!ok) return
1361
819
  }
1362
-
1363
- const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
1364
- if (ruAbsList.length === 0) {
1365
- fail(
1366
- 'Є Deployment у k8s — додай ru/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, yandex.cloud/preemptible false (abie.mdc)'
1367
- )
1368
- return
1369
- }
1370
- for (const abs of ruAbsList) {
1371
- const rel = relative(root, abs).replaceAll('\\', '/') || abs
1372
- const ok = await checkNodeSelectorKustomization(abs, rel, 'ru', deploymentDirs, root, fail, passFn)
1373
- if (!ok) return
1374
- }
1375
820
  }
1376
821
 
1377
822
  /**
1378
- * Перевіряє HTTPRoute patch для одного overlay (ua/ru).
823
+ * Перевіряє HTTPRoute patch для одного overlay (ua).
1379
824
  * @param {string} abs абсолютний шлях до kustomization.yaml
1380
825
  * @param {string} rel відносний шлях (для повідомлень)
1381
- * @param {'ua' | 'ru'} mode overlay
826
+ * @param {'ua'} mode overlay
1382
827
  * @param {string} root корінь репозиторію
1383
828
  * @param {string[]} yamlFilesAbs абсолютні шляхи yaml-файлів під k8s
1384
829
  * @param {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} cache кеш аналізу shared backend refs
@@ -1457,14 +902,14 @@ function ensureAbieBaseHttpRouteHostnames(root, yamlFilesAbs, fail, passFn) {
1457
902
  }
1458
903
 
1459
904
  /**
1460
- * Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
905
+ * Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
1461
906
  * лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
1462
907
  * @param {string} root корінь репозиторію
1463
908
  * @param {string[]} yamlFilesAbs yaml під k8s
1464
909
  * @param {(msg: string) => void} fail callback при помилці
1465
910
  * @param {(msg: string) => void} passFn callback при успішній перевірці
1466
911
  */
1467
- async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
912
+ async function ensureUaAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
1468
913
  /** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
1469
914
  const cache = new Map()
1470
915
 
@@ -1479,18 +924,6 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
1479
924
  const ok = await checkHttpRouteKustomization(abs, rel, 'ua', root, yamlFilesAbs, cache, fail, passFn)
1480
925
  if (!ok) return
1481
926
  }
1482
-
1483
- const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
1484
- if (ruAbsList.length === 0) {
1485
- passFn(
1486
- 'Немає ru/kustomization.yaml у дереві k8s — patch HTTPRoute (ru) не вимагається (abie.mdc, лише Vite-пакети)'
1487
- )
1488
- }
1489
- for (const abs of ruAbsList) {
1490
- const rel = relative(root, abs).replaceAll('\\', '/') || abs
1491
- const ok = await checkHttpRouteKustomization(abs, rel, 'ru', root, yamlFilesAbs, cache, fail, passFn)
1492
- if (!ok) return
1493
- }
1494
927
  }
1495
928
 
1496
929
  /**
@@ -1553,17 +986,10 @@ function checkCleanMergedBranch(root, pass, fail) {
1553
986
  })
1554
987
  for (const v of violations) fail(v.message)
1555
988
  if (violations.length === 0) {
1556
- pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru (rego)')
989
+ pass('clean-merged-branch.yml: ignore_branches містить dev, ua (rego)')
1557
990
  }
1558
991
  }
1559
992
 
1560
- /**
1561
- * Перевіряє один файл hc.yaml на відповідність abie.mdc.
1562
- * @param {string} root корінь репозиторію
1563
- * @param {string} dir директорія з Deployment
1564
- * @param {(msg: string) => void} pass callback при успішній перевірці
1565
- * @param {(msg: string) => void} fail callback при помилці
1566
- */
1567
993
  /**
1568
994
  * Перевіряє hc.yaml у директоріях з Deployment. JS перевіряє modeline, далі
1569
995
  * один батч conftest для усіх знайдених hc.yaml — структурна валідація HCP
@@ -1613,287 +1039,9 @@ async function checkHcYamlFiles(root, deploymentDirs, pass, fail) {
1613
1039
  }
1614
1040
  }
1615
1041
 
1616
- /**
1617
- * Чи Deployment-документ містить контейнер із образом **`hasura/graphql-engine`** (abie.mdc nginx-sidecar).
1618
- * @param {unknown} obj корінь YAML-документа
1619
- * @returns {boolean} true якщо документ є Deployment із hasura/graphql-engine
1620
- */
1621
- function deploymentDocHasHasuraImage(obj) {
1622
- if (!isDeploymentDoc(obj)) return false
1623
- const rec = /** @type {Record<string, unknown>} */ (obj)
1624
- const spec = rec.spec
1625
- if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return false
1626
- const template = /** @type {Record<string, unknown>} */ (spec).template
1627
- if (template === null || typeof template !== 'object' || Array.isArray(template)) return false
1628
- const podSpec = /** @type {Record<string, unknown>} */ (template).spec
1629
- if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) return false
1630
- const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
1631
- if (!Array.isArray(containers)) return false
1632
- for (const c of containers) {
1633
- if (c !== null && typeof c === 'object' && !Array.isArray(c)) {
1634
- const img = /** @type {Record<string, unknown>} */ (c).image
1635
- if (typeof img === 'string' && img.includes(HASURA_IMAGE_MARKER)) return true
1636
- }
1637
- }
1638
- return false
1639
- }
1640
-
1641
- /**
1642
- * Чи Kustomization-документ містить у **`images[*].newName`** рядок **`hasura/graphql-engine`** (abie.mdc nginx-sidecar).
1643
- * @param {unknown} obj корінь YAML-документа
1644
- * @returns {boolean} true якщо є images[*].newName із hasura/graphql-engine
1645
- */
1646
- function kustomizationDocHasHasuraImageNewName(obj) {
1647
- if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return false
1648
- const rec = /** @type {Record<string, unknown>} */ (obj)
1649
- if (!Array.isArray(rec.images)) return false
1650
- for (const img of rec.images) {
1651
- if (img !== null && typeof img === 'object' && !Array.isArray(img)) {
1652
- const newName = /** @type {Record<string, unknown>} */ (img).newName
1653
- if (typeof newName === 'string' && newName.includes(HASURA_IMAGE_MARKER)) return true
1654
- }
1655
- }
1656
- return false
1657
- }
1658
-
1659
- /**
1660
- * Чи patch-запис у Kustomization має **target.kind === 'Deployment'**.
1661
- * @param {unknown} patchEntry елемент масиву **patches**
1662
- * @returns {boolean} true, якщо patch цілить на Deployment
1663
- */
1664
- function isPatchTargetingDeployment(patchEntry) {
1665
- if (patchEntry === null || typeof patchEntry !== 'object' || Array.isArray(patchEntry)) return false
1666
- const pr = /** @type {Record<string, unknown>} */ (patchEntry)
1667
- const target = pr.target
1668
- if (target === null || typeof target !== 'object' || Array.isArray(target)) return false
1669
- return /** @type {Record<string, unknown>} */ (target).kind === 'Deployment'
1670
- }
1671
-
1672
- /**
1673
- * Витягує текст **patch** із запису Kustomization, якщо він непорожній рядок.
1674
- * @param {unknown} patchEntry елемент масиву **patches**
1675
- * @returns {string | null} текст patch або null
1676
- */
1677
- function extractPatchString(patchEntry) {
1678
- const pr = /** @type {Record<string, unknown>} */ (patchEntry)
1679
- const patchStr = pr.patch
1680
- if (typeof patchStr === 'string' && patchStr.trim() !== '') return patchStr
1681
- return null
1682
- }
1683
-
1684
- /**
1685
- * Збирає тексти inline **patch** для **Deployment** з одного YAML-документа Kustomization.
1686
- * @param {import('yaml').Document} doc YAML-документ
1687
- * @param {string[]} out масив для збору результатів
1688
- */
1689
- function collectDeploymentPatchTextsFromDoc(doc, out) {
1690
- if (doc.errors.length > 0) return
1691
- const root = doc.toJSON()
1692
- if (root === null || typeof root !== 'object' || Array.isArray(root)) return
1693
- const rec = /** @type {Record<string, unknown>} */ (root)
1694
- if (!Array.isArray(rec.patches)) return
1695
- for (const p of rec.patches) {
1696
- if (isPatchTargetingDeployment(p)) {
1697
- const text = extractPatchString(p)
1698
- if (text !== null) out.push(text)
1699
- }
1700
- }
1701
- }
1702
-
1703
- /**
1704
- * Збирає тексти inline **patch** для **Deployment** з **kustomization.yaml** (усі документи).
1705
- * @param {string} raw повний текст файлу
1706
- * @returns {string[]} рядки patch
1707
- */
1708
- function collectDeploymentPatchTextsFromKustomization(raw) {
1709
- const body = stripBom(raw)
1710
- const lines = body.split(LINE_SPLIT_RE)
1711
- const first = lines[0] ?? ''
1712
- const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
1713
- /** @type {import('yaml').Document[]} */
1714
- let docs
1715
- try {
1716
- docs = parseAllDocuments(rest)
1717
- } catch {
1718
- return []
1719
- }
1720
- /** @type {string[]} */
1721
- const out = []
1722
- for (const doc of docs) {
1723
- collectDeploymentPatchTextsFromDoc(doc, out)
1724
- }
1725
- return out
1726
- }
1727
-
1728
- /**
1729
- * Чи YAML-документ містить образ Hasura (Deployment або Kustomization images).
1730
- * @param {import('yaml').Document} doc YAML-документ
1731
- * @returns {boolean} true, якщо документ посилається на hasura/graphql-engine
1732
- */
1733
- function yamlDocReferencesHasuraImage(doc) {
1734
- if (doc.errors.length > 0) return false
1735
- const obj = doc.toJSON()
1736
- return deploymentDocHasHasuraImage(obj) || kustomizationDocHasHasuraImageNewName(obj)
1737
- }
1738
-
1739
- /**
1740
- * Каталоги пакетів, де в дереві **k8s** є **Deployment** з образом **`hasura/graphql-engine`** або
1741
- * **Kustomization** з **`images[*].newName`** на нього (abie.mdc nginx-sidecar).
1742
- * @param {string} root корінь репозиторію
1743
- * @param {string[]} yamlAbs абсолютні шляхи yaml під k8s
1744
- * @returns {Promise<Set<string>>} абсолютні шляхи каталогів пакетів
1745
- */
1746
- async function collectHasuraK8sPackageDirs(root, yamlAbs) {
1747
- /** @type {Set<string>} */
1748
- const dirs = new Set()
1749
- for (const abs of yamlAbs) {
1750
- const rel = relative(root, abs).replaceAll('\\', '/') || abs
1751
- const docs = await readAndParseYamlDocs(abs, rel, silentFail)
1752
- if (docs && docs.some(doc => yamlDocReferencesHasuraImage(doc))) {
1753
- const pkgDir = abiePackageDirFromK8sYamlRel(root, rel)
1754
- if (pkgDir) dirs.add(pkgDir)
1755
- }
1756
- }
1757
- return dirs
1758
- }
1759
-
1760
- /**
1761
- * Якщо в дереві **k8s** є Deployment з **`hasura/graphql-engine`** і **`ru/kustomization.yaml`** містить
1762
- * **`HASURA_GRAPHQL_JWT_SECRET`** — вимагає **nginx-sidecar** (abie.mdc):
1763
- * **`ru/configmap-nginx.yaml`**, **`resources`** у kustomization, patch **Service -hl** (port 8081),
1764
- * patch **Deployment** (nginx-sidecar image + containerPort 8081), patch **HTTPRoute** (port 8081).
1765
- * @param {string} root корінь репозиторію
1766
- * @param {string[]} yamlFilesAbs yaml під k8s
1767
- * @param {(msg: string) => void} fail callback при помилці
1768
- * @param {(msg: string) => void} passFn callback при успішній перевірці
1769
- * @returns {Promise<void>}
1770
- */
1771
- /**
1772
- * Перевіряє nginx-sidecar вимоги у **ru/kustomization.yaml** для одного Hasura-пакета.
1773
- * @param {string} relPkg відносний шлях пакета
1774
- * @param {string} relRu відносний шлях до ru/kustomization.yaml
1775
- * @param {string} pkgAbs абсолютний шлях до пакета
1776
- * @param {string} ruRaw вміст ru/kustomization.yaml
1777
- * @param {(msg: string) => void} fail callback при помилці
1778
- * @param {(msg: string) => void} passFn callback при успішній перевірці
1779
- */
1780
- function validateNginxSidecarInRuKustomization(relPkg, relRu, pkgAbs, ruRaw, fail, passFn) {
1781
- // configmap-nginx.yaml must exist
1782
- const configmapNginxAbs = join(pkgAbs, 'k8s', 'ru', 'configmap-nginx.yaml')
1783
- if (!existsSync(configmapNginxAbs)) {
1784
- fail(`${relPkg}/k8s/ru: потрібен configmap-nginx.yaml з nginx.conf (nginx-sidecar для Hasura WebSocket, abie.mdc)`)
1785
- return
1786
- }
1787
- passFn(`${relPkg}/k8s/ru/configmap-nginx.yaml: існує`)
1788
- // kustomization resources must include configmap-nginx.yaml
1789
- if (!RESOURCES_CONFIGMAP_NGINX_RE.test(ruRaw)) {
1790
- fail(`${relRu}: у resources потрібен configmap-nginx.yaml (nginx-sidecar, abie.mdc)`)
1791
- return
1792
- }
1793
- passFn(`${relRu}: resources містить configmap-nginx.yaml`)
1794
- validateNginxSidecarPatches(relRu, ruRaw, fail, passFn)
1795
- }
1796
-
1797
- /**
1798
- * Перевіряє наявність nginx-sidecar patch (Service -hl, Deployment, HTTPRoute) у **ru/kustomization.yaml**.
1799
- * @param {string} relRu відносний шлях до ru/kustomization.yaml
1800
- * @param {string} ruRaw вміст ru/kustomization.yaml
1801
- * @param {(msg: string) => void} fail callback при помилці
1802
- * @param {(msg: string) => void} passFn callback при успішній перевірці
1803
- */
1804
- function validateNginxSidecarPatches(relRu, ruRaw, fail, passFn) {
1805
- // Service -hl patch must include port: 8081 (proxy)
1806
- const svcPatchByName = collectAbieRuServicePatchTextByTargetNameFromRaw(ruRaw)
1807
- const hasHlWith8081 = [...svcPatchByName.entries()].some(
1808
- ([name, pt]) => name.endsWith('-hl') && PATCH_PROXY_PORT_8081_RE.test(pt)
1809
- )
1810
- if (!hasHlWith8081) {
1811
- fail(`${relRu}: у patch Service -hl потрібен port: 8081 (proxy) для nginx-sidecar (abie.mdc)`)
1812
- return
1813
- }
1814
- passFn(`${relRu}: Service -hl patch містить port 8081 (nginx-sidecar)`)
1815
- // Deployment patch must include nginx-sidecar (image nginx:*-alpine + containerPort: 8081)
1816
- const deployPatches = collectDeploymentPatchTextsFromKustomization(ruRaw)
1817
- const hasNginxSidecar = deployPatches.some(
1818
- pt => NGINX_SIDECAR_IMAGE_RE.test(pt) && NGINX_SIDECAR_CONTAINER_PORT_RE.test(pt)
1819
- )
1820
- if (!hasNginxSidecar) {
1821
- fail(`${relRu}: у patch Deployment потрібен nginx-sidecar (image nginx:…-alpine, containerPort: 8081) — abie.mdc`)
1822
- return
1823
- }
1824
- passFn(`${relRu}: Deployment patch містить nginx-sidecar (image + containerPort 8081)`)
1825
- // HTTPRoute patch must replace a backendRef port to 8081
1826
- const combined = getCombinedNginxRunPatchTextFromKustomization(ruRaw)
1827
- if (
1828
- !HTTPROUTE_BACKENDREF_PORT_8081_RE.test(combined) &&
1829
- !HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE.test(combined)
1830
- ) {
1831
- fail(
1832
- `${relRu}: у patch HTTPRoute потрібен JSON6902 з path /spec/rules/…/backendRefs/…/port та value: 8081 (nginx-sidecar, abie.mdc)`
1833
- )
1834
- return
1835
- }
1836
- passFn(`${relRu}: HTTPRoute patch замінює порт на 8081 (nginx-sidecar)`)
1837
- }
1838
-
1839
- /**
1840
- * Обробляє один Hasura-пакет: читає **ru/kustomization.yaml** та перевіряє nginx-sidecar вимоги.
1841
- * @param {string} root корінь репозиторію
1842
- * @param {string} pkgAbs абсолютний шлях до пакета
1843
- * @param {(msg: string) => void} fail callback при помилці
1844
- * @param {(msg: string) => void} passFn callback при успішній перевірці
1845
- * @returns {Promise<void>}
1846
- */
1847
- async function checkNginxSidecarForHasuraPackage(root, pkgAbs, fail, passFn) {
1848
- const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
1849
- const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
1850
- if (!existsSync(ruAbs)) {
1851
- passFn(`${relPkg}/k8s: є Hasura Deployment, але немає ru/kustomization.yaml — nginx-sidecar не перевіряється`)
1852
- return
1853
- }
1854
- let ruRaw
1855
- try {
1856
- ruRaw = await readFile(ruAbs, 'utf8')
1857
- } catch (error) {
1858
- const msg = error instanceof Error ? error.message : String(error)
1859
- fail(`${relPkg}/k8s/ru/kustomization.yaml: не вдалося прочитати (${msg})`)
1860
- return
1861
- }
1862
- if (!ruRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)) {
1863
- passFn(
1864
- `${relPkg}/k8s/ru/kustomization.yaml: немає ${HASURA_JWT_SECRET_IN_KUSTOMIZATION} — nginx-sidecar не вимагається (abie.mdc)`
1865
- )
1866
- return
1867
- }
1868
- const relRu = relative(root, ruAbs).replaceAll('\\', '/') || ruAbs
1869
- validateNginxSidecarInRuKustomization(relPkg, relRu, pkgAbs, ruRaw, fail, passFn)
1870
- }
1871
-
1872
- /**
1873
- * Якщо в дереві **k8s** є Deployment з **`hasura/graphql-engine`** і **`ru/kustomization.yaml`** містить
1874
- * **`HASURA_GRAPHQL_JWT_SECRET`** — вимагає **nginx-sidecar** (abie.mdc):
1875
- * **`ru/configmap-nginx.yaml`**, **`resources`** у kustomization, patch **Service -hl** (port 8081),
1876
- * patch **Deployment** (nginx-sidecar image + containerPort 8081), patch **HTTPRoute** (port 8081).
1877
- * @param {string} root корінь репозиторію
1878
- * @param {string[]} yamlFilesAbs yaml під k8s
1879
- * @param {(msg: string) => void} fail callback при помилці
1880
- * @param {(msg: string) => void} passFn callback при успішній перевірці
1881
- * @returns {Promise<void>}
1882
- */
1883
- async function ensureAbieNginxSidecarForHasura(root, yamlFilesAbs, fail, passFn) {
1884
- const hasuraPkgDirs = await collectHasuraK8sPackageDirs(root, yamlFilesAbs)
1885
- if (hasuraPkgDirs.size === 0) {
1886
- passFn('Немає Deployment із hasura/graphql-engine у дереві k8s — nginx-sidecar не вимагається (abie.mdc)')
1887
- return
1888
- }
1889
- for (const pkgAbs of [...hasuraPkgDirs].toSorted((a, b) => a.localeCompare(b))) {
1890
- await checkNginxSidecarForHasuraPackage(root, pkgAbs, fail, passFn)
1891
- }
1892
- }
1893
-
1894
1042
  /**
1895
1043
  * Збирає всі `*.env` файли в дереві (за виключенням `node_modules`, `.git` та інших службових каталогів),
1896
- * basename яких — abie env-файл (`dev.env` / `ua.env` / `ru.env` опційно з провідною крапкою). Файл `.env`
1044
+ * basename яких — abie env-файл (`dev.env` / `ua.env` опційно з провідною крапкою). Файл `.env`
1897
1045
  * без імені виключається — як і у `check-hasura.mjs`.
1898
1046
  * @param {string} root корінь репозиторію
1899
1047
  * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
@@ -1915,11 +1063,11 @@ async function collectAbieEnvFiles(root, ignorePaths) {
1915
1063
  }
1916
1064
 
1917
1065
  /**
1918
- * Сканує всі `*.env` файли abie (`.dev.env` / `.ua.env` / `.ru.env`) і для кожного знайденого
1066
+ * Сканує всі `*.env` файли abie (`.dev.env` / `.ua.env`) і для кожного знайденого
1919
1067
  * **внутрішньокластерного** URL (`http://<svc>.<ns>.svc.<dns>`) перевіряє, що DNS-суфікс і namespace-префікс
1920
1068
  * відповідають середовищу env-файла. Не лише `HASURA_GRAPHQL_ENDPOINT`, а й будь-який сервіс у env (KVCMS,
1921
1069
  * `auth-run-hl`, `file-link-hl` тощо) мусить мати кластер, що відповідає env: dev → `abie-dev.internal`,
1922
- * ua → `abie-ua.internal`, ru → `cluster.local`.
1070
+ * ua → `abie-ua.internal`.
1923
1071
  * @param {string} root корінь репозиторію
1924
1072
  * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
1925
1073
  * @param {(msg: string) => void} pass успішне повідомлення
@@ -1929,7 +1077,7 @@ async function collectAbieEnvFiles(root, ignorePaths) {
1929
1077
  async function ensureAbieEnvFilesMatchClusterDns(root, ignorePaths, pass, fail) {
1930
1078
  const envFiles = await collectAbieEnvFiles(root, ignorePaths)
1931
1079
  if (envFiles.length === 0) {
1932
- pass('Не знайдено dev.env / ua.env / ru.env у репозиторії — перевірку env→cluster DNS пропущено (abie.mdc)')
1080
+ pass('Не знайдено dev.env / ua.env у репозиторії — перевірку env→cluster DNS пропущено (abie.mdc)')
1933
1081
  return
1934
1082
  }
1935
1083
  for (const abs of envFiles) {
@@ -1987,26 +1135,17 @@ export async function check() {
1987
1135
  pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
1988
1136
  }
1989
1137
 
1990
- const healthCheckPolicyRelativePaths = await collectHealthCheckPolicyRelPaths(root, yamlFiles)
1991
- await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyRelativePaths, fail)
1992
-
1993
- pass('Перевіряємо Service → NodePort у ru/kustomization (abie.mdc)')
1994
- await ensureRuAbieServiceNodePortPatches(root, yamlFiles, fail, pass)
1995
-
1996
1138
  pass('Перевіряємо HTTPRoute spec.hostnames у …/k8s/base/… (aiml.live, abie.mdc)')
1997
1139
  await ensureAbieBaseHttpRouteHostnames(root, yamlFiles, fail, pass)
1998
1140
 
1999
1141
  if (deploymentDirs.size > 0) {
2000
- pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
2001
- await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
2002
- pass('Є Deployment — перевіряємо HTTPRoute у ua/ru kustomization (abie.mdc)')
2003
- await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
1142
+ pass('Є Deployment — перевіряємо nodeSelector у ua/kustomization (abie.mdc)')
1143
+ await ensureUaAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
1144
+ pass('Є Deployment — перевіряємо HTTPRoute у ua/kustomization (abie.mdc)')
1145
+ await ensureUaAbieHttpRoutePatches(root, yamlFiles, fail, pass)
2004
1146
  }
2005
1147
 
2006
- pass('Перевіряємо nginx-sidecar для Hasura WebSocket у ru (abie.mdc)')
2007
- await ensureAbieNginxSidecarForHasura(root, yamlFiles, fail, pass)
2008
-
2009
- pass('Перевіряємо env→cluster DNS у dev.env / ua.env / ru.env (abie.mdc)')
1148
+ pass('Перевіряємо env→cluster DNS у dev.env / ua.env (abie.mdc)')
2010
1149
  await ensureAbieEnvFilesMatchClusterDns(root, ignorePaths, pass, fail)
2011
1150
 
2012
1151
  return reporter.getExitCode()