@nitra/cursor 1.8.63 → 1.8.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mdc/abie.mdc CHANGED
@@ -1,20 +1,22 @@
1
1
  ---
2
- description: Правила для проєктів abinbevefes
2
+ description: Правила для проєктів AbInBev Efes
3
3
  alwaysApply: true
4
- version: '1.0'
4
+ version: '1.2'
5
5
  ---
6
6
 
7
- ## k8s
7
+ Правило **abie** для споживачів **@nitra/cursor**: **k8s** (**Deployment** + **HealthCheckPolicy**), overlay **ua** / **ru** (**nodeSelector**, видалення **HealthCheckPolicy** у **ru**) і гілки в **clean-merged-branch**. **`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
8
8
 
9
- Якщо в проекті є k8s deployment, рядом з ним повинен бути:
9
+ ## k8s: `hc.yaml` поруч із Deployment
10
+
11
+ Якщо під **`k8s`** є **`kind: Deployment`**, у **тій самій директорії** має бути **`hc.yaml`** з **HealthCheckPolicy** (**`networking.gke.io/v1`**): коректний modeline **`$schema`**, **`/healthz`**, порт **8080**, **`targetRef.name`** = **`metadata.name`**. Деталі — **`validateAbieHcYaml`** у **`npm/scripts/check-abie.mjs`**.
10
12
 
11
13
  ```yaml title="hc.yaml"
12
14
  # yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
13
15
  apiVersion: networking.gke.io/v1
14
16
  kind: HealthCheckPolicy
15
17
  metadata:
16
- name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
17
- namespace: dev # буде замінено через kustomize
18
+ name: СЕРВІС
19
+ namespace: dev # kustomize overlay
18
20
  spec:
19
21
  default:
20
22
  config:
@@ -25,34 +27,62 @@ spec:
25
27
  targetRef:
26
28
  group: ''
27
29
  kind: Service
28
- name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
30
+ name: СЕРВІС
29
31
  ```
30
32
 
31
- **Overlay `ru`:** у **`…/ru/kustomization.yaml`** під **`k8s`** додай **видалення** ресурсу HealthCheckPolicy для середовища, де політика не потрібна (підстав **реальне ім’я** замість `НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ`):
33
+ ## k8s: overlay **`ru`** і HealthCheckPolicy
34
+
35
+ Якщо в дереві **k8s** є **HealthCheckPolicy**, **check abie** вимагає **`ru/kustomization.yaml`** з patch **`$patch: delete`** для політики (узгоджено з **k8s.mdc** / **check-k8s**, **`ruKustomizationHasHealthCheckDeletePatch`** у **`npm/scripts/check-k8s.mjs`**). Підстав реальне ім’я замість **`СЕРВІС`**:
32
36
 
33
- ```yaml title="kustomization.yaml"
37
+ ```yaml title="…/ru/kustomization.yaml (фрагмент)"
34
38
  patches:
35
39
  - target:
36
40
  kind: HealthCheckPolicy
41
+ name: СЕРВІС
37
42
  patch: |-
38
43
  kind: HealthCheckPolicy
39
44
  metadata:
40
- name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
45
+ name: СЕРВІС
41
46
  $patch: delete
42
47
  ```
43
48
 
44
- ## branch
49
+ ## k8s: overlay **`ua`** / **`ru`** і nodeSelector
45
50
 
46
- В lean-merged-branch.yml список гілок, які повинні бути:
51
+ Якщо під **`k8s`** є **Deployment**, у **кожному** **`…/ua/kustomization.yaml`** та **`…/ru/kustomization.yaml`** має бути inline **JSON6902** у **`patches[].patch`** на **`target.kind: Deployment`** з **nodeSelector** за політикою abie (**ua** — **add** + **preem** false; **ru** — **replace** + **yandex.cloud/preemptible** false). Критерії збігу — **`kustomizationHasAbieDeploymentNodeSelectorPatch`** у **`npm/scripts/check-abie.mjs`**.
47
52
 
