@nitra/cursor 1.8.217 → 1.8.219
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/CHANGELOG.md +24 -1
- package/bin/auto-skills.md +2 -0
- package/mdc/k8s.mdc +36 -32
- package/package.json +1 -1
- package/scripts/auto-skills.mjs +3 -3
- package/scripts/check-k8s.mjs +208 -152
- package/skills/llm-patch/SKILL.md +198 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.8.219] - 2026-05-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **skill `n-llm-patch`:** наповнено `npm/skills/llm-patch/SKILL.md` (та дзеркальну копію `.cursor/skills/n-llm-patch/SKILL.md`). Скіл готує самодостатній текстовий промпт для іншого Claude/Cursor-агента у цільовому проєкті: read-only аналіз CWD (`package.json`, `tree -L 2`, `README`, релевантні конфіги), формування єдиного markdown-блоку за шаблоном `Завдання → Контекст → Релевантні файли → Що треба зробити → Обмеження → Як перевірити`. Цільова LLM — Claude / Cursor agent; жодних змін у поточному репо, тимчасові артефакти — лише у `/tmp`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **k8s / check-k8s:** канонічна структура HPA/PDB — через **Kustomize Component** з фіксованою назвою каталогу `components/` (sibling до `base/`). У `base/` HPA і PDB не існує: ні локальних `hpa.yaml` / `pdb.yaml`, ні через `resources` / `components`. Overlays підключають `components: [- ../components]` і додають JSON6902-патчі для прод-значень `/spec/minReplicas`, `/spec/maxReplicas`, `/spec/minAvailable`. Для кожного `Deployment` у `…/k8s/…/base/` тепер вимагається sibling каталог `…/k8s/…/components/` з валідним `kustomization.yaml` (`kind: Component`), `hpa.yaml` (dev-like `min=max=1`) і `pdb.yaml` (dev-like `minAvailable=0`).
|
|
16
|
+
- **k8s / check-k8s:** заборона локальних `base/hpa.yaml` і `base/pdb.yaml` (file-existence для обох). Якщо в дереві base-kustomize лишилися HPA або PDB через `resources` / `components` — fail (`HPA/PDB заборонені у base — переведіть у components/`).
|
|
17
|
+
- **k8s / check-k8s:** прод-overlay тригерить вимоги патчів `/spec/minReplicas`, `/spec/maxReplicas` (HPA), `/spec/minAvailable` (PDB), коли overlay-tree містить HPA/PDB (тобто overlay підключив `components/`).
|
|
18
|
+
- **k8s.mdc:** оновлено опис, приклади `components/kustomization.yaml`, `components/hpa.yaml`, `components/pdb.yaml` і прикладу прод-overlay із `components: [- ../components]` + JSON6902-патчами.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
|
|
22
|
+
- **k8s / check-k8s:** прибрано застарілий механізм `$patch: delete` для HorizontalPodAutoscaler у `base/kustomization.yaml`. Видалено функції `verifyK8sBaseKustomizeHpaDeletedWhenInherited`, `kustomizationDeclaresHpaStrategicDelete`, `patchTextDeclaresHpaStrategicDelete` і константи-регекспи `HPA_STRATEGIC_DELETE_RE`, `HPA_KIND_LINE_RE` як мертві. Відповідні тести оновлено / видалено.
|
|
23
|
+
|
|
24
|
+
## [1.8.218] - 2026-05-09
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **auto-skills:** `llm-patch` додано як always-on скіл (без секції `[rules]` в `npm/bin/auto-skills.md`). Оновлено `AUTO_SKILL_ORDER` і `ALWAYS_ON_SKILLS` у `npm/scripts/auto-skills.mjs` та відповідні очікування у `npm/tests/auto-skills.test.mjs`. Сам вміст `npm/skills/llm-patch/SKILL.md` поки лишається стартовим — наповнюється окремо.
|
|
29
|
+
|
|
7
30
|
## [1.8.217] - 2026-05-09
|
|
8
31
|
|
|
9
32
|
### Added
|
|
@@ -28,7 +51,7 @@
|
|
|
28
51
|
|
|
29
52
|
### Fixed
|
|
30
53
|
|
|
31
|
-
- **k8s / check-k8s:** конвертація image-replace patches → `images:` падала з `byPatch.keys(...).toSorted is not a function
|
|
54
|
+
- **k8s / check-k8s:** конвертація image-replace patches → `images:` падала з `byPatch.keys(...).toSorted is not a function` (а в `rewriteInlinePatchWithoutOps` — з `(intermediate value).toSorted is not a function`), бо `Map.keys()` повертає ітератор, а `Set` — не масив, і `toSorted` на них немає. Тепер ключі/елементи матеріалізуються у масив через spread (`[...byPatch.keys()].toSorted(...)`, `[...new Set(opIndices)].toSorted(...)`).
|
|
32
55
|
|
|
33
56
|
### Changed
|
|
34
57
|
|
package/bin/auto-skills.md
CHANGED
package/mdc/k8s.mdc
CHANGED
|
@@ -111,7 +111,7 @@ resources:
|
|
|
111
111
|
memory: '128Mi'
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
**HPA у base не
|
|
114
|
+
**HPA і PDB у base не тримаємо**: ні локальних `hpa.yaml` / `pdb.yaml` поруч із `Deployment`, ні через `resources` / `components` / `bases`. Канон — sibling каталог **`components/`** (Kustomize Component) поруч з `base/` (див. розділ нижче).
|
|
115
115
|
|
|
116
116
|
### Поза base (оверлеї, окремі каталоги)
|
|
117
117
|
|
|
@@ -373,17 +373,27 @@ images:
|
|
|
373
373
|
|
|
374
374
|
**`check k8s`:** заборонено **`kind: Ingress`**.
|
|
375
375
|
|
|
376
|
-
## Deployment: `
|
|
376
|
+
## Deployment: `topologySpreadConstraints`, HPA / PDB через `components/`
|
|
377
377
|
|
|
378
|
-
Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**)
|
|
378
|
+
Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**) сам Deployment має канонічні **`spec.template.spec.topologySpreadConstraints`**, а **HPA і PDB** живуть у **sibling каталозі** **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу — `components`). У `base/` локальні `hpa.yaml` і `pdb.yaml` **заборонені** (file-existence error). У дереві base-kustomize HPA / PDB також **не дозволені** через `resources` / `components` / `bases`.
|
|
379
379
|
|
|
380
|
-
|
|
380
|
+
**Канонічна структура `<pkg>/k8s/components/`** (sibling до `base/`):
|
|
381
|
+
|
|
382
|
+
- **`kustomization.yaml`** — `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources: [hpa.yaml, pdb.yaml]`.
|
|
383
|
+
- **`hpa.yaml`** — `autoscaling/v2`, `HorizontalPodAutoscaler`, **без** `metadata.namespace` (namespace задає kustomization-споживач), `spec.scaleTargetRef.name` **= `metadata.name`** Deployment з base, dev-like значення `minReplicas: 1`, `maxReplicas: 1`.
|
|
384
|
+
- **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, **без** `metadata.namespace`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment, dev-like `minAvailable: 0`.
|
|
385
|
+
|
|
386
|
+
Інші назви каталогу (`scale/`, `hpa-component/`, `pdb-component/`) — fail.
|
|
387
|
+
|
|
388
|
+
**Overlays** (`ua/`, `ru/`, прод-overlays) підключають `components: [- ../components]` і додають JSON6902-патчі для прод-значень: `/spec/minReplicas`, `/spec/maxReplicas` (HPA), `/spec/minAvailable` (PDB). Dev-середовище (`base`) HPA/PDB не отримує — як і потрібно.
|
|
389
|
+
|
|
390
|
+
У **не-base** оверлеях (без `components/`) поруч із `Deployment` лишається звична схема: окремий **`hpa.yaml`** / **`pdb.yaml`**, якщо такі потрібні для цього середовища. **`check k8s`** звіряє прив'язку за іменами:
|
|
381
391
|
|
|
382
392
|
- **`hpa.yaml`** (поза **`…/base/`**) — `autoscaling/v2`, `HorizontalPodAutoscaler`, `spec.scaleTargetRef.name` **= `metadata.name`** Deployment.
|
|
383
393
|
- **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment.
|
|
384
394
|
- **`topologySpreadConstraints`** — запис з `maxSkew: 1`, `topologyKey: kubernetes.io/hostname`, `whenUnsatisfiable: ScheduleAnyway`, `labelSelector.matchLabels.app` рівне тій самій мітці `app`.
|
|
385
395
|
|
|
386
|
-
|
|
396
|
+
**Перевірка структури `components/`** (для кожного Deployment у `base/`): наявність каталогу, валідний `kustomization.yaml` як Component, `hpa.yaml` і `pdb.yaml` з відповідністю до Deployment-name / app-label. Алгоритм — функція `validateComponentsForBaseDeployment` у **`check-k8s.mjs`**.
|
|
387
397
|
|
|
388
398
|
**Локальні шляхи в `kustomization.yaml`:** кожен запис без `://` (remote) з `resources` / `bases` / `components` / `crds`, `patchesStrategicMerge`, `patches[].path`, `patchesJson6902[].path`, `configurations[]`, `replacements[].path` має вказувати на **існуючий** у репозиторії файл (`.yaml` / `.yml`) або **каталог**; биті посилання — помилка **`check k8s`**.
|
|
389
399
|
|
|
@@ -403,14 +413,21 @@ images:
|
|
|
403
413
|
|
|
404
414
|
### Прод-оверрайди у `kustomization.yaml`
|
|
405
415
|
|
|
406
|
-
У прод-накладенні `kustomization.yaml
|
|
416
|
+
У прод-накладенні `kustomization.yaml`, що підключає `components: [- ../components]` (тобто overlay-tree містить **HorizontalPodAutoscaler** і **PodDisruptionBudget**), у `patches[]` **обов'язкові** JSON6902-перевизначення прод-значень:
|
|
407
417
|
|
|
408
|
-
- **`
|
|
409
|
-
- **`
|
|
418
|
+
- **`HorizontalPodAutoscaler`**: `/spec/minReplicas` і `/spec/maxReplicas` (мінімум 2).
|
|
419
|
+
- **`PodDisruptionBudget`**: `/spec/minAvailable` (мінімум 1).
|
|
410
420
|
|
|
411
421
|
Формат patch — JSON6902 або Strategic Merge; `check k8s` перевіряє **наявність** відповідних JSON Pointer-ів у **`patches[]`**.
|
|
412
422
|
|
|
413
423
|
```yaml title="k8s/prod/kustomization.yaml (фрагмент)"
|
|
424
|
+
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
425
|
+
kind: Kustomization
|
|
426
|
+
namespace: prod
|
|
427
|
+
resources:
|
|
428
|
+
- ../base
|
|
429
|
+
components:
|
|
430
|
+
- ../components
|
|
414
431
|
patches:
|
|
415
432
|
- target:
|
|
416
433
|
kind: HorizontalPodAutoscaler
|
|
@@ -431,30 +448,17 @@ patches:
|
|
|
431
448
|
value: 1
|
|
432
449
|
```
|
|
433
450
|
|
|
434
|
-
###
|
|
435
|
-
|
|
436
|
-
Якщо **HPA** підключений з **`../component`** (або іншого шляху в **`resources`**), а у dev він не потрібен:
|
|
451
|
+
### Приклади `components/`
|
|
437
452
|
|
|
438
|
-
```yaml title="k8s/
|
|
453
|
+
```yaml title="k8s/components/kustomization.yaml"
|
|
454
|
+
apiVersion: kustomize.config.k8s.io/v1alpha1
|
|
455
|
+
kind: Component
|
|
439
456
|
resources:
|
|
440
|
-
-
|
|
441
|
-
- deploy.yaml
|
|
457
|
+
- hpa.yaml
|
|
442
458
|
- pdb.yaml
|
|
443
|
-
patches:
|
|
444
|
-
- target:
|
|
445
|
-
kind: HorizontalPodAutoscaler
|
|
446
|
-
name: backend-api
|
|
447
|
-
patch: |-
|
|
448
|
-
$patch: delete
|
|
449
|
-
apiVersion: autoscaling/v2
|
|
450
|
-
kind: HorizontalPodAutoscaler
|
|
451
|
-
metadata:
|
|
452
|
-
name: backend-api
|
|
453
459
|
```
|
|
454
460
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
```yaml title="k8s/prod/hpa.yaml (або компонент з HPA — не в …/base/ локальному hpa.yaml)"
|
|
461
|
+
```yaml title="k8s/components/hpa.yaml"
|
|
458
462
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/horizontalpodautoscaler-autoscaling-v2.json
|
|
459
463
|
apiVersion: autoscaling/v2
|
|
460
464
|
kind: HorizontalPodAutoscaler
|
|
@@ -465,8 +469,8 @@ spec:
|
|
|
465
469
|
apiVersion: apps/v1
|
|
466
470
|
kind: Deployment
|
|
467
471
|
name: backend-api
|
|
468
|
-
minReplicas: 2
|
|
469
|
-
maxReplicas:
|
|
472
|
+
minReplicas: 1 # прод overlay підіймає до >= 2
|
|
473
|
+
maxReplicas: 1 # прод overlay підіймає до >= 2
|
|
470
474
|
metrics:
|
|
471
475
|
- type: Resource
|
|
472
476
|
resource:
|
|
@@ -494,14 +498,14 @@ spec:
|
|
|
494
498
|
selectPolicy: Min
|
|
495
499
|
```
|
|
496
500
|
|
|
497
|
-
```yaml title="k8s/
|
|
501
|
+
```yaml title="k8s/components/pdb.yaml"
|
|
498
502
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/poddisruptionbudget-policy-v1.json
|
|
499
503
|
apiVersion: policy/v1
|
|
500
504
|
kind: PodDisruptionBudget
|
|
501
505
|
metadata:
|
|
502
506
|
name: backend-api
|
|
503
507
|
spec:
|
|
504
|
-
minAvailable: 0 #
|
|
508
|
+
minAvailable: 0 # прод overlay підіймає до >= 1
|
|
505
509
|
selector:
|
|
506
510
|
matchLabels:
|
|
507
511
|
app: backend-api
|
|
@@ -527,7 +531,7 @@ spec:
|
|
|
527
531
|
app: backend-api
|
|
528
532
|
```
|
|
529
533
|
|
|
530
|
-
|
|
534
|
+
Точні умови та повідомлення `fail` — JSDoc на початку `npm/scripts/check-k8s.mjs` і функції `validateComponentsForBaseDeployment` / `prodOverlayHpaPdbOverrideNeeds`.
|
|
531
535
|
|
|
532
536
|
## HorizontalPodAutoscaler: `autoscaling/v2`
|
|
533
537
|
|
package/package.json
CHANGED
package/scripts/auto-skills.mjs
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* - `abie-kustomize - [abie]` — додається разом з правилом `abie`
|
|
8
8
|
* - `taze - [bun]` — додається разом з правилом `bun`
|
|
9
9
|
*
|
|
10
|
-
* Скіли без секції `[rules]` у `auto-skills.md` (`fix`, `lint`, `publish-telegram`)
|
|
10
|
+
* Скіли без секції `[rules]` у `auto-skills.md` (`fix`, `lint`, `llm-patch`, `publish-telegram`)
|
|
11
11
|
* додаються завжди, якщо доступні в пакеті й не у `disable-skills`.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/** Порядок автододавання skills відповідно до `auto-skills.md`. */
|
|
15
|
-
export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint', 'publish-telegram', 'taze'])
|
|
15
|
+
export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint', 'llm-patch', 'publish-telegram', 'taze'])
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Залежність скілів від правил (`auto-skills.md` синтаксис `skill - [rules]`).
|
|
@@ -26,7 +26,7 @@ export const AUTO_SKILL_RULE_DEPENDENCIES = Object.freeze(
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
/** Скіли без залежностей — додаються завжди (рядок «завжди» в `auto-skills.md`). */
|
|
29
|
-
const ALWAYS_ON_SKILLS = Object.freeze(['fix', 'lint', 'publish-telegram'])
|
|
29
|
+
const ALWAYS_ON_SKILLS = Object.freeze(['fix', 'lint', 'llm-patch', 'publish-telegram'])
|
|
30
30
|
|
|
31
31
|
const DEFAULT_DISABLED_LIST = Object.freeze([])
|
|
32
32
|
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -84,19 +84,25 @@
|
|
|
84
84
|
* або рядок `"true"`, без регістрової залежності).
|
|
85
85
|
*
|
|
86
86
|
* **HPA / PDB / topologySpreadConstraints:** для кожного **`Deployment`** у шарі **`…/k8s/…/base/`** (будь-який
|
|
87
|
-
* `.yaml` у цьому каталозі)
|
|
88
|
-
* **`…/
|
|
89
|
-
*
|
|
90
|
-
* `resources` / `components` / `bases
|
|
91
|
-
*
|
|
87
|
+
* `.yaml` у цьому каталозі) обов'язкові канонічні **topologySpreadConstraints**, а HPA і PDB живуть у sibling
|
|
88
|
+
* каталозі **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу `components`). У `base/`
|
|
89
|
+
* заборонено тримати локальні `hpa.yaml` і `pdb.yaml` (file-existence error) і також у дереві base-kustomize
|
|
90
|
+
* не повинно бути HPA/PDB через `resources` / `components` / `bases`. Структура `components/`:
|
|
91
|
+
* `kustomization.yaml` з `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources` що містять
|
|
92
|
+
* `hpa.yaml` і `pdb.yaml` (як єдині або принаймні обов'язкові), `hpa.yaml` (валідний `autoscaling/v2`
|
|
93
|
+
* HorizontalPodAutoscaler з `scaleTargetRef.name` = ім'я Deployment, dev-like `min=max=1`), `pdb.yaml` (валідний
|
|
94
|
+
* `policy/v1` PodDisruptionBudget з `selector.matchLabels.app` = мітка `app` Deployment, dev-like `minAvailable=0`).
|
|
95
|
+
* Overlays (`ua/`, `ru/`, прод-overlays) підключають `components: [- ../components]` і додають JSON6902-патчі для
|
|
96
|
+
* прод-значень: `/spec/minReplicas`, `/spec/maxReplicas` (HPA), `/spec/minAvailable` (PDB). HPA поруч із Deployment
|
|
97
|
+
* у не-base оверлеях — як раніше (див. k8s.mdc).
|
|
92
98
|
* Env-залежні межі за сегментом після `/k8s/`: **dev-like** (`base`, `dev`, `*-qa`) — для HPA, що лишився після
|
|
93
99
|
* збірки, `minReplicas === 1`, `maxReplicas === 1`, PDB `minAvailable === 0`; **прод** — `minReplicas >= 2`,
|
|
94
100
|
* `maxReplicas >= 2`, `minAvailable >= 1`.
|
|
95
101
|
*
|
|
96
102
|
* **Прод-оверрайди в kustomization.yaml:** для прод overlays (не dev-like) у `patches[]` потрібні перевизначення
|
|
97
|
-
* **`/spec/minReplicas`** і **`/spec/maxReplicas`** для **HorizontalPodAutoscaler**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
103
|
+
* **`/spec/minReplicas`** і **`/spec/maxReplicas`** для **HorizontalPodAutoscaler** і **`/spec/minAvailable`** для
|
|
104
|
+
* **PDB** — якщо overlay-tree (через `resources` / `components`) містить HPA / PDB (тобто overlay підключив
|
|
105
|
+
* `…/k8s/…/components/`). Формат patch — JSON6902 або Strategic Merge; наявність шляхів —
|
|
100
106
|
* `kustomizationPatchPathsByTargetKind`.
|
|
101
107
|
*
|
|
102
108
|
* **Існування шляхів у `kustomization.yaml`:** кожне локальне посилання (без `://`) з `resources` / `bases` /
|
|
@@ -111,11 +117,11 @@
|
|
|
111
117
|
* прибирається; (б) чистить існуючий блок **`images:`** — зрізає `:tag` з `name` (digest `@…` не чіпає) і видаляє
|
|
112
118
|
* `newTag`, який збігається з відрізаним тегом.
|
|
113
119
|
*
|
|
114
|
-
* **HPA / PDB
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* `kustomization.yaml` overlay, який підключає каталог `…/k8s/…/base`, не додавай окремі YAML-файли з HPA / PDB,
|
|
118
|
-
* поки в наслідуваному `base` у дереві не
|
|
120
|
+
* **HPA / PDB заборонені у base-дереві Kustomize:** у дереві з `…/k8s/…/base/kustomization.yaml` не дозволяти
|
|
121
|
+
* `HorizontalPodAutoscaler` / `PodDisruptionBudget` у `resources` / `bases` / `components` / `crds` (рекурсивно)
|
|
122
|
+
* взагалі. Канон — HPA/PDB у sibling `…/k8s/…/components/` (Kustomize Component) і підключаються лише з overlay.
|
|
123
|
+
* У `kustomization.yaml` overlay, який підключає каталог `…/k8s/…/base`, не додавай окремі YAML-файли з HPA / PDB,
|
|
124
|
+
* поки в наслідуваному `base` у дереві не з'явиться такий Deployment (k8s.mdc).
|
|
119
125
|
*/
|
|
120
126
|
import { existsSync } from 'node:fs'
|
|
121
127
|
import { readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
|
|
@@ -311,10 +317,6 @@ const KIND_FIELD_RE = /^\s*kind:\s*(\S+)\s*$/
|
|
|
311
317
|
const TYPE_FIELD_RE = /^\s*type:\s*(\S+)\s*$/
|
|
312
318
|
const YAML_DOC_SEPARATOR_LINE_RE = /^---\s*$/
|
|
313
319
|
const HEALTHCHECK_DELETE_RE = /\$patch:\s*delete/u
|
|
314
|
-
/** Strategic-merge patch видалення **HorizontalPodAutoscaler** у kustomization (k8s.mdc). */
|
|
315
|
-
const HPA_STRATEGIC_DELETE_RE = /\$patch:\s*delete/u
|
|
316
|
-
/** Рядок **kind:** для HPA у strategic-merge patch. */
|
|
317
|
-
const HPA_KIND_LINE_RE = /^kind:\s*HorizontalPodAutoscaler\b/m
|
|
318
320
|
const HEALTHCHECK_KIND_RE = /kind:\s*HealthCheckPolicy/u
|
|
319
321
|
const METADATA_LINE_RE = /metadata:/u
|
|
320
322
|
const NAME_NON_EMPTY_RE = /name:\s*\S+/u
|
|
@@ -2149,60 +2151,6 @@ export function ruKustomizationHasHealthCheckDeletePatch(raw) {
|
|
|
2149
2151
|
return true
|
|
2150
2152
|
}
|
|
2151
2153
|
|
|
2152
|
-
/**
|
|
2153
|
-
* Чи вміст strategic-merge patch оголошує видалення **HorizontalPodAutoscaler** (`$patch: delete`, `kind:`).
|
|
2154
|
-
* @param {string} text вміст поля **patch** або файлу merge
|
|
2155
|
-
* @returns {boolean} true, якщо присутні **`$patch: delete`** і **`kind: HorizontalPodAutoscaler`**
|
|
2156
|
-
*/
|
|
2157
|
-
export function patchTextDeclaresHpaStrategicDelete(text) {
|
|
2158
|
-
const t = typeof text === 'string' ? text : ''
|
|
2159
|
-
if (!HPA_STRATEGIC_DELETE_RE.test(t)) return false
|
|
2160
|
-
return HPA_KIND_LINE_RE.test(t)
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
/**
|
|
2164
|
-
* Чи **kustomization.yaml** (inline **patches** / **patchesStrategicMerge**) містить strategic-merge видалення HPA.
|
|
2165
|
-
* @param {string} kustAbs абсолютний шлях до **kustomization.yaml**
|
|
2166
|
-
* @param {string} rootNorm нормалізований корінь репо
|
|
2167
|
-
* @returns {Promise<boolean>} true, якщо знайдено patch видалення HPA
|
|
2168
|
-
*/
|
|
2169
|
-
async function kustomizationDeclaresHpaStrategicDelete(kustAbs, rootNorm) {
|
|
2170
|
-
const kust = await readFirstYamlObject(kustAbs)
|
|
2171
|
-
if (kust === null) return false
|
|
2172
|
-
const kustDir = dirname(kustAbs)
|
|
2173
|
-
const patches = kust.patches
|
|
2174
|
-
if (Array.isArray(patches)) {
|
|
2175
|
-
for (const p of patches) {
|
|
2176
|
-
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
2177
|
-
const rec = /** @type {Record<string, unknown>} */ (p)
|
|
2178
|
-
const inline = rec.patch
|
|
2179
|
-
if (typeof inline === 'string' && patchTextDeclaresHpaStrategicDelete(inline)) return true
|
|
2180
|
-
const pathRef = rec.path
|
|
2181
|
-
if (typeof pathRef === 'string' && pathRef.trim() !== '') {
|
|
2182
|
-
const abs = resolve(kustDir, pathRef.trim())
|
|
2183
|
-
if (resolvedFilePathIsUnderRoot(rootNorm, abs)) {
|
|
2184
|
-
const body = await tryReadFileUtf8(abs)
|
|
2185
|
-
if (body !== undefined && patchTextDeclaresHpaStrategicDelete(body)) return true
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
const sm = kust.patchesStrategicMerge
|
|
2192
|
-
if (Array.isArray(sm)) {
|
|
2193
|
-
for (const ref of sm) {
|
|
2194
|
-
if (typeof ref === 'string' && ref.trim() !== '') {
|
|
2195
|
-
const abs = resolve(kustDir, ref.trim())
|
|
2196
|
-
if (resolvedFilePathIsUnderRoot(rootNorm, abs)) {
|
|
2197
|
-
const body = await tryReadFileUtf8(abs)
|
|
2198
|
-
if (body !== undefined && patchTextDeclaresHpaStrategicDelete(body)) return true
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
return false
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
2154
|
/**
|
|
2207
2155
|
* Чи абсолютний шлях лежить усередині кореня репозиторію (без виходу через `..`).
|
|
2208
2156
|
* @param {string} rootAbs абсолютний корінь
|
|
@@ -4457,6 +4405,18 @@ export const HPA_FILENAME = 'hpa.yaml'
|
|
|
4457
4405
|
*/
|
|
4458
4406
|
export const PDB_FILENAME = 'pdb.yaml'
|
|
4459
4407
|
|
|
4408
|
+
/**
|
|
4409
|
+
* Фіксована назва каталогу Kustomize Component, sibling до `base/`, де живуть HPA і PDB
|
|
4410
|
+
* (за каноном — `hpa.yaml` і `pdb.yaml` з `kind: Component` у `kustomization.yaml`). Інші назви
|
|
4411
|
+
* (`scale/`, `hpa-component/`) у правилі **k8s** не дозволені (k8s.mdc).
|
|
4412
|
+
*/
|
|
4413
|
+
export const COMPONENTS_DIR = 'components'
|
|
4414
|
+
|
|
4415
|
+
/**
|
|
4416
|
+
* `apiVersion` маніфесту Kustomize **Component** (sibling до `base/`).
|
|
4417
|
+
*/
|
|
4418
|
+
const KUSTOMIZE_COMPONENT_API_VERSION = 'kustomize.config.k8s.io/v1alpha1'
|
|
4419
|
+
|
|
4460
4420
|
/**
|
|
4461
4421
|
* Канонічний topologyKey для **topologySpreadConstraints** у Deployment (див. k8s.mdc).
|
|
4462
4422
|
*/
|
|
@@ -5086,56 +5046,24 @@ async function yamlFileContainsHpaOrPdbDocument(fileAbs) {
|
|
|
5086
5046
|
}
|
|
5087
5047
|
|
|
5088
5048
|
/**
|
|
5089
|
-
* Для `…/k8s/…/base/kustomization.yaml`: HPA / PDB
|
|
5049
|
+
* Для `…/k8s/…/base/kustomization.yaml`: HPA / PDB заборонені у base-дереві Kustomize взагалі.
|
|
5050
|
+
* Канон — HPA/PDB живуть у sibling каталозі **`…/k8s/…/components/`** (Kustomize Component) і підключаються
|
|
5051
|
+
* лише з overlay (`components: [- ../components]`). Dev-середовище — `base` без HPA/PDB.
|
|
5090
5052
|
* @param {string} kustAbs kustomization.yaml
|
|
5091
5053
|
* @param {string} rel для повідомлень
|
|
5092
|
-
* @param {(msg: string) => void} fail callback
|
|
5093
|
-
* @param {(msg: string) => void} passFn
|
|
5054
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
5055
|
+
* @param {(msg: string) => void} passFn callback при успіху
|
|
5094
5056
|
* @param {(kust: string) => Promise<{ hasDeployment: boolean, hasHpa: boolean, hasPdb: boolean }>} getTreeFlags мемоізований аналіз дерева
|
|
5095
5057
|
* @returns {Promise<void>}
|
|
5096
5058
|
*/
|
|
5097
|
-
async function
|
|
5098
|
-
const {
|
|
5059
|
+
async function verifyK8sBaseKustomizeHasNoHpaPdb(kustAbs, rel, fail, passFn, getTreeFlags) {
|
|
5060
|
+
const { hasHpa, hasPdb } = await getTreeFlags(kustAbs)
|
|
5099
5061
|
if (hasHpa || hasPdb) {
|
|
5100
|
-
if (hasDeployment) {
|
|
5101
|
-
passFn(`${rel}: у дереві kustomize base є HPA/PDB і Deployment (k8s.mdc)`)
|
|
5102
|
-
} else {
|
|
5103
|
-
fail(
|
|
5104
|
-
`${rel}: у base є HorizontalPodAutoscaler і/або PodDisruptionBudget у resources/bases/…, але дерева kustomize не містить Deployment — HPA і PDB дозволені тільки разом із Deployment (k8s.mdc)`
|
|
5105
|
-
)
|
|
5106
|
-
}
|
|
5107
|
-
}
|
|
5108
|
-
}
|
|
5109
|
-
|
|
5110
|
-
/**
|
|
5111
|
-
* У **`…/k8s/…/base/kustomization.yaml`**: якщо з **resources** тягнеться HPA разом із Deployment,
|
|
5112
|
-
* обов’язковий strategic-merge patch з **`$patch: delete`** для **HorizontalPodAutoscaler** (k8s.mdc).
|
|
5113
|
-
* @param {string} kustAbs абсолютний шлях до **kustomization.yaml**
|
|
5114
|
-
* @param {string} rel відносний шлях для повідомлень
|
|
5115
|
-
* @param {string} rootNorm корінь репо
|
|
5116
|
-
* @param {(msg: string) => void} fail callback
|
|
5117
|
-
* @param {(msg: string) => void} passFn success
|
|
5118
|
-
* @param {(kust: string) => Promise<{ hasDeployment: boolean, hasHpa: boolean, hasPdb: boolean }>} getTreeFlags
|
|
5119
|
-
* @returns {Promise<void>}
|
|
5120
|
-
*/
|
|
5121
|
-
async function verifyK8sBaseKustomizeHpaDeletedWhenInherited(
|
|
5122
|
-
kustAbs,
|
|
5123
|
-
rel,
|
|
5124
|
-
rootNorm,
|
|
5125
|
-
fail,
|
|
5126
|
-
passFn,
|
|
5127
|
-
getTreeFlags
|
|
5128
|
-
) {
|
|
5129
|
-
const { hasDeployment, hasHpa } = await getTreeFlags(kustAbs)
|
|
5130
|
-
if (!hasDeployment || !hasHpa) {
|
|
5131
|
-
return
|
|
5132
|
-
}
|
|
5133
|
-
if (await kustomizationDeclaresHpaStrategicDelete(kustAbs, rootNorm)) {
|
|
5134
|
-
passFn(`${rel}: base kustomization містить $patch: delete для HorizontalPodAutoscaler (k8s.mdc)`)
|
|
5135
|
-
} else {
|
|
5136
5062
|
fail(
|
|
5137
|
-
`${rel}: у
|
|
5063
|
+
`${rel}: у base-дереві kustomize є HorizontalPodAutoscaler і/або PodDisruptionBudget — HPA/PDB заборонені у base, переведіть у sibling каталог components/ і підключайте з overlay (k8s.mdc)`
|
|
5138
5064
|
)
|
|
5065
|
+
} else {
|
|
5066
|
+
passFn(`${rel}: base-дерево kustomize без HPA/PDB (k8s.mdc)`)
|
|
5139
5067
|
}
|
|
5140
5068
|
}
|
|
5141
5069
|
|
|
@@ -5245,8 +5173,7 @@ async function validateKustomizeHpaPdbOnlyWithBaseDeployment(root, yamlFilesAbs,
|
|
|
5245
5173
|
const kust = await readFirstYamlObject(kustAbs)
|
|
5246
5174
|
if (kust !== null) {
|
|
5247
5175
|
if (isK8sBaseKustomizationRelPath(rel)) {
|
|
5248
|
-
await
|
|
5249
|
-
await verifyK8sBaseKustomizeHpaDeletedWhenInherited(kustAbs, rel, rootNorm, fail, passFn, getTreeFlags)
|
|
5176
|
+
await verifyK8sBaseKustomizeHasNoHpaPdb(kustAbs, rel, fail, passFn, getTreeFlags)
|
|
5250
5177
|
} else {
|
|
5251
5178
|
await verifyOverlayHpaPdbFileRefsRespectBaseDeployment(rootNorm, kustAbs, rel, kust, fail, passFn, getTreeFlags)
|
|
5252
5179
|
}
|
|
@@ -5299,12 +5226,13 @@ function checkProdOverridesInKustomization(kust, rel, fail, passFn, needs) {
|
|
|
5299
5226
|
}
|
|
5300
5227
|
|
|
5301
5228
|
/**
|
|
5302
|
-
* Які прод-оверрайди потрібні для **kustomization.yaml** (не dev-like)
|
|
5303
|
-
*
|
|
5304
|
-
* **HPA:** patches на **`minReplicas`/`maxReplicas`** лише якщо в base-дереві є HPA **і** base **не** містить
|
|
5305
|
-
* strategic-merge **`$patch: delete`** для **HorizontalPodAutoscaler** (k8s.mdc).
|
|
5229
|
+
* Які прод-оверрайди потрібні для **kustomization.yaml** (не dev-like).
|
|
5306
5230
|
*
|
|
5307
|
-
*
|
|
5231
|
+
* Тригер — overlay-tree (через `resources` / `components`) містить HPA / PDB. На практиці це означає,
|
|
5232
|
+
* що overlay підключив sibling каталог `…/k8s/…/components/` (Kustomize Component) з канонічними
|
|
5233
|
+
* `hpa.yaml` / `pdb.yaml`. Тоді у `patches[]` обов'язкові JSON6902-патчі прод-значень: для **HPA** —
|
|
5234
|
+
* `/spec/minReplicas` і `/spec/maxReplicas` (мінімум 2), для **PDB** — `/spec/minAvailable` (мінімум 1).
|
|
5235
|
+
* Для dev-like (`base` / `dev` / `*-qa`) overrides не потрібні (k8s.mdc).
|
|
5308
5236
|
* @param {string} rootNorm нормалізований корінь репозиторію
|
|
5309
5237
|
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
5310
5238
|
* @returns {Promise<ProdOverlayHpaPdbOverrideNeeds>} прапорці потрібних перевизначень
|
|
@@ -5316,28 +5244,11 @@ export async function prodOverlayHpaPdbOverrideNeeds(rootNorm, kustAbs) {
|
|
|
5316
5244
|
return { needsHpaReplicaPatches: false, needsPdbMinAvailablePatch: false }
|
|
5317
5245
|
}
|
|
5318
5246
|
|
|
5319
|
-
const
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
const pathRefs = resourcePathRefsFromKustomizationObject(kust)
|
|
5324
|
-
const baseDirs = await k8sBaseDirsFromKustomizeResourcePathRefs(kustDir, pathRefs, rootNorm)
|
|
5325
|
-
if (baseDirs.length === 0) return { needsHpaReplicaPatches: false, needsPdbMinAvailablePatch: false }
|
|
5326
|
-
|
|
5327
|
-
let needsHpaReplicaPatches = false
|
|
5328
|
-
let needsPdbMinAvailablePatch = false
|
|
5329
|
-
for (const bd of baseDirs) {
|
|
5330
|
-
const bk = join(bd, 'kustomization.yaml')
|
|
5331
|
-
const f = await kustomizeResourceTreeHpaPdbDeploymentFlags(bk, rootNorm)
|
|
5332
|
-
const baseDeletesHpa = await kustomizationDeclaresHpaStrategicDelete(bk, rootNorm)
|
|
5333
|
-
if (f.hasDeployment && f.hasPdb) {
|
|
5334
|
-
needsPdbMinAvailablePatch = true
|
|
5335
|
-
}
|
|
5336
|
-
if (f.hasDeployment && f.hasHpa && !baseDeletesHpa) {
|
|
5337
|
-
needsHpaReplicaPatches = true
|
|
5338
|
-
}
|
|
5247
|
+
const flags = await kustomizeResourceTreeHpaPdbDeploymentFlags(kustAbs, rootNorm)
|
|
5248
|
+
return {
|
|
5249
|
+
needsHpaReplicaPatches: flags.hasHpa,
|
|
5250
|
+
needsPdbMinAvailablePatch: flags.hasPdb
|
|
5339
5251
|
}
|
|
5340
|
-
return { needsHpaReplicaPatches, needsPdbMinAvailablePatch }
|
|
5341
5252
|
}
|
|
5342
5253
|
|
|
5343
5254
|
/**
|
|
@@ -5459,12 +5370,143 @@ function validatePdbForDeployment(pdbDocs, deployName, appLabel, isDevLike, pdbR
|
|
|
5459
5370
|
}
|
|
5460
5371
|
}
|
|
5461
5372
|
|
|
5373
|
+
/**
|
|
5374
|
+
* Перевіряє sibling каталог `…/k8s/…/components/` для одного **Deployment** з шару `…/k8s/…/base/`.
|
|
5375
|
+
*
|
|
5376
|
+
* Канон (k8s.mdc):
|
|
5377
|
+
* - Існує каталог `<baseDir>/../components/`.
|
|
5378
|
+
* - У ньому `kustomization.yaml` з `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component` і
|
|
5379
|
+
* `resources` що містять `hpa.yaml` і `pdb.yaml`.
|
|
5380
|
+
* - `components/hpa.yaml` — валідний `autoscaling/v2` `HorizontalPodAutoscaler` зі `scaleTargetRef.name`,
|
|
5381
|
+
* що дорівнює `metadata.name` цього Deployment, з dev-like значеннями `min=max=1`.
|
|
5382
|
+
* - `components/pdb.yaml` — валідний `policy/v1` `PodDisruptionBudget` зі `selector.matchLabels.app`,
|
|
5383
|
+
* що дорівнює мітці `app` Deployment, з dev-like `minAvailable=0`.
|
|
5384
|
+
* @param {string} baseDir абсолютний шлях до `…/k8s/…/base/`
|
|
5385
|
+
* @param {string} deployName ім'я Deployment з base
|
|
5386
|
+
* @param {string} appLabel мітка `app` з `spec.selector.matchLabels.app`
|
|
5387
|
+
* @param {string} root корінь репозиторію
|
|
5388
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
5389
|
+
* @param {(msg: string) => void} passFn callback при успіху
|
|
5390
|
+
* @returns {Promise<void>}
|
|
5391
|
+
*/
|
|
5392
|
+
export async function validateComponentsForBaseDeployment(baseDir, deployName, appLabel, root, fail, passFn) {
|
|
5393
|
+
const componentsDir = resolve(baseDir, '..', COMPONENTS_DIR)
|
|
5394
|
+
const componentsRel = (relative(root, componentsDir) || componentsDir).replaceAll('\\', '/')
|
|
5395
|
+
if (!existsSync(componentsDir)) {
|
|
5396
|
+
fail(
|
|
5397
|
+
`${componentsRel}: для Deployment '${deployName}' з sibling base/ обов'язковий каталог components/ з hpa.yaml і pdb.yaml (Kustomize Component) (k8s.mdc)`
|
|
5398
|
+
)
|
|
5399
|
+
return
|
|
5400
|
+
}
|
|
5401
|
+
let stat0
|
|
5402
|
+
try {
|
|
5403
|
+
stat0 = await stat(componentsDir)
|
|
5404
|
+
} catch {
|
|
5405
|
+
stat0 = null
|
|
5406
|
+
}
|
|
5407
|
+
if (stat0 === null || !stat0.isDirectory()) {
|
|
5408
|
+
fail(`${componentsRel}: очікується каталог Kustomize Component (k8s.mdc)`)
|
|
5409
|
+
return
|
|
5410
|
+
}
|
|
5411
|
+
await validateComponentsKustomizationManifest(componentsDir, componentsRel, fail, passFn)
|
|
5412
|
+
await validateComponentsHpaFile(componentsDir, componentsRel, deployName, fail, passFn)
|
|
5413
|
+
await validateComponentsPdbFile(componentsDir, componentsRel, deployName, appLabel, fail, passFn)
|
|
5414
|
+
}
|
|
5415
|
+
|
|
5416
|
+
/**
|
|
5417
|
+
* Перевіряє `components/kustomization.yaml`: `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`,
|
|
5418
|
+
* `resources` містить `hpa.yaml` і `pdb.yaml` (як мінімум).
|
|
5419
|
+
* @param {string} componentsDir абсолютний шлях до каталогу `components/`
|
|
5420
|
+
* @param {string} componentsRel відносний шлях для повідомлень
|
|
5421
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
5422
|
+
* @param {(msg: string) => void} passFn callback при успіху
|
|
5423
|
+
* @returns {Promise<void>}
|
|
5424
|
+
*/
|
|
5425
|
+
async function validateComponentsKustomizationManifest(componentsDir, componentsRel, fail, passFn) {
|
|
5426
|
+
const kustAbs = join(componentsDir, 'kustomization.yaml')
|
|
5427
|
+
if (!existsSync(kustAbs)) {
|
|
5428
|
+
fail(`${componentsRel}/kustomization.yaml: відсутній — додай Kustomize Component-маніфест (k8s.mdc)`)
|
|
5429
|
+
return
|
|
5430
|
+
}
|
|
5431
|
+
const obj = await readFirstYamlObject(kustAbs)
|
|
5432
|
+
if (obj === null) {
|
|
5433
|
+
fail(`${componentsRel}/kustomization.yaml: не вдалося розпарсити перший YAML-документ (k8s.mdc)`)
|
|
5434
|
+
return
|
|
5435
|
+
}
|
|
5436
|
+
if (obj.apiVersion !== KUSTOMIZE_COMPONENT_API_VERSION) {
|
|
5437
|
+
fail(
|
|
5438
|
+
`${componentsRel}/kustomization.yaml: apiVersion має бути '${KUSTOMIZE_COMPONENT_API_VERSION}' (зараз: ${JSON.stringify(obj.apiVersion)}) (k8s.mdc)`
|
|
5439
|
+
)
|
|
5440
|
+
}
|
|
5441
|
+
if (obj.kind !== 'Component') {
|
|
5442
|
+
fail(
|
|
5443
|
+
`${componentsRel}/kustomization.yaml: kind має бути 'Component' (зараз: ${JSON.stringify(obj.kind)}) (k8s.mdc)`
|
|
5444
|
+
)
|
|
5445
|
+
}
|
|
5446
|
+
const resources = Array.isArray(obj.resources) ? obj.resources.filter(x => typeof x === 'string') : []
|
|
5447
|
+
const hasHpa = resources.includes(HPA_FILENAME)
|
|
5448
|
+
const hasPdb = resources.includes(PDB_FILENAME)
|
|
5449
|
+
if (!hasHpa) {
|
|
5450
|
+
fail(`${componentsRel}/kustomization.yaml: у resources має бути '${HPA_FILENAME}' (k8s.mdc)`)
|
|
5451
|
+
}
|
|
5452
|
+
if (!hasPdb) {
|
|
5453
|
+
fail(`${componentsRel}/kustomization.yaml: у resources має бути '${PDB_FILENAME}' (k8s.mdc)`)
|
|
5454
|
+
}
|
|
5455
|
+
if (obj.apiVersion === KUSTOMIZE_COMPONENT_API_VERSION && obj.kind === 'Component' && hasHpa && hasPdb) {
|
|
5456
|
+
passFn(`${componentsRel}/kustomization.yaml: канонічний Kustomize Component з hpa.yaml і pdb.yaml (k8s.mdc)`)
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5460
|
+
/**
|
|
5461
|
+
* Перевіряє `components/hpa.yaml`: HPA для Deployment, dev-like `min=max=1`.
|
|
5462
|
+
* @param {string} componentsDir абсолютний шлях до каталогу `components/`
|
|
5463
|
+
* @param {string} componentsRel відносний шлях для повідомлень
|
|
5464
|
+
* @param {string} deployName ім'я Deployment з base
|
|
5465
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
5466
|
+
* @param {(msg: string) => void} passFn callback при успіху
|
|
5467
|
+
* @returns {Promise<void>}
|
|
5468
|
+
*/
|
|
5469
|
+
async function validateComponentsHpaFile(componentsDir, componentsRel, deployName, fail, passFn) {
|
|
5470
|
+
const hpaAbs = join(componentsDir, HPA_FILENAME)
|
|
5471
|
+
const hpaRel = `${componentsRel}/${HPA_FILENAME}`
|
|
5472
|
+
if (!existsSync(hpaAbs)) {
|
|
5473
|
+
fail(`${hpaRel}: відсутній — додай HorizontalPodAutoscaler для Deployment '${deployName}' (k8s.mdc)`)
|
|
5474
|
+
return
|
|
5475
|
+
}
|
|
5476
|
+
const hpaDocs = await readAllDocsByKindFromFile(hpaAbs, 'HorizontalPodAutoscaler')
|
|
5477
|
+
validateHpaForDeployment(hpaDocs, deployName, true, hpaRel, fail, passFn)
|
|
5478
|
+
}
|
|
5479
|
+
|
|
5480
|
+
/**
|
|
5481
|
+
* Перевіряє `components/pdb.yaml`: PDB для Deployment, dev-like `minAvailable=0`.
|
|
5482
|
+
* @param {string} componentsDir абсолютний шлях до каталогу `components/`
|
|
5483
|
+
* @param {string} componentsRel відносний шлях для повідомлень
|
|
5484
|
+
* @param {string} deployName ім'я Deployment з base
|
|
5485
|
+
* @param {string} appLabel мітка `app` Deployment
|
|
5486
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
5487
|
+
* @param {(msg: string) => void} passFn callback при успіху
|
|
5488
|
+
* @returns {Promise<void>}
|
|
5489
|
+
*/
|
|
5490
|
+
async function validateComponentsPdbFile(componentsDir, componentsRel, deployName, appLabel, fail, passFn) {
|
|
5491
|
+
const pdbAbs = join(componentsDir, PDB_FILENAME)
|
|
5492
|
+
const pdbRel = `${componentsRel}/${PDB_FILENAME}`
|
|
5493
|
+
if (!existsSync(pdbAbs)) {
|
|
5494
|
+
fail(`${pdbRel}: відсутній — додай PodDisruptionBudget для Deployment '${deployName}' (k8s.mdc)`)
|
|
5495
|
+
return
|
|
5496
|
+
}
|
|
5497
|
+
const pdbDocs = await readAllDocsByKindFromFile(pdbAbs, 'PodDisruptionBudget')
|
|
5498
|
+
validatePdbForDeployment(pdbDocs, deployName, appLabel, true, pdbRel, fail, passFn)
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5462
5501
|
/**
|
|
5463
5502
|
* Перевіряє один Deployment: topologySpreadConstraints, HPA та PDB.
|
|
5503
|
+
*
|
|
5504
|
+
* Для **base-шару** HPA/PDB не вимагаються поруч із Deployment — натомість викликач має звіряти sibling
|
|
5505
|
+
* каталог `components/` через `validateComponentsForBaseDeployment`.
|
|
5464
5506
|
* @param {Record<string, unknown>} deployment об'єкт Deployment
|
|
5465
5507
|
* @param {string} deployRel відносний шлях каталогу для повідомлень
|
|
5466
5508
|
* @param {boolean} isDevLike чи середовище dev-like
|
|
5467
|
-
* @param {boolean} isK8sBaseLayer чи каталог під **`…/k8s/…/base/`** (HPA поруч не вимагаємо —
|
|
5509
|
+
* @param {boolean} isK8sBaseLayer чи каталог під **`…/k8s/…/base/`** (HPA/PDB поруч не вимагаємо — живуть у `components/`)
|
|
5468
5510
|
* @param {Record<string, unknown>[]} hpaDocs HPA-документи каталогу
|
|
5469
5511
|
* @param {Record<string, unknown>[]} pdbDocs PDB-документи каталогу
|
|
5470
5512
|
* @param {(msg: string) => void} fail callback при помилці
|
|
@@ -5496,9 +5538,10 @@ function validateSingleDeploymentHpaPdbTopology(
|
|
|
5496
5538
|
} else {
|
|
5497
5539
|
fail(`${deployRel}: Deployment '${deployName}': ${tscViolation}`)
|
|
5498
5540
|
}
|
|
5499
|
-
if (
|
|
5500
|
-
|
|
5541
|
+
if (isK8sBaseLayer) {
|
|
5542
|
+
return
|
|
5501
5543
|
}
|
|
5544
|
+
validateHpaForDeployment(hpaDocs, deployName, isDevLike, `${deployRel}/${HPA_FILENAME}`, fail, passFn)
|
|
5502
5545
|
validatePdbForDeployment(pdbDocs, deployName, appLabel, isDevLike, `${deployRel}/${PDB_FILENAME}`, fail, passFn)
|
|
5503
5546
|
}
|
|
5504
5547
|
|
|
@@ -5515,18 +5558,23 @@ async function validateDeploymentsInDir(deployments, dir, root, fail, passFn) {
|
|
|
5515
5558
|
const segment = k8sEnvSegmentFromRelPath(relDir + '/')
|
|
5516
5559
|
const isDevLike = isDevLikeK8sEnvSegment(segment)
|
|
5517
5560
|
const isK8sBaseLayer = isK8sYamlUnderBaseDirectory(`${relDir}/probe.yaml`)
|
|
5561
|
+
const deployRel = relDir === '' ? '.' : relDir
|
|
5518
5562
|
if (isK8sBaseLayer && deployments.length > 0) {
|
|
5519
5563
|
const hpaAbs = join(dir, HPA_FILENAME)
|
|
5520
5564
|
if (existsSync(hpaAbs)) {
|
|
5521
|
-
const prefix = relDir === '' ? '.' : relDir
|
|
5522
5565
|
fail(
|
|
5523
|
-
`${
|
|
5566
|
+
`${deployRel}/${HPA_FILENAME}: у шарі k8s/.../base не тримай локальний hpa.yaml — HPA живе у sibling components/ (k8s.mdc)`
|
|
5567
|
+
)
|
|
5568
|
+
}
|
|
5569
|
+
const pdbAbs = join(dir, PDB_FILENAME)
|
|
5570
|
+
if (existsSync(pdbAbs)) {
|
|
5571
|
+
fail(
|
|
5572
|
+
`${deployRel}/${PDB_FILENAME}: у шарі k8s/.../base не тримай локальний pdb.yaml — PDB живе у sibling components/ (k8s.mdc)`
|
|
5524
5573
|
)
|
|
5525
5574
|
}
|
|
5526
5575
|
}
|
|
5527
|
-
const hpaDocs = await readDocsByKindInDir(dir, 'HorizontalPodAutoscaler', HPA_FILENAME)
|
|
5528
|
-
const pdbDocs = await readDocsByKindInDir(dir, 'PodDisruptionBudget', PDB_FILENAME)
|
|
5529
|
-
const deployRel = relDir === '' ? '.' : relDir
|
|
5576
|
+
const hpaDocs = isK8sBaseLayer ? [] : await readDocsByKindInDir(dir, 'HorizontalPodAutoscaler', HPA_FILENAME)
|
|
5577
|
+
const pdbDocs = isK8sBaseLayer ? [] : await readDocsByKindInDir(dir, 'PodDisruptionBudget', PDB_FILENAME)
|
|
5530
5578
|
for (const deployment of deployments) {
|
|
5531
5579
|
validateSingleDeploymentHpaPdbTopology(
|
|
5532
5580
|
deployment,
|
|
@@ -5538,6 +5586,13 @@ async function validateDeploymentsInDir(deployments, dir, root, fail, passFn) {
|
|
|
5538
5586
|
fail,
|
|
5539
5587
|
passFn
|
|
5540
5588
|
)
|
|
5589
|
+
if (isK8sBaseLayer) {
|
|
5590
|
+
const deployName = manifestMetadataName(deployment)
|
|
5591
|
+
const appLabel = deploymentAppLabel(deployment)
|
|
5592
|
+
if (deployName !== null && appLabel !== null) {
|
|
5593
|
+
await validateComponentsForBaseDeployment(dir, deployName, appLabel, root, fail, passFn)
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5541
5596
|
}
|
|
5542
5597
|
}
|
|
5543
5598
|
|
|
@@ -5556,8 +5611,9 @@ async function extractDeploymentsFromFile(filePath) {
|
|
|
5556
5611
|
|
|
5557
5612
|
/**
|
|
5558
5613
|
* Для кожного **Deployment** у шарі **`…/k8s/…/base/`** (будь-який YAML у відповідному каталозі) перевіряє:
|
|
5559
|
-
* заборона
|
|
5560
|
-
*
|
|
5614
|
+
* заборона локальних **`hpa.yaml`** і **`pdb.yaml`** (file-existence); канонічні **topologySpreadConstraints**;
|
|
5615
|
+
* наявність і канон sibling каталогу **`components/`** (Kustomize Component) з `hpa.yaml` і `pdb.yaml` через
|
|
5616
|
+
* `validateComponentsForBaseDeployment`. У не-base шарах — звична схема (`hpa.yaml` / `pdb.yaml` поруч).
|
|
5561
5617
|
* Env-залежні межі — за сегментом після `/k8s/`: dev-like vs прод.
|
|
5562
5618
|
* @param {string} root корінь репозиторію
|
|
5563
5619
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
@@ -6294,7 +6350,7 @@ function rewriteInlinePatchWithoutOps(patchText, opIndices) {
|
|
|
6294
6350
|
const seq = inner.contents
|
|
6295
6351
|
if (!isSeq(seq)) return null
|
|
6296
6352
|
|
|
6297
|
-
const toRemove = new Set(opIndices).toSorted((a, b) => b - a)
|
|
6353
|
+
const toRemove = [...new Set(opIndices)].toSorted((a, b) => b - a)
|
|
6298
6354
|
for (const i of toRemove) {
|
|
6299
6355
|
if (i < 0 || i >= seq.items.length) return null
|
|
6300
6356
|
seq.delete(i)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-llm-patch
|
|
3
|
+
description: >-
|
|
4
|
+
Підготовка самодостатнього текстового промпта для іншого Claude/Cursor-агента —
|
|
5
|
+
read-only аналіз CWD без жодних змін у поточному репо
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Підготовка LLM-патчу (текстова комунікація між агентами)
|
|
9
|
+
|
|
10
|
+
Скіл готує **самодостатній текстовий промпт** ("патч") для іншої LLM-сесії
|
|
11
|
+
(Claude / Cursor agent у цільовому проєкті). Користувач копіює готовий блок
|
|
12
|
+
з відповіді чату й вставляє його в розмову з іншим агентом — той виконує
|
|
13
|
+
запитані зміни вже у своєму середовищі.
|
|
14
|
+
|
|
15
|
+
## Принципи
|
|
16
|
+
|
|
17
|
+
- **Read-only:** скіл нічого не пише в поточне репо. Лише читає CWD.
|
|
18
|
+
- **Тимчасові артефакти — лише в `/tmp`** (наприклад, `tree`-вивід чи
|
|
19
|
+
проміжні чернетки промпту); ніколи не у CWD.
|
|
20
|
+
- **Цільова LLM:** Claude / Cursor agent — припускаємо, що читач знається
|
|
21
|
+
на XML-тегах, file refs (`path/to/file.ts:42`), markdown.
|
|
22
|
+
- **Один блок виводу:** результат — це **один** markdown-блок у відповіді
|
|
23
|
+
чату, готовий до копіювання.
|
|
24
|
+
|
|
25
|
+
## Виклик
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
/n-llm-patch <вільний опис завдання>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Аргумент має містити: **що треба зробити**, опційно — **назву пакета /
|
|
32
|
+
підмодуля** для контексту. Якщо аргумент порожній — попроси користувача
|
|
33
|
+
сформулювати завдання, не вгадуй.
|
|
34
|
+
|
|
35
|
+
Приклад:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
/n-llm-patch підготуй для проекту @nitra/eslint-config щоб він врахував останню версію node
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Workflow
|
|
42
|
+
|
|
43
|
+
1. **Розпарсити аргумент** — виокремити суть завдання та (якщо є) назву
|
|
44
|
+
пакета / шлях. Аргумент = намір користувача, передається у промпт
|
|
45
|
+
як розділ "Завдання".
|
|
46
|
+
|
|
47
|
+
2. **Зібрати read-only контекст з CWD:**
|
|
48
|
+
- `package.json` — поля `name`, `version`, `engines`, `peerDependencies`,
|
|
49
|
+
`dependencies`, `devDependencies`, `scripts`, `type`, `exports`.
|
|
50
|
+
- Структура репо: `tree -L 2 -I 'node_modules|.git|dist|build|.next|.nuxt'`
|
|
51
|
+
(або `ls -la` коли `tree` недоступний).
|
|
52
|
+
- `README.md` — перші 40-60 рядків (head).
|
|
53
|
+
- `CLAUDE.md` / `AGENTS.md` / `.cursor/rules/*.mdc` — якщо є, відмітити їх
|
|
54
|
+
існування й перелік.
|
|
55
|
+
- **Релевантні до завдання config-файли** — підбирати за ключовими
|
|
56
|
+
словами з аргументу (наприклад, "node" → `engines`, `.nvmrc`,
|
|
57
|
+
`.node-version`; "eslint" → `eslint.config.*`, `.eslintrc*`;
|
|
58
|
+
"vite" → `vite.config.*`; "ts" → `tsconfig.json`).
|
|
59
|
+
|
|
60
|
+
3. **Визначити "точку патчу"** — короткий список файлів, які найімовірніше
|
|
61
|
+
доведеться правити цільовому агенту, з обґрунтуванням.
|
|
62
|
+
|
|
63
|
+
4. **Сформувати промпт за шаблоном нижче.** Дрібні релевантні файли
|
|
64
|
+
(≤ ~80 рядків) вбудовуй **повністю**; великі — цитуй фрагмент з
|
|
65
|
+
посиланням на шлях:рядки.
|
|
66
|
+
|
|
67
|
+
5. **Вивести один markdown-блок у чат** — без додаткових коментарів
|
|
68
|
+
поза блоком, окрім однорядкового підпису "готово до копіювання".
|
|
69
|
+
|
|
70
|
+
## Шаблон вихідного промпта
|
|
71
|
+
|
|
72
|
+
````markdown
|
|
73
|
+
```markdown
|
|
74
|
+
# Завдання
|
|
75
|
+
|
|
76
|
+
<нормалізований опис з аргументу — 1-3 речення без води>
|
|
77
|
+
|
|
78
|
+
# Контекст проєкту
|
|
79
|
+
|
|
80
|
+
- Назва / версія: `<name>@<version>`
|
|
81
|
+
- Тип: <library | app | eslint-config | monorepo | …>
|
|
82
|
+
- Стек: Node `<engines.node>`, <TS/JS>, <ключові залежності>
|
|
83
|
+
- Документи правил: <CLAUDE.md / .cursor/rules — або "немає">
|
|
84
|
+
|
|
85
|
+
## Структура (skim)
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
<вивід tree -L 2>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
# Релевантні файли
|
|
92
|
+
|
|
93
|
+
## `package.json`
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
<повний вміст або ключові поля>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## `<інший релевантний файл>`
|
|
100
|
+
|
|
101
|
+
```<lang>
|
|
102
|
+
<вміст або фрагмент з посиланням на шлях:рядки>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
# Що треба зробити
|
|
106
|
+
|
|
107
|
+
- крок 1 — у файлі `X` (`<коротке пояснення>`)
|
|
108
|
+
- крок 2 — у файлі `Y`
|
|
109
|
+
- крок 3 — оновити `CHANGELOG.md` / bump `version`, якщо це npm-пакет
|
|
110
|
+
|
|
111
|
+
# Обмеження
|
|
112
|
+
|
|
113
|
+
- Не ламати публічний API
|
|
114
|
+
- <constraints, виявлені з package.json / конфігів — наприклад "engines.node вже >=22, треба >=25">
|
|
115
|
+
- Дотриматись правил репо (CLAUDE.md / .cursor/rules — посилання вище)
|
|
116
|
+
|
|
117
|
+
# Як перевірити
|
|
118
|
+
|
|
119
|
+
- `<команда з scripts — npm test / bun test / lint>`
|
|
120
|
+
- <конкретні acceptance-checks: "у `engines.node` має бути `>=25`",
|
|
121
|
+
"CI зелений" тощо>
|
|
122
|
+
```
|
|
123
|
+
````
|
|
124
|
+
|
|
125
|
+
## Правила
|
|
126
|
+
|
|
127
|
+
- **Мова промпту:** українська за замовчуванням; якщо аргумент англійською —
|
|
128
|
+
можна англійською. Технічні терміни — англійською.
|
|
129
|
+
- **Обсяг:** прагни вмістити промпт у ~200-500 рядків. Якщо релевантних
|
|
130
|
+
файлів багато — обери 3-5 найважливіших, решту згадай посиланнями.
|
|
131
|
+
- **Без галюцинацій:** не вигадуй полів `package.json`, версій, шляхів —
|
|
132
|
+
лише те, що реально прочитав з CWD. Якщо чогось бракує — явно так і
|
|
133
|
+
напиши ("`engines.node` відсутнє").
|
|
134
|
+
- **Без імперативного тону до користувача:** промпт адресований **іншому
|
|
135
|
+
агенту**, не людині. Використовуй "зроби", "онови", "додай".
|
|
136
|
+
- **Без секцій-пустушок:** якщо немає `Обмежень` — пропусти секцію.
|
|
137
|
+
- **Не вмикай у промпт:** секрети, `.env`, `node_modules`, бінарні файли,
|
|
138
|
+
довгі логи.
|
|
139
|
+
- **Усе в одному блоці:** результат — це **один** ` ```markdown ` блок;
|
|
140
|
+
ніяких додаткових міркувань поза ним (крім фінального підпису
|
|
141
|
+
"готово до копіювання").
|
|
142
|
+
|
|
143
|
+
## Що скіл **не** робить
|
|
144
|
+
|
|
145
|
+
- Не завантажує tarball з npm registry, не клонує git-репозиторії.
|
|
146
|
+
- Не редагує жоден файл у поточному проєкті (і поза ним).
|
|
147
|
+
- Не виконує сам "патч" — лише готує промпт для іншого агента.
|
|
148
|
+
- Не оптимізує під Gemini / GPT — лише Claude / Cursor agent.
|
|
149
|
+
|
|
150
|
+
## Приклад виклику й результату
|
|
151
|
+
|
|
152
|
+
Виклик:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
/n-llm-patch у @nitra/eslint-config підняти engines.node до >=25
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Очікуваний вивід (схематично):
|
|
159
|
+
|
|
160
|
+
````
|
|
161
|
+
```markdown
|
|
162
|
+
# Завдання
|
|
163
|
+
|
|
164
|
+
Підняти `engines.node` у `@nitra/eslint-config` до `>=25` і переглянути
|
|
165
|
+
сумісність peer-залежностей з новою версією.
|
|
166
|
+
|
|
167
|
+
# Контекст проєкту
|
|
168
|
+
|
|
169
|
+
- Назва / версія: `@nitra/eslint-config@2.4.0`
|
|
170
|
+
- Тип: shared eslint preset (library)
|
|
171
|
+
- Стек: Node `>=22` (поточне), ESM, eslint `^9`
|
|
172
|
+
- Документи правил: `.cursor/rules/n-js-lint.mdc`
|
|
173
|
+
|
|
174
|
+
## Структура (skim)
|
|
175
|
+
|
|
176
|
+
…
|
|
177
|
+
|
|
178
|
+
# Релевантні файли
|
|
179
|
+
|
|
180
|
+
## `package.json`
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{ "engines": { "node": ">=22" }, "peerDependencies": { "eslint": "^9" } }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
# Що треба зробити
|
|
187
|
+
|
|
188
|
+
- `package.json` → `engines.node`: `>=22` → `>=25`
|
|
189
|
+
- Перевірити, чи `peerDependencies.eslint ^9` сумісне з Node 25
|
|
190
|
+
- Bump `version` (minor) + запис у `CHANGELOG.md`
|
|
191
|
+
|
|
192
|
+
# Як перевірити
|
|
193
|
+
|
|
194
|
+
- `bun test`
|
|
195
|
+
- `node -v` у CI ≥ 25
|
|
196
|
+
```
|
|
197
|
+
готово до копіювання — встав у чат з агентом у цільовому проєкті
|
|
198
|
+
````
|