@nitra/cursor 1.8.171 → 1.8.177
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/.claude-template/npm-CLAUDE.md +9 -4
- package/CHANGELOG.md +47 -0
- package/README.md +14 -0
- package/bin/auto-rules.md +3 -1
- package/mdc/changelog.mdc +64 -0
- package/mdc/k8s.mdc +57 -0
- package/mdc/npm-module.mdc +2 -6
- package/package.json +1 -1
- package/schemas/n-cursor.json +1 -1
- package/scripts/auto-rules.mjs +4 -8
- package/scripts/check-abie.mjs +17 -11
- package/scripts/check-changelog.mjs +402 -0
- package/scripts/check-docker.mjs +12 -5
- package/scripts/check-graphql.mjs +15 -9
- package/scripts/check-hasura.mjs +14 -8
- package/scripts/check-image.mjs +15 -9
- package/scripts/check-js-bun-db.mjs +25 -15
- package/scripts/check-js-mssql.mjs +27 -17
- package/scripts/check-js-run.mjs +26 -16
- package/scripts/check-k8s.mjs +131 -14
- package/scripts/check-nginx-default-tpl.mjs +26 -16
- package/scripts/check-npm-module.mjs +13 -62
- package/scripts/check-vue.mjs +27 -17
- package/scripts/rename-yaml-extensions.mjs +35 -29
- package/scripts/run-docker.mjs +11 -5
- package/scripts/run-k8s.mjs +14 -8
- package/scripts/utils/load-cursor-config.mjs +53 -0
- package/scripts/utils/walkDir.mjs +49 -8
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
findUnsafeMssqlInListMissingEmptyGuardInText,
|
|
23
23
|
isMssqlScanSourceFile
|
|
24
24
|
} from './utils/mssql-pool-scan.mjs'
|
|
25
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
25
26
|
import { walkDir } from './utils/walkDir.mjs'
|
|
26
27
|
|
|
27
28
|
const VERSION_PREFIX_RE = /^[\^~>=<]+\s*/u
|
|
@@ -35,14 +36,18 @@ const MIN_MSSQL_VERSION = { major: 12, minor: 5, patch: 0 }
|
|
|
35
36
|
* @param {string} repoRoot абсолютний шлях до кореня репозиторію
|
|
36
37
|
* @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
|
|
37
38
|
*/
|
|
38
|
-
async function findAllPackageJsonPaths(repoRoot) {
|
|
39
|
+
async function findAllPackageJsonPaths(repoRoot, ignorePaths) {
|
|
39
40
|
/** @type {string[]} */
|
|
40
41
|
const paths = []
|
|
41
|
-
await walkDir(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
await walkDir(
|
|
43
|
+
repoRoot,
|
|
44
|
+
absPath => {
|
|
45
|
+
if (absPath.endsWith(`${sep}package.json`)) {
|
|
46
|
+
paths.push(absPath)
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
ignorePaths
|
|
50
|
+
)
|
|
46
51
|
paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
|
|
47
52
|
return paths
|
|
48
53
|
}
|
|
@@ -52,15 +57,19 @@ async function findAllPackageJsonPaths(repoRoot) {
|
|
|
52
57
|
* @param {string} repoRoot абсолютний шлях до кореня репозиторію
|
|
53
58
|
* @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
|
|
54
59
|
*/
|
|
55
|
-
async function findAllSourcePathsForMssqlScan(repoRoot) {
|
|
60
|
+
async function findAllSourcePathsForMssqlScan(repoRoot, ignorePaths) {
|
|
56
61
|
/** @type {string[]} */
|
|
57
62
|
const paths = []
|
|
58
|
-
await walkDir(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
await walkDir(
|
|
64
|
+
repoRoot,
|
|
65
|
+
absPath => {
|
|
66
|
+
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
67
|
+
if (isMssqlScanSourceFile(rel)) {
|
|
68
|
+
paths.push(absPath)
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
ignorePaths
|
|
72
|
+
)
|
|
64
73
|
paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
|
|
65
74
|
return paths
|
|
66
75
|
}
|
|
@@ -253,8 +262,8 @@ function reportZeroMssqlSourceViolations(counters, pass) {
|
|
|
253
262
|
* @param {(msg: string) => void} fail fail callback
|
|
254
263
|
* @returns {Promise<void>}
|
|
255
264
|
*/
|
|
256
|
-
async function auditMssqlSources(repoRoot, pass, fail) {
|
|
257
|
-
const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot)
|
|
265
|
+
async function auditMssqlSources(repoRoot, ignorePaths, pass, fail) {
|
|
266
|
+
const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot, ignorePaths)
|
|
258
267
|
if (sourcePaths.length === 0) {
|
|
259
268
|
pass('js-mssql: немає JS/TS файлів для скану singleton ConnectionPool')
|
|
260
269
|
return
|
|
@@ -291,7 +300,8 @@ export async function check() {
|
|
|
291
300
|
return reporter.getExitCode()
|
|
292
301
|
}
|
|
293
302
|
|
|
294
|
-
const
|
|
303
|
+
const ignorePaths = await loadCursorIgnorePaths(repoRoot)
|
|
304
|
+
const pkgJsonPaths = await findAllPackageJsonPaths(repoRoot, ignorePaths)
|
|
295
305
|
if (pkgJsonPaths.length === 0) {
|
|
296
306
|
pass('js-mssql: package.json не знайдено — перевірку пропущено')
|
|
297
307
|
return reporter.getExitCode()
|
|
@@ -307,7 +317,7 @@ export async function check() {
|
|
|
307
317
|
pass(`js-mssql: всі знайдені dependencies.mssql відповідають мінімальній версії 12.5.0 (${found})`)
|
|
308
318
|
}
|
|
309
319
|
|
|
310
|
-
await auditMssqlSources(repoRoot, pass, fail)
|
|
320
|
+
await auditMssqlSources(repoRoot, ignorePaths, pass, fail)
|
|
311
321
|
|
|
312
322
|
return reporter.getExitCode()
|
|
313
323
|
}
|
package/scripts/check-js-run.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
isInsideConnDir,
|
|
37
37
|
resolveConnDirFromPackageJson
|
|
38
38
|
} from './utils/conn-imports-scan.mjs'
|
|
39
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
39
40
|
import { walkDir } from './utils/walkDir.mjs'
|
|
40
41
|
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
41
42
|
|
|
@@ -56,15 +57,19 @@ function relPosix(absPackageRoot, absPath) {
|
|
|
56
57
|
* @param {(msg: string) => void} fail callback при помилці
|
|
57
58
|
* @returns {Promise<number>} кількість знайдених порушень
|
|
58
59
|
*/
|
|
59
|
-
async function checkBunyanImports(absPackageRoot, label, fail) {
|
|
60
|
+
async function checkBunyanImports(absPackageRoot, ignorePaths, label, fail) {
|
|
60
61
|
/** @type {string[]} */
|
|
61
62
|
const sourcePaths = []
|
|
62
|
-
await walkDir(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
await walkDir(
|
|
64
|
+
absPackageRoot,
|
|
65
|
+
absPath => {
|
|
66
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
67
|
+
if (!shouldSkipFileForBunyanScan(rel) && isBunyanScanSourceFile(rel)) {
|
|
68
|
+
sourcePaths.push(absPath)
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
ignorePaths
|
|
72
|
+
)
|
|
68
73
|
|
|
69
74
|
let violations = 0
|
|
70
75
|
for (const absPath of sourcePaths) {
|
|
@@ -83,13 +88,17 @@ async function checkBunyanImports(absPackageRoot, label, fail) {
|
|
|
83
88
|
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
84
89
|
* @returns {Promise<string[]>} абсолютні шляхи до файлів
|
|
85
90
|
*/
|
|
86
|
-
async function collectSourceFiles(absPackageRoot) {
|
|
91
|
+
async function collectSourceFiles(absPackageRoot, ignorePaths) {
|
|
87
92
|
/** @type {string[]} */
|
|
88
93
|
const out = []
|
|
89
|
-
await walkDir(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
await walkDir(
|
|
95
|
+
absPackageRoot,
|
|
96
|
+
absPath => {
|
|
97
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
98
|
+
if (isCheckEnvScanSourceFile(rel)) out.push(absPath)
|
|
99
|
+
},
|
|
100
|
+
ignorePaths
|
|
101
|
+
)
|
|
93
102
|
return out
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -153,17 +162,17 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
|
|
|
153
162
|
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
154
163
|
* @returns {Promise<void>} завершується після перевірок цього пакета
|
|
155
164
|
*/
|
|
156
|
-
async function checkWorkspacePackage(rootDir, fail, passFn) {
|
|
165
|
+
async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
|
|
157
166
|
const label = `[${rootDir}] `
|
|
158
167
|
const absPackageRoot = join(process.cwd(), rootDir)
|
|
159
168
|
const pkgJson = await loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail)
|
|
160
169
|
|
|
161
|
-
const importViolations = await checkBunyanImports(absPackageRoot, label, fail)
|
|
170
|
+
const importViolations = await checkBunyanImports(absPackageRoot, ignorePaths, label, fail)
|
|
162
171
|
if (importViolations === 0) {
|
|
163
172
|
passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
|
|
164
173
|
}
|
|
165
174
|
|
|
166
|
-
const sourcePaths = await collectSourceFiles(absPackageRoot)
|
|
175
|
+
const sourcePaths = await collectSourceFiles(absPackageRoot, ignorePaths)
|
|
167
176
|
|
|
168
177
|
const connViolations = await checkConnImports(absPackageRoot, sourcePaths, pkgJson, label, fail)
|
|
169
178
|
if (connViolations === 0) {
|
|
@@ -245,8 +254,9 @@ export async function check() {
|
|
|
245
254
|
return reporter.getExitCode()
|
|
246
255
|
}
|
|
247
256
|
|
|
257
|
+
const ignorePaths = await loadCursorIgnorePaths(process.cwd())
|
|
248
258
|
for (const r of workspaceRoots) {
|
|
249
|
-
await checkWorkspacePackage(r, fail, pass)
|
|
259
|
+
await checkWorkspacePackage(r, ignorePaths, fail, pass)
|
|
250
260
|
}
|
|
251
261
|
|
|
252
262
|
return reporter.getExitCode()
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
* кожен **Service** — **`spec.clusterIP: None`** та ім’я на **`-hl`**. У маршрутах **Gateway API**
|
|
39
39
|
* (**`HTTPRoute`**, **`GRPCRoute`**, **`TCPRoute`**, **`TLSRoute`**, **`UDPRoute`**, група **`gateway.networking.k8s.io`**)
|
|
40
40
|
* посилання **`backendRefs` / `backendRef`** на **Service** мають вказувати лише сервіси з суфіксом **`-hl`** у **`name`**.
|
|
41
|
+
* Поле **`namespace`** у **`backendRef`**, що збігається з **`metadata.namespace`** самого маршруту, — надлишкове:
|
|
42
|
+
* прибери його, бо за замовчуванням Gateway API резолвить backend у тому ж namespace, що й маршрут (див. k8s.mdc).
|
|
41
43
|
* **HealthCheckPolicy** (**`networking.gke.io/v1`**, GKE): **`spec.targetRef`** на **Service** — **`name`** з суфіксом **`-hl`** (див. k8s.mdc).
|
|
42
44
|
* Якщо **`kustomization.yaml`** посилається на **`svc.yaml`** (**`resources`**, **`bases`**, **`components`**, **`crds`**,
|
|
43
45
|
* **`patches[].path`**, **`patchesStrategicMerge`**), у **тому ж** файлі має бути посилання на відповідний **`svc-hl.yaml`**
|
|
@@ -55,6 +57,11 @@
|
|
|
55
57
|
* Для **`patchesStrategicMerge`** і для **`patches[].path`** без **`target`** і без inline **`patch`** (зовнішній strategic-merge)
|
|
56
58
|
* кожен YAML-документ з кореневим **`kind`** і **`metadata.name`** також звіряється з цим каталогом.
|
|
57
59
|
*
|
|
60
|
+
* **Зайві `group` / `version` у `patches[].target` / `patchesJson6902[].target`:** якщо в інвентарі **`resources`** /
|
|
61
|
+
* **`bases`** / **`components`** / **`crds`** (рекурсивно) за **`kind`** + **`name`** немає колізії між різними
|
|
62
|
+
* API-групами/версіями, поля **`group`** і **`version`** у **`target`** треба прибрати — Kustomize резолвить ціль
|
|
63
|
+
* за **GVK + name**, а зайві поля ламаються мовчки під час змін API (k8s.mdc «patches[].target: лише kind і name»).
|
|
64
|
+
*
|
|
58
65
|
* Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
|
|
59
66
|
* **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
|
|
60
67
|
* компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
|
|
@@ -109,6 +116,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
|
109
116
|
import { isSeq, parseAllDocuments, parseDocument } from 'yaml'
|
|
110
117
|
|
|
111
118
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
119
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
112
120
|
import { walkDir } from './utils/walkDir.mjs'
|
|
113
121
|
|
|
114
122
|
/** Версія набору схем yannh — узгоджено з k8s.mdc */
|
|
@@ -1322,6 +1330,52 @@ function failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail) {
|
|
|
1322
1330
|
}
|
|
1323
1331
|
}
|
|
1324
1332
|
|
|
1333
|
+
/**
|
|
1334
|
+
* Зайві **`group`** / **`version`** у **`patches[].target`** / **`patchesJson6902[].target`**: якщо в інвентарі за **`kind`** + **`name`** немає колізії між різними API-групами/версіями, ці поля треба прибрати (k8s.mdc «patches[].target: лише kind і name»).
|
|
1335
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
1336
|
+
* @param {Record<string, unknown>} first корінь Kustomization
|
|
1337
|
+
* @param {KustomizeResourceDescriptor[]} catalog інвентар resources/bases/…
|
|
1338
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
1339
|
+
* @returns {void}
|
|
1340
|
+
*/
|
|
1341
|
+
function failIfExplicitPatchTargetsHaveRedundantGroupVersion(rel, first, catalog, fail) {
|
|
1342
|
+
for (const { section, index, target } of extractExplicitPatchTargetsFromKustomization(first)) {
|
|
1343
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
1344
|
+
continue
|
|
1345
|
+
}
|
|
1346
|
+
const t = /** @type {Record<string, unknown>} */ (target)
|
|
1347
|
+
const kind = typeof t.kind === 'string' ? t.kind.trim() : ''
|
|
1348
|
+
const name = typeof t.name === 'string' ? t.name.trim() : ''
|
|
1349
|
+
if (kind === '' || name === '') {
|
|
1350
|
+
continue
|
|
1351
|
+
}
|
|
1352
|
+
if (patchTargetUsesSelector(t)) {
|
|
1353
|
+
continue
|
|
1354
|
+
}
|
|
1355
|
+
const tgtGroup = typeof t.group === 'string' ? t.group.trim() : ''
|
|
1356
|
+
const tgtVersion = typeof t.version === 'string' ? t.version.trim() : ''
|
|
1357
|
+
if (tgtGroup === '' && tgtVersion === '') {
|
|
1358
|
+
continue
|
|
1359
|
+
}
|
|
1360
|
+
const matchingByKindName = catalog.filter(r => r.kind === kind && r.name === name)
|
|
1361
|
+
const distinctGvk = new Set(matchingByKindName.map(r => `${r.group}/${r.version}`))
|
|
1362
|
+
if (distinctGvk.size > 1) {
|
|
1363
|
+
continue
|
|
1364
|
+
}
|
|
1365
|
+
/** @type {string[]} */
|
|
1366
|
+
const redundant = []
|
|
1367
|
+
if (tgtGroup !== '') {
|
|
1368
|
+
redundant.push('group')
|
|
1369
|
+
}
|
|
1370
|
+
if (tgtVersion !== '') {
|
|
1371
|
+
redundant.push('version')
|
|
1372
|
+
}
|
|
1373
|
+
fail(
|
|
1374
|
+
`${rel}: ${section}[${index}].target — прибери зайві поля ${redundant.join(', ')}; для kind=${kind}, name=${name} в інвентарі немає колізії між різними API-групами/версіями (див. k8s.mdc «patches[].target: лише kind і name»)`
|
|
1375
|
+
)
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1325
1379
|
/**
|
|
1326
1380
|
* Документи з YAML-файлу мають мати дескриптор у **catalog** (інвентар resources).
|
|
1327
1381
|
* @param {string} rel відносний шлях до kustomization.yaml
|
|
@@ -1524,6 +1578,7 @@ async function validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm,
|
|
|
1524
1578
|
const kustDir = dirname(resolve(kustAbs))
|
|
1525
1579
|
const kustNs = typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
|
|
1526
1580
|
failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail)
|
|
1581
|
+
failIfExplicitPatchTargetsHaveRedundantGroupVersion(rel, first, catalog, fail)
|
|
1527
1582
|
await failIfPathOnlyPatchesNotInCatalog(rel, rec.patches, kustDir, rootNorm, root, catalog, kustNs, fail)
|
|
1528
1583
|
await failIfStrategicMergePatchesNotInCatalog(
|
|
1529
1584
|
rel,
|
|
@@ -1581,16 +1636,21 @@ export function baseKustomizationNamespaceViolation(obj) {
|
|
|
1581
1636
|
/**
|
|
1582
1637
|
* Збирає всі `*.yaml` та `*.yml` під деревом від кореня cwd, якщо шлях містить сегмент `k8s` (для `.yml` далі — помилка перейменування).
|
|
1583
1638
|
* @param {string} root корінь репозиторію (cwd)
|
|
1639
|
+
* @param {string[]} [ignorePaths=[]] шляхи каталогів, повністю виключених з обходу
|
|
1584
1640
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до файлів
|
|
1585
1641
|
*/
|
|
1586
|
-
async function findK8sYamlFiles(root) {
|
|
1642
|
+
async function findK8sYamlFiles(root, ignorePaths = []) {
|
|
1587
1643
|
/** @type {string[]} */
|
|
1588
1644
|
const out = []
|
|
1589
|
-
await walkDir(
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1645
|
+
await walkDir(
|
|
1646
|
+
root,
|
|
1647
|
+
p => {
|
|
1648
|
+
if (!pathHasK8sSegment(p)) return
|
|
1649
|
+
if (!YAML_EXTENSION_RE.test(p)) return
|
|
1650
|
+
out.push(p)
|
|
1651
|
+
},
|
|
1652
|
+
ignorePaths
|
|
1653
|
+
)
|
|
1594
1654
|
|
|
1595
1655
|
return out.toSorted((a, b) => a.localeCompare(b))
|
|
1596
1656
|
}
|
|
@@ -1659,12 +1719,13 @@ export function classifyBackendConfigManifestPresence(body) {
|
|
|
1659
1719
|
/**
|
|
1660
1720
|
* Видаляє під **`k8s`** YAML-файли, що містять **лише** ресурси **BackendConfig**; змішані файли — `fail`.
|
|
1661
1721
|
* @param {string} root корінь репозиторію
|
|
1722
|
+
* @param {string[]} ignorePaths шляхи каталогів, повністю виключених з обходу
|
|
1662
1723
|
* @param {(msg: string) => void} fail реєстрація порушення
|
|
1663
1724
|
* @param {(msg: string) => void} pass реєстрація успіху
|
|
1664
1725
|
* @returns {Promise<void>}
|
|
1665
1726
|
*/
|
|
1666
|
-
async function removeBackendConfigOnlyK8sYamlFiles(root, fail, pass) {
|
|
1667
|
-
const yamlFiles = await findK8sYamlFiles(root)
|
|
1727
|
+
async function removeBackendConfigOnlyK8sYamlFiles(root, ignorePaths, fail, pass) {
|
|
1728
|
+
const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
|
|
1668
1729
|
for (const abs of yamlFiles) {
|
|
1669
1730
|
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
1670
1731
|
try {
|
|
@@ -1736,12 +1797,13 @@ export function replaceBatchV1beta1ApiVersionInYamlText(raw) {
|
|
|
1736
1797
|
/**
|
|
1737
1798
|
* Проходить усі `*.yaml` / `*.yml` під сегментом `k8s` і на диску застосовує **`replaceBatchV1beta1ApiVersionInYamlText`**.
|
|
1738
1799
|
* @param {string} root корінь репозиторію
|
|
1800
|
+
* @param {string[]} ignorePaths шляхи каталогів, повністю виключених з обходу
|
|
1739
1801
|
* @param {(msg: string) => void} fail колбек повідомлення про помилку
|
|
1740
1802
|
* @param {(msg: string) => void} pass колбек успішного повідомлення
|
|
1741
1803
|
* @returns {Promise<void>}
|
|
1742
1804
|
*/
|
|
1743
|
-
async function rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass) {
|
|
1744
|
-
const yamlFiles = await findK8sYamlFiles(root)
|
|
1805
|
+
async function rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, ignorePaths, fail, pass) {
|
|
1806
|
+
const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
|
|
1745
1807
|
for (const abs of yamlFiles) {
|
|
1746
1808
|
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
1747
1809
|
try {
|
|
@@ -2872,7 +2934,49 @@ export function collectGatewayApiRouteBackendServiceNames(spec) {
|
|
|
2872
2934
|
}
|
|
2873
2935
|
|
|
2874
2936
|
/**
|
|
2875
|
-
*
|
|
2937
|
+
* Збирає **`backendRef`** до **Service** з полем **`namespace`**, що збігається з namespace маршруту.
|
|
2938
|
+
*
|
|
2939
|
+
* Поле **`namespace`** у такому **`backendRef`** надлишкове: за замовчуванням Gateway API резолвить backend
|
|
2940
|
+
* у тому ж namespace, що й сам маршрут (див. k8s.mdc). Зайві поля у YAML — джерело розсинхрону між середовищами.
|
|
2941
|
+
* @param {unknown} spec значення **`spec`** маршруту
|
|
2942
|
+
* @param {string} routeNs **`metadata.namespace`** маршруту (непорожній рядок)
|
|
2943
|
+
* @returns {string[]} імена backend-сервісів з надлишковим **`namespace`** (можливі дублікати)
|
|
2944
|
+
*/
|
|
2945
|
+
export function collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, routeNs) {
|
|
2946
|
+
/** @type {string[]} */
|
|
2947
|
+
const out = []
|
|
2948
|
+
|
|
2949
|
+
/**
|
|
2950
|
+
* @param {unknown} node вузол для обходу
|
|
2951
|
+
* @returns {void}
|
|
2952
|
+
*/
|
|
2953
|
+
function walk(node) {
|
|
2954
|
+
if (node === null || node === undefined) return
|
|
2955
|
+
if (Array.isArray(node)) {
|
|
2956
|
+
for (const x of node) {
|
|
2957
|
+
walk(x)
|
|
2958
|
+
}
|
|
2959
|
+
return
|
|
2960
|
+
}
|
|
2961
|
+
if (typeof node !== 'object') return
|
|
2962
|
+
if (isGatewayApiBackendRefToService(node)) {
|
|
2963
|
+
const o = /** @type {Record<string, unknown>} */ (node)
|
|
2964
|
+
if (typeof o.namespace === 'string' && o.namespace === routeNs) {
|
|
2965
|
+
out.push(String(o.name))
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
for (const v of Object.values(node)) {
|
|
2969
|
+
walk(v)
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
walk(spec)
|
|
2974
|
+
return out
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
/**
|
|
2978
|
+
* Один документ: маршрут Gateway API має посилатися на **Service** з суфіксом **`-hl`**;
|
|
2979
|
+
* у **`backendRef`** не має дублюватися **`namespace`**, що збігається з **`metadata.namespace`** маршруту.
|
|
2876
2980
|
* @param {string} rel відносний шлях до файлу
|
|
2877
2981
|
* @param {number} docIndex 1-based індекс документа
|
|
2878
2982
|
* @param {Record<string, unknown>} rec корінь маніфесту
|
|
@@ -2898,6 +3002,18 @@ function failIfGatewayRouteUsesNonHeadlessService(rel, docIndex, rec, fail) {
|
|
|
2898
3002
|
)
|
|
2899
3003
|
}
|
|
2900
3004
|
}
|
|
3005
|
+
const meta = rec.metadata
|
|
3006
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
3007
|
+
const routeNs = /** @type {Record<string, unknown>} */ (meta).namespace
|
|
3008
|
+
if (typeof routeNs === 'string' && routeNs !== '') {
|
|
3009
|
+
const redundant = collectGatewayApiRouteBackendRefsWithRedundantNamespace(rec.spec, routeNs)
|
|
3010
|
+
for (const svcName of redundant) {
|
|
3011
|
+
fail(
|
|
3012
|
+
`${rel}: Gateway API ${kind} (документ ${docIndex}): backendRef «${svcName}» має namespace «${routeNs}», що збігається з metadata.namespace маршруту — прибери поле namespace з backendRef (див. k8s.mdc)`
|
|
3013
|
+
)
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
2901
3017
|
}
|
|
2902
3018
|
|
|
2903
3019
|
/**
|
|
@@ -5762,12 +5878,13 @@ export async function check() {
|
|
|
5762
5878
|
const { pass, fail } = reporter
|
|
5763
5879
|
|
|
5764
5880
|
const root = process.cwd()
|
|
5881
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
5765
5882
|
|
|
5766
|
-
await rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass)
|
|
5883
|
+
await rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, ignorePaths, fail, pass)
|
|
5767
5884
|
|
|
5768
|
-
await removeBackendConfigOnlyK8sYamlFiles(root, fail, pass)
|
|
5885
|
+
await removeBackendConfigOnlyK8sYamlFiles(root, ignorePaths, fail, pass)
|
|
5769
5886
|
|
|
5770
|
-
const yamlFiles = await findK8sYamlFiles(root)
|
|
5887
|
+
const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
|
|
5771
5888
|
|
|
5772
5889
|
if (yamlFiles.length === 0) {
|
|
5773
5890
|
pass('Немає *.yaml під k8s — перевірку $schema пропущено')
|
|
@@ -19,6 +19,7 @@ import { basename, dirname, join, relative } from 'node:path'
|
|
|
19
19
|
|
|
20
20
|
import { findDockerfilePaths } from './check-docker.mjs'
|
|
21
21
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
22
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
22
23
|
import { walkDir } from './utils/walkDir.mjs'
|
|
23
24
|
|
|
24
25
|
const LINE_SPLIT_RE = /\r?\n/u
|
|
@@ -36,15 +37,19 @@ const GZIP_EXTENSION_RE = /\*\.(?:js|css)/u
|
|
|
36
37
|
* @param {string} root корінь cwd
|
|
37
38
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до шаблонів
|
|
38
39
|
*/
|
|
39
|
-
export async function findDefaultConfTemplatePaths(root) {
|
|
40
|
+
export async function findDefaultConfTemplatePaths(root, ignorePaths = []) {
|
|
40
41
|
/** @type {string[]} */
|
|
41
42
|
const out = []
|
|
42
|
-
await walkDir(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
await walkDir(
|
|
44
|
+
root,
|
|
45
|
+
p => {
|
|
46
|
+
if (basename(p) !== 'default.conf.template') return
|
|
47
|
+
const rel = relative(root, p).replaceAll('\\', '/')
|
|
48
|
+
if (rel.includes('tests/fixtures/')) return
|
|
49
|
+
out.push(p)
|
|
50
|
+
},
|
|
51
|
+
ignorePaths
|
|
52
|
+
)
|
|
48
53
|
return out.toSorted((a, b) => a.localeCompare(b))
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -54,12 +59,16 @@ export async function findDefaultConfTemplatePaths(root) {
|
|
|
54
59
|
* @param {string} root корінь обходу (зазвичай cwd репозиторію)
|
|
55
60
|
* @returns {Promise<{ renamed: string[], overwritten: string[] }>} відносні шляхи до обробленого **default.tpl.conf** (для звіту)
|
|
56
61
|
*/
|
|
57
|
-
export async function migrateDefaultTplConfFiles(root) {
|
|
62
|
+
export async function migrateDefaultTplConfFiles(root, ignorePaths = []) {
|
|
58
63
|
/** @type {string[]} */
|
|
59
64
|
const oldPaths = []
|
|
60
|
-
await walkDir(
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
await walkDir(
|
|
66
|
+
root,
|
|
67
|
+
p => {
|
|
68
|
+
if (basename(p) === 'default.tpl.conf') oldPaths.push(p)
|
|
69
|
+
},
|
|
70
|
+
ignorePaths
|
|
71
|
+
)
|
|
63
72
|
oldPaths.sort((a, b) => a.localeCompare(b))
|
|
64
73
|
|
|
65
74
|
/** @type {string[]} */
|
|
@@ -316,8 +325,8 @@ async function checkTemplateFile(abs, root, passFn, failFn) {
|
|
|
316
325
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
317
326
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
318
327
|
*/
|
|
319
|
-
async function checkDockerfiles(root, passFn, failFn) {
|
|
320
|
-
const dockerPaths = await findDockerfilePaths(root)
|
|
328
|
+
async function checkDockerfiles(root, ignorePaths, passFn, failFn) {
|
|
329
|
+
const dockerPaths = await findDockerfilePaths(root, ignorePaths)
|
|
321
330
|
if (dockerPaths.length === 0) {
|
|
322
331
|
failFn(
|
|
323
332
|
'Є default.conf.template, але немає Dockerfile / Containerfile — додай gzip для статики та envsubst (див. nginx-default-tpl.mdc)'
|
|
@@ -380,8 +389,9 @@ export async function check() {
|
|
|
380
389
|
const { pass, fail } = reporter
|
|
381
390
|
|
|
382
391
|
const root = process.cwd()
|
|
392
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
383
393
|
|
|
384
|
-
const { renamed: tplRenamed, overwritten: tplOverwritten } = await migrateDefaultTplConfFiles(root)
|
|
394
|
+
const { renamed: tplRenamed, overwritten: tplOverwritten } = await migrateDefaultTplConfFiles(root, ignorePaths)
|
|
385
395
|
for (const rel of tplRenamed) {
|
|
386
396
|
pass(`Перейменовано default.tpl.conf → default.conf.template: ${rel}`)
|
|
387
397
|
}
|
|
@@ -389,7 +399,7 @@ export async function check() {
|
|
|
389
399
|
pass(`Перезаписано default.conf.template змістом default.tpl.conf: ${rel}`)
|
|
390
400
|
}
|
|
391
401
|
|
|
392
|
-
const templates = await findDefaultConfTemplatePaths(root)
|
|
402
|
+
const templates = await findDefaultConfTemplatePaths(root, ignorePaths)
|
|
393
403
|
|
|
394
404
|
if (templates.length === 0) {
|
|
395
405
|
pass('Немає default.conf.template — перевірку nginx-default-tpl пропущено')
|
|
@@ -402,7 +412,7 @@ export async function check() {
|
|
|
402
412
|
await checkTemplateFile(abs, root, pass, fail)
|
|
403
413
|
}
|
|
404
414
|
|
|
405
|
-
await checkDockerfiles(root, pass, fail)
|
|
415
|
+
await checkDockerfiles(root, ignorePaths, pass, fail)
|
|
406
416
|
await checkVscodeNginx(pass, fail)
|
|
407
417
|
|
|
408
418
|
return reporter.getExitCode()
|
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
* Якщо таких файлів немає — layout через `npm/tsconfig.emit-types.json`: поле `types` має вказувати на існуючий
|
|
10
10
|
* файл під `./types/…`, у hk — `tsc -p tsconfig.emit-types.json`, у JSON-конфігу — потрібні compilerOptions для emit.
|
|
11
11
|
*
|
|
12
|
-
* Окремо перевіряється `npm/CHANGELOG.md`: файл існує, є в `files` у `npm/package.json` і містить запис
|
|
13
|
-
* для поточної версії (формат `## [X.Y.Z] - YYYY-MM-DD`, Keep a Changelog).
|
|
14
|
-
*
|
|
15
12
|
* Поля workflow перевіряються після **YAML parse**, щоб не плутати з коментарями.
|
|
16
13
|
*/
|
|
17
14
|
import { existsSync } from 'node:fs'
|
|
@@ -26,6 +23,7 @@ import {
|
|
|
26
23
|
pushHasMainBranch,
|
|
27
24
|
pushPathsIncludeNpmGlob
|
|
28
25
|
} from './utils/gha-workflow.mjs'
|
|
26
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
29
27
|
import { walkDir } from './utils/walkDir.mjs'
|
|
30
28
|
|
|
31
29
|
const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
|
|
@@ -36,24 +34,25 @@ const TYPES_INDEX = './types/index.d.ts'
|
|
|
36
34
|
/** Файл проєкту TypeScript для emit без каталогу `src` (див. npm-module.mdc) */
|
|
37
35
|
const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
|
|
38
36
|
|
|
39
|
-
/** Шлях до `CHANGELOG.md` в каталозі npm-модуля */
|
|
40
|
-
const CHANGELOG_PATH = 'npm/CHANGELOG.md'
|
|
41
|
-
|
|
42
37
|
/**
|
|
43
38
|
* Чи є під `npm/src` хоча б один `.js` (рекурсивно).
|
|
44
39
|
* @returns {Promise<boolean>} `true`, якщо знайдено хоча б один `.js`
|
|
45
40
|
*/
|
|
46
|
-
async function npmSrcTreeHasJsFile() {
|
|
41
|
+
async function npmSrcTreeHasJsFile(ignorePaths = []) {
|
|
47
42
|
const root = 'npm/src'
|
|
48
43
|
if (!existsSync(root)) {
|
|
49
44
|
return false
|
|
50
45
|
}
|
|
51
46
|
let found = false
|
|
52
|
-
await walkDir(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
await walkDir(
|
|
48
|
+
root,
|
|
49
|
+
p => {
|
|
50
|
+
if (p.endsWith('.js')) {
|
|
51
|
+
found = true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
ignorePaths
|
|
55
|
+
)
|
|
57
56
|
return found
|
|
58
57
|
}
|
|
59
58
|
|
|
@@ -200,53 +199,6 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
|
|
|
200
199
|
}
|
|
201
200
|
}
|
|
202
201
|
|
|
203
|
-
/**
|
|
204
|
-
* Чи містить текст `CHANGELOG.md` запис заголовка для конкретної версії
|
|
205
|
-
* у форматі Keep a Changelog: `## [X.Y.Z]` (з опційним `- YYYY-MM-DD`).
|
|
206
|
-
* @param {string} text вміст `CHANGELOG.md`
|
|
207
|
-
* @param {string} version номер версії з `npm/package.json`
|
|
208
|
-
* @returns {boolean} `true`, якщо знайдено заголовок версії
|
|
209
|
-
*/
|
|
210
|
-
function changelogHasVersionEntry(text, version) {
|
|
211
|
-
const escaped = version.replaceAll(/[.+*?^$()[\]{}|\\]/g, String.raw`\$&`)
|
|
212
|
-
const re = new RegExp(String.raw`^##\s+\[${escaped}\]`, 'm')
|
|
213
|
-
return re.test(text)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Перевіряє наявність і вміст `npm/CHANGELOG.md`, а також що він є в `files` у `npm/package.json`.
|
|
218
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
219
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
220
|
-
*/
|
|
221
|
-
async function checkChangelog(passFn, failFn) {
|
|
222
|
-
if (!existsSync(CHANGELOG_PATH)) {
|
|
223
|
-
failFn(`Відсутній ${CHANGELOG_PATH} (npm-module.mdc: CHANGELOG)`)
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
passFn(`${CHANGELOG_PATH} існує`)
|
|
227
|
-
|
|
228
|
-
if (existsSync('npm/package.json')) {
|
|
229
|
-
const npmPkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
|
|
230
|
-
if (Array.isArray(npmPkg.files) && npmPkg.files.includes('CHANGELOG.md')) {
|
|
231
|
-
passFn('npm/package.json: files містить "CHANGELOG.md"')
|
|
232
|
-
} else {
|
|
233
|
-
failFn('npm/package.json: масив files має містити "CHANGELOG.md"')
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const version = typeof npmPkg.version === 'string' ? npmPkg.version : null
|
|
237
|
-
if (version) {
|
|
238
|
-
const text = await readFile(CHANGELOG_PATH, 'utf8')
|
|
239
|
-
if (changelogHasVersionEntry(text, version)) {
|
|
240
|
-
passFn(`${CHANGELOG_PATH}: знайдено запис для версії ${version}`)
|
|
241
|
-
} else {
|
|
242
|
-
failFn(
|
|
243
|
-
`${CHANGELOG_PATH}: відсутній запис для поточної версії ${version} (формат Keep a Changelog: "## [${version}] - YYYY-MM-DD")`
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
202
|
/**
|
|
251
203
|
* Перевіряє npm/tsconfig.emit-types.json.
|
|
252
204
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
@@ -387,12 +339,11 @@ export async function check() {
|
|
|
387
339
|
|
|
388
340
|
await checkNpmModuleBasicStructure(pass, fail)
|
|
389
341
|
|
|
390
|
-
const
|
|
342
|
+
const ignorePaths = await loadCursorIgnorePaths(process.cwd())
|
|
343
|
+
const useSrcJsLayout = await npmSrcTreeHasJsFile(ignorePaths)
|
|
391
344
|
|
|
392
345
|
await checkNpmPackageJson(useSrcJsLayout, pass, fail)
|
|
393
346
|
|
|
394
|
-
await checkChangelog(pass, fail)
|
|
395
|
-
|
|
396
347
|
if (!useSrcJsLayout) {
|
|
397
348
|
await checkEmitTypesConfig(pass, fail)
|
|
398
349
|
}
|