48
- ```yaml title=".github/workflows/clean-merged-branch.yml"
49
- ignore_branches: dev,ua,ru
53
+ ```yaml title="…/ua/kustomization.yaml (фрагмент)"
54
+ patches:
55
+ - target:
56
+ kind: Deployment
57
+ name: my-app
58
+ patch: |-
59
+ - op: add
60
+ path: /spec/template/spec/nodeSelector
61
+ value:
62
+ preem: 'false'
50
63
  ```
51
64
 
52
- ## Перевірка
65
+ ```yaml title="…/ru/kustomization.yaml (фрагмент)"
66
+ patches:
67
+ - target:
68
+ kind: Deployment
69
+ name: my-app
70
+ patch: |-
71
+ - op: replace
72
+ path: /spec/template/spec/nodeSelector
73
+ value:
74
+ yandex.cloud/preemptible: "false"
75
+ ```
76
+
77
+ ## Git branches
53
78
 
54
- `npx @nitra/cursor check abie`
79
+ У **`.github/workflows/clean-merged-branch.yml`** у кроці **`phpdocker-io/github-actions-delete-abandoned-branches`** значення **`with.ignore_branches`** має містити **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно), наприклад:
55
80
 
56
- Повна матриця полів **`hc.yaml`**, **`ignore_branches`** і умов для **`ru/kustomization.yaml`** — у **`npm/scripts/check-abie.mjs`**.
81
+ ```yaml title=".github/workflows/clean-merged-branch.yml (фрагмент)"
82
+ with:
83
+ ignore_branches: main,dev,ua,ru
84
+ ```
85
+
86
+ ## Перевірка
57
87
 
58
- Програмна перевірка (**`check-abie.mjs`**) виконується лише якщо у **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень (щоб не вимагати **ua**/**ru** у репозиторіях без цього правила).
88
+ **`npx @nitra/cursor check abie`**
package/mdc/k8s.mdc CHANGED
@@ -42,7 +42,7 @@ alwaysApply: false
42
42
  }
43
43
  ```
44
44
 
45
- Якщо правило **`k8s`** підключено в **`.n-cursor.json`** (масив **`rules`**), у **кореневому** `package.json` **обов'язково** мають бути скрипт **`lint-k8s`** і виклик **`bun run lint-k8s`** у агрегованому **`lint`** (див. **`bun.mdc`**). Це перевіряє **`npx @nitra/cursor check bun`**.
45
+ Якщо правило **`k8s`** підключено в **`.n-cursor.json`** (масив **`rules`**), у **кореневому** `package.json` **мають** бути скрипт **`lint-k8s`** і виклик **`bun run lint-k8s`** у агрегованому **`lint`** (див. **`bun.mdc`**). Це перевіряє **`npx @nitra/cursor check bun`**.
46
46
 
47
47
  Шлях до скрипта підстав свій (`./scripts/…` після копіювання, `node_modules/@nitra/cursor/scripts/…` якщо пакет у залежностях).
48
48
 
@@ -96,34 +96,25 @@ jobs:
96
96
 
97
97
  ## Deployment: `resources`
98
98
 
99
- Для **`kind: Deployment`** у кожному контейнері **`spec.template.spec.containers[]`** має бути явне поле **`resources`**. Якщо ліміти та requests ще не задані, додай порожній об'єкт:
99
+ У **`Deployment`** у кожному **`containers`** / **`initContainers`** має бути **`resources`**; якщо лімітів ще немає мінімум порожній запис:
100
100
 
101
101
  ```yaml
102
102
  resources: {}
103
103
  ```
104
104
 
105
- Так маніфест явно резервує місце під **`requests` / `limits`** і уникає випадкового пропуску секції. **`check k8s`** перевіряє це для кожного YAML-документа **`Deployment`** у файлах під **`k8s`**.
105
+ **`check k8s`** перевіряє наявність **`resources`** у кожному документі **Deployment** під **`k8s`**. Поле **`imagePullPolicy`** скрипт **не** перевіряє (залишається політиці Kubernetes за тегом образу).
106
106
 
107
- **`imagePullPolicy`:** у маніфестах не вимагається; Kubernetes за замовчуванням: образ **без тега** або з **`:latest`** **Always**, з іншим тегом **IfNotPresent**. **`check k8s`** це поле не перевіряє.
108
-
109
- Якщо в **`Deployment`** у **`spec.template.spec.containers`** або **`initContainers`** задано образ **`hasura/graphql-engine`**, у полі **`image`** має бути саме **`hasura/graphql-engine:v2.48.15.ubi.amd64`** (для еквівалента Docker Hub допускається префікс **`docker.io/`**). Інші теги або сторонні реєстри з тим самим репозиторієм **`hasura/graphql-engine`** — порушення **`check k8s`**.
107
+ Образ **`hasura/graphql-engine`**: дозволений лише канонічний тег із константи **`HASURA_GRAPHQL_ENGINE_IMAGE`** у **`check-k8s.mjs`** (допускається префікс **`docker.io/`**); решта помилка **check k8s**.
110
108
 
111
109
  ## Service: заборонені анотації GKE
112
110
 
113
- У **`kind: Service`** не використовуй у **`metadata.annotations`** ключі:
114
-
115
- - **`cloud.google.com/neg`**
116
- - **`cloud.google.com/backend-config`**
117
-
118
- Вони потрібні для інтеграції з **Ingress** / класичним балансуванням GKE; після переходу на **Gateway API** їх слід **прибрати** з маніфестів. **`check k8s`** завершиться помилкою, якщо хоча б один із цих ключів присутній.
111
+ У **`kind: Service`** не додавай у **`metadata.annotations`** **`cloud.google.com/neg`** і **`cloud.google.com/backend-config`** (legacy під Ingress / старе балансування GKE). **check k8s** падає, якщо ключ є.
119
112
 
