@nitra/cursor 1.9.17 → 1.9.19

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,53 @@ 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
79
  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
80
  const PATCH_NODE_SELECTOR_PATH_RE = /path:\s*\/spec\/template\/spec\/nodeSelector\b/u
93
81
  const PATCH_PREEM_FALSE_RE = /\bpreem:\s*['"]?false['"]?\b/u
94
- const PATCH_YANDEX_PREEMPTIBLE_FALSE_RE = /yandex\.cloud\/preemptible:\s*['"]?false['"]?/u
95
82
  const TRAILING_SLASH_RE = /\/$/u
96
83
  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).
84
+ // Overlay namespaces: allow `ua` and `ua-*` (e.g. ua-b2b).
98
85
  const PATCH_PARENT_REF_NS_UA_RE =
99
86
  /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
87
 
125
88
  /**
126
- * Регекс basename env-файлу abie: `dev.env` / `ua.env` / `ru.env`, опційно з провідною крапкою (`.dev.env` тощо).
89
+ * Регекс basename env-файлу abie: `dev.env` / `ua.env`, опційно з провідною крапкою (`.dev.env` тощо).
127
90
  * Файл рівно `.env` (без імені) — виключення з правила: локальний файл розробника, `check-abie` його не сканує
128
91
  * (так само як `check-hasura`, див. `isEnvFile`).
129
92
  */
130
- const ABIE_ENV_FILE_BASENAME_RE = /^\.?(dev|ua|ru)\.env$/u
93
+ const ABIE_ENV_FILE_BASENAME_RE = /^\.?(dev|ua)\.env$/u
131
94
 
132
95
  /**
133
96
  * Глобальний регекс кластерного internal URL у тексті env-файлу.
134
97
  * Використовується з `String.prototype.matchAll`, тому має флаг `g`.
135
- * Дозволяє два DNS-формати: `<cluster>.internal` (GKE) і `cluster.local` (YC / стандартний k8s).
98
+ * Допустимий DNS-формат `<cluster>.internal` (GKE).
136
99
  * Порт необов'язковий — у KVCMS-конфігах інколи лежить URL без порту (8080 додається сервісом за замовчуванням).
137
100
  */
138
101
  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
102
+ /\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
103
 
141
104
  /**
142
105
  * Очікуваний кластерний DNS-суфікс і namespace-префікс для кожного env-файлу abie.
143
- * `dev` / `ua` живуть у GKE з власним `<cluster>.internal`; `ru` — у YC, де DNS-суфікс
144
- * стандартний `cluster.local` (без імені кластера).
106
+ * `dev` / `ua` живуть у двох GKE-кластерах з власним `<cluster>.internal`.
145
107
  */
146
108
  const ABIE_ENV_CLUSTER_DNS_MAP = Object.freeze({
147
109
  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-' })
110
+ ua: Object.freeze({ clusterDns: 'abie-ua.internal', namespacePrefix: 'ua-' })
150
111
  })
151
112
 
152
113
  /**
153
- * Дістає ім'я env (`dev` / `ua` / `ru`) з basename env-файлу abie.
114
+ * Дістає ім'я env (`dev` / `ua`) з basename env-файлу abie.
154
115
  * Для не-abie env-файлів (наприклад `production.env`, `.env` без імені) повертає `null`.
155
116
  * @param {string} basenameOfEnvFile basename файла (без шляху)
156
- * @returns {('dev' | 'ua' | 'ru') | null} ім'я env або `null`
117
+ * @returns {('dev' | 'ua') | null} ім'я env або `null`
157
118
  */
158
119
  export function abieEnvNameFromBasename(basenameOfEnvFile) {
159
120
  const m = basenameOfEnvFile.match(ABIE_ENV_FILE_BASENAME_RE)
160
- return m ? /** @type {'dev' | 'ua' | 'ru'} */ (m[1]) : null
121
+ return m ? /** @type {'dev' | 'ua'} */ (m[1]) : null
161
122
  }
162
123
 
163
124
  /**
@@ -165,7 +126,7 @@ export function abieEnvNameFromBasename(basenameOfEnvFile) {
165
126
  * для кожного знайденого internal URL. URL шукається глобально (`matchAll`), тож одне й те саме
166
127
  * порушення в кількох змінних дасть стільки ж окремих помилок.
167
128
  * @param {string} content вміст env-файлу (UTF-8)
168
- * @param {'dev' | 'ua' | 'ru'} envName ім'я env, отримане з `abieEnvNameFromBasename`
129
+ * @param {'dev' | 'ua'} envName ім'я env, отримане з `abieEnvNameFromBasename`
169
130
  * @returns {string[]} порожній масив, якщо все OK; інакше — список повідомлень про порушення
170
131
  */
171
132
  export function validateAbieEnvInternalUrls(content, envName) {
@@ -189,16 +150,6 @@ export function validateAbieEnvInternalUrls(content, envName) {
189
150
  return errors
190
151
  }
191
152
 
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
153
  /**
203
154
  * Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед ім'ям файлу) — специфіка abie overlay.
204
155
  * @param {string} rel шлях від кореня репозиторію
@@ -210,10 +161,10 @@ export function isUaKustomizationPath(rel) {
210
161
  }
211
162
 
212
163
  /**
213
- * Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/(ua|ru)/kustomization.yaml`**.
164
+ * Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/ua/kustomization.yaml`**.
214
165
  * @param {string} root корінь репозиторію
215
- * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
216
- * @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua чи ru
166
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
167
+ * @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua
217
168
  */
218
169
  export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
219
170
  const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
@@ -224,7 +175,7 @@ export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
224
175
  /**
225
176
  * Чи для цього overlay застосовувати вимоги **HTTPRoute** (лише Vite-пакети).
226
177
  * @param {string} root корінь репозиторію
227
- * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
178
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
228
179
  * @returns {boolean} **true**, якщо поруч із **k8s** є **vite.config** (**js** / **mjs** / **ts**)
229
180
  */
230
181
  export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
@@ -240,10 +191,10 @@ export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
240
191
  }
241
192
 
242
193
  /**
243
- * Чи в дереві **k8s** того ж пакета, що й overlay **ua** або **ru**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
194
+ * Чи в дереві **k8s** того ж пакета, що й overlay **ua**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
244
195
  * @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
245
196
  * @param {string} root корінь репозиторію
246
- * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
197
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
247
198
  * @returns {boolean} **true**, якщо хоч один каталог із **deploymentDirs** лежить під **`…/k8s/`** цього пакета
248
199
  */
249
200
  export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizationAbs) {
@@ -307,7 +258,7 @@ export async function isAbieRuleEnabled(root) {
307
258
  }
308
259
 
309
260
  // Per-document валідація `clean-merged-branch.yml` (with.ignore_branches з
310
- // dev/ua/ru) делегована rego-пакету `abie.clean_merged_ignore_branches`
261
+ // dev/ua) делегована rego-пакету `abie.clean_merged_ignore_branches`
311
262
  // (`npm/policy/abie/clean_merged_ignore_branches/`). JS викликає
312
263
  // `runConftestBatch` у `checkCleanMergedBranch`.
313
264
 
@@ -355,383 +306,6 @@ function isDeploymentDoc(obj) {
355
306
  )
356
307
  }
