@nitra/cursor 1.8.75 → 1.8.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mdc/abie.mdc +4 -4
- package/mdc/docker.mdc +8 -3
- package/mdc/k8s.mdc +0 -2
- package/mdc/style-lint.mdc +3 -3
- package/package.json +1 -1
- package/scripts/check-abie.mjs +138 -68
- package/scripts/check-k8s.mjs +94 -98
- package/scripts/check-style-lint.mjs +6 -6
- package/scripts/utils/gha-workflow.mjs +2 -3
package/mdc/abie.mdc
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила для проєктів AbInBev Efes
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.8'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**), видалення **HealthCheckPolicy** у **ru**), а також гілки **dev**, **ua**, **ru** у **clean-merged-branch**.
|
|
8
8
|
|
|
9
9
|
**`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
|
|
10
10
|
|
|
11
|
-
**Канон перевірки** — **`npm/scripts/check-abie.mjs`**: верхній JSDoc і реалізація задають точні умови, допустимі домени для hostnames, тексти помилок. Нижче — зміст правила й орієнтовні фрагменти YAML; не дублюй тут покроковий алгоритм зі скрипта.
|
|
11
|
+
**Канон перевірки** — **`npm/scripts/check-abie.mjs`** у пакеті **`@nitra/cursor`**: верхній JSDoc і реалізація задають точні умови, допустимі домени для hostnames, тексти помилок. Нижче — зміст правила й орієнтовні фрагменти YAML; не дублюй тут покроковий алгоритм зі скрипта.
|
|
12
12
|
|
|
13
13
|
## k8s: `hc.yaml` поруч із Deployment
|
|
14
14
|
|
|
@@ -36,7 +36,7 @@ spec:
|
|
|
36
36
|
|
|
37
37
|
## k8s: overlay **HTTPRoute** (**ua** / **ru**)
|
|
38
38
|
|
|
39
|
-
За наявності **Deployment** під **k8s** у
|
|
39
|
+
За наявності **Deployment** під **k8s** і наявності **Vite** (**`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`** у каталозі пакета) у **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** цього пакета потрібні **inline JSON6902** у **`patches`**: **target** **`kind: HTTPRoute`**, **непорожній `name`** (як у маніфесті маршруту). Мають бути зміни **`/spec/hostnames`** (домени abie — у скрипті) та **`/spec/parentRefs/0/namespace`** (**`ua`** / **`ru`**). Для **ru** — анотація **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**. Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
|
|
40
40
|
|
|
41
41
|
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
42
42
|
- target:
|
|
@@ -95,7 +95,7 @@ patches:
|
|
|
95
95
|
|
|
96
96
|
## k8s: overlay **ua** / **ru** і nodeSelector
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
У **`…/ua/kustomization.yaml`** та **`…/ru/kustomization.yaml`** того пакета, у дереві **`k8s`** якого є **Deployment**, потрібен patch на **`kind: Deployment`**: **ua** — **`spec.template.spec.nodeSelector`** з **`preem: false`**; **ru** — **`spec.template.spec.nodeSelector`** з **`yandex.cloud/preemptible: false`**. Форму **JSON6902** (шлях **`/spec/template/spec/nodeSelector`**, **`op`**) див. **k8s.mdc**.
|
|
99
99
|
|
|
100
100
|
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
101
101
|
patches:
|
package/mdc/docker.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Dockerfile — lint-docker / hadolint; перевірка check-docker
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.7'
|
|
4
4
|
globs: "**/Dockerfile*"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -103,14 +103,19 @@ jobs:
|
|
|
103
103
|
|
|
104
104
|
1. **`bun run lint-docker`** — **`run-docker.mjs`**: **`Dockerfile`** та **`*.Dockerfile`** (див. **`lint-docker`**); у CI встанови hadolint (приклад у workflow).
|
|
105
105
|
2. **`npx @nitra/cursor check docker`** — **`check-docker.mjs`**, виклик hadolint як у **`docker-hadolint.mjs`** (**`PATH`** або **`docker run`** з **`hadolint/hadolint:v2.12.0`**).
|
|
106
|
-
3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure).
|
|
106
|
+
3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure). Щоб не додавати **`# hadolint ignore=DL3007`** у кожному **`FROM`** з **`:latest`**, у корені репозиторію задати глобально:
|
|
107
|
+
|
|
108
|
+
```yaml title=".hadolint.yaml"
|
|
109
|
+
ignored:
|
|
110
|
+
- DL3007
|
|
111
|
+
```
|
|
107
112
|
|
|
108
113
|
Якщо немає файлів у межах відповідного набору (**`lint-docker`** або **`check docker`**) — перевірка пропускається (exit 0).
|
|
109
114
|
|
|
110
115
|
## Агентам
|
|
111
116
|
|
|
112
117
|
- Після правок у Dockerfile проганяй **`bun run lint-docker`** і/або **`check docker`**.
|
|
113
|
-
- Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile або
|
|
118
|
+
- Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile або **`ignored`** у **`.hadolint.yaml`** (наприклад **DL3007** для **`:latest`** — див. вище).
|
|
114
119
|
- Образи на базі Bun — див. **`n-bun.mdc`**.
|
|
115
120
|
|
|
116
121
|
## Редактор
|
package/mdc/k8s.mdc
CHANGED
|
@@ -226,8 +226,6 @@ patches:
|
|
|
226
226
|
|
|
227
227
|
**Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
231
229
|
## Коли застосовувати (агентам)
|
|
232
230
|
|
|
233
231
|
- Після змін у k8s YAML: **`npx @nitra/cursor check k8s`** і за наявності правила — **`bun run lint-k8s`**.
|
package/mdc/style-lint.mdc
CHANGED
|
@@ -8,7 +8,7 @@ version: '1.2'
|
|
|
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
|
-
- **Запуск stylelint:** лише **`npx stylelint`** (
|
|
11
|
+
- **Запуск stylelint:** лише **`npx stylelint`**. Локально — через скрипт **`lint-style`** (`bun run lint-style`); у **GitHub Actions** у кроці **`run`** викликай `npx stylelint '**/*.{css,scss,vue}' --fix` напряму (не через **`bun run lint-style`**). Не використовуй **`bunx stylelint`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний набір `lint-*` (навичка **`n-fix`**).
|
|
12
12
|
- **Не розширюй винятки:** не додавай зайві **`stylelint-disable`** без потреби; краще підлаштувати стилі під правила проєкту.
|
|
13
13
|
|
|
14
14
|
**VSCode:** у **`.vscode/extensions.json`** рекомендуй **`stylelint.vscode-stylelint`**. У **`.vscode/settings.json`** вимкни вбудовану валідацію CSS/SCSS/Less і увімкни явні code actions:
|
|
@@ -44,7 +44,7 @@ version: '1.2'
|
|
|
44
44
|
},
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Додай **`.github/workflows/lint-style.yml`** (лише **`.yml`**, **`ga.mdc`**): після **`checkout`** — локальний composite **`setup-bun-deps`**, далі
|
|
47
|
+
Додай **`.github/workflows/lint-style.yml`** (лише **`.yml`**, **`ga.mdc`**): після **`checkout`** — локальний composite **`setup-bun-deps`**, далі `npx stylelint '**/*.{css,scss,vue}' --fix` у кроці **`run`**. **Не** дублюй окремі кроки **`setup-node`** / **`oven-sh/setup-bun`** / кеш / **`npm install`**.
|
|
48
48
|
|
|
49
49
|
```yaml title=".github/workflows/lint-style.yml"
|
|
50
50
|
name: StyleLint
|
|
@@ -85,7 +85,7 @@ jobs:
|
|
|
85
85
|
- uses: ./.github/actions/setup-bun-deps
|
|
86
86
|
|
|
87
87
|
- name: StyleLint
|
|
88
|
-
run:
|
|
88
|
+
run: npx stylelint '**/*.{css,scss,vue}' --fix
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
У корені проєкту має бути **`.stylelintignore`**:
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
* **nodeSelector (base):** якщо **Deployment** лежить у шляху з сегментом **`base`** (наприклад **`…/k8s/base/deploy.yaml`**),
|
|
18
18
|
* у **`spec.template.spec.nodeSelector`** має бути **`preem`** з булевим значенням **true** або рядком **`'true'`** — overlay **ua** та **ru** далі підміняють селектор.
|
|
19
19
|
*
|
|
20
|
-
* **nodeSelector (overlay):** якщо
|
|
20
|
+
* **nodeSelector (overlay):** якщо в дереві **k8s** пакета є **Deployment**, у **`ua`/`ru` kustomization** цього пакета — inline patch на **`kind: Deployment`**
|
|
21
21
|
* з **`path: /spec/template/spec/nodeSelector`**: **ua** — **`preem: false`**; **ru** — **`yandex.cloud/preemptible: false`**.
|
|
22
22
|
* Узагальнені вимоги **k8s.mdc** до JSON6902 (зокрема заборона **remove** + **add** на той самий **path**) перевіряє **check-k8s.mjs**; **check-abie** — лише abie-специфічний вміст (без дублювання цього правила).
|
|
23
23
|
*
|
|
24
|
-
* **HTTPRoute (overlay):**
|
|
24
|
+
* **HTTPRoute (overlay):** лише якщо в каталозі пакета (батько **`k8s`**) є **`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`**
|
|
25
|
+
* — тоді в **`ua`/`ru` kustomization** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
|
|
25
26
|
* (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
|
|
26
27
|
* Вибір **`op`** — **k8s.mdc**.
|
|
27
28
|
*/
|
|
@@ -66,6 +67,58 @@ export function isUaKustomizationPath(rel) {
|
|
|
66
67
|
return /(^|\/)ua\/kustomization\.yaml$/u.test(norm)
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/(ua|ru)/kustomization.yaml`**.
|
|
72
|
+
* @param {string} root корінь репозиторію
|
|
73
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
|
|
74
|
+
* @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua чи ru
|
|
75
|
+
*/
|
|
76
|
+
export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
|
|
77
|
+
const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
|
|
78
|
+
const m = rel.match(/^(.+)\/k8s\/(?:ua|ru)\/kustomization\.yaml$/u)
|
|
79
|
+
return m ? join(root, m[1]) : null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Чи для цього overlay застосовувати вимоги **HTTPRoute** (лише Vite-пакети).
|
|
84
|
+
* @param {string} root корінь репозиторію
|
|
85
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
|
|
86
|
+
* @returns {boolean} **true**, якщо поруч із **k8s** є **vite.config** (**js** / **mjs** / **ts**)
|
|
87
|
+
*/
|
|
88
|
+
export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
|
|
89
|
+
const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
|
|
90
|
+
if (!pkg) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
return (
|
|
94
|
+
existsSync(join(pkg, 'vite.config.js')) ||
|
|
95
|
+
existsSync(join(pkg, 'vite.config.mjs')) ||
|
|
96
|
+
existsSync(join(pkg, 'vite.config.ts'))
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Чи в дереві **k8s** того ж пакета, що й overlay **ua** або **ru**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
|
|
102
|
+
* @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
|
|
103
|
+
* @param {string} root корінь репозиторію
|
|
104
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
|
|
105
|
+
* @returns {boolean} **true**, якщо хоч один каталог із **deploymentDirs** лежить під **`…/k8s/`** цього пакета
|
|
106
|
+
*/
|
|
107
|
+
export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizationAbs) {
|
|
108
|
+
const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
|
|
109
|
+
if (!pkg) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
const k8sRoot = join(pkg, 'k8s').replaceAll('\\', '/')
|
|
113
|
+
for (const dir of deploymentDirs) {
|
|
114
|
+
const norm = dir.replaceAll('\\', '/')
|
|
115
|
+
if (norm === k8sRoot || norm.startsWith(`${k8sRoot}/`)) {
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
/**
|
|
70
123
|
* Чи відносний шлях до YAML під **k8s** вказує на файл у каталозі **`base`** (сегмент **`base`** у шляху), abie.mdc.
|
|
71
124
|
* @param {string} rel шлях від кореня репозиторію
|
|
@@ -766,14 +819,16 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
|
|
|
766
819
|
}
|
|
767
820
|
|
|
768
821
|
/**
|
|
769
|
-
* Якщо є **Deployment** під **k8s**, вимагає в
|
|
822
|
+
* Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** (**kustomization.yaml**) JSON6902 patch nodeSelector (abie.mdc)
|
|
823
|
+
* лише для kustomization того пакета, у дереві **k8s** якого є **Deployment**.
|
|
770
824
|
* @param {string} root корінь репозиторію
|
|
771
825
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
826
|
+
* @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
|
|
772
827
|
* @param {(msg: string) => void} fail callback
|
|
773
828
|
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
774
829
|
* @returns {Promise<void>}
|
|
775
830
|
*/
|
|
776
|
-
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passFn) {
|
|
831
|
+
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
|
|
777
832
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
778
833
|
if (uaAbsList.length === 0) {
|
|
779
834
|
fail(
|
|
@@ -783,21 +838,25 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
783
838
|
}
|
|
784
839
|
for (const abs of uaAbsList) {
|
|
785
840
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
841
|
+
if (abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
|
|
842
|
+
let raw
|
|
843
|
+
try {
|
|
844
|
+
raw = await readFile(abs, 'utf8')
|
|
845
|
+
} catch (error) {
|
|
846
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
847
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
|
|
851
|
+
fail(
|
|
852
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
|
|
853
|
+
)
|
|
854
|
+
return
|
|
855
|
+
}
|
|
856
|
+
passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
857
|
+
} else {
|
|
858
|
+
passFn(`${rel}: nodeSelector patch (ua) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
799
859
|
}
|
|
800
|
-
passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
801
860
|
}
|
|
802
861
|
|
|
803
862
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
@@ -809,26 +868,31 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
809
868
|
}
|
|
810
869
|
for (const abs of ruAbsList) {
|
|
811
870
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
871
|
+
if (abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
|
|
872
|
+
let raw
|
|
873
|
+
try {
|
|
874
|
+
raw = await readFile(abs, 'utf8')
|
|
875
|
+
} catch (error) {
|
|
876
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
877
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
878
|
+
return
|
|
879
|
+
}
|
|
880
|
+
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
|
|
881
|
+
fail(
|
|
882
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
|
|
883
|
+
)
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
|
|
887
|
+
} else {
|
|
888
|
+
passFn(`${rel}: nodeSelector patch (ru) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
825
889
|
}
|
|
826
|
-
passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
|
|
827
890
|
}
|
|
828
891
|
}
|
|
829
892
|
|
|
830
893
|
/**
|
|
831
|
-
* Якщо є **Deployment** під **k8s**, вимагає в
|
|
894
|
+
* Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
|
|
895
|
+
* лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
|
|
832
896
|
* @param {string} root корінь репозиторію
|
|
833
897
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
834
898
|
* @param {(msg: string) => void} fail callback
|
|
@@ -838,54 +902,60 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
838
902
|
async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
|
|
839
903
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
840
904
|
if (uaAbsList.length === 0) {
|
|
841
|
-
|
|
842
|
-
'
|
|
905
|
+
passFn(
|
|
906
|
+
'Немає ua/kustomization.yaml у дереві k8s — patch HTTPRoute (ua) не вимагається (abie.mdc, лише Vite-пакети)'
|
|
843
907
|
)
|
|
844
|
-
return
|
|
845
908
|
}
|
|
846
909
|
for (const abs of uaAbsList) {
|
|
847
910
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
911
|
+
if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
|
|
912
|
+
let raw
|
|
913
|
+
try {
|
|
914
|
+
raw = await readFile(abs, 'utf8')
|
|
915
|
+
} catch (error) {
|
|
916
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
917
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
921
|
+
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua')
|
|
922
|
+
if (v !== null) {
|
|
923
|
+
fail(`${rel}: ${v}`)
|
|
924
|
+
return
|
|
925
|
+
}
|
|
926
|
+
passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
|
|
927
|
+
} else {
|
|
928
|
+
passFn(`${rel}: HTTPRoute patch (ua) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
861
929
|
}
|
|
862
|
-
passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
|
|
863
930
|
}
|
|
864
931
|
|
|
865
932
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
866
933
|
if (ruAbsList.length === 0) {
|
|
867
|
-
|
|
868
|
-
'
|
|
934
|
+
passFn(
|
|
935
|
+
'Немає ru/kustomization.yaml у дереві k8s — patch HTTPRoute (ru) не вимагається (abie.mdc, лише Vite-пакети)'
|
|
869
936
|
)
|
|
870
|
-
return
|
|
871
937
|
}
|
|
872
938
|
for (const abs of ruAbsList) {
|
|
873
939
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
940
|
+
if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
|
|
941
|
+
let raw
|
|
942
|
+
try {
|
|
943
|
+
raw = await readFile(abs, 'utf8')
|
|
944
|
+
} catch (error) {
|
|
945
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
946
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
947
|
+
return
|
|
948
|
+
}
|
|
949
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
950
|
+
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru')
|
|
951
|
+
if (v !== null) {
|
|
952
|
+
fail(`${rel}: ${v}`)
|
|
953
|
+
return
|
|
954
|
+
}
|
|
955
|
+
passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
|
|
956
|
+
} else {
|
|
957
|
+
passFn(`${rel}: HTTPRoute patch (ru) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
887
958
|
}
|
|
888
|
-
passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
|
|
889
959
|
}
|
|
890
960
|
}
|
|
891
961
|
|
|
@@ -977,7 +1047,7 @@ export async function check() {
|
|
|
977
1047
|
|
|
978
1048
|
if (deploymentDirs.size > 0) {
|
|
979
1049
|
pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
|
|
980
|
-
await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail, pass)
|
|
1050
|
+
await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
|
|
981
1051
|
pass('Є Deployment — перевіряємо HTTPRoute у ua/ru kustomization (abie.mdc)')
|
|
982
1052
|
await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
|
|
983
1053
|
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -735,7 +735,7 @@ function extractJson6902OpsFromArray(arr) {
|
|
|
735
735
|
* Витягує операції JSON6902 з тексту inline **patch** або окремого файлу patch (YAML-масив або JSON-масив).
|
|
736
736
|
* Інший вміст (strategic merge, `$patch: delete` тощо) дає порожній масив.
|
|
737
737
|
* @param {string} patchText вміст поля **patch** або файлу
|
|
738
|
-
* @returns {Array<{ op: string, path: string }>}
|
|
738
|
+
* @returns {Array<{ op: string, path: string }>} нормалізовані **op** / **path** або порожній масив, якщо не JSON6902-масив
|
|
739
739
|
*/
|
|
740
740
|
export function collectJson6902OperationsFromPatchText(patchText) {
|
|
741
741
|
const t = typeof patchText === 'string' ? patchText.trim() : ''
|
|
@@ -745,12 +745,11 @@ export function collectJson6902OperationsFromPatchText(patchText) {
|
|
|
745
745
|
try {
|
|
746
746
|
const docs = parseAllDocuments(t)
|
|
747
747
|
for (const d of docs) {
|
|
748
|
-
if (d.errors.length
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
return extractJson6902OpsFromArray(j)
|
|
748
|
+
if (d.errors.length === 0) {
|
|
749
|
+
const j = d.toJSON()
|
|
750
|
+
if (Array.isArray(j)) {
|
|
751
|
+
return extractJson6902OpsFromArray(j)
|
|
752
|
+
}
|
|
754
753
|
}
|
|
755
754
|
}
|
|
756
755
|
} catch {
|
|
@@ -778,13 +777,12 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
|
|
|
778
777
|
/** @type {Map<string, Set<string>>} */
|
|
779
778
|
const byPath = new Map()
|
|
780
779
|
for (const { op, path } of ops) {
|
|
781
|
-
if (
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
byPath.
|
|
780
|
+
if (path) {
|
|
781
|
+
if (!byPath.has(path)) {
|
|
782
|
+
byPath.set(path, new Set())
|
|
783
|
+
}
|
|
784
|
+
byPath.get(path).add(op)
|
|
786
785
|
}
|
|
787
|
-
byPath.get(path).add(op)
|
|
788
786
|
}
|
|
789
787
|
/** @type {string[]} */
|
|
790
788
|
const out = []
|
|
@@ -806,93 +804,91 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
|
|
|
806
804
|
async function validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFilesAbs, fail) {
|
|
807
805
|
const rootNorm = resolve(root)
|
|
808
806
|
for (const kustAbs of yamlFilesAbs) {
|
|
809
|
-
if (basename(kustAbs).toLowerCase()
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
const lines = toLines(raw)
|
|
822
|
-
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
823
|
-
/** @type {import('yaml').Document[]} */
|
|
824
|
-
let docs
|
|
825
|
-
try {
|
|
826
|
-
docs = parseAllDocuments(body)
|
|
827
|
-
} catch {
|
|
828
|
-
continue
|
|
829
|
-
}
|
|
830
|
-
for (const doc of docs) {
|
|
831
|
-
if (doc.errors.length > 0) {
|
|
832
|
-
continue
|
|
833
|
-
}
|
|
834
|
-
const rootObj = doc.toJSON()
|
|
835
|
-
if (rootObj === null || typeof rootObj !== 'object' || Array.isArray(rootObj)) {
|
|
836
|
-
continue
|
|
837
|
-
}
|
|
838
|
-
const rec = /** @type {Record<string, unknown>} */ (rootObj)
|
|
839
|
-
if (rec.kind !== 'Kustomization') {
|
|
840
|
-
continue
|
|
841
|
-
}
|
|
842
|
-
const patches = rec.patches
|
|
843
|
-
if (!Array.isArray(patches)) {
|
|
844
|
-
continue
|
|
807
|
+
if (basename(kustAbs).toLowerCase() === 'kustomization.yaml') {
|
|
808
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
809
|
+
/** @type {string | undefined} */
|
|
810
|
+
let raw
|
|
811
|
+
let readOk = false
|
|
812
|
+
try {
|
|
813
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
814
|
+
readOk = true
|
|
815
|
+
} catch (error) {
|
|
816
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
817
|
+
fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
|
|
845
818
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
856
|
-
if (bad.length > 0) {
|
|
857
|
-
fail(
|
|
858
|
-
`${rel}: patches[${patchIdx}] inline JSON6902: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
859
|
-
)
|
|
860
|
-
}
|
|
819
|
+
if (readOk && raw !== undefined) {
|
|
820
|
+
const lines = toLines(raw)
|
|
821
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
822
|
+
/** @type {import('yaml').Document[] | null} */
|
|
823
|
+
let docs = null
|
|
824
|
+
try {
|
|
825
|
+
docs = parseAllDocuments(body)
|
|
826
|
+
} catch {
|
|
827
|
+
docs = null
|
|
861
828
|
}
|
|
862
|
-
if (
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
829
|
+
if (docs !== null) {
|
|
830
|
+
for (const doc of docs) {
|
|
831
|
+
if (doc.errors.length === 0) {
|
|
832
|
+
const rootObj = doc.toJSON()
|
|
833
|
+
if (rootObj !== null && typeof rootObj === 'object' && !Array.isArray(rootObj)) {
|
|
834
|
+
const rec = /** @type {Record<string, unknown>} */ (rootObj)
|
|
835
|
+
if (rec.kind === 'Kustomization') {
|
|
836
|
+
const patches = rec.patches
|
|
837
|
+
if (Array.isArray(patches)) {
|
|
838
|
+
let patchIdx = 0
|
|
839
|
+
for (const p of patches) {
|
|
840
|
+
patchIdx++
|
|
841
|
+
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
842
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
843
|
+
if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
|
|
844
|
+
const ops = collectJson6902OperationsFromPatchText(pr.patch)
|
|
845
|
+
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
846
|
+
if (bad.length > 0) {
|
|
847
|
+
fail(
|
|
848
|
+
`${rel}: patches[${patchIdx}] inline JSON6902: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
849
|
+
)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (typeof pr.path === 'string' && pr.path.trim() !== '') {
|
|
853
|
+
const patchRef = pr.path.trim()
|
|
854
|
+
const resolved = resolve(dirname(kustAbs), patchRef)
|
|
855
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && existsSync(resolved)) {
|
|
856
|
+
/** @type {import('node:fs').Stats | null} */
|
|
857
|
+
let st = null
|
|
858
|
+
try {
|
|
859
|
+
st = await stat(resolved)
|
|
860
|
+
} catch {
|
|
861
|
+
st = null
|
|
862
|
+
}
|
|
863
|
+
if (st !== null && st.isFile()) {
|
|
864
|
+
/** @type {string | undefined} */
|
|
865
|
+
let pRaw
|
|
866
|
+
try {
|
|
867
|
+
pRaw = await readFile(resolved, 'utf8')
|
|
868
|
+
} catch {
|
|
869
|
+
pRaw = undefined
|
|
870
|
+
}
|
|
871
|
+
if (pRaw !== undefined) {
|
|
872
|
+
const ops = collectJson6902OperationsFromPatchText(pRaw)
|
|
873
|
+
if (ops.length > 0) {
|
|
874
|
+
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
875
|
+
if (bad.length > 0) {
|
|
876
|
+
const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
|
|
877
|
+
fail(
|
|
878
|
+
`${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
879
|
+
)
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
896
892
|
}
|
|
897
893
|
}
|
|
898
894
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Перевіряє CSS/SCSS лінт за правилом style-lint.mdc.
|
|
3
3
|
*
|
|
4
4
|
* Очікування: `@nitra/stylelint-config`, `lint-style` через `npx stylelint`, `.stylelintignore`,
|
|
5
|
-
* workflow `lint-style.yml` (у `run` — `npx stylelint
|
|
5
|
+
* workflow `lint-style.yml` (у `run` — лише `npx stylelint`, не `bun run lint-style`), VSCode stylelint,
|
|
6
6
|
* `css.validate` / `scss.validate` / `less.validate`: false.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync } from 'node:fs'
|
|
@@ -60,13 +60,13 @@ export async function check() {
|
|
|
60
60
|
const content = await readFile('.github/workflows/lint-style.yml', 'utf8')
|
|
61
61
|
pass('lint-style.yml існує')
|
|
62
62
|
const root = parseWorkflowYaml(content)
|
|
63
|
-
const ok = root
|
|
64
|
-
? anyRunStepIncludesStylelint(root)
|
|
65
|
-
: content.includes('npx stylelint') || content.includes('bun run lint-style')
|
|
63
|
+
const ok = root ? anyRunStepIncludesStylelint(root) : content.includes('npx stylelint')
|
|
66
64
|
if (ok) {
|
|
67
|
-
pass('lint-style.yml містить npx stylelint
|
|
65
|
+
pass('lint-style.yml містить npx stylelint у кроці run')
|
|
68
66
|
} else {
|
|
69
|
-
fail(
|
|
67
|
+
fail(
|
|
68
|
+
"lint-style.yml має викликати stylelint у CI через npx — наприклад: npx stylelint '**/*.{css,scss,vue}' --fix"
|
|
69
|
+
)
|
|
70
70
|
}
|
|
71
71
|
} else {
|
|
72
72
|
fail('.github/workflows/lint-style.yml не існує — створи його')
|
|
@@ -341,11 +341,10 @@ export function anyRunStepIncludes(root, needle) {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
/**
|
|
344
|
-
* Чи викликається stylelint
|
|
345
|
-
* (скрипт `lint-style` у package.json має містити `npx stylelint`).
|
|
344
|
+
* Чи викликається stylelint у workflow через `npx stylelint` у кроці `run` (вимога для CI).
|
|
346
345
|
* @param {Record<string, unknown>} root корінь workflow
|
|
347
346
|
* @returns {boolean} `true`, якщо умова виконана
|
|
348
347
|
*/
|
|
349
348
|
export function anyRunStepIncludesStylelint(root) {
|
|
350
|
-
return anyRunStepIncludes(root, 'npx stylelint')
|
|
349
|
+
return anyRunStepIncludes(root, 'npx stylelint')
|
|
351
350
|
}
|