120
113
  ## Service: `svc.yaml` і `svc-hl.yaml` (Gateway API)
121
114
 
122
- - У тому самому каталозі, де лежить **`svc.yaml`**, обов’язково має бути **`svc-hl.yaml`**. Якщо файлу ще немає створи його як копію **`svc.yaml`**: для кожного **Service** задай **`metadata.name`** з суфіксом **`-hl`**, **`spec.clusterIP: None`** (headless), збережи селектор і порти як у кластерного сервісу; **`check k8s`** не генерує файли автоматично.
123
- - У **`svc.yaml`** кожен **Service** має мати **`spec.type: ClusterIP`**.
124
- - У **`svc-hl.yaml`** кожен **Service** має мати **`spec.clusterIP: None`** та **`metadata.name`**, що закінчується на **`-hl`**, узгоджено з відповідним ім’ям у **`svc.yaml`** (наприклад **`app`** **`app-hl`**).
125
- - У маршрутах **Gateway API** з групи **`gateway.networking.k8s.io`** (**`HTTPRoute`**, **`GRPCRoute`**, **`TCPRoute`**, **`TLSRoute`**, **`UDPRoute`**) посилання **`backendRefs`** / **`backendRef`** на **Service** мають вказувати лише сервіси з цього headless-файлу (ім’я з суфіксом **`-hl`**).
126
- - Якщо **`svc.yaml`** підключено через **`kustomization.yaml`** (**`resources`**, **`bases`**, **`components`**, **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**), у **тому ж** **`kustomization.yaml`** додай посилання на відповідний **`svc-hl.yaml`** (той самий відносний префікс каталогу, що й для **`svc.yaml`**).
115
+ Пара файлів для кластерного й headless **Service**: **`svc.yaml`** + **`svc-hl.yaml`** в одному каталозі, **`spec.type: ClusterIP`** / **`clusterIP: None`**, імена **`-hl`**, узгодженість пар, маршрути **`gateway.networking.k8s.io`** (**HTTPRoute**, **GRPCRoute**, **TCPRoute**, **TLSRoute**, **UDPRoute**) **backendRef** лише на сервіси з суфіксом **`-hl`**; якщо **kustomization** посилається на **`svc.yaml`**, у **тому ж** **`kustomization.yaml`** має бути посилання на sibling **`svc-hl.yaml`**. Скрипт **не** створює файли — додай **`svc-hl.yaml`** вручну (копія з правками **name** / **clusterIP**).
116
+
117
+ **Точні умови та повідомлення `fail`** верхній JSDoc **`npm/scripts/check-k8s.mjs`**.
127
118
 
128
119
  ## Kustomize: структура каталогів (`base` / overlays)
129
120
 
@@ -142,11 +133,11 @@ resources: {}
142
133
 
143
134
  ### Namespace
144
135
 
145
- - **`base/kustomization.yaml`:** у цьому файлі поле **`namespace:`** **завжди** має бути присутнє й **непорожнє**, щоб Kustomize застосував один цільовий namespace до ресурсів **base**. **`check k8s`** перевіряє це, коли файл є в репозиторії.
136
+ - **`base/kustomization.yaml`:** поле **`namespace:`** має бути **непорожнім** (перевіряє **check k8s**, якщо файл є).
146
137
 
147
- - **Де не дублювати `metadata.namespace`:** у YAML під **`k8s`**, чиї шляхи **досяжні** з будь-якого **`kustomization.yaml`** через **`resources`**, **`bases`**, **`components`**, **`crds`**, елементи **`patches`** з полем **`path`**, **`patchesStrategicMerge`** (транзитивно, зокрема через каталог із дочірнім **`kustomization.yaml`**). У таких файлах **не** вказуй **`metadata.namespace`** — його виставляє Kustomize за полем **`namespace:`** у kustomization.
138
+ - **Де не дублювати `metadata.namespace`:** у YAML, досяжних через **граф Kustomize** (шляхи з **`kustomization.yaml`**, як у логіці **`collectKustomizeManagedRelPaths`** / **check k8s**). **Namespace** задає **`namespace:`** у kustomization.
148
139
 
149
- - **Коли `metadata.namespace` обов’язковий:** YAML під **`k8s`**, який **ніде** не перелічений у згаданих полях жодного kustomization-файлу, має містити **непорожній** **`metadata.namespace`** у кожному документі з **`apiVersion`** та **`kind`**, **окрім** кластерних ресурсів (наприклад **`Namespace`**, **`ClusterRole`**, **`PersistentVolume`** — повний перелік у **`check-k8s.mjs`**, **`CLUSTER_SCOPED_KINDS`**). Якщо файл має бути без namespace у маніфесті — підключи його до kustomization через **`resources`** / **`patches`** тощо.
140
+ - **Коли `metadata.namespace` обов’язковий у файлі:** YAML під **`k8s`**, який **не** в графі жодного kustomization непорожній **`metadata.namespace`** для namespaced **kind** (винятки кластерні **kind**, перелік **`CLUSTER_SCOPED_KINDS`** у **`check-k8s.mjs`**). Якщо namespace у маніфесті не потрібен — підключи файл через **`resources`** / **`patches`** тощо.
150
141
 