357
308
 
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
309
  /**
736
310
  * Директорії, де є хоча б один **Deployment** у файлах **k8s**.
737
311
  * @param {string} root корінь cwd
@@ -793,7 +367,7 @@ function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, pas
793
367
  * @returns {string} той самий рядок без BOM (U+FEFF) на початку
794
368
  */
795
369
  function stripBom(s) {
796
- return s.startsWith('\uFEFF') ? s.slice(1) : s
370
+ return s.startsWith('') ? s.slice(1) : s
797
371
  }
798
372
 
799
373
  /**
@@ -855,29 +429,10 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
855
429
  return true
856
430
  }
857
431
 
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
432
  /**
878
433
  * Чи один елемент **patches** у kustomization відповідає abie nodeSelector для заданого **mode**.
879
434
  * @param {unknown} p елемент масиву **patches**
880
- * @param {'ua' | 'ru'} mode який overlay перевіряти
435
+ * @param {'ua'} mode який overlay перевіряти
881
436
  * @returns {boolean} true, якщо patch відповідає abie для **mode**
882
437
  */
883
438
  function inlineKustomizationPatchMatchesAbieMode(p, mode) {
@@ -900,16 +455,13 @@ function inlineKustomizationPatchMatchesAbieMode(p, mode) {
900
455
  if (mode === 'ua' && jsonPatchTextHasUaDeploymentNodeSelector(patchStr)) {
901
456
  return true
902
457
  }
903
- if (mode === 'ru' && jsonPatchTextHasRuDeploymentNodeSelector(patchStr)) {
904
- return true
905
- }
906
458
  return false
907
459
  }
908
460
 
909
461
  /**
910
462
  * Чи один YAML-документ kustomization містить відповідний inline patch на Deployment.
911
463
  * @param {import('yaml').Document} doc документ після **parseAllDocuments**
912
- * @param {'ua' | 'ru'} mode який overlay перевіряти
464
+ * @param {'ua'} mode який overlay перевіряти
913
465
  * @returns {boolean} true, якщо знайдено відповідний patch
914
466
  */
915
467
  function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
@@ -937,9 +489,9 @@ function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
937
489
  }
