@nitra/cursor 1.8.74 → 1.8.79
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/docker.mdc +8 -3
- package/mdc/k8s.mdc +0 -4
- package/mdc/style-lint.mdc +3 -3
- package/package.json +1 -1
- package/scripts/check-abie.mjs +18 -24
- package/scripts/check-k8s.mjs +223 -0
- package/scripts/check-style-lint.mjs +6 -6
- package/scripts/utils/gha-workflow.mjs +2 -3
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,11 +226,7 @@ patches:
|
|
|
226
226
|
|
|
227
227
|
**Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
|
|
228
228
|
|
|
229
|
-
## Що саме в скрипті `check-k8s.mjs`
|
|
230
229
|
|
|
231
|
-
Повний перелік умов, константи (**`YANNH_PIN`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо) і допоміжні функції — у файлі скрипта; змінив вимогу для **check** — онови **JSDoc** і за потреби тести в **`npm/tests/check-k8s-schema.test.mjs`**.
|
|
232
|
-
|
|
233
|
-
При зміні **PIN** версії Kubernetes узгодь **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і цей файл (**lint-k8s**, **Визначення схеми YAML**).
|
|
234
230
|
|
|
235
231
|
## Коли застосовувати (агентам)
|
|
236
232
|
|
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,14 +17,13 @@
|
|
|
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):** якщо є **Deployment** під **k8s**, у
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* **nodeSelector (overlay):** якщо є **Deployment** під **k8s**, у **`ua`/`ru` kustomization** — inline patch на **`kind: Deployment`**
|
|
21
|
+
* з **`path: /spec/template/spec/nodeSelector`**: **ua** — **`preem: false`**; **ru** — **`yandex.cloud/preemptible: false`**.
|
|
22
|
+
* Узагальнені вимоги **k8s.mdc** до JSON6902 (зокрема заборона **remove** + **add** на той самий **path**) перевіряє **check-k8s.mjs**; **check-abie** — лише abie-специфічний вміст (без дублювання цього правила).
|
|
23
23
|
*
|
|
24
|
-
* **HTTPRoute (overlay):**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
|
|
24
|
+
* **HTTPRoute (overlay):** тієї ж умови — patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
|
|
25
|
+
* (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
|
|
26
|
+
* Вибір **`op`** — **k8s.mdc**.
|
|
28
27
|
*/
|
|
29
28
|
import { existsSync } from 'node:fs'
|
|
30
29
|
import { readFile } from 'node:fs/promises'
|
|
@@ -344,7 +343,8 @@ function stripBom(s) {
|
|
|
344
343
|
}
|
|
345
344
|
|
|
346
345
|
/**
|
|
347
|
-
* Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**
|
|
346
|
+
* Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**preem: false** на **`/spec/template/spec/nodeSelector`**).
|
|
347
|
+
* Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
|
|
348
348
|
* @param {string} patchText поле **patch** у kustomization
|
|
349
349
|
* @returns {boolean} true, якщо критерії abie.mdc виконано
|
|
350
350
|
*/
|
|
@@ -352,9 +352,6 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
|
352
352
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
353
353
|
return false
|
|
354
354
|
}
|
|
355
|
-
if (!/op:\s*add\b/u.test(patchText)) {
|
|
356
|
-
return false
|
|
357
|
-
}
|
|
358
355
|
if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
|
|
359
356
|
return false
|
|
360
357
|
}
|
|
@@ -365,7 +362,8 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
|
365
362
|
}
|
|
366
363
|
|
|
367
364
|
/**
|
|
368
|
-
* Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**
|
|
365
|
+
* Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**yandex.cloud/preemptible: false** на **`/spec/template/spec/nodeSelector`**).
|
|
366
|
+
* Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
|
|
369
367
|
* @param {string} patchText поле **patch** у kustomization
|
|
370
368
|
* @returns {boolean} true, якщо критерії abie.mdc виконано
|
|
371
369
|
*/
|
|
@@ -373,9 +371,6 @@ function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
|
|
|
373
371
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
374
372
|
return false
|
|
375
373
|
}
|
|
376
|
-
if (!/op:\s*replace\b/u.test(patchText)) {
|
|
377
|
-
return false
|
|
378
|
-
}
|
|
379
374
|
if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
|
|
380
375
|
return false
|
|
381
376
|
}
|
|
@@ -558,11 +553,10 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
|
558
553
|
*/
|
|
559
554
|
export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
|
|
560
555
|
if (typeof combined !== 'string' || combined.trim() === '') {
|
|
561
|
-
return `очікується patch target kind HTTPRoute з непорожнім target.name (
|
|
556
|
+
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — також gwin… websocket) — abie.mdc`
|
|
562
557
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
return 'HTTPRoute: потрібен блок op replace з path /spec/hostnames (abie.mdc)'
|
|
558
|
+
if (!/path:\s*\/spec\/hostnames\b/m.test(combined)) {
|
|
559
|
+
return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
|
|
566
560
|
}
|
|
567
561
|
const markers = mode === 'ua' ? ABIE_UA_HTTPROUTE_HOST_MARKERS : ABIE_RU_HTTPROUTE_HOST_MARKERS
|
|
568
562
|
if (!markers.some(m => combined.includes(m))) {
|
|
@@ -573,7 +567,7 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
|
|
|
573
567
|
? /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu.test(combined)
|
|
574
568
|
: /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu.test(combined)
|
|
575
569
|
if (!namespaceOk) {
|
|
576
|
-
return `HTTPRoute: потрібен
|
|
570
|
+
return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
|
|
577
571
|
}
|
|
578
572
|
if (mode === 'ru' && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
|
|
579
573
|
return 'HTTPRoute (ru): потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
|
|
@@ -783,7 +777,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
783
777
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
784
778
|
if (uaAbsList.length === 0) {
|
|
785
779
|
fail(
|
|
786
|
-
'Є Deployment у k8s — додай ua/kustomization.yaml з
|
|
780
|
+
'Є Deployment у k8s — додай ua/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
|
|
787
781
|
)
|
|
788
782
|
return
|
|
789
783
|
}
|
|
@@ -799,7 +793,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
799
793
|
}
|
|
800
794
|
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
|
|
801
795
|
fail(
|
|
802
|
-
`${rel}: потрібен patch target kind Deployment
|
|
796
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
|
|
803
797
|
)
|
|
804
798
|
return
|
|
805
799
|
}
|
|
@@ -809,7 +803,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
809
803
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
810
804
|
if (ruAbsList.length === 0) {
|
|
811
805
|
fail(
|
|
812
|
-
'Є Deployment у k8s — додай ru/kustomization.yaml з
|
|
806
|
+
'Є Deployment у k8s — додай ru/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, yandex.cloud/preemptible false (abie.mdc)'
|
|
813
807
|
)
|
|
814
808
|
return
|
|
815
809
|
}
|
|
@@ -825,7 +819,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
825
819
|
}
|
|
826
820
|
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
|
|
827
821
|
fail(
|
|
828
|
-
`${rel}: потрібен patch target kind Deployment
|
|
822
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
|
|
829
823
|
)
|
|
830
824
|
return
|
|
831
825
|
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
* Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**; у **`k8s/base/kustomization.yaml`**
|
|
41
41
|
* завжди має бути непорожнє поле **`namespace:`** (перевірка, якщо файл існує).
|
|
42
42
|
*
|
|
43
|
+
* **Inline JSON6902** у **`patches`** (і зовнішні файли з **`patches[].path`** під **`k8s`**, якщо вміст — масив JSON Patch): не допускається пара **`remove`** і **`add`**
|
|
44
|
+
* на один і той самий **`path`** у межах одного фрагмента — потрібен **`op: replace`** (k8s.mdc). **check-k8s** це перевіряє.
|
|
45
|
+
*
|
|
43
46
|
* Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
|
|
44
47
|
* **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
|
|
45
48
|
* компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
|
|
@@ -679,6 +682,224 @@ export function ruKustomizationHasHealthCheckDeletePatch(raw) {
|
|
|
679
682
|
return true
|
|
680
683
|
}
|
|
681
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Чи абсолютний шлях лежить усередині кореня репозиторію (без виходу через `..`).
|
|
687
|
+
* @param {string} rootAbs абсолютний корінь
|
|
688
|
+
* @param {string} fileAbs абсолютний шлях до файлу
|
|
689
|
+
* @returns {boolean} true, якщо `fileAbs` усередині `rootAbs`
|
|
690
|
+
*/
|
|
691
|
+
function resolvedFilePathIsUnderRoot(rootAbs, fileAbs) {
|
|
692
|
+
const r = resolve(rootAbs)
|
|
693
|
+
const f = resolve(fileAbs)
|
|
694
|
+
const rel = relative(r, f).replaceAll('\\', '/')
|
|
695
|
+
if (rel === '') {
|
|
696
|
+
return true
|
|
697
|
+
}
|
|
698
|
+
return !rel.startsWith('../') && rel !== '..'
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Нормалізує **`path`** з операції JSON Patch (RFC 6902).
|
|
703
|
+
* @param {string} p значення поля **path**
|
|
704
|
+
* @returns {string} обрізаний рядок
|
|
705
|
+
*/
|
|
706
|
+
function normalizeJsonPatchPath(p) {
|
|
707
|
+
return typeof p === 'string' ? p.trim() : ''
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Витягує пари **op** / **path** з масиву операцій JSON6902.
|
|
712
|
+
* @param {unknown[]} arr корінь-масив з YAML/JSON
|
|
713
|
+
* @returns {Array<{ op: string, path: string }>} **op** у нижньому регістрі
|
|
714
|
+
*/
|
|
715
|
+
function extractJson6902OpsFromArray(arr) {
|
|
716
|
+
/** @type {Array<{ op: string, path: string }>} */
|
|
717
|
+
const out = []
|
|
718
|
+
for (const item of arr) {
|
|
719
|
+
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
|
720
|
+
const rec = /** @type {Record<string, unknown>} */ (item)
|
|
721
|
+
const op = rec.op
|
|
722
|
+
const path = rec.path
|
|
723
|
+
if (typeof op === 'string' && typeof path === 'string') {
|
|
724
|
+
const p = normalizeJsonPatchPath(path)
|
|
725
|
+
if (p !== '') {
|
|
726
|
+
out.push({ op: op.trim().toLowerCase(), path: p })
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return out
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Витягує операції JSON6902 з тексту inline **patch** або окремого файлу patch (YAML-масив або JSON-масив).
|
|
736
|
+
* Інший вміст (strategic merge, `$patch: delete` тощо) дає порожній масив.
|
|
737
|
+
* @param {string} patchText вміст поля **patch** або файлу
|
|
738
|
+
* @returns {Array<{ op: string, path: string }>}
|
|
739
|
+
*/
|
|
740
|
+
export function collectJson6902OperationsFromPatchText(patchText) {
|
|
741
|
+
const t = typeof patchText === 'string' ? patchText.trim() : ''
|
|
742
|
+
if (t === '') {
|
|
743
|
+
return []
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
const docs = parseAllDocuments(t)
|
|
747
|
+
for (const d of docs) {
|
|
748
|
+
if (d.errors.length > 0) {
|
|
749
|
+
continue
|
|
750
|
+
}
|
|
751
|
+
const j = d.toJSON()
|
|
752
|
+
if (Array.isArray(j)) {
|
|
753
|
+
return extractJson6902OpsFromArray(j)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} catch {
|
|
757
|
+
/* пробуємо JSON */
|
|
758
|
+
}
|
|
759
|
+
if (t.startsWith('[')) {
|
|
760
|
+
try {
|
|
761
|
+
const j = JSON.parse(t)
|
|
762
|
+
if (Array.isArray(j)) {
|
|
763
|
+
return extractJson6902OpsFromArray(j)
|
|
764
|
+
}
|
|
765
|
+
} catch {
|
|
766
|
+
/* ignore */
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return []
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Шляхи JSON Patch, де в одному наборі операцій є і **remove**, і **add** (k8s.mdc: краще **replace**).
|
|
774
|
+
* @param {Array<{ op: string, path: string }>} ops нормалізовані **op**
|
|
775
|
+
* @returns {string[]} унікальні **path** з порушенням (відсортовано)
|
|
776
|
+
*/
|
|
777
|
+
export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
|
|
778
|
+
/** @type {Map<string, Set<string>>} */
|
|
779
|
+
const byPath = new Map()
|
|
780
|
+
for (const { op, path } of ops) {
|
|
781
|
+
if (!path) {
|
|
782
|
+
continue
|
|
783
|
+
}
|
|
784
|
+
if (!byPath.has(path)) {
|
|
785
|
+
byPath.set(path, new Set())
|
|
786
|
+
}
|
|
787
|
+
byPath.get(path).add(op)
|
|
788
|
+
}
|
|
789
|
+
/** @type {string[]} */
|
|
790
|
+
const out = []
|
|
791
|
+
for (const [path, set] of byPath) {
|
|
792
|
+
if (set.has('remove') && set.has('add')) {
|
|
793
|
+
out.push(path)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return out.toSorted((a, b) => a.localeCompare(b))
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Перевіряє всі **`kustomization.yaml`** під **`k8s`**: у inline **`patch`** і у зовнішніх patch-файлах не має бути **remove** і **add** на той самий **path**.
|
|
801
|
+
* @param {string} root корінь репозиторію
|
|
802
|
+
* @param {string[]} yamlFilesAbs абсолютні шляхи до yaml під k8s
|
|
803
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
804
|
+
* @returns {Promise<void>}
|
|
805
|
+
*/
|
|
806
|
+
async function validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFilesAbs, fail) {
|
|
807
|
+
const rootNorm = resolve(root)
|
|
808
|
+
for (const kustAbs of yamlFilesAbs) {
|
|
809
|
+
if (basename(kustAbs).toLowerCase() !== 'kustomization.yaml') {
|
|
810
|
+
continue
|
|
811
|
+
}
|
|
812
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
813
|
+
let raw
|
|
814
|
+
try {
|
|
815
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
816
|
+
} catch (error) {
|
|
817
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
818
|
+
fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
|
|
819
|
+
continue
|
|
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
|
|
845
|
+
}
|
|
846
|
+
let patchIdx = 0
|
|
847
|
+
for (const p of patches) {
|
|
848
|
+
patchIdx++
|
|
849
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) {
|
|
850
|
+
continue
|
|
851
|
+
}
|
|
852
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
853
|
+
if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
|
|
854
|
+
const ops = collectJson6902OperationsFromPatchText(pr.patch)
|
|
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
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (typeof pr.path === 'string' && pr.path.trim() !== '') {
|
|
863
|
+
const patchRef = pr.path.trim()
|
|
864
|
+
const resolved = resolve(dirname(kustAbs), patchRef)
|
|
865
|
+
if (!resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
|
|
866
|
+
continue
|
|
867
|
+
}
|
|
868
|
+
if (!existsSync(resolved)) {
|
|
869
|
+
continue
|
|
870
|
+
}
|
|
871
|
+
let st
|
|
872
|
+
try {
|
|
873
|
+
st = await stat(resolved)
|
|
874
|
+
} catch {
|
|
875
|
+
continue
|
|
876
|
+
}
|
|
877
|
+
if (!st.isFile()) {
|
|
878
|
+
continue
|
|
879
|
+
}
|
|
880
|
+
let pRaw
|
|
881
|
+
try {
|
|
882
|
+
pRaw = await readFile(resolved, 'utf8')
|
|
883
|
+
} catch {
|
|
884
|
+
continue
|
|
885
|
+
}
|
|
886
|
+
const ops = collectJson6902OperationsFromPatchText(pRaw)
|
|
887
|
+
if (ops.length === 0) {
|
|
888
|
+
continue
|
|
889
|
+
}
|
|
890
|
+
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
891
|
+
if (bad.length > 0) {
|
|
892
|
+
const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
|
|
893
|
+
fail(
|
|
894
|
+
`${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
682
903
|
/**
|
|
683
904
|
* Шукає **Ingress** у розібраних документах; реєструє порушення.
|
|
684
905
|
* @param {string} rel відносний шлях до файлу
|
|
@@ -1510,6 +1731,8 @@ export async function check() {
|
|
|
1510
1731
|
|
|
1511
1732
|
await validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
|
|
1512
1733
|
|
|
1734
|
+
await validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFiles, fail)
|
|
1735
|
+
|
|
1513
1736
|
await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
|
|
1514
1737
|
|
|
1515
1738
|
return reporter.getExitCode()
|
|
@@ -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
|
}
|