@nitra/cursor 1.8.221 → 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/.claude-template/npm-CLAUDE.md +4 -0
- package/CHANGELOG.md +69 -0
- package/bin/auto-rules.md +2 -0
- package/bin/n-cursor.js +3 -2
- package/mdc/abie.mdc +13 -0
- package/mdc/ci4.mdc +8 -0
- package/mdc/tauri.mdc +20 -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/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
- package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
- package/policy/k8s/base_manifest/base_manifest.rego +154 -0
- package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
- package/policy/k8s/gateway/gateway.rego +151 -0
- package/policy/k8s/gateway/gateway_test.rego +122 -0
- package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
- package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
- package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
- package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
- package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
- package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
- package/policy/k8s/kustomization/kustomization.rego +220 -0
- package/policy/k8s/kustomization/kustomization_test.rego +128 -0
- package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
- package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
- package/policy/k8s/manifest/manifest.rego +151 -4
- package/policy/k8s/manifest/manifest_test.rego +309 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
- package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
- package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -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 +98 -3
- package/scripts/lint-ga.mjs +18 -132
- package/scripts/lint-rego.mjs +19 -4
- package/scripts/utils/run-conftest-batch.mjs +117 -0
|
@@ -79,6 +79,18 @@ const K8S_DIR_PATH_RE = /(^|\/)k8s\//u
|
|
|
79
79
|
const K8S_HC_YAML_PATH_RE = /(^|\/)k8s\/.+\/hc\.yaml$/u
|
|
80
80
|
/** `…/k8s/…/base/…/hr.yaml` (HTTPRoute у base-шарі). */
|
|
81
81
|
const K8S_BASE_HR_YAML_PATH_RE = /(^|\/)k8s\/.*base\/.*hr\.yaml$/u
|
|
82
|
+
/** Будь-який ресурсний YAML під `…/k8s/.../base/...` (для abie.base_deployment_preem). */
|
|
83
|
+
const K8S_BASE_RESOURCE_PATH_RE = /(^|\/)k8s\/.*base\//u
|
|
84
|
+
/** `kustomization.yaml` будь-де під сегментом `k8s/`. */
|
|
85
|
+
const K8S_KUSTOMIZATION_PATH_RE = /(^|\/)k8s\/.*\/kustomization\.yaml$/u
|
|
86
|
+
/** `…/k8s/.../base/.../kustomization.yaml`. */
|
|
87
|
+
const K8S_BASE_KUSTOMIZATION_PATH_RE = /(^|\/)k8s\/.*base\/(?:.*\/)?kustomization\.yaml$/u
|
|
88
|
+
/** Будь-який ресурсний `*.yaml` під сегментом `…/k8s/.../base/...`, окрім `kustomization.yaml`. */
|
|
89
|
+
const K8S_BASE_MANIFEST_PATH_RE = /(^|\/)k8s\/.*base\//u
|
|
90
|
+
/** `…/k8s/.../svc.yaml` (cluster-IP Service). */
|
|
91
|
+
const K8S_SVC_YAML_PATH_RE = /(^|\/)k8s\/.+\/svc\.yaml$/u
|
|
92
|
+
/** `…/k8s/.../svc-hl.yaml` (headless Service). */
|
|
93
|
+
const K8S_SVC_HL_YAML_PATH_RE = /(^|\/)k8s\/.+\/svc-hl\.yaml$/u
|
|
82
94
|
|
|
83
95
|
/** @type {ConftestTarget[]} */
|
|
84
96
|
const TARGETS = [
|
|
@@ -221,15 +233,77 @@ const TARGETS = [
|
|
|
221
233
|
// Усі YAML у дереві з сегментом `k8s` — пер-документні структурні правила.
|
|
222
234
|
{
|
|
223
235
|
namespace: 'k8s.manifest',
|
|
224
|
-
policyDir: 'k8s',
|
|
236
|
+
policyDir: 'k8s/manifest',
|
|
225
237
|
rule: 'k8s',
|
|
226
238
|
walk: { match: rel => K8S_DIR_PATH_RE.test(rel) && (rel.endsWith('.yaml') || rel.endsWith('.yml')) }
|
|
227
239
|
},
|
|
228
240
|
|
|
241
|
+
// Gateway API + HealthCheckPolicy — застосовується до будь-якого YAML під k8s
|
|
242
|
+
// (правила перевіряють лише відповідні kind / apiVersion).
|
|
243
|
+
{
|
|
244
|
+
namespace: 'k8s.gateway',
|
|
245
|
+
policyDir: 'k8s/gateway',
|
|
246
|
+
rule: 'k8s',
|
|
247
|
+
walk: { match: rel => K8S_DIR_PATH_RE.test(rel) && (rel.endsWith('.yaml') || rel.endsWith('.yml')) }
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
// Структурні перевірки HPA / PDB (apiVersion / behavior / metrics / selector).
|
|
251
|
+
{
|
|
252
|
+
namespace: 'k8s.hpa_pdb',
|
|
253
|
+
policyDir: 'k8s/hpa_pdb',
|
|
254
|
+
rule: 'k8s',
|
|
255
|
+
walk: { match: rel => K8S_DIR_PATH_RE.test(rel) && (rel.endsWith('.yaml') || rel.endsWith('.yml')) }
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
// Kustomization-файли: resources sort, patches sort, JSON6902 conflicts.
|
|
259
|
+
{
|
|
260
|
+
namespace: 'k8s.kustomization',
|
|
261
|
+
policyDir: 'k8s/kustomization',
|
|
262
|
+
rule: 'k8s',
|
|
263
|
+
walk: { match: rel => K8S_KUSTOMIZATION_PATH_RE.test(rel) }
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
// svc.yaml — cluster-IP Service.
|
|
267
|
+
{
|
|
268
|
+
namespace: 'k8s.svc_yaml',
|
|
269
|
+
policyDir: 'k8s/svc_yaml',
|
|
270
|
+
rule: 'k8s',
|
|
271
|
+
walk: { match: rel => K8S_SVC_YAML_PATH_RE.test(rel) }
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// svc-hl.yaml — headless Service з суфіксом `-hl`.
|
|
275
|
+
{
|
|
276
|
+
namespace: 'k8s.svc_hl_yaml',
|
|
277
|
+
policyDir: 'k8s/svc_hl_yaml',
|
|
278
|
+
rule: 'k8s',
|
|
279
|
+
walk: { match: rel => K8S_SVC_HL_YAML_PATH_RE.test(rel) }
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
// base/kustomization.yaml — обов'язкове непорожнє поле `namespace:`.
|
|
283
|
+
{
|
|
284
|
+
namespace: 'k8s.base_kustomization',
|
|
285
|
+
policyDir: 'k8s/base_kustomization',
|
|
286
|
+
rule: 'k8s',
|
|
287
|
+
walk: { match: rel => K8S_BASE_KUSTOMIZATION_PATH_RE.test(rel) }
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// Ресурсні маніфести під `…/k8s/.../base/...` (окрім kustomization.yaml).
|
|
291
|
+
{
|
|
292
|
+
namespace: 'k8s.base_manifest',
|
|
293
|
+
policyDir: 'k8s/base_manifest',
|
|
294
|
+
rule: 'k8s',
|
|
295
|
+
walk: {
|
|
296
|
+
match: rel =>
|
|
297
|
+
K8S_BASE_MANIFEST_PATH_RE.test(rel) &&
|
|
298
|
+
!K8S_BASE_KUSTOMIZATION_PATH_RE.test(rel) &&
|
|
299
|
+
(rel.endsWith('.yaml') || rel.endsWith('.yml'))
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
|
|
229
303
|
// abie HealthCheckPolicy: `hc.yaml` у дереві k8s.
|
|
230
304
|
{
|
|
231
305
|
namespace: 'abie.health_check_policy',
|
|
232
|
-
policyDir: 'abie',
|
|
306
|
+
policyDir: 'abie/health_check_policy',
|
|
233
307
|
rule: 'abie',
|
|
234
308
|
walk: { match: rel => K8S_HC_YAML_PATH_RE.test(rel) }
|
|
235
309
|
},
|
|
@@ -237,9 +311,30 @@ const TARGETS = [
|
|
|
237
311
|
// abie HTTPRoute у `base/`.
|
|
238
312
|
{
|
|
239
313
|
namespace: 'abie.http_route_base',
|
|
240
|
-
policyDir: 'abie',
|
|
314
|
+
policyDir: 'abie/http_route_base',
|
|
241
315
|
rule: 'abie',
|
|
242
316
|
walk: { match: rel => K8S_BASE_HR_YAML_PATH_RE.test(rel) }
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
// abie Deployment у `…/k8s/.../base/...` має preem nodeSelector.
|
|
320
|
+
{
|
|
321
|
+
namespace: 'abie.base_deployment_preem',
|
|
322
|
+
policyDir: 'abie/base_deployment_preem',
|
|
323
|
+
rule: 'abie',
|
|
324
|
+
walk: {
|
|
325
|
+
match: rel =>
|
|
326
|
+
K8S_BASE_RESOURCE_PATH_RE.test(rel) &&
|
|
327
|
+
!K8S_BASE_KUSTOMIZATION_PATH_RE.test(rel) &&
|
|
328
|
+
(rel.endsWith('.yaml') || rel.endsWith('.yml'))
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
// abie clean-merged-branch.yml: with.ignore_branches має містити dev/ua/ru.
|
|
333
|
+
{
|
|
334
|
+
namespace: 'abie.clean_merged_ignore_branches',
|
|
335
|
+
policyDir: 'abie/clean_merged_ignore_branches',
|
|
336
|
+
rule: 'abie',
|
|
337
|
+
single: '.github/workflows/clean-merged-branch.yml'
|
|
243
338
|
}
|
|
244
339
|
]
|
|
245
340
|
|
package/scripts/lint-ga.mjs
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI-обгортка над канонічним `lint-ga` (ga.mdc): робить preflight на `shellcheck` і `uv` (для `uvx`),
|
|
3
3
|
* тоді послідовно виконує `bunx github-actionlint`, `uvx zizmor --offline --collect=workflows .` і
|
|
4
|
-
* (
|
|
4
|
+
* делегує до `check-ga.mjs::check()` — там і Rego-частина (через `runConftestBatch`),
|
|
5
|
+
* і JS cross-file перевірки правил `ga.mdc`.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 1) per-workflow polysi (`ga.<name>`) — для канонічних `clean-ga-workflows`, `clean-merged-branch`,
|
|
12
|
-
* `lint-ga`, `git-ai`, що мають фіксовані поля (cron, ім'я кроку тощо);
|
|
13
|
-
* 2) `ga.workflow_common` — універсальні правила (concurrency, заборонені setup-bun/cache/install у
|
|
14
|
-
* кроках, shell line-continuation у `run:`, checkout перед локальним setup-bun-deps), які
|
|
15
|
-
* застосовуються до **кожного** `.github/workflows/*.yml`.
|
|
7
|
+
* Plan B-патерн (rego-authoritative): Rego-полісі (`npm/policy/ga/`) запускає вже сам
|
|
8
|
+
* `check-ga.mjs::check()` як перший крок — `lint-ga.mjs` про це не знає. Раніше `lint-ga.mjs` сам
|
|
9
|
+
* спавнив conftest для `ga.<name>` per-workflow і `ga.workflow_common` (PoC); тепер ця логіка
|
|
10
|
+
* централізована у `check-ga.mjs`, тож одне джерело істини, без дублювання між
|
|
11
|
+
* `lint-ga` і `npx @nitra/cursor check ga`.
|
|
16
12
|
*
|
|
17
13
|
* Без preflight `actionlint` (через `bunx github-actionlint`) мовчки пропускає shell-перевірки в
|
|
18
14
|
* `run:` блоках, коли `shellcheck` відсутній у PATH; локально `bun lint-ga` лишається зеленим, а CI
|
|
@@ -23,49 +19,12 @@
|
|
|
23
19
|
*
|
|
24
20
|
* Експортовано окремо `runLintGaCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-ga`.
|
|
25
21
|
*/
|
|
26
|
-
import { existsSync, readdirSync } from 'node:fs'
|
|
27
22
|
import { spawnSync } from 'node:child_process'
|
|
28
|
-
import { dirname, join } from 'node:path'
|
|
29
23
|
import { platform } from 'node:process'
|
|
30
|
-
import { fileURLToPath } from 'node:url'
|
|
31
24
|
|
|
25
|
+
import { check as checkGa } from './check-ga.mjs'
|
|
32
26
|
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
33
27
|
|
|
34
|
-
/** Каталог пакету `@nitra/cursor`, від якого ресолвимо вшиту директорію policy/. */
|
|
35
|
-
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
36
|
-
|
|
37
|
-
/** Шлях до кореня Rego-полісі для GA. У npm-tarball публікується через `files: ["policy"]` у package.json. */
|
|
38
|
-
const GA_POLICY_DIR = join(PACKAGE_ROOT, 'policy', 'ga')
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Workflow-файли, для яких маємо відповідну Rego-полісі. Кожен таргет посилається на під-пакет
|
|
42
|
-
* `ga.<name>` у `policy/ga/<name>/<name>.rego`; conftest викликаємо з `--namespace`, щоб правила
|
|
43
|
-
* іншого workflow не застосовувалися до чужого файлу.
|
|
44
|
-
* @type {Array<{ workflow: string, namespace: string, label: string }>}
|
|
45
|
-
*/
|
|
46
|
-
const CONFTEST_TARGETS = [
|
|
47
|
-
{
|
|
48
|
-
workflow: '.github/workflows/clean-ga-workflows.yml',
|
|
49
|
-
namespace: 'ga.clean_ga_workflows',
|
|
50
|
-
label: 'clean-ga-workflows.yml structure'
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
workflow: '.github/workflows/clean-merged-branch.yml',
|
|
54
|
-
namespace: 'ga.clean_merged_branch',
|
|
55
|
-
label: 'clean-merged-branch.yml structure'
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
workflow: '.github/workflows/lint-ga.yml',
|
|
59
|
-
namespace: 'ga.lint_ga',
|
|
60
|
-
label: 'lint-ga.yml structure'
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
workflow: '.github/workflows/git-ai.yml',
|
|
64
|
-
namespace: 'ga.git_ai',
|
|
65
|
-
label: 'git-ai.yml structure'
|
|
66
|
-
}
|
|
67
|
-
]
|
|
68
|
-
|
|
69
28
|
/**
|
|
70
29
|
* Опис залежності preflight-ом: бінарник, для чого потрібен, і команди встановлення.
|
|
71
30
|
* @typedef {object} PreflightDep
|
|
@@ -177,22 +136,24 @@ function runStep(title, cmd, args) {
|
|
|
177
136
|
}
|
|
178
137
|
|
|
179
138
|
/**
|
|
180
|
-
* Виконує канонічний `lint-ga` з preflight-перевірками
|
|
139
|
+
* Виконує канонічний `lint-ga` з preflight-перевірками і делегує до `check-ga.check()`.
|
|
181
140
|
*
|
|
182
141
|
* Послідовність:
|
|
183
142
|
* 1) preflight: `shellcheck` (для actionlint SC-правил) і `uv` (для `uvx zizmor`); відсутній → exit 1;
|
|
184
143
|
* 2) `bunx github-actionlint`;
|
|
185
|
-
* 3) `uvx zizmor --offline --collect=workflows
|
|
144
|
+
* 3) `uvx zizmor --offline --collect=workflows .`;
|
|
145
|
+
* 4) `check-ga.mjs::check()` — Rego-полісі (батч conftest з `npm/policy/ga/`) + JS cross-file
|
|
146
|
+
* перевірки правил `ga.mdc`. Це **те саме**, що робить `npx @nitra/cursor check ga`, тож
|
|
147
|
+
* `lint-ga` тепер є суперсетом перевірки правила: external-tools + check.
|
|
186
148
|
*
|
|
187
149
|
* Якщо хоча б один preflight не пройшов — виходимо одразу з кодом 1, **до** запуску actionlint/zizmor,
|
|
188
150
|
* бо їхні власні повідомлення про відсутність залежностей менш інформативні (особливо для shellcheck —
|
|
189
151
|
* actionlint мовчки пропускає SC-правила; ця перевірка — головний сенс обгортки).
|
|
190
152
|
*
|
|
191
|
-
* Першу помилку від actionlint/zizmor повертаємо як код виходу; наступні кроки не
|
|
192
|
-
*
|
|
193
|
-
* @returns {number} 0 — все OK, інакше — код першого кроку, що впав
|
|
153
|
+
* Першу помилку від actionlint/zizmor/check повертаємо як код виходу; наступні кроки не запускаються.
|
|
154
|
+
* @returns {Promise<number>} 0 — все OK, інакше — код першого кроку, що впав
|
|
194
155
|
*/
|
|
195
|
-
export function runLintGaCli() {
|
|
156
|
+
export async function runLintGaCli() {
|
|
196
157
|
let preflightOk = true
|
|
197
158
|
for (const dep of [SHELLCHECK_PREFLIGHT, UV_PREFLIGHT]) {
|
|
198
159
|
if (!preflight(dep)) preflightOk = false
|
|
@@ -205,81 +166,6 @@ export function runLintGaCli() {
|
|
|
205
166
|
const zizmorCode = runStep('zizmor', 'uvx', ['zizmor', '--offline', '--collect=workflows', '.'])
|
|
206
167
|
if (zizmorCode !== 0) return zizmorCode
|
|
207
168
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* PoC-крок: запускає conftest на YAML workflow проти Rego-полісі з пакету (`policy/ga/`).
|
|
213
|
-
*
|
|
214
|
-
* Поведінка fallback:
|
|
215
|
-
* - якщо `conftest` не знайдено в PATH — друкуємо `ℹ` повідомлення з підказкою встановлення й
|
|
216
|
-
* повертаємо 0 (тобто конфтест поки що **не** є обовʼязковою залежністю lint-ga; перевірки лежать
|
|
217
|
-
* паралельно в `check-ga.mjs`, і `npx \@nitra/cursor check ga` все одно їх запустить);
|
|
218
|
-
* - якщо `conftest` є й полісі-каталог відсутній (нетипова інсталяція) — також `ℹ` skip;
|
|
219
|
-
* - якщо є цільовий workflow і conftest — запускаємо `conftest test <workflow> -p <policy-dir>` і
|
|
220
|
-
* повертаємо його exit-код, щоб порушення зупиняли lint-ga, як це робить actionlint/zizmor.
|
|
221
|
-
*
|
|
222
|
-
* Локальний `conftest` встановлюється через `brew install conftest` / `go install ...` — деталі в
|
|
223
|
-
* https://www.conftest.dev/install/.
|
|
224
|
-
* @returns {number} 0 — OK або skip, інакше — exit-код conftest
|
|
225
|
-
*/
|
|
226
|
-
function runConftestStep() {
|
|
227
|
-
const conftestBin = resolveCmd('conftest')
|
|
228
|
-
if (!conftestBin) {
|
|
229
|
-
console.log(
|
|
230
|
-
'\nℹ conftest не знайдено в PATH — пропускаю PoC-перевірку структури workflow через Rego-полісі.\n' +
|
|
231
|
-
' Встанови, щоб запустити її локально: brew install conftest (macOS) або https://www.conftest.dev/install/'
|
|
232
|
-
)
|
|
233
|
-
return 0
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (!existsSync(GA_POLICY_DIR)) {
|
|
237
|
-
console.log(`\nℹ Каталог Rego-полісі не знайдено (${GA_POLICY_DIR}) — пропускаю conftest.`)
|
|
238
|
-
return 0
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
for (const target of CONFTEST_TARGETS) {
|
|
242
|
-
if (!existsSync(target.workflow)) continue
|
|
243
|
-
const code = runStep(`conftest (${target.label})`, conftestBin, [
|
|
244
|
-
'test',
|
|
245
|
-
target.workflow,
|
|
246
|
-
'-p',
|
|
247
|
-
GA_POLICY_DIR,
|
|
248
|
-
'--namespace',
|
|
249
|
-
target.namespace,
|
|
250
|
-
'--no-color'
|
|
251
|
-
])
|
|
252
|
-
if (code !== 0) return code
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return runConftestWorkflowCommon(conftestBin)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Прогоняє `ga.workflow_common` на кожному `.github/workflows/*.yml` — універсальні перевірки
|
|
260
|
-
* (concurrency, заборонені setup-bun/cache/install, shell line-continuation, checkout перед
|
|
261
|
-
* локальним setup-bun-deps). Якщо директорії немає або файлів немає — мовчки skip.
|
|
262
|
-
*
|
|
263
|
-
* Викликаємо conftest на всіх файлах одним прогоном (`conftest test <files...>`) — швидше, ніж
|
|
264
|
-
* по одному, і summary-лог зрозуміліший. Перший ненульовий exit-код повертаємо як результат.
|
|
265
|
-
* @param {string} conftestBin абсолютний шлях до бінарника conftest
|
|
266
|
-
* @returns {number} 0 — OK, інакше exit-код conftest
|
|
267
|
-
*/
|
|
268
|
-
function runConftestWorkflowCommon(conftestBin) {
|
|
269
|
-
const wfDir = '.github/workflows'
|
|
270
|
-
if (!existsSync(wfDir)) return 0
|
|
271
|
-
const ymlFiles = readdirSync(wfDir)
|
|
272
|
-
.filter(f => f.endsWith('.yml'))
|
|
273
|
-
.map(f => join(wfDir, f))
|
|
274
|
-
if (ymlFiles.length === 0) return 0
|
|
275
|
-
|
|
276
|
-
return runStep('conftest (workflow_common — усі workflow)', conftestBin, [
|
|
277
|
-
'test',
|
|
278
|
-
...ymlFiles,
|
|
279
|
-
'-p',
|
|
280
|
-
GA_POLICY_DIR,
|
|
281
|
-
'--namespace',
|
|
282
|
-
'ga.workflow_common',
|
|
283
|
-
'--no-color'
|
|
284
|
-
])
|
|
169
|
+
console.log('\n▶ check-ga (rego-полісі npm/policy/ga/ + JS cross-file перевірки)')
|
|
170
|
+
return await checkGa()
|
|
285
171
|
}
|
package/scripts/lint-rego.mjs
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Лінт Rego-полісі (`conftest.mdc` + `rego.mdc`): preflight на `opa` і `regal`,
|
|
3
|
-
* далі послідовно `opa check --strict
|
|
3
|
+
* далі послідовно `opa check --strict`, `regal lint` і опційний
|
|
4
|
+
* `conftest verify` (для `*_test.rego`-файлів) якщо conftest у PATH.
|
|
4
5
|
*
|
|
5
|
-
* Чому
|
|
6
|
+
* Чому два-три інструменти:
|
|
6
7
|
* - `opa check --strict` — компіляція з типами і строгим режимом (мертвий код, неоднозначні
|
|
7
8
|
* правила, незадекларовані змінні). Ловить помилки, які `regal` навмисно лишає поза скоупом
|
|
8
9
|
* (він — про стиль і ідіоматичність, а не про компіляцію).
|
|
9
10
|
* - `regal lint` (https://docs.styra.com/regal) — статичний лінтер Rego: ловить v0-синтаксис,
|
|
10
11
|
* неявні set-rules та інші відхилення від `rego.v1`, плюс bugs/idiomatic/performance-правила.
|
|
12
|
+
* - `conftest verify` (опційно) — виконує `test_*` правила у `*_test.rego` (юніт-тести політик).
|
|
13
|
+
* Якщо conftest відсутній у PATH — пропускаємо без помилки (тести опційні в локальному середовищі;
|
|
14
|
+
* у CI потрібно встановити conftest).
|
|
11
15
|
*
|
|
12
16
|
* Без preflight-у на бінарники лінт мовчки злетить з невиразним повідомленням від shell —
|
|
13
17
|
* друкуємо явні install-hints (як це робить `lint-ga.mjs` для shellcheck/uv). `opa` додатково
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
*
|
|
17
21
|
* Цілі лінту: `npm/policy/` (місце, де поки що живуть Rego-полісі пакета `@nitra/cursor`).
|
|
18
22
|
* Якщо в репозиторії з’являться інші *.rego поза цим деревом, додай шлях у `LINT_TARGETS` —
|
|
19
|
-
*
|
|
23
|
+
* усі три інструменти приймають кілька шляхів і самі рекурсивно обходять директорії.
|
|
20
24
|
*/
|
|
21
25
|
import { spawnSync } from 'node:child_process'
|
|
22
26
|
import { existsSync } from 'node:fs'
|
|
@@ -110,7 +114,18 @@ export function runLintRego(cwd = process.cwd()) {
|
|
|
110
114
|
const opaCode = runStep(opa, ['check', '--strict', ...targets], root)
|
|
111
115
|
if (opaCode !== 0) return opaCode
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
const regalCode = runStep(regal, ['lint', ...targets], root)
|
|
118
|
+
if (regalCode !== 0) return regalCode
|
|
119
|
+
|
|
120
|
+
const conftest = resolveCmd('conftest')
|
|
121
|
+
if (!conftest) {
|
|
122
|
+
console.log(
|
|
123
|
+
'ℹ conftest не знайдено в PATH — пропускаю `conftest verify` (юніт-тести *_test.rego).\n' +
|
|
124
|
+
' Встанови, щоб запустити локально: brew install conftest (macOS) або https://www.conftest.dev/install/'
|
|
125
|
+
)
|
|
126
|
+
return 0
|
|
127
|
+
}
|
|
128
|
+
return runStep(conftest, ['verify', ...targets.flatMap(t => ['-p', t])], root)
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
process.exitCode = runLintRego()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Запускає `conftest test` на batched-списку файлів і повертає всі порушення
|
|
3
|
+
* у структурованому вигляді. Використовується з `check-*.mjs`-скриптів, де
|
|
4
|
+
* пер-документні правила винесені у `npm/policy/<rule>/<name>/` як rego-полісі
|
|
5
|
+
* (Rego-authoritative). JS у `check-*.mjs` робить cross-file частину (walking
|
|
6
|
+
* дерева, парність, kustomize-резолюція), а пер-документне валідаційне ядро
|
|
7
|
+
* делегується сюди — один спавн `conftest` на (`namespace`, `policyDir`),
|
|
8
|
+
* незалежно від кількості файлів. Це закриває дублювання JS↔rego і прибирає
|
|
9
|
+
* ризик дрифту (типу `spec.config` vs `spec.default.config` у
|
|
10
|
+
* `health_check_policy.rego`, що ми ловили cross-check тестами).
|
|
11
|
+
*
|
|
12
|
+
* Hard-fail на відсутність `conftest` у PATH — узгоджено з рішенням Plan B:
|
|
13
|
+
* якщо правило делегує свою логіку до Rego, а інструмент відсутній, тиха
|
|
14
|
+
* відмова приховує реальні порушення. Друкуємо install-hint (як `lint-rego.mjs`
|
|
15
|
+
* робить для opa/regal).
|
|
16
|
+
*/
|
|
17
|
+
import { spawnSync } from 'node:child_process'
|
|
18
|
+
import { existsSync } from 'node:fs'
|
|
19
|
+
import { dirname, join } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
import { resolveCmd } from './resolve-cmd.mjs'
|
|
23
|
+
|
|
24
|
+
/** Каталог пакета `@nitra/cursor`, від якого ресолвимо вшиту директорію `policy/`. */
|
|
25
|
+
const PACKAGE_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))))
|
|
26
|
+
|
|
27
|
+
/** Шлях до кореня rego-полісі. У npm-tarball публікується через `files: ["policy"]`. */
|
|
28
|
+
const POLICY_ROOT = join(PACKAGE_ROOT, 'policy')
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Друкує install-hint для conftest і кидає виняток, щоб викликана `check-*`
|
|
32
|
+
* команда ясно завершилась з кодом 1.
|
|
33
|
+
* @returns {never}
|
|
34
|
+
*/
|
|
35
|
+
function failConftestMissing() {
|
|
36
|
+
throw new Error(
|
|
37
|
+
[
|
|
38
|
+
'❌ conftest не знайдено в PATH.',
|
|
39
|
+
' Без нього не запускається пер-документна валідація через rego-полісі (npm/policy/).',
|
|
40
|
+
' Встанови:',
|
|
41
|
+
' macOS: brew install conftest',
|
|
42
|
+
' Universal: https://www.conftest.dev/install/'
|
|
43
|
+
].join('\n')
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {object} ConftestViolation
|
|
49
|
+
* @property {string} filename абсолютний шлях до файла, що дав порушення (з output conftest)
|
|
50
|
+
* @property {string} message текст порушення (як у `deny` rego-пакета)
|
|
51
|
+
* @property {string} namespace namespace rego-пакета (наприклад `abie.base_deployment_preem`)
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} ConftestBatchOptions
|
|
56
|
+
* @property {string} policyDirRel шлях до підкаталогу `npm/policy/...` (наприклад `abie/base_deployment_preem`)
|
|
57
|
+
* @property {string} namespace повне імʼя rego-пакета (наприклад `abie.base_deployment_preem`)
|
|
58
|
+
* @property {string[]} files список абсолютних шляхів файлів для перевірки (порожній — повертаємо порожньо)
|
|
59
|
+
* @property {string[]} [extraArgs] додаткові аргументи для conftest (наприклад `--combine` для крос-документних правил)
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Виконує `conftest test` для всіх файлів одним спавном і повертає масив
|
|
64
|
+
* порушень. Якщо `files` порожній — повертає `[]` без спавна. Якщо `conftest`
|
|
65
|
+
* не у PATH — кидає виняток (hard fail, див. модульний docstring).
|
|
66
|
+
* @param {ConftestBatchOptions} opts параметри запуску
|
|
67
|
+
* @returns {ConftestViolation[]} масив порушень (порожній — все ок)
|
|
68
|
+
*/
|
|
69
|
+
export function runConftestBatch(opts) {
|
|
70
|
+
if (opts.files.length === 0) return []
|
|
71
|
+
const conftestBin = resolveCmd('conftest')
|
|
72
|
+
if (!conftestBin) {
|
|
73
|
+
failConftestMissing()
|
|
74
|
+
}
|
|
75
|
+
const policyAbs = join(POLICY_ROOT, opts.policyDirRel)
|
|
76
|
+
if (!existsSync(policyAbs)) {
|
|
77
|
+
throw new Error(`runConftestBatch: rego-каталог не знайдено: ${policyAbs}`)
|
|
78
|
+
}
|
|
79
|
+
const args = [
|
|
80
|
+
'test',
|
|
81
|
+
...opts.files,
|
|
82
|
+
'-p',
|
|
83
|
+
policyAbs,
|
|
84
|
+
'--namespace',
|
|
85
|
+
opts.namespace,
|
|
86
|
+
'--output',
|
|
87
|
+
'json',
|
|
88
|
+
'--no-color',
|
|
89
|
+
...(opts.extraArgs ?? [])
|
|
90
|
+
]
|
|
91
|
+
const result = spawnSync(conftestBin, args, { encoding: 'utf8' })
|
|
92
|
+
if (result.error) {
|
|
93
|
+
throw result.error
|
|
94
|
+
}
|
|
95
|
+
// conftest exit 1 = є failures (це валідно для нас); >1 = справжня помилка.
|
|
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
|
+
)
|
|
100
|
+
}
|
|
101
|
+
/** @type {Array<{ filename: string, namespace: string, failures?: Array<{ msg: string }> }>} */
|
|
102
|
+
let parsed
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(result.stdout)
|
|
105
|
+
} catch (e) {
|
|
106
|
+
throw new Error(`conftest stdout не парситься як JSON: ${(result.stdout || '').slice(0, 200)}`)
|
|
107
|
+
}
|
|
108
|
+
/** @type {ConftestViolation[]} */
|
|
109
|
+
const out = []
|
|
110
|
+
for (const entry of parsed) {
|
|
111
|
+
const failures = entry.failures ?? []
|
|
112
|
+
for (const f of failures) {
|
|
113
|
+
out.push({ filename: entry.filename, namespace: entry.namespace, message: f.msg })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out
|
|
117
|
+
}
|