938
490
 
939
491
  /**
940
- * Чи **kustomization.yaml** містить inline **patches** на **Deployment** з nodeSelector за abie.mdc (**ua** або **ru**).
492
+ * Чи **kustomization.yaml** містить inline **patches** на **Deployment** з nodeSelector за abie.mdc (overlay **ua**).
941
493
  * @param {string} raw повний текст файлу
942
- * @param {'ua' | 'ru'} mode який overlay перевіряти
494
+ * @param {'ua'} mode який overlay перевіряти
943
495
  * @returns {boolean} true, якщо знайдено відповідний patch
944
496
  */
945
497
  export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
@@ -963,12 +515,12 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
963
515
  }
964
516
 
965
517
  /**
966
- * Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза піддеревами **`ua/`** та **`ru/`** (base-шар abie).
518
+ * Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза піддеревом **`ua/`** (base-шар abie).
967
519
  * @param {string} relFromRoot відносний шлях від кореня
968
520
  * @param {string} pkgRelFromRoot каталог пакета відносно кореня (без завершального слеша після імені пакета)
969
521
  * @returns {boolean} `true`, якщо шлях належить до base-шару abie
970
522
  */
971
- export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelFromRoot) {
523
+ export function isK8sYamlInAbiePackageExcludingUaOverlay(relFromRoot, pkgRelFromRoot) {
972
524
  const normRel = relFromRoot.replaceAll('\\', '/')
973
525
  const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(TRAILING_SLASH_RE, '')
974
526
  const prefix = `${pkg}/k8s/`
@@ -976,7 +528,7 @@ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelF
976
528
  return false
977
529
  }
978
530
  const after = normRel.slice(prefix.length)
979
- return !after.startsWith('ua/') && !after.startsWith('ru/')
531
+ return !after.startsWith('ua/')
980
532
  }
981
533
 
982
534
  /**
@@ -1028,7 +580,7 @@ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
1028
580
  }
1029
581
 
1030
582
  /**
1031
- * З YAML під **k8s** пакета (без overlay **ua** та **ru**) збирає кількість **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** і порушення **`namespace: dev`**.
583
+ * З YAML під **k8s** пакета (без overlay **ua**) збирає кількість **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** і порушення **`namespace: dev`**.
1032
584
  * @param {string} root корінь репозиторію
1033
585
  * @param {string} pkgAbs абсолютний шлях до каталогу пакета
1034
586
  * @param {string[]} yamlFilesAbs усі **yaml** під **k8s** (як **findK8sYamlFiles**)
@@ -1041,7 +593,7 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
1041
593
  const baseErrors = []
1042
594
  for (const abs of yamlFilesAbs) {
1043
595
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
1044
- if (isK8sYamlInAbiePackageExcludingUaRuOverlays(rel, pkgRel)) {
596
+ if (isK8sYamlInAbiePackageExcludingUaOverlay(rel, pkgRel)) {
1045
597
  const docs = await readAndParseYamlDocs(abs, rel, silentFail)
1046
598
  if (docs) {
1047
599
  for (const doc of docs) {
@@ -1061,28 +613,19 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
1061
613
  /**
1062
614
  * Рахує операції JSON6902 з **`path`**: **`/spec/rules/…/backendRefs/…/namespace`** та **`value`** overlay.
1063
615
  * @param {string} combined сукупний текст patch **HTTPRoute**
1064
- * @param {'ua' | 'ru'} mode overlay
616
+ * @param {'ua'} mode overlay
1065
617
  * @returns {number} кількість знайдених патчів namespace
1066
618
  */