151
142
  - **Не додавай** окремі **patches** Kustomize, які лише змінюють **namespace**: **namespace** визначає Kustomize; у overlays додаткові зміни — без дублювання логіки **namespace**.
152
143
 
@@ -182,27 +173,56 @@ resources: {}
182
173
 
183
174
  **`check k8s`:** заборонено **`kind: Ingress`**.
184
175
 
185
- ## Перевірка
176
+ 3. Якщо **HTTPRoute** посилається на **Service** в іншому **namespace**, потрібен **ReferenceGrant** (дозвіл). Наприклад **HTTPRoute** в **`contract`** і **Service** в **`dev`** — додай маніфест на кшталт **`base/rg.yaml`** (фрагмент нижче).
177
+
178
+ ```yaml title="base/rg.yaml"
179
+ # yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/gateway.networking.k8s.io/referencegrant_v1beta1.json
180
+ apiVersion: gateway.networking.k8s.io/v1beta1
181
+ kind: ReferenceGrant
182
+ metadata:
183
+ name: contract-to-dev
184
+ namespace: dev
185
+ spec:
186
+ from:
187
+ - group: gateway.networking.k8s.io
188
+ kind: HTTPRoute
189
+ namespace: contract
190
+ to:
191
+ - group: ''
192
+ kind: Service
193
+ # Якщо name не вказано, доступ дозволено до всіх Service в цьому namespace
194
+ ```
195
+
196
+ **ReferenceGrant** має бути в **namespace** тих **Service**, до яких відкривається доступ (у прикладі — **`dev`**). Якщо **`namespace:`** у **overlay** не збігається з **namespace** гранта, **не** додавай **`base/rg.yaml`** у **`resources:`** того overlay — Kustomize може перезаписати **`metadata.namespace`** гранта. Натомість застосуй **`patches`** (JSON patch), щоб явно виставити потрібний **namespace**:
197
+
198
+ ```yaml title="overlay/kustomization.yaml (фрагмент)"
199
+ patches:
200
+ - target:
201
+ kind: ReferenceGrant
202
+ name: contract-to-dev
203
+ patch: |-
204
+ - op: replace
205
+ path: /metadata/namespace
206
+ value: dev
207
+ ```
186
208
 
187
- **`npx @nitra/cursor check k8s`** — деталі умов у **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**; канон URL **`$schema`** для редактору — розділ **«Визначення схеми YAML»** нижче. Якщо під `k8s` немає `*.yaml` — перевірку пропущено. Решту політик кластера / compliance закриває **`lint-k8s`**.
209
+ ## Перевірка
188
210
 
189
- Після змін у маніфестах: **`bun run lint-k8s`** (kubeconform + kubescape).
211
+ **`npx @nitra/cursor check k8s`** програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
190
212
 
191
- ## Що закодовано в `check-k8s.mjs`
213
+ **Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
192
214
 
193
- Не дублюй тут повний перелік — він у **верхньому JSDoc** **`npm/scripts/check-k8s.mjs`** і в константах (**`YANNH_PIN`**, **`YANNH_GROUPS`**, **`EXPLICIT_K8S_SCHEMAS`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо).
215
+ ## Що саме в скрипті `check-k8s.mjs`
194
216
 
195
- Коротко: **`$schema`** (один modeline, без **`.yml`** під **`k8s`**), заборона **Ingress**, **Deployment.resources**, пін **hasura/graphql-engine**, **Service** без анотацій NEG/backend-config, пари **`svc.yaml`** / **`svc-hl.yaml`** (**ClusterIP** / headless **`-hl`**) і їхні записи в **`kustomization.yaml`**, **Gateway API** маршрути **backendRef** лише на **`-hl`**, **`metadata.namespace`** залежно від **base** і графа Kustomize, заборона **`…/k8s/dev/…`**, непорожній **`namespace:`** у **`k8s/base/kustomization.yaml`**, видалення файлів, де всі документи — лише **BackendConfig** (змішані файли — помилка).
217
+ Повний перелік умов, константи (**`YANNH_PIN`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо) і допоміжні функції у файлі скрипта; змінив вимогу для **check** онови **JSDoc** і за потреби тести в **`npm/tests/check-k8s-schema.test.mjs`**.
196
218
 
197
- При зміні **PIN** версії Kubernetes оновлюй узгоджено **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і розділи **lint-k8s** / **Визначення схеми YAML** у цьому файлі (**`YANNH_REF`**, **`YANNH_GROUPS`**, **`DATREE_CRD_BASE`** — за потреби в скрипті).
219
+ При зміні **PIN** версії Kubernetes узгодь **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і цей файл (**lint-k8s**, **Визначення схеми YAML**).
198
220
 
