@nitra/cursor 1.9.2 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -1
- package/mdc/k8s.mdc +3 -1
- package/mdc/tauri.mdc +1 -2
- package/package.json +1 -1
- package/scripts/check-abie.mjs +12 -6
- package/scripts/check-ga.mjs +2 -2
- package/scripts/check-k8s.mjs +33 -23
- package/scripts/run-k8s.mjs +17 -7
- package/scripts/utils/run-conftest-batch.mjs +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.9.3] - 2026-05-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **k8s — `pathHasK8sSegment` тепер відносно кореня репо; `.github/` явно поза скоупом:** функція `pathHasK8sSegment(filePath)` у `npm/scripts/check-k8s.mjs` та `npm/scripts/run-k8s.mjs` розбивала **абсолютний** шлях і шукала компонент `k8s`. У проєктах, де сам корінь репо називається `k8s/` (напр. `/Users/.../abie/k8s/`), сегмент `k8s` присутній в абсолютному шляху **усіх** файлів — і весь репозиторій, включно з `.github/workflows/*.yml`, потрапляв у `findK8sYamlFiles` як k8s-маніфести, після чого `checkK8sYamlFile` падав на «розширення .yml — перейменуй на .yaml» (територія `ga.mdc`, де канон протилежний). Виправлено: (1) сигнатура тепер `pathHasK8sSegment(filePath, root?)` — коли `root` передано, шлях спершу нормалізується через `node:path` `relative(root, filePath)`, і компоненти беруться **відносно кореня** (порожній relative — це сам root, повертає false); (2) `findK8sYamlFiles` у `check-k8s.mjs` і `check-abie.mjs`, а також `findK8sRoots` у `run-k8s.mjs` тепер передають `root` і додатково мають defense-in-depth ранній `return` для шляхів, що починаються з `.github/`; (3) `k8s.mdc` явно фіксує: правило стосується каталогів `k8s` відносно кореня; `.github/workflows/` і `.github/actions/` — поза скоупом (їх веде `ga.mdc`). Без `root` (юніт-тести з відносним шляхом) функція веде себе як раніше. Додано тести у `tests/check-k8s-schema.test.mjs` (worst-case з префіксом `/home/test/some/k8s/`) і `tests/run-k8s-roots.test.mjs` (інтеграційний — `findK8sRoots` у репі, корінь якого називається `k8s/`).
|
|
12
|
+
|
|
7
13
|
## [1.9.2] - 2026-05-11
|
|
8
14
|
|
|
9
15
|
### Changed
|
|
10
16
|
|
|
11
|
-
- **k8s — modeline `$schema` тепер опційний; `file:…` заборонено як
|
|
17
|
+
- **k8s — modeline `$schema` тепер опційний; `file:…` заборонено як плейсхолдер:** правило `k8s.mdc` уточнено — рядок `# yaml-language-server: $schema=…` обов'язковий **лише** коли для поєднання `apiVersion`/`kind` існує надійна публічна схема (kustomization / yannh / datree CRDs-catalog). Якщо публічної схеми немає, modeline **не додається зовсім** (раніше п. 5 розділу «Визначення схеми YAML» допускав `file:` за узгодженням — це створювало фальшиву видимість валідації, а автовиправлення n-fix залишало плейсхолдер `# yaml-language-server: $schema=file:.`). У `check-k8s.mjs`: (1) файли без modeline більше не падають як «перший рядок має бути коментарем», натомість `pass` із позначкою «без modeline — перевірка $schema пропущена»; (2) `$schema=file:…`тепер реєструється як помилка з підказкою прибрати modeline; (3) modeline нижче першого рядка все ще порушення; (4)`HttpBackendGroup`(Yandex ALB) як виняток без modeline залишається без змін.`lint-k8s`(kubeconform з прапорцем ignore-missing-schemas) продовжує покривати валідацію і для файлів без modeline. JSDoc на початку`check-k8s.mjs` оновлено.
|
|
12
18
|
|
|
13
19
|
## [1.9.1] - 2026-05-11
|
|
14
20
|
|
package/mdc/k8s.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.29'
|
|
4
4
|
globs: "**/k8s/**/*.yaml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -21,6 +21,8 @@ alwaysApply: false
|
|
|
21
21
|
|
|
22
22
|
**Розширення:** усі маніфести під **`k8s`**, включно з **`kustomization.yaml`**, — лише **`.yaml`** (розширення **`.yml`** не використовуй).
|
|
23
23
|
|
|
24
|
+
**Скоп — поза `.github/`:** правило стосується YAML-маніфестів у каталогах **`k8s`** (визначаються відносно кореня репо, не за абсолютним шляхом). Файли під **`.github/workflows/`** та **`.github/actions/`** ця перевірка **не** зачіпає — їхній скоп визначає **`ga.mdc`** (там канон — **`.yml`**). Це робить два правила несуперечливими навіть у проєктах, де сам корінь репо випадково має ім'я `k8s/` (тоді сегмент `k8s` присутній у абсолютному шляху всіх файлів, але **відносно кореня** його там немає).
|
|
25
|
+
|
|
24
26
|
**Dockerfile / hadolint** — окреме правило **`docker.mdc`** і **`npx @nitra/cursor check docker`**.
|
|
25
27
|
|
|
26
28
|
## lint-k8s: kubeconform і kubescape
|
package/mdc/tauri.mdc
CHANGED
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -323,7 +323,12 @@ async function findK8sYamlFiles(root, ignorePaths = []) {
|
|
|
323
323
|
await walkDir(
|
|
324
324
|
root,
|
|
325
325
|
p => {
|
|
326
|
-
|
|
326
|
+
const rel = relative(root, p).replaceAll('\\', '/')
|
|
327
|
+
// `.github/` належить `ga.mdc`; check-abie не зачіпає workflow-файли.
|
|
328
|
+
if (rel.startsWith('.github/')) {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
if (!pathHasK8sSegment(p, root)) {
|
|
327
332
|
return
|
|
328
333
|
}
|
|
329
334
|
if (!YAML_EXTENSION_RE.test(p)) {
|
|
@@ -760,9 +765,9 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
|
760
765
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
761
766
|
* @param {(msg: string) => void} fail callback
|
|
762
767
|
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
763
|
-
* @returns {
|
|
768
|
+
* @returns {void}
|
|
764
769
|
*/
|
|
765
|
-
|
|
770
|
+
function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
|
|
766
771
|
const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
767
772
|
if (baseFiles.length === 0) {
|
|
768
773
|
passFn('Немає файлів у шляхах …/base/… — перевірку preem у base пропущено')
|
|
@@ -1425,9 +1430,9 @@ async function checkHttpRouteKustomization(abs, rel, mode, root, yamlFilesAbs, c
|
|
|
1425
1430
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
1426
1431
|
* @param {(msg: string) => void} fail callback при помилці
|
|
1427
1432
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1428
|
-
* @returns {
|
|
1433
|
+
* @returns {void}
|
|
1429
1434
|
*/
|
|
1430
|
-
|
|
1435
|
+
function ensureAbieBaseHttpRouteHostnames(root, yamlFilesAbs, fail, passFn) {
|
|
1431
1436
|
const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
1432
1437
|
if (baseFiles.length === 0) {
|
|
1433
1438
|
passFn('Немає файлів у шляхах …/k8s/base/… — перевірку HTTPRoute hostnames пропущено')
|
|
@@ -1531,8 +1536,9 @@ async function ensureNoFirebaseHostingArtifacts(root, passFn, failFn) {
|
|
|
1531
1536
|
* @param {string} root корінь репозиторію
|
|
1532
1537
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
1533
1538
|
* @param {(msg: string) => void} fail callback при помилці
|
|
1539
|
+
* @returns {void}
|
|
1534
1540
|
*/
|
|
1535
|
-
|
|
1541
|
+
function checkCleanMergedBranch(root, pass, fail) {
|
|
1536
1542
|
const cleanMergedPath = join(root, '.github/workflows/clean-merged-branch.yml')
|
|
1537
1543
|
if (!existsSync(cleanMergedPath)) {
|
|
1538
1544
|
fail(`Відсутній ${cleanMergedPath} — потрібен для ignore_branches (abie.mdc)`)
|
package/scripts/check-ga.mjs
CHANGED
|
@@ -365,9 +365,9 @@ const GA_PER_WORKFLOW_REGO_TARGETS = [
|
|
|
365
365
|
* @param {string[]} ymlWorkflows відносні (від `wfDir`) імена файлів `*.yml`
|
|
366
366
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
367
367
|
* @param {(msg: string) => void} fail callback при помилці
|
|
368
|
-
* @returns {
|
|
368
|
+
* @returns {void}
|
|
369
369
|
*/
|
|
370
|
-
|
|
370
|
+
function runAllGaRego(wfDir, ymlWorkflows, pass, fail) {
|
|
371
371
|
for (const target of GA_PER_WORKFLOW_REGO_TARGETS) {
|
|
372
372
|
if (!existsSync(target.workflow)) continue
|
|
373
373
|
const violations = runConftestBatch({
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -349,11 +349,23 @@ const BATCH_V1BETA1_API_VERSION_LINE_RE = /^(\s*apiVersion:\s*)["']?batch\/v1bet
|
|
|
349
349
|
|
|
350
350
|
/**
|
|
351
351
|
* Чи містить шлях сегмент директорії `k8s` (рівно ця назва компонента).
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
352
|
+
*
|
|
353
|
+
* Якщо передано `root`, перевірка ведеться **відносно** кореня репо — інакше випадає
|
|
354
|
+
* false-positive, коли сам корінь репо вже містить компонент `k8s` (напр.
|
|
355
|
+
* `/Users/.../abie/k8s/`): без relativize функція б повертала true для **усіх** файлів
|
|
356
|
+
* у проєкті, включно з `.github/workflows/*.yml`, які належать іншому правилу (`ga.mdc`).
|
|
357
|
+
*
|
|
358
|
+
* Без `root` (як у юніт-тестах або коли шлях уже відносний) спрацьовує старий шлях:
|
|
359
|
+
* розбиття за `/`/`\` і пошук компонента `k8s`.
|
|
360
|
+
* @param {string} filePath абсолютний або відносний шлях до файлу
|
|
361
|
+
* @param {string} [root] корінь репо для relativize (типово — без relativize)
|
|
362
|
+
* @returns {boolean} true, якщо серед компонентів шляху **відносно root** є каталог `k8s`
|
|
363
|
+
*/
|
|
364
|
+
export function pathHasK8sSegment(filePath, root) {
|
|
365
|
+
const target = root ? relative(root, filePath).replaceAll('\\', '/') : filePath
|
|
366
|
+
// Порожній relative означає сам root — у ньому компонента `k8s` відносно себе немає.
|
|
367
|
+
if (target === '') return false
|
|
368
|
+
const parts = target.split(PATH_SPLIT_RE)
|
|
357
369
|
return parts.includes('k8s')
|
|
358
370
|
}
|
|
359
371
|
|
|
@@ -1723,7 +1735,11 @@ async function findK8sYamlFiles(root, ignorePaths = []) {
|
|
|
1723
1735
|
await walkDir(
|
|
1724
1736
|
root,
|
|
1725
1737
|
p => {
|
|
1726
|
-
|
|
1738
|
+
const rel = relative(root, p).replaceAll('\\', '/')
|
|
1739
|
+
// `.github/` належить правилу `ga.mdc` (там канон — `.yml`); не зачіпай тут навіть
|
|
1740
|
+
// якщо `pathHasK8sSegment` колись зіб'ється на крайовому кейсі.
|
|
1741
|
+
if (rel.startsWith('.github/')) return
|
|
1742
|
+
if (!pathHasK8sSegment(p, root)) return
|
|
1727
1743
|
if (!YAML_EXTENSION_RE.test(p)) return
|
|
1728
1744
|
out.push(p)
|
|
1729
1745
|
},
|
|
@@ -2890,18 +2906,11 @@ export function collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, ro
|
|
|
2890
2906
|
return out
|
|
2891
2907
|
}
|
|
2892
2908
|
|
|
2893
|
-
/**
|
|
2894
|
-
* Один документ: маршрут Gateway API має посилатися на **Service** з суфіксом **`-hl`**;
|
|
2895
|
-
* у **`backendRef`** не має дублюватися **`namespace`**, що збігається з **`metadata.namespace`** маршруту.
|
|
2896
|
-
* @param {string} rel відносний шлях до файлу
|
|
2897
|
-
* @param {number} docIndex 1-based індекс документа
|
|
2898
|
-
* @param {Record<string, unknown>} rec корінь маніфесту
|
|
2899
|
-
* @param {(msg: string) => void} fail callback помилки
|
|
2900
|
-
* @returns {void}
|
|
2901
|
-
*/
|
|
2902
2909
|
// Plan B: Gateway API маршрут backendRef з суфіксом `-hl` і redundant namespace —
|
|
2903
2910
|
// у rego-пакеті `k8s.gateway`, виклик через `runAllK8sRego`. JS-функції
|
|
2904
|
-
// failIfGatewayRouteUsesNonHeadlessService, scanGatewayApiRouteBackendRefsInYamlBody
|
|
2911
|
+
// failIfGatewayRouteUsesNonHeadlessService, scanGatewayApiRouteBackendRefsInYamlBody видалено;
|
|
2912
|
+
// JSDoc-блок для них прибрано (eslint-plugin-jsdoc trip-ив на «orphan» JSDoc, який
|
|
2913
|
+
// прилипав до asPlainRecord як другий @returns).
|
|
2905
2914
|
|
|
2906
2915
|
/**
|
|
2907
2916
|
* Звузити `unknown` до `Record<string, unknown>` (`null`, масиви, примітиви → null).
|
|
@@ -3552,13 +3561,12 @@ function countSchemaModelines(lines) {
|
|
|
3552
3561
|
return lines.filter(l => OXLINT_SCHEMA_MODELINE_RE.test(l.trim())).length
|
|
3553
3562
|
}
|
|
3554
3563
|
|
|
3555
|
-
|
|
3556
3564
|
/**
|
|
3557
3565
|
* Файл з першим документом **HttpBackendGroup** (ALB Yandex): без modeline **$schema**.
|
|
3558
3566
|
* @param {string} rel відносний шлях
|
|
3559
|
-
* @param {string}
|
|
3560
|
-
* @param {string[]}
|
|
3561
|
-
* @param {(msg: string) => void}
|
|
3567
|
+
* @param {string} _baseLower basename (лишений для уніфікованої сигнатури `checkK8sYamlFile*`)
|
|
3568
|
+
* @param {string[]} _lines рядки файлу (лишені з тієї ж причини)
|
|
3569
|
+
* @param {(msg: string) => void} _fail реєстрація помилки (rego гейтує per-document)
|
|
3562
3570
|
* @param {(msg: string) => void} pass реєстрація успіху
|
|
3563
3571
|
* @returns {void}
|
|
3564
3572
|
*/
|
|
@@ -3679,9 +3687,7 @@ async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
|
3679
3687
|
// Але `# yaml-language-server: $schema=…` дозволено **лише** у першому рядку — якщо він
|
|
3680
3688
|
// зустрічається нижче, це порушення (yaml-language-server чекає на нього у заголовку файлу).
|
|
3681
3689
|
if (countSchemaModelines(lines) > 0) {
|
|
3682
|
-
fail(
|
|
3683
|
-
`${rel}: рядок # yaml-language-server: $schema=… має бути першим у файлі (без префіксів перед #; k8s.mdc)`
|
|
3684
|
-
)
|
|
3690
|
+
fail(`${rel}: рядок # yaml-language-server: $schema=… має бути першим у файлі (без префіксів перед #; k8s.mdc)`)
|
|
3685
3691
|
return
|
|
3686
3692
|
}
|
|
3687
3693
|
pass(`${rel}: без modeline — перевірка $schema пропущена (немає публічної схеми; k8s.mdc)`)
|
|
@@ -5965,6 +5971,10 @@ function runAllK8sRego(root, yamlFiles, fail) {
|
|
|
5965
5971
|
}
|
|
5966
5972
|
}
|
|
5967
5973
|
|
|
5974
|
+
/**
|
|
5975
|
+
* Точка входу `check k8s`: повний набір перевірок маніфестів і структури `…/k8s` (див. JSDoc на початку файлу).
|
|
5976
|
+
* @returns {Promise<number>} `process.exitCode`: 0 при успіху, 1 при будь-якому `fail(...)`
|
|
5977
|
+
*/
|
|
5968
5978
|
export async function check() {
|
|
5969
5979
|
const reporter = createCheckReporter()
|
|
5970
5980
|
const { pass, fail } = reporter
|
package/scripts/run-k8s.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Kubescape не має аналога цього прапорця; орієнтир цільового кластера — та сама лінія релізу (див. k8s.mdc).
|
|
14
14
|
*/
|
|
15
15
|
import { spawnSync } from 'node:child_process'
|
|
16
|
-
import { basename, dirname } from 'node:path'
|
|
16
|
+
import { basename, dirname, relative } from 'node:path'
|
|
17
17
|
|
|
18
18
|
import { isRunAsCli } from './cli-entry.mjs'
|
|
19
19
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
@@ -31,12 +31,19 @@ const DATREE_CRD_SCHEMA_LOCATION =
|
|
|
31
31
|
'https://datreeio.github.io/CRDs-catalog/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Чи містить шлях сегмент директорії `k8s
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Чи містить шлях сегмент директорії `k8s` (відносно `root`, якщо передано).
|
|
35
|
+
*
|
|
36
|
+
* Якщо корінь репо сам має компонент `k8s` (напр. `/Users/.../abie/k8s/`), без relativize
|
|
37
|
+
* функція повертала б true для **усіх** файлів проєкту — включно з `.github/workflows/*.yml`,
|
|
38
|
+
* які належать `ga.mdc`. Передавай `root` у викликах з walkDir щоб уникнути false-positive.
|
|
39
|
+
* @param {string} filePath абсолютний або відносний шлях до файлу
|
|
40
|
+
* @param {string} [root] корінь репо для relativize (типово — без relativize)
|
|
41
|
+
* @returns {boolean} true, якщо серед компонентів шляху **відносно root** є каталог `k8s`
|
|
37
42
|
*/
|
|
38
|
-
export function pathHasK8sSegment(filePath) {
|
|
39
|
-
const
|
|
43
|
+
export function pathHasK8sSegment(filePath, root) {
|
|
44
|
+
const target = root ? relative(root, filePath).replaceAll('\\', '/') : filePath
|
|
45
|
+
if (target === '') return false
|
|
46
|
+
const parts = target.split(PATH_SEPARATOR_RE)
|
|
40
47
|
return parts.includes('k8s')
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -68,7 +75,10 @@ export async function findK8sRoots(root, ignorePaths = []) {
|
|
|
68
75
|
await walkDir(
|
|
69
76
|
root,
|
|
70
77
|
p => {
|
|
71
|
-
|
|
78
|
+
const rel = relative(root, p).replaceAll('\\', '/')
|
|
79
|
+
// `.github/` належить `ga.mdc`; kubeconform/kubescape там не запускаємо.
|
|
80
|
+
if (rel.startsWith('.github/')) return
|
|
81
|
+
if (!pathHasK8sSegment(p, root)) return
|
|
72
82
|
if (!YAML_EXT_RE.test(p)) return
|
|
73
83
|
const k8sRoot = k8sRootFromFile(p)
|
|
74
84
|
if (k8sRoot) roots.add(k8sRoot)
|
|
@@ -30,7 +30,7 @@ const POLICY_ROOT = join(PACKAGE_ROOT, 'policy')
|
|
|
30
30
|
/**
|
|
31
31
|
* Друкує install-hint для conftest і кидає виняток, щоб викликана `check-*`
|
|
32
32
|
* команда ясно завершилась з кодом 1.
|
|
33
|
-
* @returns {never}
|
|
33
|
+
* @returns {never} завжди кидає; для точки виклику — non-returning
|
|
34
34
|
*/
|
|
35
35
|
function failConftestMissing() {
|
|
36
36
|
throw new Error(
|
|
@@ -94,9 +94,7 @@ export function runConftestBatch(opts) {
|
|
|
94
94
|
}
|
|
95
95
|
// conftest exit 1 = є failures (це валідно для нас); >1 = справжня помилка.
|
|
96
96
|
if (result.status !== 0 && result.status !== 1) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`conftest exit ${result.status}: ${(result.stderr || result.stdout || '').slice(0, 500)}`
|
|
99
|
-
)
|
|
97
|
+
throw new Error(`conftest exit ${result.status}: ${(result.stderr || result.stdout || '').slice(0, 500)}`)
|
|
100
98
|
}
|
|
101
99
|
/** @type {Array<{ filename: string, namespace: string, failures?: Array<{ msg: string }> }>} */
|
|
102
100
|
let parsed
|