1067
619
  function countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode) {
620
+ if (mode !== 'ua') return 0
1068
621
  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
622
+ /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
623
  return [...combined.matchAll(re)].length
1073
624
  }
1074
625
 
1075
626
  /** Домени **hostnames** для overlay **ua** (підрядки у JSON6902-тексті patch), abie.mdc. */
1076
627
  const ABIE_UA_HTTPROUTE_HOST_MARKERS = ['abie.app', 'vybeerai.com.ua', '*.abie.app', '*.vybeerai.com.ua']
1077
628
 
1078
- /** Домени **hostnames** для overlay **ru** (підрядки), abie.mdc. */
1079
- const ABIE_RU_HTTPROUTE_HOST_MARKERS = [
1080
- 'napitkivmeste.tech',
1081
- 'выбирайонлайн.рф',
1082
- '*.napitkivmeste.tech',
1083
- '*.выбирайонлайн.рф'
1084
- ]
1085
-
1086
629
  /**
1087
630
  * Витягує текст patch HTTPRoute з елемента patches.
1088
631
  * @param {unknown} p елемент масиву patches
@@ -1147,39 +690,30 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
1147
690
  /**
1148
691
  * Перевіряє сукупний текст patch(ів) **HTTPRoute** (будь-яке **target.name**) на відповідність abie.mdc.
1149
692
  * @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`**)
693
+ * @param {'ua'} mode overlay (наразі лише **ua**)
694
+ * @param {string} [_fullKustomizationRaw] збережено для зворотної сумісності API (не використовується)
1152
695
  * @param {number} [sharedCrossNsBackendRefCount] скільки **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** у base **HTTPRoute** пакета — стільки ж patch-ів **`…/backendRefs/…/namespace`** з **`value`** overlay
1153
696
  * @returns {string | null} повідомлення про помилку або **null**
1154
697
  */
1155
698
  export function validateAbieNginxRunHttpRoutePatches(
1156
699
  combined,
1157
700
  mode,
1158
- fullKustomizationRaw,
701
+ _fullKustomizationRaw,
1159
702
  sharedCrossNsBackendRefCount = 0
1160
703
  ) {
1161
704
  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`
705
+ return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}) — abie.mdc`
1163
706
  }
1164
707
  if (!PATCH_HOSTNAMES_PATH_RE.test(combined)) {
1165
708
  return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
1166
709
  }
1167
- const markers = mode === 'ua' ? ABIE_UA_HTTPROUTE_HOST_MARKERS : ABIE_RU_HTTPROUTE_HOST_MARKERS
710
+ const markers = ABIE_UA_HTTPROUTE_HOST_MARKERS
1168
711
  if (!markers.some(m => combined.includes(m))) {
1169
712
  return `HTTPRoute: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
1170
713
  }
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) {
714
+ if (!PATCH_PARENT_REF_NS_UA_RE.test(combined)) {
1174
715
  return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
1175
716
  }
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
717
  const sharedCount =
1184
718
  typeof sharedCrossNsBackendRefCount === 'number' && Number.isFinite(sharedCrossNsBackendRefCount)
1185
719
  ? Math.max(0, Math.floor(sharedCrossNsBackendRefCount))
@@ -1194,9 +728,9 @@ export function validateAbieNginxRunHttpRoutePatches(
1194
728
  }
1195
729
 
1196
730
  /**
1197
- * Чи **kustomization** містить валідні для abie **patch** для **HTTPRoute** з непорожнім **target.name** (**ua** або **ru**).
731
+ * Чи **kustomization** містить валідні для abie **patch** для **HTTPRoute** з непорожнім **target.name** (**ua**).
1198
732
  * @param {string} raw повний текст **kustomization.yaml**
1199
- * @param {'ua' | 'ru'} mode overlay
733
+ * @param {'ua'} mode overlay
1200
734
  * @returns {boolean} true, якщо **`validateAbieNginxRunHttpRoutePatches`** повертає **null**
1201
735
  */
1202
736
  export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
@@ -1231,85 +765,11 @@ export function validateAbieHcModeline(raw, relPath) {
1231
765
  return null
1232
766
  }
1233
767
 
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
768
  /**
1309
769
  * Перевіряє одну kustomization.yaml на nodeSelector patch для заданого overlay.
1310
770
  * @param {string} abs абсолютний шлях до файлу
1311
771
  * @param {string} rel відносний шлях (для повідомлень)
1312
- * @param {'ua' | 'ru'} mode параметр mode
772
+ * @param {'ua'} mode параметр mode
1313
773
  * @param {Set<string>} deploymentDirs директорії з Deployment (Set)
1314
774
  * @param {string} root корінь репозиторію
1315
775
  * @param {(msg: string) => void} fail callback при помилці
@@ -1330,8 +790,7 @@ async function checkNodeSelectorKustomization(abs, rel, mode, deploymentDirs, ro
1330
790
  return false
1331
791
  }
1332
792
  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)`)