199
221
  ## Коли застосовувати (агентам)
200
222
 
201
- - Зміни в k8s YAML після правок **`check k8s`**.
202
- - Якщо перший рядок уже коректний і URL відповідає `apiVersion` / `kind`не дублюй; змінився ресурс онови лише `$schema`.
203
- - У **`Deployment`** без поля **`resources`** у контейнері додай **`resources: {}`** (див. розділ **Deployment: `resources`**). **`imagePullPolicy`** за потреби не дублюй — достатньо політики Kubernetes за тегом образу.
204
- - Дотримуйся структури **Kustomize** (`base` = dev, overlays без дублювання `base`, коментарі для рядків, що змінюються в overlay). У **`base/kustomization.yaml`** завжди задавай непорожній **`namespace:`**. У **`k8s/base`** у кожному ресурсному YAML має бути явний **`metadata.namespace`**. Поза **base**, якщо не хочеш **`metadata.namespace`** у файлі — підключи його до kustomization (**`resources`** / **`patches`** тощо); інакше додай явний **`metadata.namespace`**.
205
- - Після міграції на нову структуру **видали** застарілі файли та каталоги, які вже замінені (див. **Міграція зі старої структури**).
223
+ - Після змін у k8s YAML: **`npx @nitra/cursor check k8s`** і за наявності правила — **`bun run lint-k8s`**.
224
+ - Оновив **`apiVersion` / `kind`**підправ **перший** рядок **`$schema`** (див. **Визначення схеми YAML**).
225
+ - Дотримуйся **Kustomize** з цього правила; деталі **namespace** / графа ресурсів **check k8s** + підказки в JSDoc скрипта.
206
226
 
207
227
  ## Визначення схеми YAML (канон)
208
228
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.63",
3
+ "version": "1.8.69",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Перевіряє відповідність проєкту правилу abie.mdc (проєкти abinbevefes).
2
+ * Перевіряє відповідність проєкту правилу abie.mdc (проєкти AbInBev Efes).
3
3
  *
4
4
  * Застосовується лише якщо у **`.n-cursor.json`** у масиві **`rules`** є **`abie`** — інакше вихід **0**
5
5
  * без перевірок (щоб не суперечити типовому **ga.mdc** з **`ignore_branches: main,dev`**).
@@ -12,7 +12,11 @@
12
12
  * має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
13
13
  * як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **Service** з тим самим **`metadata.name`**.
14
14
  * Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
