@nitra/cursor 1.8.40 → 1.8.55
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/bin/n-cursor.js +27 -4
- package/bin/rename-yaml-extensions.mjs +44 -0
- package/mdc/abie.mdc +3 -1
- package/mdc/k8s.mdc +20 -51
- package/mdc/style-lint.mdc +16 -21
- package/package.json +5 -3
- package/scripts/check-k8s.mjs +283 -84
- package/scripts/check-style-lint.mjs +20 -6
- package/scripts/rename-yaml-extensions.mjs +159 -0
- package/scripts/run-k8s.mjs +5 -5
- package/scripts/utils/gha-workflow.mjs +4 -3
- package/skills/abie-kustomize/SKILL.md +1 -1
- package/skills/fix/SKILL.md +1 -1
- package/skills/publish-telegram/SKILL.md +1 -1
package/bin/n-cursor.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* `npx \@nitra/cursor check` — перевірити правила, перелічені в AGENTS.md (якщо є check-*.mjs);
|
|
9
9
|
* якщо в корені вже є `.n-cursor.json`, спочатку зчитується конфіг і за потреби дописується `$schema`
|
|
10
10
|
* `npx \@nitra/cursor check bun` — перевірити лише вказані правила (ігнорує AGENTS.md)
|
|
11
|
+
* `npx \@nitra/cursor rename-yaml-extensions` — k8s `*.yml` → `*.yaml`, `.github` `*.yaml` → `*.yml` (опції: `--dry-run`, `--root=…`; див. bin/rename-yaml-extensions.mjs)
|
|
11
12
|
*
|
|
12
13
|
* Якщо у корені репозиторію немає .n-cursor.json, спочатку перейменовується за наявності nitra-cursor.json;
|
|
13
14
|
* у `.cursor/rules` файли `nitra-*.mdc` перейменовуються на `n-*.mdc`; інакше конфіг створюється автоматично
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
ensureNitraCursorInRootDevDependencies,
|
|
44
45
|
readBundledPackageVersion
|
|
45
46
|
} from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
|
|
47
|
+
import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
|
|
46
48
|
import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
|
|
47
49
|
|
|
48
50
|
const PACKAGE_NAME = '@nitra/cursor'
|
|
@@ -863,10 +865,31 @@ const [command, ...args] = process.argv.slice(2)
|
|
|
863
865
|
|
|
864
866
|
try {
|
|
865
867
|
await ensureNitraCursorInRootDevDependencies(cwd())
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
868
|
+
switch (command) {
|
|
869
|
+
case 'check': {
|
|
870
|
+
await runChecks(args)
|
|
871
|
+
|
|
872
|
+
break
|
|
873
|
+
}
|
|
874
|
+
case 'rename-yaml-extensions': {
|
|
875
|
+
const code = await runRenameYamlExtensionsCli(args)
|
|
876
|
+
if (code !== 0) {
|
|
877
|
+
process.exitCode = 1
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
break
|
|
881
|
+
}
|
|
882
|
+
case undefined:
|
|
883
|
+
case '': {
|
|
884
|
+
await runSync()
|
|
885
|
+
|
|
886
|
+
break
|
|
887
|
+
}
|
|
888
|
+
default: {
|
|
889
|
+
console.error(`❌ Невідома команда: ${command}`)
|
|
890
|
+
console.error(` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions`)
|
|
891
|
+
process.exitCode = 1
|
|
892
|
+
}
|
|
870
893
|
}
|
|
871
894
|
} catch {
|
|
872
895
|
process.exitCode = 1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI для перейменування розширень YAML (k8s та `.github`). Бізнес-логіка — у **`scripts/rename-yaml-extensions.mjs`**.
|
|
5
|
+
*
|
|
6
|
+
* Точки входу:
|
|
7
|
+
* - **`npx \@nitra/cursor rename-yaml-extensions`** [опції]
|
|
8
|
+
* - **`npx n-rename-yaml-extensions`** [опції] (глобальний shim з `package.json` bin)
|
|
9
|
+
* - **`bun ./node_modules/\@nitra/cursor/bin/rename-yaml-extensions.mjs`**
|
|
10
|
+
*
|
|
11
|
+
* Опції: **`--dry-run`**, **`--root=<шлях>`** (корінь обходу; за замовчуванням **`process.cwd()`**).
|
|
12
|
+
*/
|
|
13
|
+
import { isRunAsCli } from '../scripts/cli-entry.mjs'
|
|
14
|
+
import { parseRenameYamlArgs, renameYamlExtensions } from '../scripts/rename-yaml-extensions.mjs'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Запускає перейменування з виводом у консоль (для підкоманди **`n-cursor`** або прямого bin).
|
|
18
|
+
* @param {string[]} argv аргументи без імені команди (усі після `rename-yaml-extensions` у **`n-cursor`**)
|
|
19
|
+
* @returns {Promise<number>} **0** — успіх; **1** — були помилки (існуючий цільовий файл тощо)
|
|
20
|
+
*/
|
|
21
|
+
export async function runRenameYamlExtensionsCli(argv) {
|
|
22
|
+
const { dryRun, root } = parseRenameYamlArgs(argv)
|
|
23
|
+
const label = dryRun ? '[dry-run] ' : ''
|
|
24
|
+
const { renamed, errors } = await renameYamlExtensions(root, { dryRun })
|
|
25
|
+
|
|
26
|
+
for (const { relFrom, relTo } of renamed) {
|
|
27
|
+
console.log(`${label}${relFrom} → ${relTo}`)
|
|
28
|
+
}
|
|
29
|
+
if (renamed.length === 0 && errors.length === 0) {
|
|
30
|
+
console.log(`${label}Немає файлів для перейменування (k8s + .yml → .yaml; .github + .yaml → .yml).`)
|
|
31
|
+
}
|
|
32
|
+
for (const err of errors) {
|
|
33
|
+
console.error(` ❌ ${err}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return errors.length > 0 ? 1 : 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isRunAsCli()) {
|
|
40
|
+
const code = await runRenameYamlExtensionsCli(process.argv.slice(2))
|
|
41
|
+
if (code !== 0) {
|
|
42
|
+
process.exitCode = 1
|
|
43
|
+
}
|
|
44
|
+
}
|
package/mdc/abie.mdc
CHANGED
|
@@ -28,7 +28,7 @@ spec:
|
|
|
28
28
|
name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
**Overlay `ru`:** у **`…/ru/kustomization.yaml`** під **`k8s`** додай **видалення** ресурсу HealthCheckPolicy для середовища, де політика не потрібна (підстав **реальне ім’я** замість `НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ`):
|
|
32
32
|
|
|
33
33
|
```yaml title="kustomization.yaml"
|
|
34
34
|
patches:
|
|
@@ -53,4 +53,6 @@ ignore_branches: dev,ua,ru
|
|
|
53
53
|
|
|
54
54
|
`npx @nitra/cursor check abie`
|
|
55
55
|
|
|
56
|
+
Повна матриця полів **`hc.yaml`**, **`ignore_branches`** і умов для **`ru/kustomization.yaml`** — у **`npm/scripts/check-abie.mjs`**.
|
|
57
|
+
|
|
56
58
|
Програмна перевірка (**`check-abie.mjs`**) виконується лише якщо у **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень (щоб не вимагати **ua**/**ru** у репозиторіях без цього правила).
|
package/mdc/k8s.mdc
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
|
|
3
|
-
version: '1.
|
|
4
|
-
globs: "**/k8s/**/*.
|
|
3
|
+
version: '1.23'
|
|
4
|
+
globs: "**/k8s/**/*.yaml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Kubernetes YAML у шляхах з `k8s`
|
|
9
9
|
|
|
10
|
-
Для кожного файлу `*.yaml
|
|
10
|
+
Для кожного файлу `*.yaml`, у шляху якого є сегмент директорії **`k8s`** (наприклад `site/k8s/base/deployment.yaml`), **перший рядок** — коментар-директива для [YAML Language Server](https://github.com/redhat-developer/yaml-language-server):
|
|
11
11
|
|
|
12
12
|
```yaml
|
|
13
13
|
# yaml-language-server: $schema=https://...
|
|
@@ -15,7 +15,7 @@ alwaysApply: false
|
|
|
15
15
|
|
|
16
16
|
Далі — вміст маніфесту. Зайвий порожній рядок між коментарем і YAML не додавай, якщо в проєкті не прийнято інше.
|
|
17
17
|
|
|
18
|
-
**Розширення:** усі маніфести
|
|
18
|
+
**Розширення:** усі маніфести під **`k8s`**, включно з **`kustomization.yaml`**, — лише **`.yaml`** (розширення **`.yml`** не використовуй).
|
|
19
19
|
|
|
20
20
|
**Dockerfile / hadolint** — окреме правило **`docker.mdc`** і **`npx @nitra/cursor check docker`**.
|
|
21
21
|
|
|
@@ -46,7 +46,7 @@ alwaysApply: false
|
|
|
46
46
|
|
|
47
47
|
Шлях до скрипта підстав свій (`./scripts/…` після копіювання, `node_modules/@nitra/cursor/scripts/…` якщо пакет у залежностях).
|
|
48
48
|
|
|
49
|
-
Додай workflow **`.github/workflows/lint-k8s.yml`** (гілки **`dev`** і **`main`**, лише **`.yml`**, узгоджено з **`ga.mdc`**)
|
|
49
|
+
Додай workflow **`.github/workflows/lint-k8s.yml`** (гілки **`dev`** і **`main`**, лише **`.yml`**, узгоджено з **`ga.mdc`**). **Не** дублюй **`setup-node`**, **`oven-sh/setup-bun`**, **`actions/cache`** і **`bun install`** у job — після **`checkout`** використовуй локальний composite **`setup-bun-deps`** (шлях **`./.github/actions/setup-bun-deps`** або **`./npm/github-actions/setup-bun-deps`**, як у репозиторії). Встановлення **kubeconform** / **kubescape** лишаються окремими кроками.
|
|
50
50
|
|
|
51
51
|
```yaml title=".github/workflows/lint-k8s.yml"
|
|
52
52
|
name: Lint K8s
|
|
@@ -57,7 +57,7 @@ on:
|
|
|
57
57
|
- dev
|
|
58
58
|
- main
|
|
59
59
|
paths:
|
|
60
|
-
- '**/k8s/**/*.
|
|
60
|
+
- '**/k8s/**/*.yaml'
|
|
61
61
|
|
|
62
62
|
pull_request:
|
|
63
63
|
branches:
|
|
@@ -78,6 +78,8 @@ jobs:
|
|
|
78
78
|
with:
|
|
79
79
|
persist-credentials: false
|
|
80
80
|
|
|
81
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
82
|
+
|
|
81
83
|
- name: Install kubeconform
|
|
82
84
|
run: |
|
|
83
85
|
curl -sSL "https://github.com/yannh/kubeconform/releases/download/v0.7.0/kubeconform-linux-amd64.tar.gz" | tar xz
|
|
@@ -88,33 +90,10 @@ jobs:
|
|
|
88
90
|
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
|
|
89
91
|
echo "$HOME/.kubescape/bin" >> $GITHUB_PATH
|
|
90
92
|
|
|
91
|
-
- uses: actions/setup-node@v6
|
|
92
|
-
with:
|
|
93
|
-
node-version: '24'
|
|
94
|
-
|
|
95
|
-
- uses: oven-sh/setup-bun@v2
|
|
96
|
-
|
|
97
|
-
- name: Cache Bun dependencies
|
|
98
|
-
uses: actions/cache@v5
|
|
99
|
-
with:
|
|
100
|
-
path: |
|
|
101
|
-
~/.bun/install/cache
|
|
102
|
-
node_modules
|
|
103
|
-
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
|
104
|
-
restore-keys: |
|
|
105
|
-
${{ runner.os }}-bun-
|
|
106
|
-
|
|
107
|
-
- name: Install dependencies
|
|
108
|
-
run: bun install --frozen-lockfile
|
|
109
|
-
|
|
110
93
|
- name: Lint K8s
|
|
111
94
|
run: bun run lint-k8s
|
|
112
95
|
```
|
|
113
96
|
|
|
114
|
-
Після **`install.sh`** kubescape може опинитися поза стандартним PATH; за потреби додай крок **`echo "$HOME/.kubescape/bin" >> $GITHUB_PATH`** (див. документацію [kubescape](https://github.com/kubescape/kubescape#readme)).
|
|
115
|
-
|
|
116
|
-
Кореневий скрипт **`lint`** (див. **`n-bun.mdc`**) **обов'язково** містить **`bun run lint-k8s`**, коли в проєкті підключено правило **`k8s`**.
|
|
117
|
-
|
|
118
97
|
## Deployment: `resources`
|
|
119
98
|
|
|
120
99
|
Для **`kind: Deployment`** у кожному контейнері **`spec.template.spec.containers[]`** має бути явне поле **`resources`**. Якщо ліміти та requests ще не задані, додай порожній об'єкт:
|
|
@@ -144,7 +123,11 @@ resources: {}
|
|
|
144
123
|
|
|
145
124
|
### Namespace
|
|
146
125
|
|
|
147
|
-
-
|
|
126
|
+
- **`base/kustomization.yaml`:** у цьому файлі поле **`namespace:`** **завжди** має бути присутнє й **непорожнє**, щоб Kustomize застосував один цільовий namespace до ресурсів **base**. **`check k8s`** перевіряє це, коли файл є в репозиторії.
|
|
127
|
+
|
|
128
|
+
- **Де не дублювати `metadata.namespace`:** у YAML під **`k8s`**, чиї шляхи **досяжні** з будь-якого **`kustomization.yaml`** через **`resources`**, **`bases`**, **`components`**, **`crds`**, елементи **`patches`** з полем **`path`**, **`patchesStrategicMerge`** (транзитивно, зокрема через каталог із дочірнім **`kustomization.yaml`**). У таких файлах **не** вказуй **`metadata.namespace`** — його виставляє Kustomize за полем **`namespace:`** у kustomization.
|
|
129
|
+
|
|
130
|
+
- **Коли `metadata.namespace` обов’язковий:** YAML під **`k8s`**, який **ніде** не перелічений у згаданих полях жодного kustomization-файлу, має містити **непорожній** **`metadata.namespace`** у кожному документі з **`apiVersion`** та **`kind`**, **окрім** кластерних ресурсів (наприклад **`Namespace`**, **`ClusterRole`**, **`PersistentVolume`** — повний перелік у **`check-k8s.mjs`**, **`CLUSTER_SCOPED_KINDS`**). Якщо файл має бути без namespace у маніфесті — підключи його до kustomization через **`resources`** / **`patches`** тощо.
|
|
148
131
|
|
|
149
132
|
- **Не додавай** окремі **patches** Kustomize, які лише змінюють **namespace**: **namespace** визначає Kustomize; у overlays додаткові зміни — без дублювання логіки **namespace**.
|
|
150
133
|
|
|
@@ -176,28 +159,15 @@ resources: {}
|
|
|
176
159
|
|
|
177
160
|
Для `$schema` у першому рядку див. приклад **HealthCheckPolicy** у тому ж розділі (datree CRDs-catalog).
|
|
178
161
|
|
|
179
|
-
3. **Overlay `ru`:** у **`ru/kustomization.yaml`** (шлях з сегментами **`…/ru/kustomization.yaml`** під **`k8s`**) додай **видалення** ресурсу HealthCheckPolicy для середовища, де політика не потрібна (підстав **реальне ім’я** замість `SERVICE_NAME`):
|
|
180
|
-
|
|
181
|
-
```yaml
|
|
182
|
-
patches:
|
|
183
|
-
- target:
|
|
184
|
-
kind: HealthCheckPolicy
|
|
185
|
-
patch: |-
|
|
186
|
-
kind: HealthCheckPolicy
|
|
187
|
-
metadata:
|
|
188
|
-
name: SERVICE_NAME
|
|
189
|
-
$patch: delete
|
|
190
|
-
```
|
|
191
|
-
|
|
192
162
|
За потреби розшир **`target`** (`name`, `namespace`), щоб однозначно вказати об’єкт.
|
|
193
163
|
|
|
194
|
-
**`check k8s`:** заборонено **`kind: Ingress
|
|
164
|
+
**`check k8s`:** заборонено **`kind: Ingress`**.
|
|
195
165
|
|
|
196
166
|
## Перевірка
|
|
197
167
|
|
|
198
|
-
**`npx @nitra/cursor check k8s`** —
|
|
168
|
+
**`npx @nitra/cursor check k8s`** — критерії збігаються з розділом **«Що закодовано в `check-k8s.mjs`»** і з **«Визначення схеми YAML»** вище. Якщо під `k8s` немає `*.yaml` — перевірку пропущено. Інший зміст маніфесту — вручну / **`lint-k8s`**.
|
|
199
169
|
|
|
200
|
-
Після змін у маніфестах: **`bun run lint-k8s`** (kubeconform + kubescape
|
|
170
|
+
Після змін у маніфестах: **`bun run lint-k8s`** (kubeconform + kubescape).
|
|
201
171
|
|
|
202
172
|
## Що закодовано в `check-k8s.mjs`
|
|
203
173
|
|
|
@@ -206,25 +176,24 @@ resources: {}
|
|
|
206
176
|
- Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
|
|
207
177
|
- **`file:`** у `$schema` — URL до apiVersion/kind не звіряється; **`https:`** — kustomization за іменем файлу → Schema Store; далі перевіряється **`EXPLICIT_K8S_SCHEMAS`** (`Map`: `apiVersion` + `kind` + `type`, для записів без `type` у маніфесті — третій компонент **`*`**); потім `v1` → yannh; група з `YANNH_GROUPS` → yannh; інакше → datree (GitHub Pages), крім рядків явної таблиці (наприклад **InfisicalSecret** — raw на `main`).
|
|
208
178
|
- У кожному YAML-документі з **`kind: Deployment`** — парсинг через **`yaml`**: у кожного елемента **`spec.template.spec.containers[]`** має існувати ключ **`resources`** зі значенням-об'єктом (допускається порожній **`{}`**); у кожного контейнера — **`imagePullPolicy: Always`**.
|
|
209
|
-
-
|
|
179
|
+
- **Namespace у маніфестах (не ім’я `kustomization`):** ресурсні YAML у **`…/k8s/base/`** — **завжди** непорожній **`metadata.namespace`**. Інакше будується множина шляхів, досяжних з **`kustomization.yaml`** через **`resources`**, **`bases`**, **`components`**, **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**, з рекурсією в каталоги з дочірнім **`kustomization.yaml`**: для таких файлів **поза** **`k8s/base`** — **заборона** **`metadata.namespace`**; для файлів поза **`k8s/base`** і поза множиною — **вимога** непорожнього **`metadata.namespace`** (крім **`CLUSTER_SCOPED_KINDS`**).
|
|
210
180
|
- Документи з **`kind: Ingress`** — заборонені (потрібен перехід на Gateway API, див. розділ **Ingress → Gateway API**).
|
|
211
|
-
- Якщо в будь-якому файлі під **`k8s`** є **`kind: HealthCheckPolicy`**, серед файлів має бути **`ru/kustomization.yaml`** (сегмент шляху **`ru`** перед іменем файлу), а його вміст — patch видалення **HealthCheckPolicy** з **`$patch: delete`** (див. той самий розділ).
|
|
212
181
|
- Заборона шляхів **`…/k8s/dev/…`** (окремої директорії **`dev`** під **`k8s`** не має бути).
|
|
213
|
-
- Якщо
|
|
182
|
+
- Якщо в дереві є **`k8s/base/kustomization.yaml`**, у першому документі **завжди** має бути непорожнє поле **`namespace`** (типово **`dev`**; див. розділ **Namespace**).
|
|
214
183
|
|
|
215
184
|
## Коли застосовувати (агентам)
|
|
216
185
|
|
|
217
186
|
- Зміни в k8s YAML — після правок **`check k8s`**.
|
|
218
187
|
- Якщо перший рядок уже коректний і URL відповідає `apiVersion` / `kind` — не дублюй; змінився ресурс — онови лише `$schema`.
|
|
219
188
|
- У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**); додай **`imagePullPolicy: Always`** для кожного контейнера.
|
|
220
|
-
- Дотримуйся структури **Kustomize** (`base` = dev, overlays без дублювання `base`, коментарі для рядків, що змінюються в overlay).
|
|
189
|
+
- Дотримуйся структури **Kustomize** (`base` = dev, overlays без дублювання `base`, коментарі для рядків, що змінюються в overlay). У **`base/kustomization.yaml`** завжди задавай непорожній **`namespace:`**. У **`k8s/base`** у кожному ресурсному YAML має бути явний **`metadata.namespace`**. Поза **base**, якщо не хочеш **`metadata.namespace`** у файлі — підключи його до kustomization (**`resources`** / **`patches`** тощо); інакше додай явний **`metadata.namespace`**.
|
|
221
190
|
- Після міграції на нову структуру **видали** застарілі файли та каталоги, які вже замінені (див. **Міграція зі старої структури**).
|
|
222
191
|
|
|
223
192
|
## Визначення схеми YAML (канон)
|
|
224
193
|
|
|
225
194
|
Орієнтир — **перший документ** (до наступного `---`).
|
|
226
195
|
|
|
227
|
-
1. **Ім’я** `kustomization.yaml`
|
|
196
|
+
1. **Ім’я** `kustomization.yaml` → `https://json.schemastore.org/kustomization.json`.
|
|
228
197
|
2. **`apiVersion: v1`** → yannh, PIN набору схем **`v1.33.9-standalone-strict`**, ref репозиторію для raw URL — **`master`** (каталог версії не є коренем репо на GitHub):
|
|
229
198
|
`https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/<PIN>/<kind>-v1.json`
|
|
230
199
|
`<kind>`: літери в нижньому регістрі без роздільників між CamelCase (наприклад `Service` → `service`).
|
package/mdc/style-lint.mdc
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила стилів CSS та SCSS
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.2'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Генерація та редагування стилів (Cursor і інші агенти)
|
|
8
8
|
|
|
9
9
|
- **Джерело правил:** перед тим як писати або суттєво змінювати **`.css`**, **`.scss`** або стилі в **`.vue`**, переглянь у корені проєкту (і в релевантних пакетах монорепо, якщо є) поле **`stylelint`** у **`package.json`** (зокрема `extends`), наявні **`.stylelintrc.*`**, **`stylelint.config.*`** та **`.stylelintignore`**. Не покладайся на «типові» правила stylelint з пам’яті — дотримуйся **проєктного** **`@nitra/stylelint-config`** і будь-яких локальних доповнень у репозиторії.
|
|
10
10
|
- **Форматування** узгоджуй з **`n-js-format.mdc`** (oxfmt / `.oxfmtrc.json` для css, scss тощо), щоб форматер і stylelint не суперечили один одному.
|
|
11
|
-
-
|
|
12
|
-
- **Не розширюй винятки:** не додавай зайві **`stylelint-disable`**
|
|
11
|
+
- **Запуск stylelint:** лише **`npx stylelint`** (у **`lint-style`**, у CI, вручну). Не використовуй **`bunx stylelint`** і не запускай **stylelint** напряму без **`npx`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний набір `lint-*` (навичка **`n-fix`**).
|
|
12
|
+
- **Не розширюй винятки:** не додавай зайві **`stylelint-disable`** без потреби; краще підлаштувати стилі під правила проєкту.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
**VSCode:** у **`.vscode/extensions.json`** рекомендуй **`stylelint.vscode-stylelint`**. У **`.vscode/settings.json`** вимкни вбудовану валідацію CSS/SCSS/Less і увімкни явні code actions:
|
|
15
15
|
|
|
16
16
|
```json title=".vscode/extensions.json"
|
|
17
17
|
{
|
|
@@ -19,8 +19,6 @@ version: '1.1'
|
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
в файлі .vscode/settings.json є налаштування для stylelint:
|
|
23
|
-
|
|
24
22
|
```json title=".vscode/settings.json"
|
|
25
23
|
{
|
|
26
24
|
"css.validate": false,
|
|
@@ -32,11 +30,11 @@ version: '1.1'
|
|
|
32
30
|
}
|
|
33
31
|
```
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
**`package.json`:**
|
|
36
34
|
|
|
37
35
|
```json title="package.json"
|
|
38
36
|
"scripts": {
|
|
39
|
-
"lint-style": "stylelint '**/*.{css,scss,vue}' --fix",
|
|
37
|
+
"lint-style": "npx stylelint '**/*.{css,scss,vue}' --fix",
|
|
40
38
|
},
|
|
41
39
|
"devDependencies": {
|
|
42
40
|
"@nitra/stylelint-config": "^1.4.0"
|
|
@@ -46,9 +44,9 @@ version: '1.1'
|
|
|
46
44
|
},
|
|
47
45
|
```
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
Додай **`.github/workflows/lint-style.yml`** (лише **`.yml`**, **`ga.mdc`**): після **`checkout`** — локальний composite **`setup-bun-deps`**, далі **`bun run lint-style`** (у скрипті вже **`npx stylelint`**). **Не** дублюй окремі кроки **`setup-node`** / **`oven-sh/setup-bun`** / кеш / **`npm install`**.
|
|
50
48
|
|
|
51
|
-
```yaml
|
|
49
|
+
```yaml title=".github/workflows/lint-style.yml"
|
|
52
50
|
name: StyleLint
|
|
53
51
|
|
|
54
52
|
on:
|
|
@@ -77,23 +75,20 @@ concurrency:
|
|
|
77
75
|
jobs:
|
|
78
76
|
stylelint:
|
|
79
77
|
runs-on: ubuntu-latest
|
|
78
|
+
permissions:
|
|
79
|
+
contents: read
|
|
80
80
|
steps:
|
|
81
81
|
- uses: actions/checkout@v6
|
|
82
|
+
with:
|
|
83
|
+
persist-credentials: false
|
|
84
|
+
|
|
85
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
82
86
|
|
|
83
87
|
- name: StyleLint
|
|
84
|
-
run:
|
|
85
|
-
node - <<'NODE'
|
|
86
|
-
const fs = require('fs')
|
|
87
|
-
const data = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
|
88
|
-
delete data.workspaces
|
|
89
|
-
data.devDependencies = {}
|
|
90
|
-
fs.writeFileSync('package.json', JSON.stringify(data, null, 2) + '\n')
|
|
91
|
-
NODE
|
|
92
|
-
npm install @nitra/stylelint-config
|
|
93
|
-
npx stylelint '**/*.{css,scss,vue}'
|
|
88
|
+
run: bun run lint-style
|
|
94
89
|
```
|
|
95
90
|
|
|
96
|
-
|
|
91
|
+
У корені проєкту має бути **`.stylelintignore`**:
|
|
97
92
|
|
|
98
93
|
```text title=".stylelintignore"
|
|
99
94
|
dist/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.55",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"url": "git+https://github.com/n/cursor.git"
|
|
21
21
|
},
|
|
22
22
|
"bin": {
|
|
23
|
-
"n-cursor": "./bin/n-cursor.js"
|
|
23
|
+
"n-cursor": "./bin/n-cursor.js",
|
|
24
|
+
"n-rename-yaml-extensions": "./bin/rename-yaml-extensions.mjs"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"mdc",
|
|
@@ -33,7 +34,8 @@
|
|
|
33
34
|
],
|
|
34
35
|
"type": "module",
|
|
35
36
|
"scripts": {
|
|
36
|
-
"test": "bun test tests"
|
|
37
|
+
"test": "bun test tests",
|
|
38
|
+
"rename-yaml-extensions": "bun ./bin/rename-yaml-extensions.mjs"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
41
|
"oxc-parser": "^0.124.0",
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -2,29 +2,33 @@
|
|
|
2
2
|
* Перевіряє Kubernetes YAML у шляхах з сегментом `k8s` (див. k8s.mdc).
|
|
3
3
|
*
|
|
4
4
|
* Перший рядок `# yaml-language-server: $schema=…`, без дублікатів, розширення `.yaml`
|
|
5
|
-
* (окрім `kustomization.
|
|
5
|
+
* (окрім `kustomization.yaml`); URL схеми за першим документом — kustomization / yannh / datree
|
|
6
6
|
* (datree за замовчуванням: GitHub Pages `https://datreeio.github.io/CRDs-catalog/…`).
|
|
7
7
|
*
|
|
8
8
|
* Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
|
|
9
9
|
* **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення — об'єкт, допускається
|
|
10
10
|
* порожній **`{}`**) та **`imagePullPolicy: Always`**.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* **Namespace і Kustomize:** YAML у **`…/k8s/base/`** (окрім імені **`kustomization.yaml`**)
|
|
13
|
+
* завжди має **непорожній** **`metadata.namespace`** у відповідних документах (узгоджено з dev у репозиторії),
|
|
14
|
+
* навіть якщо **`namespace:`** заданий у **`base/kustomization.yaml`**.
|
|
15
|
+
* Поза **`k8s/base`**: для файлів, досяжних з kustomization через **`resources`**, **`bases`**, **`components`**,
|
|
16
|
+
* **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**, **`metadata.namespace`** у маніфесті **не** додають;
|
|
17
|
+
* файли **поза** цим графом — **непорожній** **`metadata.namespace`** (крім **кластерних** kind; див. k8s.mdc).
|
|
14
18
|
*
|
|
15
|
-
* **`kind: Ingress`** заборонено (потрібен перехід на Gateway API).
|
|
16
|
-
* має існувати **`ru/kustomization.yaml`** з patch видалення цього kind (`$patch: delete`).
|
|
19
|
+
* **`kind: Ingress`** заборонено (потрібен перехід на Gateway API).
|
|
17
20
|
*
|
|
18
|
-
* Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**;
|
|
19
|
-
*
|
|
21
|
+
* Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**; у **`k8s/base/kustomization.yaml`**
|
|
22
|
+
* завжди має бути непорожнє поле **`namespace:`** (перевірка, якщо файл існує).
|
|
20
23
|
*
|
|
21
24
|
* Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
|
|
22
25
|
* **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
|
|
23
26
|
* компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
|
|
24
27
|
* Dockerfile — правило docker.mdc, скрипт check-docker.mjs.
|
|
25
28
|
*/
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
29
|
+
import { existsSync } from 'node:fs'
|
|
30
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
31
|
+
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
28
32
|
|
|
29
33
|
import { parseAllDocuments } from 'yaml'
|
|
30
34
|
|
|
@@ -164,34 +168,218 @@ export function isForbiddenK8sDevPath(rel) {
|
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
/**
|
|
167
|
-
*
|
|
171
|
+
* Відносний шлях від кореня репозиторію у вигляді з `/` (для множини kustomize).
|
|
172
|
+
* @param {string} root корінь cwd
|
|
173
|
+
* @param {string} abs абсолютний шлях
|
|
174
|
+
* @returns {string | null} posix-відносний шлях або null, якщо поза root
|
|
175
|
+
*/
|
|
176
|
+
function posixRelFromAbs(root, abs) {
|
|
177
|
+
const r = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
178
|
+
if (r.startsWith('..')) return null
|
|
179
|
+
return r
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Вбудовані та поширені **кластерні** `kind`, для яких **`metadata.namespace`** не застосовується.
|
|
184
|
+
* CRD з невідомим kind лишаються з вимогою namespace, якщо файл не в kustomization — за потреби додай path у `resources`.
|
|
185
|
+
* @type {Set<string>}
|
|
186
|
+
*/
|
|
187
|
+
const CLUSTER_SCOPED_KINDS = new Set([
|
|
188
|
+
'APIService',
|
|
189
|
+
'CertificateSigningRequest',
|
|
190
|
+
'ClusterCIDR',
|
|
191
|
+
'ClusterRole',
|
|
192
|
+
'ClusterRoleBinding',
|
|
193
|
+
'ComponentStatus',
|
|
194
|
+
'CSIDriver',
|
|
195
|
+
'CSINode',
|
|
196
|
+
'CustomResourceDefinition',
|
|
197
|
+
'FlowSchema',
|
|
198
|
+
'IPAddress',
|
|
199
|
+
'IngressClass',
|
|
200
|
+
'MutatingWebhookConfiguration',
|
|
201
|
+
'Namespace',
|
|
202
|
+
'Node',
|
|
203
|
+
'PersistentVolume',
|
|
204
|
+
'PriorityClass',
|
|
205
|
+
'PriorityLevelConfiguration',
|
|
206
|
+
'RuntimeClass',
|
|
207
|
+
'ServiceCIDR',
|
|
208
|
+
'StorageClass',
|
|
209
|
+
'StorageVersionMigration',
|
|
210
|
+
'ValidatingAdmissionPolicy',
|
|
211
|
+
'ValidatingAdmissionPolicyBinding',
|
|
212
|
+
'ValidatingWebhookConfiguration',
|
|
213
|
+
'VolumeAttachment'
|
|
214
|
+
])
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Чи `kind` за замовчуванням **кластерний** (без namespace у маніфесті).
|
|
218
|
+
* @param {string} kind значення `kind`
|
|
219
|
+
* @returns {boolean} true для кластерних built-in / поширених API
|
|
220
|
+
*/
|
|
221
|
+
export function isClusterScopedKubernetesKind(kind) {
|
|
222
|
+
return typeof kind === 'string' && kind !== '' && CLUSTER_SCOPED_KINDS.has(kind)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Додає рядки шляхів з поля-масиву kustomization.
|
|
227
|
+
* @param {unknown} arr значення з YAML
|
|
228
|
+
* @param {string[]} acc накопичувач
|
|
229
|
+
*/
|
|
230
|
+
function pushStringPaths(arr, acc) {
|
|
231
|
+
if (!Array.isArray(arr)) return
|
|
232
|
+
for (const item of arr) {
|
|
233
|
+
if (typeof item === 'string' && item.trim() !== '') acc.push(item.trim())
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Шляхи з полів Kustomization для resolve відносно каталогу **`kustomization.yaml`**.
|
|
239
|
+
* @param {unknown} obj корінь першого документа Kustomization
|
|
240
|
+
* @returns {string[]} відносні або абсолютні посилання з маніфесту
|
|
241
|
+
*/
|
|
242
|
+
function pathsFromKustomizationObject(obj) {
|
|
243
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return []
|
|
244
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
245
|
+
/** @type {string[]} */
|
|
246
|
+
const out = []
|
|
247
|
+
pushStringPaths(rec.resources, out)
|
|
248
|
+
pushStringPaths(rec.bases, out)
|
|
249
|
+
pushStringPaths(rec.components, out)
|
|
250
|
+
pushStringPaths(rec.crds, out)
|
|
251
|
+
pushStringPaths(rec.patchesStrategicMerge, out)
|
|
252
|
+
const patches = rec.patches
|
|
253
|
+
if (Array.isArray(patches)) {
|
|
254
|
+
for (const p of patches) {
|
|
255
|
+
if (
|
|
256
|
+
p !== null &&
|
|
257
|
+
typeof p === 'object' &&
|
|
258
|
+
!Array.isArray(p) &&
|
|
259
|
+
typeof p.path === 'string' &&
|
|
260
|
+
p.path.trim() !== ''
|
|
261
|
+
) {
|
|
262
|
+
out.push(p.path.trim())
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return out
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Збирає відносні шляхи (posix) до YAML, підключених до Kustomize з будь-якого **`kustomization.yaml`** під `k8s`.
|
|
271
|
+
* Обходить **`resources`**, **`bases`**, **`components`**, **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**;
|
|
272
|
+
* для каталогу з **`kustomization.yaml`** виконує рекурсивний обхід.
|
|
273
|
+
* @param {string} root корінь репозиторію
|
|
274
|
+
* @param {string[]} yamlFilesAbs відсортовані абсолютні шляхи до `*.yaml` / `*.yml` під k8s (для `.yml` check-k8s вимагає перейменувати на `.yaml`)
|
|
275
|
+
* @returns {Promise<Set<string>>} множина відносних шляхів до керованих файлів
|
|
276
|
+
*/
|
|
277
|
+
export async function collectKustomizeManagedRelPaths(root, yamlFilesAbs) {
|
|
278
|
+
/** @type {Set<string>} */
|
|
279
|
+
const managed = new Set()
|
|
280
|
+
const kustomizationAbsList = yamlFilesAbs.filter(abs => {
|
|
281
|
+
const b = basename(abs).toLowerCase()
|
|
282
|
+
return b === 'kustomization.yaml'
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
/** @type {Set<string>} */
|
|
286
|
+
const visitedKustomization = new Set()
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
290
|
+
* @returns {Promise<void>}
|
|
291
|
+
*/
|
|
292
|
+
async function walkKustomization(kustAbs) {
|
|
293
|
+
const normKust = resolve(kustAbs)
|
|
294
|
+
if (visitedKustomization.has(normKust)) return
|
|
295
|
+
visitedKustomization.add(normKust)
|
|
296
|
+
|
|
297
|
+
let raw
|
|
298
|
+
try {
|
|
299
|
+
raw = await readFile(normKust, 'utf8')
|
|
300
|
+
} catch {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
const lines = toLines(raw)
|
|
304
|
+
const body = yamlBodyAfterModeline(lines)
|
|
305
|
+
|
|
306
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
307
|
+
let docs
|
|
308
|
+
try {
|
|
309
|
+
docs = parseAllDocuments(body)
|
|
310
|
+
} catch {
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
const first = docs[0]?.toJSON()
|
|
314
|
+
if (first === null || first === undefined || typeof first !== 'object' || Array.isArray(first)) return
|
|
315
|
+
|
|
316
|
+
const kustDir = dirname(normKust)
|
|
317
|
+
const pathRefs = pathsFromKustomizationObject(first)
|
|
318
|
+
|
|
319
|
+
for (const ref of pathRefs) {
|
|
320
|
+
if (!ref.includes('://')) {
|
|
321
|
+
const resolved = resolve(kustDir, ref)
|
|
322
|
+
let st
|
|
323
|
+
try {
|
|
324
|
+
st = await stat(resolved)
|
|
325
|
+
} catch {
|
|
326
|
+
st = undefined
|
|
327
|
+
}
|
|
328
|
+
if (st) {
|
|
329
|
+
if (st.isFile()) {
|
|
330
|
+
if (/\.ya?ml$/iu.test(resolved)) {
|
|
331
|
+
const pr = posixRelFromAbs(root, resolved)
|
|
332
|
+
if (pr !== null) managed.add(pr)
|
|
333
|
+
}
|
|
334
|
+
} else if (st.isDirectory()) {
|
|
335
|
+
const childK = existsSync(join(resolved, 'kustomization.yaml'))
|
|
336
|
+
? join(resolved, 'kustomization.yaml')
|
|
337
|
+
: null
|
|
338
|
+
if (childK !== null) {
|
|
339
|
+
await walkKustomization(childK)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const k of kustomizationAbsList) {
|
|
348
|
+
await walkKustomization(k)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return managed
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Чи це **`k8s/base/kustomization.yaml`** (перевірка обов’язкового непорожнього **`namespace:`**).
|
|
168
356
|
* @param {string} rel шлях від кореня репозиторію
|
|
169
|
-
* @returns {boolean} true
|
|
357
|
+
* @returns {boolean} true для шляху виду `…/k8s/base/kustomization.yaml`
|
|
170
358
|
*/
|
|
171
359
|
export function isBaseKustomizationPath(rel) {
|
|
172
360
|
const n = rel.replaceAll('\\', '/')
|
|
173
|
-
return /(^|\/)k8s\/base\/kustomization\.yaml$/u.test(n)
|
|
361
|
+
return /(^|\/)k8s\/base\/kustomization\.yaml$/u.test(n)
|
|
174
362
|
}
|
|
175
363
|
|
|
176
364
|
/**
|
|
177
|
-
* Чи
|
|
365
|
+
* Чи є в Kustomization для **`base`** завжди обов’язкове непорожнє поле **`namespace:`** (k8s.mdc).
|
|
178
366
|
* @param {unknown} obj перший документ YAML
|
|
179
367
|
* @returns {string | null} текст порушення або null, якщо ок
|
|
180
368
|
*/
|
|
181
369
|
export function baseKustomizationNamespaceViolation(obj) {
|
|
182
370
|
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
183
|
-
return 'у base/kustomization.yaml має бути непорожній namespace (див. k8s.mdc)'
|
|
371
|
+
return 'у base/kustomization.yaml завжди має бути непорожній namespace: (див. k8s.mdc)'
|
|
184
372
|
}
|
|
185
373
|
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
186
374
|
const ns = rec.namespace
|
|
187
375
|
if (typeof ns === 'string' && ns.trim() !== '') {
|
|
188
376
|
return null
|
|
189
377
|
}
|
|
190
|
-
return 'у base/kustomization.yaml
|
|
378
|
+
return 'у base/kustomization.yaml завжди додай непорожній namespace: (наприклад namespace: dev; див. k8s.mdc)'
|
|
191
379
|
}
|
|
192
380
|
|
|
193
381
|
/**
|
|
194
|
-
* Збирає всі yaml
|
|
382
|
+
* Збирає всі `*.yaml` та `*.yml` під деревом від кореня cwd, якщо шлях містить сегмент `k8s` (для `.yml` далі — помилка перейменування).
|
|
195
383
|
* @param {string} root корінь репозиторію (cwd)
|
|
196
384
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до файлів
|
|
197
385
|
*/
|
|
@@ -276,14 +464,13 @@ export function ruKustomizationHasHealthCheckDeletePatch(raw) {
|
|
|
276
464
|
}
|
|
277
465
|
|
|
278
466
|
/**
|
|
279
|
-
* Шукає **Ingress**
|
|
467
|
+
* Шукає **Ingress** у розібраних документах; реєструє порушення.
|
|
280
468
|
* @param {string} rel відносний шлях до файлу
|
|
281
469
|
* @param {string} body YAML після modeline
|
|
282
470
|
* @param {(msg: string) => void} fail callback для помилки (Ingress)
|
|
283
|
-
* @param {string[]} healthCheckPolicyFiles накопичувач шляхів, де зустріли HealthCheckPolicy
|
|
284
471
|
* @returns {void}
|
|
285
472
|
*/
|
|
286
|
-
function
|
|
473
|
+
function scanIngressInYamlDocuments(rel, body, fail) {
|
|
287
474
|
/** @type {import('yaml').Document[]} */
|
|
288
475
|
let docs
|
|
289
476
|
try {
|
|
@@ -299,56 +486,14 @@ function scanIngressAndHealthCheckPolicy(rel, body, fail, healthCheckPolicyFiles
|
|
|
299
486
|
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
300
487
|
if (rec.kind === 'Ingress') {
|
|
301
488
|
fail(
|
|
302
|
-
`${rel}: знайдено kind: Ingress (документ ${di + 1}) — заміни на Gateway API: HTTPRoute (hr.yaml), HealthCheckPolicy (hc.yaml)
|
|
489
|
+
`${rel}: знайдено kind: Ingress (документ ${di + 1}) — заміни на Gateway API: HTTPRoute (hr.yaml), HealthCheckPolicy (hc.yaml) (див. k8s.mdc)`
|
|
303
490
|
)
|
|
304
|
-
} else if (rec.kind === 'HealthCheckPolicy' && !healthCheckPolicyFiles.includes(rel)) {
|
|
305
|
-
healthCheckPolicyFiles.push(rel)
|
|
306
491
|
}
|
|
307
492
|
}
|
|
308
493
|
}
|
|
309
494
|
}
|
|
310
495
|
}
|
|
311
496
|
|
|
312
|
-
/**
|
|
313
|
-
* Якщо у дереві k8s є HealthCheckPolicy, вимагає **ru/kustomization.yaml** з patch видалення.
|
|
314
|
-
* @param {string} root корінь cwd
|
|
315
|
-
* @param {string[]} yamlFiles абсолютні шляхи до yaml під k8s
|
|
316
|
-
* @param {string[]} healthCheckPolicyFiles відносні шляхи з HealthCheckPolicy
|
|
317
|
-
* @param {(msg: string) => void} fail callback для помилки (немає ru або немає patch)
|
|
318
|
-
* @returns {Promise<void>} завершення після перевірки overlay ru
|
|
319
|
-
*/
|
|
320
|
-
async function ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyFiles, fail) {
|
|
321
|
-
if (healthCheckPolicyFiles.length === 0) {
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const ruAbsList = yamlFiles.filter(abs => isRuKustomizationPath(relative(root, abs) || abs))
|
|
326
|
-
if (ruAbsList.length === 0) {
|
|
327
|
-
fail(
|
|
328
|
-
`Знайдено HealthCheckPolicy у ${healthCheckPolicyFiles.join(', ')} — додай ru/kustomization.yaml з patch видалення (див. k8s.mdc)`
|
|
329
|
-
)
|
|
330
|
-
return
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
for (const abs of ruAbsList) {
|
|
334
|
-
let raw
|
|
335
|
-
try {
|
|
336
|
-
raw = await readFile(abs, 'utf8')
|
|
337
|
-
} catch (error) {
|
|
338
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
339
|
-
fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
|
|
340
|
-
return
|
|
341
|
-
}
|
|
342
|
-
if (ruKustomizationHasHealthCheckDeletePatch(raw)) {
|
|
343
|
-
return
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
fail(
|
|
348
|
-
'Є HealthCheckPolicy, але жоден ru/kustomization.yaml не містить очікуваного patch видалення (kind: HealthCheckPolicy, metadata.name, $patch: delete) — див. k8s.mdc'
|
|
349
|
-
)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
497
|
/**
|
|
353
498
|
* Чи порушує маніфест вимогу **`Deployment.spec.template.spec.containers[].resources`** (див. k8s.mdc).
|
|
354
499
|
* @param {unknown} manifest корінь YAML-документа як об'єкт JavaScript
|
|
@@ -426,7 +571,7 @@ export function deploymentImagePullPolicyViolation(manifest) {
|
|
|
426
571
|
}
|
|
427
572
|
|
|
428
573
|
/**
|
|
429
|
-
*
|
|
574
|
+
* Для маніфестів, **підключених** до Kustomize (шлях у `resources` / `patches` / …), **metadata.namespace** не додають.
|
|
430
575
|
* @param {unknown} manifest корінь YAML-документа
|
|
431
576
|
* @returns {string | null} текст порушення або null, якщо поля немає
|
|
432
577
|
*/
|
|
@@ -436,7 +581,37 @@ export function metadataNamespaceForbiddenViolation(manifest) {
|
|
|
436
581
|
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
437
582
|
const meta = rec.metadata
|
|
438
583
|
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta) && 'namespace' in meta) {
|
|
439
|
-
return 'metadata.namespace заборонено —
|
|
584
|
+
return 'metadata.namespace заборонено — namespace задає kustomization.yaml (поле namespace); файл підключено через resources / patches / … (див. k8s.mdc)'
|
|
585
|
+
}
|
|
586
|
+
return null
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Вимагає непорожній **metadata.namespace** для namespaced-документів (крім кластерних kind).
|
|
591
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
592
|
+
* @param {boolean} [inBaseDir] true — файл у **`k8s/base/`** (текст повідомлення для base)
|
|
593
|
+
* @returns {string | null} текст порушення або null, якщо перевірка не застосовується / ок
|
|
594
|
+
*/
|
|
595
|
+
export function metadataNamespaceRequiredViolation(manifest, inBaseDir = false) {
|
|
596
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
597
|
+
return null
|
|
598
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
599
|
+
if (rec.kind === 'List' || rec.kind === 'Kustomization') return null
|
|
600
|
+
if (typeof rec.kind !== 'string' || rec.kind === '') return null
|
|
601
|
+
if (typeof rec.apiVersion !== 'string' || rec.apiVersion === '') return null
|
|
602
|
+
if (isClusterScopedKubernetesKind(rec.kind)) return null
|
|
603
|
+
const meta = rec.metadata
|
|
604
|
+
if (meta === null || meta === undefined || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
605
|
+
return inBaseDir
|
|
606
|
+
? 'додай metadata з непорожнім metadata.namespace — у k8s/base у кожному ресурсному YAML має бути явний namespace (див. k8s.mdc)'
|
|
607
|
+
: 'додай metadata з непорожнім metadata.namespace — файл не підключено до жодного kustomization.yaml (resources, patches, …) під k8s (див. k8s.mdc)'
|
|
608
|
+
}
|
|
609
|
+
const m = /** @type {Record<string, unknown>} */ (meta)
|
|
610
|
+
const ns = m.namespace
|
|
611
|
+
if (typeof ns !== 'string' || ns.trim() === '') {
|
|
612
|
+
return inBaseDir
|
|
613
|
+
? 'metadata.namespace обов’язковий у k8s/base — додай явний namespace у маніфесті (див. k8s.mdc)'
|
|
614
|
+
: 'metadata.namespace обов’язковий — файл не перелічений у kustomization.yaml під k8s; додай path у kustomization або явний namespace (див. k8s.mdc)'
|
|
440
615
|
}
|
|
441
616
|
return null
|
|
442
617
|
}
|
|
@@ -444,10 +619,22 @@ export function metadataNamespaceForbiddenViolation(manifest) {
|
|
|
444
619
|
/**
|
|
445
620
|
* Чи ім’я файлу — kustomization (дозволяє не застосовувати перевірку metadata.namespace до вмісту).
|
|
446
621
|
* @param {string} baseLower basename у нижньому регістрі
|
|
447
|
-
* @returns {boolean} true для `kustomization.yaml`
|
|
622
|
+
* @returns {boolean} true для `kustomization.yaml`
|
|
448
623
|
*/
|
|
449
624
|
function isKustomizationFileName(baseLower) {
|
|
450
|
-
return baseLower === 'kustomization.yaml'
|
|
625
|
+
return baseLower === 'kustomization.yaml'
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Чи це **ресурсний** YAML у каталозі **`k8s/base`** (не `kustomization.yaml`).
|
|
630
|
+
* @param {string} rel відносний шлях від кореня репозиторію
|
|
631
|
+
* @param {string} baseLower basename у нижньому регістрі
|
|
632
|
+
* @returns {boolean} true для `…/k8s/base/*.yaml` окрім kustomization
|
|
633
|
+
*/
|
|
634
|
+
export function isK8sBaseManifestYamlPath(rel, baseLower) {
|
|
635
|
+
if (isKustomizationFileName(baseLower)) return false
|
|
636
|
+
const n = rel.replaceAll('\\', '/')
|
|
637
|
+
return /(^|\/)k8s\/base\//u.test(n)
|
|
451
638
|
}
|
|
452
639
|
|
|
453
640
|
/**
|
|
@@ -456,8 +643,9 @@ function isKustomizationFileName(baseLower) {
|
|
|
456
643
|
* @param {string} baseLower basename файлу (нижній регістр)
|
|
457
644
|
* @param {string} body вміст після modeline
|
|
458
645
|
* @param {(msg: string) => void} fail реєстрація помилки
|
|
646
|
+
* @param {boolean} kustomizeManaged чи файл досяжний з kustomization.yaml (resources / patches / …)
|
|
459
647
|
*/
|
|
460
|
-
function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail) {
|
|
648
|
+
function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged) {
|
|
461
649
|
/** @type {import('yaml').Document[]} */
|
|
462
650
|
let docs
|
|
463
651
|
try {
|
|
@@ -469,6 +657,7 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail) {
|
|
|
469
657
|
}
|
|
470
658
|
|
|
471
659
|
const skipMetaNs = isKustomizationFileName(baseLower)
|
|
660
|
+
const inBaseManifest = isK8sBaseManifestYamlPath(rel, baseLower)
|
|
472
661
|
|
|
473
662
|
for (const [di, doc] of docs.entries()) {
|
|
474
663
|
if (doc.errors.length > 0) {
|
|
@@ -476,9 +665,21 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail) {
|
|
|
476
665
|
} else {
|
|
477
666
|
const obj = doc.toJSON()
|
|
478
667
|
if (!skipMetaNs) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
668
|
+
if (inBaseManifest) {
|
|
669
|
+
const req = metadataNamespaceRequiredViolation(obj, true)
|
|
670
|
+
if (req !== null) {
|
|
671
|
+
fail(`${rel}: документ ${di + 1}: ${req}`)
|
|
672
|
+
}
|
|
673
|
+
} else if (kustomizeManaged) {
|
|
674
|
+
const ns = metadataNamespaceForbiddenViolation(obj)
|
|
675
|
+
if (ns !== null) {
|
|
676
|
+
fail(`${rel}: документ ${di + 1}: ${ns}`)
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
const req = metadataNamespaceRequiredViolation(obj, false)
|
|
680
|
+
if (req !== null) {
|
|
681
|
+
fail(`${rel}: документ ${di + 1}: ${req}`)
|
|
682
|
+
}
|
|
482
683
|
}
|
|
483
684
|
}
|
|
484
685
|
const resV = deploymentResourcesViolation(obj)
|
|
@@ -512,7 +713,7 @@ export function expectedSchemaUrl(filePath, doc) {
|
|
|
512
713
|
const base = basename(filePath)
|
|
513
714
|
const baseLower = base.toLowerCase()
|
|
514
715
|
|
|
515
|
-
if (baseLower === 'kustomization.yaml'
|
|
716
|
+
if (baseLower === 'kustomization.yaml') {
|
|
516
717
|
return { expected: KUSTOMIZATION_SCHEMA, reason: 'kustomization (ім’я файлу)' }
|
|
517
718
|
}
|
|
518
719
|
|
|
@@ -574,15 +775,15 @@ function countSchemaModelines(lines) {
|
|
|
574
775
|
* @param {string} root корінь репозиторію
|
|
575
776
|
* @param {(msg: string) => void} fail реєстрація помилки
|
|
576
777
|
* @param {(msg: string) => void} pass реєстрація успіху
|
|
577
|
-
* @param {string
|
|
778
|
+
* @param {Set<string>} kustomizeManagedRel відносні posix-шляхи з collectKustomizeManagedRelPaths
|
|
578
779
|
* @returns {Promise<void>}
|
|
579
780
|
*/
|
|
580
|
-
async function checkK8sYamlFile(abs, root, fail, pass,
|
|
581
|
-
const rel = relative(root, abs) || abs
|
|
781
|
+
async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
|
|
782
|
+
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
582
783
|
const base = basename(abs)
|
|
583
784
|
const baseLower = base.toLowerCase()
|
|
584
785
|
|
|
585
|
-
if (baseLower.endsWith('.yml')
|
|
786
|
+
if (baseLower.endsWith('.yml')) {
|
|
586
787
|
fail(`${rel}: розширення .yml — перейменуй на .yaml (див. k8s.mdc)`)
|
|
587
788
|
return
|
|
588
789
|
}
|
|
@@ -615,7 +816,7 @@ async function checkK8sYamlFile(abs, root, fail, pass, healthCheckPolicyFiles) {
|
|
|
615
816
|
|
|
616
817
|
const body = yamlBodyAfterModeline(lines)
|
|
617
818
|
|
|
618
|
-
|
|
819
|
+
scanIngressInYamlDocuments(rel, body, fail)
|
|
619
820
|
|
|
620
821
|
if (schemaUrl.startsWith('file:')) {
|
|
621
822
|
pass(`${rel}: локальна схема (file:) — перевірка URL за apiVersion/kind пропущена`)
|
|
@@ -639,7 +840,8 @@ async function checkK8sYamlFile(abs, root, fail, pass, healthCheckPolicyFiles) {
|
|
|
639
840
|
return
|
|
640
841
|
}
|
|
641
842
|
|
|
642
|
-
|
|
843
|
+
const kustomizeManaged = kustomizeManagedRel.has(rel)
|
|
844
|
+
validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged)
|
|
643
845
|
}
|
|
644
846
|
|
|
645
847
|
/**
|
|
@@ -659,7 +861,7 @@ function assertNoForbiddenK8sDevPaths(yamlFiles, root, fail) {
|
|
|
659
861
|
}
|
|
660
862
|
|
|
661
863
|
/**
|
|
662
|
-
* Якщо є **`k8s/base/kustomization.yaml`**, у ньому має бути непорожній **`namespace
|
|
864
|
+
* Якщо є **`k8s/base/kustomization.yaml`**, у ньому **завжди** має бути непорожній **`namespace:`**.
|
|
663
865
|
* @param {string} root корінь репозиторію
|
|
664
866
|
* @param {string[]} yamlFiles абсолютні шляхи
|
|
665
867
|
* @param {(msg: string) => void} fail callback для реєстрації порушення
|
|
@@ -710,7 +912,7 @@ export async function check() {
|
|
|
710
912
|
const yamlFiles = await findK8sYamlFiles(root)
|
|
711
913
|
|
|
712
914
|
if (yamlFiles.length === 0) {
|
|
713
|
-
pass('Немає yaml
|
|
915
|
+
pass('Немає *.yaml під k8s — перевірку $schema пропущено')
|
|
714
916
|
return 0
|
|
715
917
|
}
|
|
716
918
|
|
|
@@ -718,15 +920,12 @@ export async function check() {
|
|
|
718
920
|
|
|
719
921
|
assertNoForbiddenK8sDevPaths(yamlFiles, root, fail)
|
|
720
922
|
|
|
721
|
-
|
|
722
|
-
const healthCheckPolicyFiles = []
|
|
923
|
+
const kustomizeManagedRel = await collectKustomizeManagedRelPaths(root, yamlFiles)
|
|
723
924
|
|
|
724
925
|
for (const abs of yamlFiles) {
|
|
725
|
-
await checkK8sYamlFile(abs, root, fail, pass,
|
|
926
|
+
await checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel)
|
|
726
927
|
}
|
|
727
928
|
|
|
728
|
-
await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyFiles, fail)
|
|
729
|
-
|
|
730
929
|
await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
|
|
731
930
|
|
|
732
931
|
return exitCode
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Перевіряє CSS/SCSS лінт за правилом style-lint.mdc.
|
|
3
3
|
*
|
|
4
|
-
* `@nitra/stylelint-config`,
|
|
5
|
-
*
|
|
4
|
+
* Очікування: `@nitra/stylelint-config`, `lint-style` через `npx stylelint`, `.stylelintignore`,
|
|
5
|
+
* workflow `lint-style.yml` (у `run` — `npx stylelint` або `bun run lint-style`), VSCode stylelint,
|
|
6
|
+
* `css.validate` / `scss.validate` / `less.validate`: false.
|
|
6
7
|
*/
|
|
7
8
|
import { existsSync } from 'node:fs'
|
|
8
9
|
import { readFile } from 'node:fs/promises'
|
|
@@ -24,8 +25,14 @@ export async function check() {
|
|
|
24
25
|
if (existsSync('package.json')) {
|
|
25
26
|
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
const lintStyle = pkg.scripts?.['lint-style']
|
|
29
|
+
if (lintStyle) {
|
|
28
30
|
pass('package.json містить скрипт lint-style')
|
|
31
|
+
if (String(lintStyle).includes('npx stylelint')) {
|
|
32
|
+
pass('lint-style викликає stylelint через npx')
|
|
33
|
+
} else {
|
|
34
|
+
fail("lint-style має викликати stylelint через npx — наприклад: npx stylelint '**/*.{css,scss,vue}' --fix")
|
|
35
|
+
}
|
|
29
36
|
} else {
|
|
30
37
|
fail('package.json не містить скрипт "lint-style"')
|
|
31
38
|
}
|
|
@@ -56,11 +63,13 @@ export async function check() {
|
|
|
56
63
|
const content = await readFile('.github/workflows/lint-style.yml', 'utf8')
|
|
57
64
|
pass('lint-style.yml існує')
|
|
58
65
|
const root = parseWorkflowYaml(content)
|
|
59
|
-
const ok = root
|
|
66
|
+
const ok = root
|
|
67
|
+
? anyRunStepIncludesStylelint(root)
|
|
68
|
+
: content.includes('npx stylelint') || content.includes('bun run lint-style')
|
|
60
69
|
if (ok) {
|
|
61
|
-
pass('lint-style.yml містить stylelint у кроці run')
|
|
70
|
+
pass('lint-style.yml містить npx stylelint або bun run lint-style у кроці run')
|
|
62
71
|
} else {
|
|
63
|
-
fail('lint-style.yml
|
|
72
|
+
fail('lint-style.yml має містити npx stylelint або bun run lint-style у кроці run')
|
|
64
73
|
}
|
|
65
74
|
} else {
|
|
66
75
|
fail('.github/workflows/lint-style.yml не існує — створи його')
|
|
@@ -89,6 +98,11 @@ export async function check() {
|
|
|
89
98
|
} else {
|
|
90
99
|
fail('settings.json: scss.validate має бути false')
|
|
91
100
|
}
|
|
101
|
+
if (s['less.validate'] === false) {
|
|
102
|
+
pass('less.validate вимкнено')
|
|
103
|
+
} else {
|
|
104
|
+
fail('settings.json: less.validate має бути false')
|
|
105
|
+
}
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
return exitCode
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перейменовує розширення YAML за домовленістю репозиторію (k8s.mdc, ga.mdc). Лише логіка; **CLI** — **`bin/rename-yaml-extensions.mjs`**
|
|
3
|
+
* та підкоманда **`npx \@nitra/cursor rename-yaml-extensions`**.
|
|
4
|
+
*
|
|
5
|
+
* - Файли з сегментом шляху `k8s` та суфіксом `.yml` → `.yaml` (маніфести під k8s).
|
|
6
|
+
* - Файли з сегментом `.github` та суфіксом `.yaml` → `.yml` (workflows тощо; у workflows лише `.yml`).
|
|
7
|
+
*
|
|
8
|
+
* Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next` — як у **`walkDir`**.
|
|
9
|
+
*
|
|
10
|
+
* Розбір аргументів для CLI: **`parseRenameYamlArgs`** (`--dry-run`, `--root=…`).
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import { rename } from 'node:fs/promises'
|
|
14
|
+
import { cwd } from 'node:process'
|
|
15
|
+
import { relative, resolve } from 'node:path'
|
|
16
|
+
|
|
17
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Відносний шлях від кореня з `/`; `null`, якщо поза root.
|
|
21
|
+
* @param {string} rootAbs абсолютний корінь
|
|
22
|
+
* @param {string} fileAbs абсолютний шлях до файлу
|
|
23
|
+
* @returns {string | null} відносний шлях з `/` або null, якщо fileAbs поза rootAbs
|
|
24
|
+
*/
|
|
25
|
+
export function posixRelFromRoot(rootAbs, fileAbs) {
|
|
26
|
+
const r = (relative(rootAbs, resolve(fileAbs)) || fileAbs).replaceAll('\\', '/')
|
|
27
|
+
if (r.startsWith('..')) return null
|
|
28
|
+
return r
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Чи шлях підходить під k8s-маніфести: є сегмент `k8s`, суфікс `.yml` (регістр розширення ігнорується).
|
|
33
|
+
* @param {string} relPosix відносний шлях
|
|
34
|
+
* @returns {boolean} true, якщо є сегмент k8s і суфікс .yml
|
|
35
|
+
*/
|
|
36
|
+
export function pathMatchesK8sYml(relPosix) {
|
|
37
|
+
if (!/\.yml$/iu.test(relPosix)) return false
|
|
38
|
+
return relPosix.split('/').includes('k8s')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Чи шлях підходить під `.github`: є сегмент `.github`, суфікс `.yaml` (регістр розширення ігнорується).
|
|
43
|
+
* @param {string} relPosix відносний шлях
|
|
44
|
+
* @returns {boolean} true, якщо є сегмент .github і суфікс .yaml
|
|
45
|
+
*/
|
|
46
|
+
export function pathMatchesGithubYaml(relPosix) {
|
|
47
|
+
if (!/\.yaml$/iu.test(relPosix)) return false
|
|
48
|
+
return relPosix.split('/').includes('.github')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Замінює останнє розширення файлу на **newExt** (з крапкою, наприклад **`.yaml`**).
|
|
53
|
+
* @param {string} relPosix відносний шлях
|
|
54
|
+
* @param {string} newExt нове розширення
|
|
55
|
+
* @returns {string} шлях з останнім розширенням, заміненим на newExt
|
|
56
|
+
*/
|
|
57
|
+
export function replaceExtension(relPosix, newExt) {
|
|
58
|
+
const m = relPosix.match(/^(.+)(\.[^./\\]+)$/u)
|
|
59
|
+
if (!m) return relPosix + newExt
|
|
60
|
+
return m[1] + newExt
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Збирає операції перейменування (без виконання).
|
|
65
|
+
* @param {string} rootAbs абсолютний корінь репозиторію
|
|
66
|
+
* @returns {Promise<Array<{ kind: 'k8s' | 'github', fromAbs: string, toAbs: string, relFrom: string, relTo: string }>>} відсортовані операції перейменування без запису на диск
|
|
67
|
+
*/
|
|
68
|
+
async function collectRenameOps(rootAbs) {
|
|
69
|
+
/** @type {Array<{ kind: 'k8s' | 'github', fromAbs: string, toAbs: string, relFrom: string, relTo: string }>} */
|
|
70
|
+
const ops = []
|
|
71
|
+
|
|
72
|
+
await walkDir(rootAbs, fileAbs => {
|
|
73
|
+
const rel = posixRelFromRoot(rootAbs, fileAbs)
|
|
74
|
+
if (rel === null) return
|
|
75
|
+
if (pathMatchesK8sYml(rel)) {
|
|
76
|
+
const relTo = replaceExtension(rel, '.yaml')
|
|
77
|
+
if (relTo === rel) return
|
|
78
|
+
ops.push({
|
|
79
|
+
kind: 'k8s',
|
|
80
|
+
fromAbs: resolve(rootAbs, rel),
|
|
81
|
+
toAbs: resolve(rootAbs, relTo),
|
|
82
|
+
relFrom: rel,
|
|
83
|
+
relTo
|
|
84
|
+
})
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
if (pathMatchesGithubYaml(rel)) {
|
|
88
|
+
const relTo = replaceExtension(rel, '.yml')
|
|
89
|
+
if (relTo === rel) return
|
|
90
|
+
ops.push({
|
|
91
|
+
kind: 'github',
|
|
92
|
+
fromAbs: resolve(rootAbs, rel),
|
|
93
|
+
toAbs: resolve(rootAbs, relTo),
|
|
94
|
+
relFrom: rel,
|
|
95
|
+
relTo
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
ops.sort((a, b) => {
|
|
101
|
+
const ko = (a.kind === 'k8s' ? 0 : 1) - (b.kind === 'k8s' ? 0 : 1)
|
|
102
|
+
if (ko !== 0) return ko
|
|
103
|
+
return a.relFrom.localeCompare(b.relFrom)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return ops
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Виконує перейменування за правилами k8s / .github.
|
|
111
|
+
* @param {string} root корінь обходу (відносний або абсолютний)
|
|
112
|
+
* @param {{ dryRun?: boolean }} [options] опції: лише симуляція без `rename`, якщо dryRun true
|
|
113
|
+
* @returns {Promise<{ renamed: { relFrom: string, relTo: string }[], errors: string[] }>} списки успішних перейменувань і текстів помилок
|
|
114
|
+
*/
|
|
115
|
+
export async function renameYamlExtensions(root, options = {}) {
|
|
116
|
+
const dryRun = options.dryRun === true
|
|
117
|
+
const rootAbs = resolve(root)
|
|
118
|
+
const ops = await collectRenameOps(rootAbs)
|
|
119
|
+
|
|
120
|
+
/** @type { { relFrom: string, relTo: string }[]} */
|
|
121
|
+
const renamed = []
|
|
122
|
+
/** @type {string[]} */
|
|
123
|
+
const errors = []
|
|
124
|
+
|
|
125
|
+
for (const op of ops) {
|
|
126
|
+
if (!existsSync(op.fromAbs)) {
|
|
127
|
+
errors.push(`${op.relFrom}: файл зник перед перейменуванням`)
|
|
128
|
+
} else if (existsSync(op.toAbs)) {
|
|
129
|
+
errors.push(`${op.relFrom} → ${op.relTo}: цільовий файл уже існує, пропущено`)
|
|
130
|
+
} else if (dryRun) {
|
|
131
|
+
renamed.push({ relFrom: op.relFrom, relTo: op.relTo })
|
|
132
|
+
} else {
|
|
133
|
+
try {
|
|
134
|
+
await rename(op.fromAbs, op.toAbs)
|
|
135
|
+
renamed.push({ relFrom: op.relFrom, relTo: op.relTo })
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
138
|
+
errors.push(`${op.relFrom}: ${msg}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { renamed, errors }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Розбір CLI: **`--dry-run`**, **`--root=...`**.
|
|
148
|
+
* @param {string[]} argv зазвичай **`process.argv.slice(2)`**
|
|
149
|
+
* @returns {{ dryRun: boolean, root: string }} прапор симуляції та абсолютний корінь обходу
|
|
150
|
+
*/
|
|
151
|
+
export function parseRenameYamlArgs(argv) {
|
|
152
|
+
let dryRun = false
|
|
153
|
+
let root = cwd()
|
|
154
|
+
for (const a of argv) {
|
|
155
|
+
if (a === '--dry-run') dryRun = true
|
|
156
|
+
else if (a.startsWith('--root=')) root = resolve(a.slice('--root='.length))
|
|
157
|
+
}
|
|
158
|
+
return { dryRun, root }
|
|
159
|
+
}
|
package/scripts/run-k8s.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Запуск kubeconform та kubescape для каталогів `…/k8s`, де є YAML-маніфести (див. k8s.mdc).
|
|
3
3
|
*
|
|
4
|
-
* Знаходить унікальні корені каталогів із іменем `k8s` за шляхами файлів
|
|
5
|
-
* (той самий принцип сегмента `k8s`, що й у check-k8s.mjs). Якщо таких файлів немає — вихід 0
|
|
4
|
+
* Знаходить унікальні корені каталогів із іменем `k8s` за шляхами файлів **`*.yaml`**
|
|
5
|
+
* (той самий принцип сегмента `k8s`, що й у check-k8s.mjs; розширення **`.yml`** під `k8s` не використовується). Якщо таких файлів немає — вихід 0
|
|
6
6
|
* без виклику зовнішніх CLI.
|
|
7
7
|
*
|
|
8
8
|
* kubeconform перевіряє маніфести проти OpenAPI-схем Kubernetes; kubescape — сканування на
|
|
@@ -52,7 +52,7 @@ export function k8sRootFromFile(absFile) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Унікальні корені `k8s`
|
|
55
|
+
* Унікальні корені `k8s` за наявності `*.yaml` під деревом cwd.
|
|
56
56
|
* @param {string} root корінь репозиторію
|
|
57
57
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до каталогів `k8s`
|
|
58
58
|
*/
|
|
@@ -61,7 +61,7 @@ export async function findK8sRoots(root) {
|
|
|
61
61
|
const roots = new Set()
|
|
62
62
|
await walkDir(root, p => {
|
|
63
63
|
if (!pathHasK8sSegment(p)) return
|
|
64
|
-
if (!/\.
|
|
64
|
+
if (!/\.yaml$/iu.test(p)) return
|
|
65
65
|
const k8sRoot = k8sRootFromFile(p)
|
|
66
66
|
if (k8sRoot) roots.add(k8sRoot)
|
|
67
67
|
})
|
|
@@ -123,7 +123,7 @@ async function main() {
|
|
|
123
123
|
const dirs = await findK8sRoots(root)
|
|
124
124
|
|
|
125
125
|
if (dirs.length === 0) {
|
|
126
|
-
console.log('run-k8s: немає yaml
|
|
126
|
+
console.log('run-k8s: немає *.yaml під k8s — kubeconform і kubescape пропущено')
|
|
127
127
|
return 0
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -341,10 +341,11 @@ export function anyRunStepIncludes(root, needle) {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
/**
|
|
344
|
-
* Чи
|
|
344
|
+
* Чи викликається stylelint коректно: `npx stylelint` у run або `bun run lint-style`
|
|
345
|
+
* (скрипт `lint-style` у package.json має містити `npx stylelint`).
|
|
345
346
|
* @param {Record<string, unknown>} root корінь workflow
|
|
346
|
-
* @returns {boolean} `true`, якщо
|
|
347
|
+
* @returns {boolean} `true`, якщо умова виконана
|
|
347
348
|
*/
|
|
348
349
|
export function anyRunStepIncludesStylelint(root) {
|
|
349
|
-
return anyRunStepIncludes(root, 'stylelint')
|
|
350
|
+
return anyRunStepIncludes(root, 'npx stylelint') || anyRunStepIncludes(root, 'bun run lint-style')
|
|
350
351
|
}
|
package/skills/fix/SKILL.md
CHANGED