793
+ fail(`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`)
1335
794
  return false
1336
795
  }
1337
796
  passFn(`${rel}: nodeSelector patch (${mode}) відповідає abie.mdc`)
@@ -1339,14 +798,14 @@ async function checkNodeSelectorKustomization(abs, rel, mode, deploymentDirs, ro
1339
798
  }
1340
799
 
1341
800
  /**
1342
- * Перевіряє наявність патчів nodeSelector для ua/ru overlay у k8s.
801
+ * Перевіряє наявність патчів nodeSelector для ua overlay у k8s.
1343
802
  * @param {string} root корінь репозиторію
1344
803
  * @param {string[]} yamlFilesAbs абсолютні шляхи yaml-файлів під k8s
1345
804
  * @param {Set<string>} deploymentDirs директорії з Deployment (Set)
1346
805
  * @param {(msg: string) => void} fail callback при помилці
1347
806
  * @param {(msg: string) => void} passFn callback при успішній перевірці
1348
807
  */
1349
- async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
808
+ async function ensureUaAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
1350
809
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
1351
810
  if (uaAbsList.length === 0) {
1352
811
  fail(
@@ -1359,26 +818,13 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
1359
818
  const ok = await checkNodeSelectorKustomization(abs, rel, 'ua', deploymentDirs, root, fail, passFn)
1360
819
  if (!ok) return
1361
820
  }
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
821
  }
1376
822
 
1377
823
  /**
1378
- * Перевіряє HTTPRoute patch для одного overlay (ua/ru).
824
+ * Перевіряє HTTPRoute patch для одного overlay (ua).
1379
825
  * @param {string} abs абсолютний шлях до kustomization.yaml
1380
826
  * @param {string} rel відносний шлях (для повідомлень)
1381
- * @param {'ua' | 'ru'} mode overlay
827
+ * @param {'ua'} mode overlay
1382
828
  * @param {string} root корінь репозиторію
1383
829
  * @param {string[]} yamlFilesAbs абсолютні шляхи yaml-файлів під k8s
1384
830
  * @param {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} cache кеш аналізу shared backend refs
@@ -1457,14 +903,14 @@ function ensureAbieBaseHttpRouteHostnames(root, yamlFilesAbs, fail, passFn) {
1457
903
  }
1458
904
 
1459
905
  /**
1460
- * Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
906
+ * Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
1461
907
  * лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
1462
908
  * @param {string} root корінь репозиторію
1463
909
  * @param {string[]} yamlFilesAbs yaml під k8s
1464
910
  * @param {(msg: string) => void} fail callback при помилці
1465
911
  * @param {(msg: string) => void} passFn callback при успішній перевірці
1466
912
  */