15
- * (узгоджено з **k8s.mdc** / **check-k8s.mjs**).
15
+ * (логіка вмісту **`ruKustomizationHasHealthCheckDeletePatch`** у **check-k8s.mjs**, узгоджено з **k8s.mdc**).
16
+ *
17
+ * **nodeSelector:** якщо є **Deployment** під **k8s**, у кожному **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`**
18
+ * має бути inline **JSON6902** patch на **`kind: Deployment`**: для **ua** — **`op: add`**, **`path: /spec/template/spec/nodeSelector`**,
19
+ * **`preem: false`**; для **ru** — **`op: replace`**, той самий **path**, **`yandex.cloud/preemptible: false`** (див. abie.mdc).
16
20
  */
17
21
  import { existsSync } from 'node:fs'
18
22
  import { readFile } from 'node:fs/promises'
@@ -20,7 +24,7 @@ import { dirname, join, relative } from 'node:path'
20
24
 
21
25
  import { parseAllDocuments } from 'yaml'
22
26
 
23
- import { isRuKustomizationPath, pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
27
+ import { pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
24
28
  import { pass } from './utils/pass.mjs'
25
29
  import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
26
30
  import { walkDir } from './utils/walkDir.mjs'
@@ -35,6 +39,26 @@ const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
35
39
  /** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
36
40
  export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
37
41
 
42
+ /**
43
+ * Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім’ям файлу) — специфіка abie overlay.
44
+ * @param {string} rel шлях від кореня репозиторію
45
+ * @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
46
+ */
47
+ export function isRuKustomizationPath(rel) {
48
+ const norm = rel.replaceAll('\\', '/')
49
+ return /(^|\/)ru\/kustomization\.yaml$/u.test(norm)
50
+ }
51
+
52
+ /**
53
+ * Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед ім’ям файлу) — специфіка abie overlay.
54
+ * @param {string} rel шлях від кореня репозиторію
55
+ * @returns {boolean} true, якщо це `…/ua/kustomization.yaml`
56
+ */
57
+ export function isUaKustomizationPath(rel) {
58
+ const norm = rel.replaceAll('\\', '/')
59
+ return /(^|\/)ua\/kustomization\.yaml$/u.test(norm)
60
+ }
61
+
38
62
  /**
39
63
  * Чи увімкнено правило **abie** у конфігу репозиторію.
40
64
  * @param {string} root корінь репозиторію (cwd)
@@ -198,6 +222,136 @@ function stripBom(s) {
198
222
  return s.startsWith('\uFEFF') ? s.slice(1) : s
199
223
  }
200
224
 
225
+ /**
226
+ * Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**op: add**, **preem: false**).
227
+ * @param {string} patchText поле **patch** у kustomization
228
+ * @returns {boolean} true, якщо критерії abie.mdc виконано
229
+ */
230
+ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
231
+ if (typeof patchText !== 'string' || patchText.trim() === '') {
232
+ return false
233
+ }
234
+ if (!/op:\s*add\b/u.test(patchText)) {
235
+ return false
236
+ }
237
+ if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
238
+ return false
239
+ }
240
+ if (!/\bpreem:\s*['"]?false['"]?\b/u.test(patchText)) {
241
+ return false
242
+ }
243
+ return true
244
+ }
245
+
246
+ /**
247
+ * Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**op: replace**, **yandex.cloud/preemptible: false**).
248
+ * @param {string} patchText поле **patch** у kustomization
249
+ * @returns {boolean} true, якщо критерії abie.mdc виконано
250
+ */
251
+ function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
252
+ if (typeof patchText !== 'string' || patchText.trim() === '') {
253
+ return false
254
+ }
255
+ if (!/op:\s*replace\b/u.test(patchText)) {
256
+ return false
257
+ }
258
+ if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
259
+ return false
260
+ }
261
+ if (!/yandex\.cloud\/preemptible:\s*['"]?false['"]?/u.test(patchText)) {
262
+ return false
263
+ }
264
+ return true
265
+ }
266
+
267
+ /**
268
+ * Чи один елемент **patches** у kustomization відповідає abie nodeSelector для заданого **mode**.
269
+ * @param {unknown} p елемент масиву **patches**
270
+ * @param {'ua' | 'ru'} mode який overlay перевіряти
271
+ * @returns {boolean} true, якщо patch відповідає abie для **mode**
272
+ */
273
+ function inlineKustomizationPatchMatchesAbieMode(p, mode) {
274
+ if (p === null || typeof p !== 'object' || Array.isArray(p)) {
275
+ return false
276
+ }
277
+ const pr = /** @type {Record<string, unknown>} */ (p)
278
+ const target = pr.target
279
+ if (target === null || typeof target !== 'object' || Array.isArray(target)) {
280
+ return false
281
+ }
282
+ const tg = /** @type {Record<string, unknown>} */ (target)
283
+ if (tg.kind !== 'Deployment') {
284
+ return false
285
+ }
286
+ const patchStr = pr.patch
287
+ if (typeof patchStr !== 'string') {
288
+ return false
289
+ }
290
+ if (mode === 'ua' && jsonPatchTextHasUaDeploymentNodeSelector(patchStr)) {
291
+ return true
292
+ }
293
+ if (mode === 'ru' && jsonPatchTextHasRuDeploymentNodeSelector(patchStr)) {
294
+ return true
295
+ }
296
+ return false
297
+ }
298
+
299
+ /**
300
+ * Чи один YAML-документ kustomization містить відповідний inline patch на Deployment.
301
+ * @param {import('yaml').Document} doc документ після **parseAllDocuments**
302
+ * @param {'ua' | 'ru'} mode який overlay перевіряти
303
+ * @returns {boolean} true, якщо знайдено відповідний patch
304
+ */
305
+ function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
306
+ if (doc.errors.length > 0) {
307
+ return false
308
+ }
309
+ const root = doc.toJSON()
310
+ if (root === null || typeof root !== 'object' || Array.isArray(root)) {
311
+ return false
312
+ }
313
+ const rec = /** @type {Record<string, unknown>} */ (root)
314
+ if (rec.kind !== 'Kustomization') {
315
+ return false
316
+ }
317
+ const patches = rec.patches
318
+ if (!Array.isArray(patches)) {
319
+ return false
320
+ }
321
+ for (const p of patches) {
322
+ if (inlineKustomizationPatchMatchesAbieMode(p, mode)) {
323
+ return true
324
+ }
325
+ }
326
+ return false
327
+ }
328
+
329
+ /**
330
+ * Чи **kustomization.yaml** містить inline **patches** на **Deployment** з nodeSelector за abie.mdc (**ua** або **ru**).
331
+ * @param {string} raw повний текст файлу
332
+ * @param {'ua' | 'ru'} mode який overlay перевіряти
333
+ * @returns {boolean} true, якщо знайдено відповідний patch
334
+ */
335
+ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
336
+ const body = stripBom(raw)
337
+ const lines = body.split(/\r?\n/u)
338
+ const first = lines[0] ?? ''
339
+ const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
340
+ /** @type {import('yaml').Document[]} */
341
+ let docs
342
+ try {
343
+ docs = parseAllDocuments(rest)
344
+ } catch {
345
+ return false
346
+ }
347
+ for (const doc of docs) {
348
+ if (kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode)) {
349
+ return true
350
+ }
351
+ }
352
+ return false
353
+ }
354
+
201
355
  /**
202
356
  * Перевіряє **hc.yaml** на відповідність abie.mdc.
203
357
  * @param {string} raw повний текст файлу
@@ -341,7 +495,7 @@ async function collectHealthCheckPolicyRelPaths(root, yamlAbs) {
341
495
  }
342
496
 
343
497
  /**
344
- * Якщо є **HealthCheckPolicy**, вимагає **ru/kustomization.yaml** з patch видалення (як **check-k8s**).
498
+ * Якщо є **HealthCheckPolicy**, вимагає **ru/kustomization.yaml** з patch видалення (**ruKustomizationHasHealthCheckDeletePatch** у **check-k8s**).
345
499
  * @param {string} root корінь
346
500
  * @param {string[]} yamlFilesAbs абсолютні шляхи yaml k8s
347
501
  * @param {string[]} healthCheckPolicyRelativePaths відносні шляхи
@@ -377,6 +531,67 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
377
531
  )
378
532
  }
379
533
 
534
+ /**
535
+ * Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** (**kustomization.yaml**) JSON6902 patch nodeSelector (abie.mdc).
536
+ * @param {string} root корінь репозиторію
537
+ * @param {string[]} yamlFilesAbs yaml під k8s
538
+ * @param {(msg: string) => void} fail callback
539
+ * @returns {Promise<void>}
540
+ */
541
+ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
542
+ const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
543
+ if (uaAbsList.length === 0) {
544
+ fail(
545
+ 'Є Deployment у k8s — додай ua/kustomization.yaml з inline patch на Deployment: op add, path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
546
+ )
547
+ return
548
+ }
549
+ for (const abs of uaAbsList) {
550
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
551
+ let raw
552
+ try {
553
+ raw = await readFile(abs, 'utf8')
554
+ } catch (error) {
555
+ const msg = error instanceof Error ? error.message : String(error)
556
+ fail(`${rel}: не вдалося прочитати (${msg})`)
557
+ return
558
+ }
559
+ if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
560
+ fail(
561
+ `${rel}: потрібен patch target kind Deployment з op: add, path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
562
+ )
563
+ return
564
+ }
565
+ pass(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
566
+ }
567
+
568
+ const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
569
+ if (ruAbsList.length === 0) {
570
+ fail(
571
+ 'Є Deployment у k8s — додай ru/kustomization.yaml з inline patch на Deployment: op replace, path /spec/template/spec/nodeSelector, yandex.cloud/preemptible false (abie.mdc)'
572
+ )
573
+ return
574
+ }
575
+ for (const abs of ruAbsList) {
576
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
577
+ let raw
578
+ try {
579
+ raw = await readFile(abs, 'utf8')
580
+ } catch (error) {
581
+ const msg = error instanceof Error ? error.message : String(error)
582
+ fail(`${rel}: не вдалося прочитати (${msg})`)
583
+ return
584
+ }
585
+ if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
586
+ fail(
587
+ `${rel}: потрібен patch target kind Deployment з op: replace, path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
588
+ )
589
+ return
590
+ }
591
+ pass(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
592
+ }
593
+ }
594
+
380
595
  /**
381
596
  * Перевіряє відповідність проєкту правилам abie.mdc.
382
597
  * @returns {Promise<number>} 0 — OK, 1 — є порушення
@@ -464,5 +679,10 @@ export async function check() {
464
679
  const healthCheckPolicyRelativePaths = await collectHealthCheckPolicyRelPaths(root, yamlFiles)
465
680
  await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyRelativePaths, fail)
466
681
 
682
+ if (deploymentDirs.size > 0) {
683
+ pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
684
+ await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail)
685
+ }
686
+
467
687
  return exitCode
468
688
  }
@@ -6,9 +6,9 @@
6
6
  * (datree за замовчуванням: GitHub Pages `https://datreeio.github.io/CRDs-catalog/…`).
7
7
  *
8
8
  * Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
9
- * **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення — об'єкт, допускається
9
+ * **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення — об’єкт, допускається
10
10
  * порожній **`{}`**). Поле **`imagePullPolicy`** не перевіряється — діють типові правила Kubernetes
11
- * (`:latest` або без тега → **Always**, інші теги → **IfNotPresent**). Якщо серед **`containers`** /
11
+ * (`:latest` або коли тег не вказано → **Always**, інші теги → **IfNotPresent**). Якщо серед **`containers`** /
12
12
  * **`initContainers`** є образ **`hasura/graphql-engine`**, дозволено лише пін **`HASURA_GRAPHQL_ENGINE_IMAGE`**
13
13
  * (див. k8s.mdc).
14
14
  *
@@ -327,7 +327,7 @@ export function kustomizationSvcYamlMissingSvcHlViolation(kustomizationDir, path
327
327
  if (basename(abs).toLowerCase() === 'svc.yaml') {
328
328
  const hlAbs = resolve(dirname(abs), 'svc-hl.yaml')
329
329
  if (!resolved.has(hlAbs)) {
330
- return `kustomization посилається на «${ref}» — додай у тому ж kustomization.yaml посилання на відповідний svc-hl.yaml (очікуваний шлях поруч, напр. той самий префікс каталогу + svc-hl.yaml; див. k8s.mdc)`
330
+ return `kustomization посилається на «${ref}» — додай у тому ж kustomization.yaml посилання на відповідний svc-hl.yaml (очікуваний шлях поруч, наприклад той самий префікс каталогу + svc-hl.yaml; див. k8s.mdc)`
331
331
  }
332
332
  }
333
333
  }
@@ -627,9 +627,9 @@ async function readK8sYamlBodyAfterModelineForSvcPair(abs) {
627
627
  }
628
628
 
629
629
  /**
630
- * Розбирає YAML на корені документів-об’єктів (ігнорує зламані документи).
630
+ * Розбирає YAML на корені документів (ігнорує зламані документи).
631
631
  * @param {string} body фрагмент YAML
632
- * @returns {unknown[]} масив об’єктів-документів
632
+ * @returns {unknown[]} масив успішно розібраних коренів YAML-документів
633
633
  */
634
634
  function parseK8sYamlDocumentObjectRoots(body) {
635
635
  try {
@@ -666,16 +666,6 @@ function extractApiVersionAndKind(doc) {
666
666
  }
667
667
  }
668
668
 
669
- /**
670
- * Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім’ям файлу).
671
- * @param {string} rel шлях від кореня репозиторію
672
- * @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
673
- */
674
- export function isRuKustomizationPath(rel) {
675
- const norm = rel.replaceAll('\\', '/')
676
- return /(^|\/)ru\/kustomization\.yaml$/u.test(norm)
677
- }
678
-
679
669
  /**
680
670
  * Чи вміст overlay **`ru/kustomization.yaml`** містить Kustomize patch видалення **HealthCheckPolicy**.
681
671
  * @param {string} raw повний текст файлу
@@ -722,7 +712,7 @@ function scanIngressInYamlDocuments(rel, body, fail) {
722
712
 
723
713
  /**
724
714
  * Чи порушує маніфест вимогу **`Deployment.spec.template.spec.containers[].resources`** (див. k8s.mdc).
725
- * @param {unknown} manifest корінь YAML-документа як об'єкт JavaScript
715
+ * @param {unknown} manifest корінь YAML-документа як запис JavaScript
726
716
  * @returns {string | null} текст порушення для `fail` або null, якщо перевірка не застосовується / ок
727
717
  */
728
718
  export function deploymentResourcesViolation(manifest) {
@@ -752,7 +742,7 @@ export function deploymentResourcesViolation(manifest) {
752
742
  }
753
743
  const r = cont.resources
754
744
  if (r === null || typeof r !== 'object' || Array.isArray(r)) {
755
- return `контейнер "${label}": resources має бути об'єктом (наприклад порожній об'єкт у YAML: resources: {})`
745
+ return `контейнер "${label}": resources має бути записом у YAML (наприклад порожній: resources: {})`
756
746
  }
757
747
  }
758
748
  }
@@ -761,7 +751,7 @@ export function deploymentResourcesViolation(manifest) {
761
751
  }
762
752
 
763
753
  /**
764
- * Прибирає digest з посилання на образ (`@sha256:…`) для порівняння тега.
754
+ * Прибирає digest з посилання на образ (`@sha256:…`) для порівняння тегу образу.
765
755
  * @param {string} image значення поля `image`
766
756
  * @returns {string} той самий рядок без суфікса `@…` (digest), з `.trim()`
767
757
  */
@@ -771,7 +761,7 @@ function stripImageDigest(image) {
771
761
  }
772
762
 
773
763
  /**
774
- * Чи рядок `image` вказує на репозиторій **hasura/graphql-engine** (будь-який тег / без тега).
764
+ * Чи рядок `image` вказує на репозиторій **hasura/graphql-engine** (будь-який тег / без вказаного тегу).
775
765
  * @param {string} image значення поля `image`
776
766
  * @returns {boolean} true, якщо шлях образу закінчується на `hasura/graphql-engine` з тегом або без
777
767
  */
@@ -808,7 +798,7 @@ function hasuraGraphqlEngineViolationInContainerList(list, containers) {
808
798
  }
809
799
 
810
800
  /**
811
- * Чи порушує **Deployment** вимогу пінованого образу **hasura/graphql-engine** (k8s.mdc).
801
+ * Чи порушує **Deployment** вимогу щодо зафіксованого образу **hasura/graphql-engine** (k8s.mdc).
812
802
  * @param {unknown} manifest корінь YAML-документа
813
803
  * @returns {string | null} текст порушення або null, якщо не Deployment / образу немає / ок
814
804
  */