@nitra/cursor 1.8.222 → 1.8.228
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 +54 -0
- package/bin/n-cursor.js +3 -2
- package/mdc/abie.mdc +13 -0
- package/mdc/ci4.mdc +8 -0
- package/package.json +1 -1
- package/policy/abie/base_deployment_preem/base_deployment_preem.rego +56 -0
- package/policy/abie/base_deployment_preem/base_deployment_preem_test.rego +60 -0
- package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +100 -0
- package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +48 -0
- package/policy/abie/health_check_policy/health_check_policy.rego +91 -22
- package/policy/abie/health_check_policy/health_check_policy_test.rego +99 -0
- package/policy/abie/http_route_base/http_route_base_test.rego +64 -0
- package/scripts/check-abie.mjs +102 -369
- package/scripts/check-ga.mjs +89 -9
- package/scripts/check-k8s.mjs +128 -569
- package/scripts/lint-conftest.mjs +25 -2
- package/scripts/lint-ga.mjs +18 -132
- package/scripts/utils/run-conftest-batch.mjs +117 -0
package/scripts/check-abie.mjs
CHANGED
|
@@ -53,8 +53,8 @@ import { parseAllDocuments } from 'yaml'
|
|
|
53
53
|
|
|
54
54
|
import { pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
|
|
55
55
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
56
|
-
import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
|
|
57
56
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
57
|
+
import { runConftestBatch } from './utils/run-conftest-batch.mjs'
|
|
58
58
|
import { walkDir } from './utils/walkDir.mjs'
|
|
59
59
|
|
|
60
60
|
const CONFIG_FILE = '.n-cursor.json'
|
|
@@ -100,7 +100,6 @@ const PATCH_PARENT_REF_NS_UA_RE =
|
|
|
100
100
|
const PATCH_PARENT_REF_NS_RU_RE =
|
|
101
101
|
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/imu
|
|
102
102
|
const WEBSOCKET_ANNOTATION_RE = /gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/mu
|
|
103
|
-
const LEADING_EMPTY_LINE_RE = /^\s*\n/u
|
|
104
103
|
const REMOVE_CLUSTER_IP_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIP\b/mu
|
|
105
104
|
const REMOVE_CLUSTER_IP_BEFORE_OP_RE = /path:\s*\/spec\/clusterIP\b[\s\S]{0,200}?op:\s*remove\b/mu
|
|
106
105
|
const REMOVE_CLUSTER_IPS_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIPs\b/mu
|
|
@@ -123,9 +122,6 @@ const HTTPROUTE_BACKENDREF_PORT_8081_RE =
|
|
|
123
122
|
const HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE =
|
|
124
123
|
/value:\s*8081\b[\s\S]{0,200}?path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/port\b/mu
|
|
125
124
|
|
|
126
|
-
/** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
|
|
127
|
-
export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
|
|
128
|
-
|
|
129
125
|
/**
|
|
130
126
|
* Регекс basename env-файлу abie: `dev.env` / `ua.env` / `ru.env`, опційно з провідною крапкою (`.dev.env` тощо).
|
|
131
127
|
* Файл рівно `.env` (без імені) — виключення з правила: локальний файл розробника, `check-abie` його не сканує
|
|
@@ -275,115 +271,11 @@ export function isAbieK8sBaseYamlPath(rel) {
|
|
|
275
271
|
return BASE_SEGMENT_RE.test(norm)
|
|
276
272
|
}
|
|
277
273
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
export function isAllowedAbieBaseDevHostname(hostname) {
|
|
284
|
-
if (typeof hostname !== 'string') {
|
|
285
|
-
return false
|
|
286
|
-
}
|
|
287
|
-
const h = hostname.trim().toLowerCase()
|
|
288
|
-
if (h === '') {
|
|
289
|
-
return false
|
|
290
|
-
}
|
|
291
|
-
const root = ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT
|
|
292
|
-
if (h === root) {
|
|
293
|
-
return true
|
|
294
|
-
}
|
|
295
|
-
if (h === `*.${root}`) {
|
|
296
|
-
return true
|
|
297
|
-
}
|
|
298
|
-
if (h.endsWith(`.${root}`)) {
|
|
299
|
-
return true
|
|
300
|
-
}
|
|
301
|
-
return false
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* @param {unknown} hostnames значення поля spec.hostnames
|
|
306
|
-
* @returns {string[]} непорожні рядки-хости
|
|
307
|
-
*/
|
|
308
|
-
function collectAbieHostnames(hostnames) {
|
|
309
|
-
if (Array.isArray(hostnames)) {
|
|
310
|
-
return hostnames.filter(h => typeof h === 'string' && h.trim() !== '')
|
|
311
|
-
}
|
|
312
|
-
if (typeof hostnames === 'string' && hostnames.trim() !== '') {
|
|
313
|
-
return [hostnames]
|
|
314
|
-
}
|
|
315
|
-
return []
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Повідомлення про недопустимі **spec.hostnames** у **HTTPRoute** у шляху **…/base/…** (abie.mdc).
|
|
320
|
-
* @param {unknown} obj корінь YAML-документа
|
|
321
|
-
* @param {string} rel відносний шлях від кореня репозиторію
|
|
322
|
-
* @returns {string[]} порожньо, якщо перевірка не застосовується або hostnames коректні
|
|
323
|
-
*/
|
|
324
|
-
export function abieBaseHttpRouteHostnamesErrors(obj, rel) {
|
|
325
|
-
if (!isAbieK8sBaseYamlPath(rel)) return []
|
|
326
|
-
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return []
|
|
327
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
328
|
-
if (rec.kind !== 'HTTPRoute') return []
|
|
329
|
-
const spec = rec.spec
|
|
330
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return []
|
|
331
|
-
const hostnames = /** @type {Record<string, unknown>} */ (spec).hostnames
|
|
332
|
-
if (hostnames === undefined) return []
|
|
333
|
-
const hosts = collectAbieHostnames(hostnames)
|
|
334
|
-
if (hosts.length === 0) return []
|
|
335
|
-
const root = ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT
|
|
336
|
-
return hosts
|
|
337
|
-
.filter(h => !isAllowedAbieBaseDevHostname(h))
|
|
338
|
-
.map(
|
|
339
|
-
h =>
|
|
340
|
-
`${rel}: HTTPRoute у base (dev): hostname "${h}" недопустимий — дозволені лише ${root} та піддомени, зокрема *.${root} (abie.mdc)`
|
|
341
|
-
)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Чи значення **`preem`** у base **Deployment** вважається «істинним» за abie.mdc (**true** або рядок **`true`** без урахування регістру).
|
|
346
|
-
* @param {unknown} v значення з YAML
|
|
347
|
-
* @returns {boolean} **true**, якщо значення вважається істинним за abie.mdc
|
|
348
|
-
*/
|
|
349
|
-
function isAbiePreemTruthy(v) {
|
|
350
|
-
if (v === true) {
|
|
351
|
-
return true
|
|
352
|
-
}
|
|
353
|
-
if (typeof v === 'string' && v.trim().toLowerCase() === 'true') {
|
|
354
|
-
return true
|
|
355
|
-
}
|
|
356
|
-
return false
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Чи документ **Deployment** у **`…/base/…`** містить **`spec.template.spec.nodeSelector.preem`** зі значенням **true** (abie.mdc).
|
|
361
|
-
* @param {unknown} obj корінь YAML-документа (**Deployment**)
|
|
362
|
-
* @returns {boolean} true, якщо критерії виконано
|
|
363
|
-
*/
|
|
364
|
-
export function deploymentDocumentHasAbieBasePreemNodeSelector(obj) {
|
|
365
|
-
if (!isDeploymentDoc(obj)) {
|
|
366
|
-
return false
|
|
367
|
-
}
|
|
368
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
369
|
-
const spec = rec.spec
|
|
370
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
371
|
-
return false
|
|
372
|
-
}
|
|
373
|
-
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
374
|
-
if (template === null || typeof template !== 'object' || Array.isArray(template)) {
|
|
375
|
-
return false
|
|
376
|
-
}
|
|
377
|
-
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
378
|
-
if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) {
|
|
379
|
-
return false
|
|
380
|
-
}
|
|
381
|
-
const nodeSelector = /** @type {Record<string, unknown>} */ (podSpec).nodeSelector
|
|
382
|
-
if (nodeSelector === null || typeof nodeSelector !== 'object' || Array.isArray(nodeSelector)) {
|
|
383
|
-
return false
|
|
384
|
-
}
|
|
385
|
-
return isAbiePreemTruthy(nodeSelector.preem)
|
|
386
|
-
}
|
|
274
|
+
// Per-document валідація hostnames у `…/k8s/.../base/.../*.yaml` HTTPRoute
|
|
275
|
+
// (Plan B: Rego-authoritative) — повністю в `npm/policy/abie/http_route_base/`.
|
|
276
|
+
// Per-document валідація `nodeSelector.preem` для Deployment у base — у
|
|
277
|
+
// `npm/policy/abie/base_deployment_preem/`. JS у `check-abie.mjs` робить лише
|
|
278
|
+
// path-фільтрацію + батч-виклик conftest через `runConftestBatch`.
|
|
387
279
|
|
|
388
280
|
/**
|
|
389
281
|
* Чи увімкнено правило **abie** у конфігу репозиторію.
|
|
@@ -414,46 +306,10 @@ export async function isAbieRuleEnabled(root) {
|
|
|
414
306
|
return rules.some(r => String(r).trim().toLowerCase() === 'abie')
|
|
415
307
|
}
|
|
416
308
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
*/
|
|
422
|
-
export function parseCleanMergedIgnoreBranches(content) {
|
|
423
|
-
const root = parseWorkflowYaml(content)
|
|
424
|
-
if (!root) {
|
|
425
|
-
return null
|
|
426
|
-
}
|
|
427
|
-
for (const { step } of flattenWorkflowSteps(root)) {
|
|
428
|
-
const uses = getStepUses(step)
|
|
429
|
-
if (uses.includes('phpdocker-io/github-actions-delete-abandoned-branches')) {
|
|
430
|
-
const w = step.with
|
|
431
|
-
if (w && typeof w === 'object' && !Array.isArray(w)) {
|
|
432
|
-
const ib = /** @type {Record<string, unknown>} */ (w).ignore_branches
|
|
433
|
-
if (typeof ib === 'string') {
|
|
434
|
-
return ib
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return null
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Чи рядок **ignore_branches** містить усі гілки з **required** (для abie — dev, ua, ru).
|
|
444
|
-
* @param {string} ignoreBranches значення **ignore_branches**
|
|
445
|
-
* @param {string[]} required імена гілок (нижній регістр для порівняння)
|
|
446
|
-
* @returns {boolean} true, якщо всі **required** присутні як окремі токени
|
|
447
|
-
*/
|
|
448
|
-
export function ignoreBranchesIncludesRequired(ignoreBranches, required) {
|
|
449
|
-
const parts = new Set(
|
|
450
|
-
ignoreBranches
|
|
451
|
-
.split(',')
|
|
452
|
-
.map(s => s.trim().toLowerCase())
|
|
453
|
-
.filter(Boolean)
|
|
454
|
-
)
|
|
455
|
-
return required.every(r => parts.has(r.toLowerCase()))
|
|
456
|
-
}
|
|
309
|
+
// Per-document валідація `clean-merged-branch.yml` (with.ignore_branches з
|
|
310
|
+
// dev/ua/ru) делегована rego-пакету `abie.clean_merged_ignore_branches`
|
|
311
|
+
// (`npm/policy/abie/clean_merged_ignore_branches/`). JS викликає
|
|
312
|
+
// `runConftestBatch` у `checkCleanMergedBranch`.
|
|
457
313
|
|
|
458
314
|
/**
|
|
459
315
|
* Збирає абсолютні шляхи до **.yaml** / **.yml** під деревом, де є сегмент **k8s**.
|
|
@@ -895,33 +751,11 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
|
895
751
|
return dirs
|
|
896
752
|
}
|
|
897
753
|
|
|
898
|
-
/**
|
|
899
|
-
* Перевіряє документи з одного файлу на наявність Deployment з preem nodeSelector.
|
|
900
|
-
* @param {import('yaml').Document[]} docs документи з файлу
|
|
901
|
-
* @param {string} rel відносний шлях файлу
|
|
902
|
-
* @param {(msg: string) => void} fail callback
|
|
903
|
-
* @returns {'violation' | 'found' | 'none'} результат перевірки
|
|
904
|
-
*/
|
|
905
|
-
function checkBaseDeploymentDocsForPreem(docs, rel, fail) {
|
|
906
|
-
for (const doc of docs) {
|
|
907
|
-
if (doc.errors.length === 0) {
|
|
908
|
-
const obj = doc.toJSON()
|
|
909
|
-
if (isDeploymentDoc(obj)) {
|
|
910
|
-
if (!deploymentDocumentHasAbieBasePreemNodeSelector(obj)) {
|
|
911
|
-
fail(
|
|
912
|
-
`${rel}: Deployment у base: потрібен spec.template.spec.nodeSelector.preem: true (або 'true') — abie.mdc`
|
|
913
|
-
)
|
|
914
|
-
return 'violation'
|
|
915
|
-
}
|
|
916
|
-
return 'found'
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
return 'none'
|
|
921
|
-
}
|
|
922
|
-
|
|
923
754
|
/**
|
|
924
755
|
* Для кожного **Deployment** у YAML під **`k8s`** з шляхом **`…/base/…`** вимагає **`spec.template.spec.nodeSelector.preem: true`** (abie.mdc).
|
|
756
|
+
*
|
|
757
|
+
* Per-document валідація делегована у rego-пакет **`abie.base_deployment_preem`**
|
|
758
|
+
* (`npm/policy/abie/base_deployment_preem/`) — JS лише фільтрує файли за path-патерном `base/` і батчем спавнить conftest.
|
|
925
759
|
* @param {string} root корінь репозиторію
|
|
926
760
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
927
761
|
* @param {(msg: string) => void} fail callback
|
|
@@ -930,19 +764,21 @@ function checkBaseDeploymentDocsForPreem(docs, rel, fail) {
|
|
|
930
764
|
*/
|
|
931
765
|
async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
|
|
932
766
|
const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const docs = await readAndParseYamlDocs(abs, rel, fail)
|
|
937
|
-
if (!docs) return
|
|
938
|
-
const r = checkBaseDeploymentDocsForPreem(docs, rel, fail)
|
|
939
|
-
if (r === 'violation') return
|
|
940
|
-
if (r === 'found') anyBaseDeployment = true
|
|
767
|
+
if (baseFiles.length === 0) {
|
|
768
|
+
passFn('Немає файлів у шляхах …/base/… — перевірку preem у base пропущено')
|
|
769
|
+
return
|
|
941
770
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
771
|
+
const violations = runConftestBatch({
|
|
772
|
+
policyDirRel: 'abie/base_deployment_preem',
|
|
773
|
+
namespace: 'abie.base_deployment_preem',
|
|
774
|
+
files: baseFiles
|
|
775
|
+
})
|
|
776
|
+
for (const v of violations) {
|
|
777
|
+
const rel = relative(root, v.filename).replaceAll('\\', '/') || v.filename
|
|
778
|
+
fail(`${rel}: ${v.message}`)
|
|
779
|
+
}
|
|
780
|
+
if (violations.length === 0) {
|
|
781
|
+
passFn('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc (rego)')
|
|
946
782
|
}
|
|
947
783
|
}
|
|
948
784
|
|
|
@@ -1363,89 +1199,22 @@ export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
|
|
|
1363
1199
|
return validateAbieNginxRunHttpRoutePatches(combined, mode, raw) === null
|
|
1364
1200
|
}
|
|
1365
1201
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
*/
|
|
1372
|
-
function validateAbieHcPolicy(policy, relPath) {
|
|
1373
|
-
if (policy.apiVersion !== 'networking.gke.io/v1') {
|
|
1374
|
-
return `${relPath}: apiVersion має бути networking.gke.io/v1 (abie.mdc)`
|
|
1375
|
-
}
|
|
1376
|
-
const meta = policy.metadata
|
|
1377
|
-
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
1378
|
-
return `${relPath}: відсутній metadata (abie.mdc)`
|
|
1379
|
-
}
|
|
1380
|
-
const name = /** @type {Record<string, unknown>} */ (meta).name
|
|
1381
|
-
if (typeof name !== 'string' || name.trim() === '') {
|
|
1382
|
-
return `${relPath}: metadata.name має бути непорожнім рядком (abie.mdc)`
|
|
1383
|
-
}
|
|
1384
|
-
const spec = policy.spec
|
|
1385
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
1386
|
-
return `${relPath}: відсутній spec (abie.mdc)`
|
|
1387
|
-
}
|
|
1388
|
-
const specRec = /** @type {Record<string, unknown>} */ (spec)
|
|
1389
|
-
const def = specRec.default
|
|
1390
|
-
if (def === null || typeof def !== 'object' || Array.isArray(def)) {
|
|
1391
|
-
return `${relPath}: відсутній spec.default (abie.mdc)`
|
|
1392
|
-
}
|
|
1393
|
-
const config = /** @type {Record<string, unknown>} */ (def).config
|
|
1394
|
-
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
1395
|
-
return `${relPath}: відсутній spec.default.config (abie.mdc)`
|
|
1396
|
-
}
|
|
1397
|
-
if (config.type !== 'HTTP') return `${relPath}: spec.default.config.type має бути HTTP (abie.mdc)`
|
|
1398
|
-
const httpHc = /** @type {Record<string, unknown>} */ (config).httpHealthCheck
|
|
1399
|
-
if (httpHc === null || typeof httpHc !== 'object' || Array.isArray(httpHc)) {
|
|
1400
|
-
return `${relPath}: відсутній httpHealthCheck (abie.mdc)`
|
|
1401
|
-
}
|
|
1402
|
-
const requestPath = typeof httpHc.requestPath === 'string' ? httpHc.requestPath.trim() : ''
|
|
1403
|
-
if (requestPath === '' || !requestPath.startsWith('/')) {
|
|
1404
|
-
return `${relPath}: httpHealthCheck.requestPath має бути непорожнім шляхом від кореня (рядок, що починається з /) (abie.mdc)`
|
|
1405
|
-
}
|
|
1406
|
-
if (httpHc.port !== 8080) return `${relPath}: httpHealthCheck.port має бути 8080 (abie.mdc)`
|
|
1407
|
-
const targetRef = specRec.targetRef
|
|
1408
|
-
if (targetRef === null || typeof targetRef !== 'object' || Array.isArray(targetRef)) {
|
|
1409
|
-
return `${relPath}: відсутній targetRef (abie.mdc)`
|
|
1410
|
-
}
|
|
1411
|
-
const tr = /** @type {Record<string, unknown>} */ (targetRef)
|
|
1412
|
-
if (tr.kind !== 'Service') return `${relPath}: targetRef.kind має бути Service (abie.mdc)`
|
|
1413
|
-
const expectedHl = name.endsWith('-hl') ? name : `${name}-hl`
|
|
1414
|
-
if (typeof tr.name !== 'string' || tr.name !== expectedHl) {
|
|
1415
|
-
return `${relPath}: targetRef.name має посилатися на headless Service (очікується ${expectedHl}, суфікс -hl) (abie.mdc)`
|
|
1416
|
-
}
|
|
1417
|
-
return null
|
|
1418
|
-
}
|
|
1202
|
+
// Per-document валідація HealthCheckPolicy (apiVersion / spec.default.config /
|
|
1203
|
+
// httpHealthCheck / targetRef з суфіксом `-hl` exact match) делегована
|
|
1204
|
+
// rego-пакету `abie.health_check_policy` (`npm/policy/abie/health_check_policy/`).
|
|
1205
|
+
// JS у `checkHcYamlFiles` робить лише modeline-перевірку (`validateAbieHcModeline`)
|
|
1206
|
+
// і батч-виклик conftest.
|
|
1419
1207
|
|
|
1420
1208
|
/**
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
*/
|
|
1426
|
-
function findHealthCheckPolicyInDocs(docs, relPath) {
|
|
1427
|
-
for (const doc of docs) {
|
|
1428
|
-
if (doc.errors.length > 0) {
|
|
1429
|
-
return { error: `${relPath}: YAML: ${doc.errors.map(e => e.message).join('; ')}` }
|
|
1430
|
-
}
|
|
1431
|
-
const obj = doc.toJSON()
|
|
1432
|
-
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1433
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1434
|
-
if (rec.kind === 'HealthCheckPolicy') {
|
|
1435
|
-
return { policy: rec }
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
return { policy: /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (null)) }
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
/**
|
|
1443
|
-
* Перевіряє hc.yaml на відповідність схемі та структурі HealthCheckPolicy (abie.mdc).
|
|
1209
|
+
* JS-частина перевірки hc.yaml — лише modeline (`# yaml-language-server: $schema=…`).
|
|
1210
|
+
* Парсинг YAML і структурна валідація HealthCheckPolicy делеговано в rego-пакет
|
|
1211
|
+
* **`abie.health_check_policy`** (`npm/policy/abie/health_check_policy/`),
|
|
1212
|
+
* викликається з `checkHcYamlFile` через `runConftestBatch`.
|
|
1444
1213
|
* @param {string} raw вміст файлу
|
|
1445
1214
|
* @param {string} relPath відносний шлях (для повідомлень)
|
|
1446
1215
|
* @returns {string | null} null якщо OK, рядок з помилкою
|
|
1447
1216
|
*/
|
|
1448
|
-
export function
|
|
1217
|
+
export function validateAbieHcModeline(raw, relPath) {
|
|
1449
1218
|
const body = stripBom(raw)
|
|
1450
1219
|
const lines = body.split(LINE_SPLIT_RE)
|
|
1451
1220
|
if (lines.length === 0 || lines[0].trim() === '') {
|
|
@@ -1454,20 +1223,7 @@ export function validateAbieHcYaml(raw, relPath) {
|
|
|
1454
1223
|
const m = lines[0].match(MODELINE_RE)
|
|
1455
1224
|
if (!m) return `${relPath}: перший рядок має бути modeline $schema (abie.mdc)`
|
|
1456
1225
|
if (m[1] !== ABIE_HC_SCHEMA_URL) return `${relPath}: $schema має бути\n ${ABIE_HC_SCHEMA_URL}\n (abie.mdc)`
|
|
1457
|
-
|
|
1458
|
-
const yamlBody = lines.slice(1).join('\n').replace(LEADING_EMPTY_LINE_RE, '')
|
|
1459
|
-
/** @type {import('yaml').Document[]} */
|
|
1460
|
-
let docs
|
|
1461
|
-
try {
|
|
1462
|
-
docs = parseAllDocuments(yamlBody)
|
|
1463
|
-
} catch (error) {
|
|
1464
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1465
|
-
return `${relPath}: не вдалося розібрати YAML (${msg})`
|
|
1466
|
-
}
|
|
1467
|
-
const result = findHealthCheckPolicyInDocs(docs, relPath)
|
|
1468
|
-
if ('error' in result) return result.error
|
|
1469
|
-
if (!result.policy) return `${relPath}: очікується документ kind: HealthCheckPolicy (abie.mdc)`
|
|
1470
|
-
return validateAbieHcPolicy(result.policy, relPath)
|
|
1226
|
+
return null
|
|
1471
1227
|
}
|
|
1472
1228
|
|
|
1473
1229
|
/**
|
|
@@ -1663,37 +1419,6 @@ async function checkHttpRouteKustomization(abs, rel, mode, root, yamlFilesAbs, c
|
|
|
1663
1419
|
return true
|
|
1664
1420
|
}
|
|
1665
1421
|
|
|
1666
|
-
/**
|
|
1667
|
-
* @param {unknown} json YAML-документ
|
|
1668
|
-
* @returns {boolean} true, якщо HTTPRoute має непорожні spec.hostnames
|
|
1669
|
-
*/
|
|
1670
|
-
function httpRouteHasNonEmptyHostnames(json) {
|
|
1671
|
-
if (json === null || typeof json !== 'object' || Array.isArray(json)) return false
|
|
1672
|
-
const rec = /** @type {Record<string, unknown>} */ (json)
|
|
1673
|
-
if (rec.kind !== 'HTTPRoute') return false
|
|
1674
|
-
const spec = rec.spec
|
|
1675
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return false
|
|
1676
|
-
const hostnames = /** @type {Record<string, unknown>} */ (spec).hostnames
|
|
1677
|
-
return collectAbieHostnames(hostnames).length > 0
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
/**
|
|
1681
|
-
* @param {import('yaml').Document} doc YAML-документ з файлу
|
|
1682
|
-
* @param {string} rel відносний шлях для повідомлень
|
|
1683
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
1684
|
-
* @returns {{ hasErrors: boolean, hasHostnames: boolean }} результат обробки документа
|
|
1685
|
-
*/
|
|
1686
|
-
function processBaseHttpRouteDoc(doc, rel, fail) {
|
|
1687
|
-
if (doc.errors.length !== 0) return { hasErrors: false, hasHostnames: false }
|
|
1688
|
-
const json = doc.toJSON()
|
|
1689
|
-
const errs = abieBaseHttpRouteHostnamesErrors(json, rel)
|
|
1690
|
-
if (errs.length > 0) {
|
|
1691
|
-
for (const e of errs) fail(e)
|
|
1692
|
-
return { hasErrors: true, hasHostnames: false }
|
|
1693
|
-
}
|
|
1694
|
-
return { hasErrors: false, hasHostnames: httpRouteHasNonEmptyHostnames(json) }
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
1422
|
/**
|
|
1698
1423
|
* Для кожного **HTTPRoute** у **`…/k8s/base/…`** з непорожніми **`spec.hostnames`** — лише **aiml.live** та піддомени (abie.mdc).
|
|
1699
1424
|
* @param {string} root корінь репозиторію
|
|
@@ -1703,24 +1428,26 @@ function processBaseHttpRouteDoc(doc, rel, fail) {
|
|
|
1703
1428
|
* @returns {Promise<void>}
|
|
1704
1429
|
*/
|
|
1705
1430
|
async function ensureAbieBaseHttpRouteHostnames(root, yamlFilesAbs, fail, passFn) {
|
|
1706
|
-
let baseHttpRoutesWithHostnames = 0
|
|
1707
1431
|
const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1432
|
+
if (baseFiles.length === 0) {
|
|
1433
|
+
passFn('Немає файлів у шляхах …/k8s/base/… — перевірку HTTPRoute hostnames пропущено')
|
|
1434
|
+
return
|
|
1435
|
+
}
|
|
1436
|
+
// Per-document валідація делегована rego-пакету `abie.http_route_base`
|
|
1437
|
+
// (`npm/policy/abie/http_route_base/`) — rego гейтує по `kind == "HTTPRoute"`.
|
|
1438
|
+
const violations = runConftestBatch({
|
|
1439
|
+
policyDirRel: 'abie/http_route_base',
|
|
1440
|
+
namespace: 'abie.http_route_base',
|
|
1441
|
+
files: baseFiles
|
|
1442
|
+
})
|
|
1443
|
+
for (const v of violations) {
|
|
1444
|
+
const rel = relative(root, v.filename).replaceAll('\\', '/') || v.filename
|
|
1445
|
+
fail(`${rel}: ${v.message}`)
|
|
1717
1446
|
}
|
|
1718
|
-
if (
|
|
1447
|
+
if (violations.length === 0) {
|
|
1719
1448
|
passFn(
|
|
1720
|
-
`HTTPRoute у …/k8s/base/…: spec.hostnames відповідають ${ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT} та піддоменам (
|
|
1449
|
+
`HTTPRoute у …/k8s/base/…: spec.hostnames відповідають ${ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT} та піддоменам (rego)`
|
|
1721
1450
|
)
|
|
1722
|
-
} else {
|
|
1723
|
-
passFn('Немає HTTPRoute у …/k8s/base/… з непорожніми spec.hostnames — перевірку aiml.live пропущено')
|
|
1724
1451
|
}
|
|
1725
1452
|
}
|
|
1726
1453
|
|
|
@@ -1811,23 +1538,16 @@ async function checkCleanMergedBranch(root, pass, fail) {
|
|
|
1811
1538
|
fail(`Відсутній ${cleanMergedPath} — потрібен для ignore_branches (abie.mdc)`)
|
|
1812
1539
|
return
|
|
1813
1540
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
fail(
|
|
1825
|
-
'clean-merged-branch.yml: не знайдено with.ignore_branches у кроці phpdocker-io/github-actions-delete-abandoned-branches (abie.mdc)'
|
|
1826
|
-
)
|
|
1827
|
-
} else if (ignoreBranchesIncludesRequired(ib, ABIE_REQUIRED_IGNORE_BRANCHES)) {
|
|
1828
|
-
pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru')
|
|
1829
|
-
} else {
|
|
1830
|
-
fail(`clean-merged-branch.yml: ignore_branches має містити dev, ua та ru (зараз: ${JSON.stringify(ib)}) — abie.mdc`)
|
|
1541
|
+
// Per-document валідація делегована у rego-пакет `abie.clean_merged_ignore_branches`
|
|
1542
|
+
// (`npm/policy/abie/clean_merged_ignore_branches/`). conftest сам читає та парсить YAML.
|
|
1543
|
+
const violations = runConftestBatch({
|
|
1544
|
+
policyDirRel: 'abie/clean_merged_ignore_branches',
|
|
1545
|
+
namespace: 'abie.clean_merged_ignore_branches',
|
|
1546
|
+
files: [cleanMergedPath]
|
|
1547
|
+
})
|
|
1548
|
+
for (const v of violations) fail(v.message)
|
|
1549
|
+
if (violations.length === 0) {
|
|
1550
|
+
pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru (rego)')
|
|
1831
1551
|
}
|
|
1832
1552
|
}
|
|
1833
1553
|
|
|
@@ -1838,39 +1558,52 @@ async function checkCleanMergedBranch(root, pass, fail) {
|
|
|
1838
1558
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
1839
1559
|
* @param {(msg: string) => void} fail callback при помилці
|
|
1840
1560
|
*/
|
|
1841
|
-
async function checkHcYamlFile(root, dir, pass, fail) {
|
|
1842
|
-
const hcAbs = join(dir, 'hc.yaml')
|
|
1843
|
-
const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
|
|
1844
|
-
if (!existsSync(hcAbs)) {
|
|
1845
|
-
fail(`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`)
|
|
1846
|
-
return
|
|
1847
|
-
}
|
|
1848
|
-
let hcRaw
|
|
1849
|
-
try {
|
|
1850
|
-
hcRaw = await readFile(hcAbs, 'utf8')
|
|
1851
|
-
} catch (error) {
|
|
1852
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1853
|
-
fail(`${relHc}: не вдалося прочитати (${msg})`)
|
|
1854
|
-
return
|
|
1855
|
-
}
|
|
1856
|
-
const v = validateAbieHcYaml(hcRaw, relHc)
|
|
1857
|
-
if (v === null) {
|
|
1858
|
-
pass(`${relHc}: відповідає abie.mdc`)
|
|
1859
|
-
} else {
|
|
1860
|
-
fail(v)
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
1561
|
/**
|
|
1865
|
-
* Перевіряє hc.yaml у директоріях з Deployment.
|
|
1562
|
+
* Перевіряє hc.yaml у директоріях з Deployment. JS перевіряє modeline, далі
|
|
1563
|
+
* один батч conftest для усіх знайдених hc.yaml — структурна валідація HCP
|
|
1564
|
+
* делегується rego (`abie.health_check_policy`).
|
|
1866
1565
|
* @param {string} root корінь репозиторію
|
|
1867
1566
|
* @param {Set<string>} deploymentDirs директорії з Deployment (Set)
|
|
1868
1567
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
1869
1568
|
* @param {(msg: string) => void} fail callback при помилці
|
|
1870
1569
|
*/
|
|
1871
1570
|
async function checkHcYamlFiles(root, deploymentDirs, pass, fail) {
|
|
1571
|
+
/** @type {string[]} файли, які пройшли modeline-check і йдуть у conftest */
|
|
1572
|
+
const hcFilesForRego = []
|
|
1872
1573
|
for (const dir of [...deploymentDirs].toSorted((a, b) => a.localeCompare(b))) {
|
|
1873
|
-
|
|
1574
|
+
const hcAbs = join(dir, 'hc.yaml')
|
|
1575
|
+
const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
|
|
1576
|
+
if (!existsSync(hcAbs)) {
|
|
1577
|
+
fail(`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`)
|
|
1578
|
+
continue
|
|
1579
|
+
}
|
|
1580
|
+
let hcRaw
|
|
1581
|
+
try {
|
|
1582
|
+
hcRaw = await readFile(hcAbs, 'utf8')
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1585
|
+
fail(`${relHc}: не вдалося прочитати (${msg})`)
|
|
1586
|
+
continue
|
|
1587
|
+
}
|
|
1588
|
+
const modelineErr = validateAbieHcModeline(hcRaw, relHc)
|
|
1589
|
+
if (modelineErr !== null) {
|
|
1590
|
+
fail(modelineErr)
|
|
1591
|
+
continue
|
|
1592
|
+
}
|
|
1593
|
+
hcFilesForRego.push(hcAbs)
|
|
1594
|
+
}
|
|
1595
|
+
if (hcFilesForRego.length === 0) return
|
|
1596
|
+
const violations = runConftestBatch({
|
|
1597
|
+
policyDirRel: 'abie/health_check_policy',
|
|
1598
|
+
namespace: 'abie.health_check_policy',
|
|
1599
|
+
files: hcFilesForRego
|
|
1600
|
+
})
|
|
1601
|
+
for (const v of violations) {
|
|
1602
|
+
const rel = relative(root, v.filename).replaceAll('\\', '/') || v.filename
|
|
1603
|
+
fail(`${rel}: ${v.message}`)
|
|
1604
|
+
}
|
|
1605
|
+
if (violations.length === 0 && hcFilesForRego.length > 0) {
|
|
1606
|
+
pass(`HealthCheckPolicy: ${hcFilesForRego.length} файл(ів) hc.yaml відповідають abie.mdc (rego)`)
|
|
1874
1607
|
}
|
|
1875
1608
|
}
|
|
1876
1609
|
|