1467
- async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
913
+ async function ensureUaAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
1468
914
  /** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
1469
915
  const cache = new Map()
1470
916
 
@@ -1479,18 +925,6 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
1479
925
  const ok = await checkHttpRouteKustomization(abs, rel, 'ua', root, yamlFilesAbs, cache, fail, passFn)
1480
926
  if (!ok) return
1481
927
  }
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
928
  }
1495
929
 
1496
930
  /**
@@ -1553,17 +987,10 @@ function checkCleanMergedBranch(root, pass, fail) {
1553
987
  })
1554
988
  for (const v of violations) fail(v.message)
1555
989
  if (violations.length === 0) {
1556
- pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru (rego)')
990
+ pass('clean-merged-branch.yml: ignore_branches містить dev, ua (rego)')
1557
991
  }
1558
992
  }
1559
993
 
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
994
  /**
1568
995
  * Перевіряє hc.yaml у директоріях з Deployment. JS перевіряє modeline, далі
1569
996
  * один батч conftest для усіх знайдених hc.yaml — структурна валідація HCP
@@ -1613,287 +1040,9 @@ async function checkHcYamlFiles(root, deploymentDirs, pass, fail) {
1613
1040
  }
1614
1041
  }
1615
1042
 
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
1043
  /**
1895
1044
  * Збирає всі `*.env` файли в дереві (за виключенням `node_modules`, `.git` та інших службових каталогів),
1896
- * basename яких — abie env-файл (`dev.env` / `ua.env` / `ru.env` опційно з провідною крапкою). Файл `.env`
1045
+ * basename яких — abie env-файл (`dev.env` / `ua.env` опційно з провідною крапкою). Файл `.env`
1897
1046
  * без імені виключається — як і у `check-hasura.mjs`.
1898
1047
  * @param {string} root корінь репозиторію
1899
1048
  * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
@@ -1915,11 +1064,11 @@ async function collectAbieEnvFiles(root, ignorePaths) {
1915
1064
  }
1916
1065
 
1917
1066
  /**
1918
- * Сканує всі `*.env` файли abie (`.dev.env` / `.ua.env` / `.ru.env`) і для кожного знайденого
1067
+ * Сканує всі `*.env` файли abie (`.dev.env` / `.ua.env`) і для кожного знайденого
1919
1068
  * **внутрішньокластерного** URL (`http://<svc>.<ns>.svc.<dns>`) перевіряє, що DNS-суфікс і namespace-префікс
1920
1069
  * відповідають середовищу env-файла. Не лише `HASURA_GRAPHQL_ENDPOINT`, а й будь-який сервіс у env (KVCMS,
1921
1070
  * `auth-run-hl`, `file-link-hl` тощо) мусить мати кластер, що відповідає env: dev → `abie-dev.internal`,
1922
- * ua → `abie-ua.internal`, ru → `cluster.local`.
1071
+ * ua → `abie-ua.internal`.
1923
1072
  * @param {string} root корінь репозиторію
1924
1073
  * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
1925
1074
  * @param {(msg: string) => void} pass успішне повідомлення
@@ -1929,7 +1078,7 @@ async function collectAbieEnvFiles(root, ignorePaths) {
1929
1078
  async function ensureAbieEnvFilesMatchClusterDns(root, ignorePaths, pass, fail) {
1930
1079
  const envFiles = await collectAbieEnvFiles(root, ignorePaths)
1931
1080
  if (envFiles.length === 0) {
1932
- pass('Не знайдено dev.env / ua.env / ru.env у репозиторії — перевірку env→cluster DNS пропущено (abie.mdc)')
1081
+ pass('Не знайдено dev.env / ua.env у репозиторії — перевірку env→cluster DNS пропущено (abie.mdc)')
1933
1082
  return
1934
1083
  }
1935
1084
  for (const abs of envFiles) {
@@ -1987,26 +1136,17 @@ export async function check() {
1987
1136
  pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
1988
1137
  }
1989
1138
 
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
1139
  pass('Перевіряємо HTTPRoute spec.hostnames у …/k8s/base/… (aiml.live, abie.mdc)')
1997
1140
  await ensureAbieBaseHttpRouteHostnames(root, yamlFiles, fail, pass)
1998
1141
 
1999
1142
  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)
1143
+ pass('Є Deployment — перевіряємо nodeSelector у ua/kustomization (abie.mdc)')
1144
+ await ensureUaAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
1145
+ pass('Є Deployment — перевіряємо HTTPRoute у ua/kustomization (abie.mdc)')
1146
+ await ensureUaAbieHttpRoutePatches(root, yamlFiles, fail, pass)
2004
1147
  }
2005
1148
 
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)')
1149
+ pass('Перевіряємо env→cluster DNS у dev.env / ua.env (abie.mdc)')
2010
1150
  await ensureAbieEnvFilesMatchClusterDns(root, ignorePaths, pass, fail)
2011
1151
 
2012
1152
  return reporter.getExitCode()