@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 CHANGED
@@ -4,6 +4,60 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.8.228] - 2026-05-10
8
+
9
+ ### Changed
10
+
11
+ - **k8s / Plan B (rego-authoritative, повна централізація):** rego-крок переїхав на початок `check-k8s.mjs::check()` через новий helper `runAllK8sRego` — батч-виклик `runConftestBatch` для 9 пакетів (`k8s.manifest`, `k8s.gateway`, `k8s.hpa_pdb`, `k8s.kustomization`, `k8s.svc_yaml`, `k8s.svc_hl_yaml`, `k8s.base_kustomization`, `k8s.base_manifest`, `k8s.kustomize_managed`). JS у `check-k8s.mjs` робить лише cross-file orchestration + autofix + modeline. Cross-file orchestrators `validateHasuraConfigMapRemoteSchemaPermissions` і `validateHasuraHttpRouteCanon` рефакторнуто: JS відбирає paired-with-Hasura-Deployment файли, далі батч-conftest на `k8s.hasura_configmap`/`k8s.hasura_httproute`. Видалено JS-orchestrator-функції-дублі (≈10 шт): `scanForbiddenManifestsInYamlDocuments`, `failIfIngressInDocument`, `failIfAutoscalingV1InDocument`, `validateK8sYamlPolicyDocuments`, `failIfK8sPolicyNamespaceRulesViolated`, `failIfK8sPolicyResourceRulesViolated`, `runK8sYamlPolicyAndGatewayScans`, `scanGatewayApiRouteBackendRefsInYamlBody`, `failIfGatewayRouteUsesNonHeadlessService`, `validateKustomizationResourcesSortedAlphabetically`, `validateKustomizationPatchesStructuralSort`, `validateInlinePatchesSorted`, `validateKustomizationJson6902NoRemoveAddSamePath`, `auditJson6902OneKustomizationYamlFile`, `auditJson6902ForKustomizationYamlDoc`, `auditKustomizationPatchesJson6902`, `auditOneKustomizationJson6902Patch`, `auditJson6902PatchExternalFile`, `failIfJson6902RemoveAddConflictOnSamePath`, `verifyBaseKustomizationNamespaceOnFile`, `ensureBaseKustomizationHasNamespace`, `readFirstConfigMapDoc`. Видалено публічний predicate `isForbiddenAutoscalingV1Manifest` + його тест (rego `k8s.manifest` авторитативно). Решта predicates лишилися як публічні exports для back-compat (`hpaManifestViolations`, `pdbManifestViolations`, `deploymentTopologySpreadConstraintsViolation` все ще активно використовуються JS cross-file для expected-name/dev-like; інші — тестові shim, можна прибрати окремо).
12
+ - **`checkK8sYamlFile`** залишає тільки modeline + `$schema`-URL перевірки; per-document валідація (Ingress/autoscaling/v1 заборонено, Service GCP-анотації, Deployment resources/Hasura image/topologySpread, Gateway API backendRef правила, HCP, svc/svc-hl, namespace правила) — у rego, виконано на початку `check()`.
13
+
14
+ ## [1.8.227] - 2026-05-10
15
+
16
+ ### Changed
17
+
18
+ - **conftest.mdc (alwaysApply):** канонізовано патерн «Rego-authoritative + JS-orchestrator» (Plan B) як основний для всіх перевірок у репо. Розділ «Гібрид» переписано: замість «JS authoritative + rego-копія» (Plan A) — тепер чітко: пер-документне правило існує **рівно в одному місці** (rego), а `check-<rule>.mjs` делегує його через `runConftestBatch` (`npm/scripts/utils/run-conftest-batch.mjs`), один спавн на namespace. Додано конкретний шаблон `check()` (rego-крок перший, JS cross-file — після) і опис інтеграції з `lint-<rule>.mjs` (external-tools wrapper викликає `await checkX()` як останній крок). Реальні приклади — abie (пілот) і ga (повна централізація). Новий «червоний прапор» забороняє лишати JS-копію rego-правила «про всяк випадок» — це плодить дрифт.
19
+
20
+ ## [1.8.226] - 2026-05-10
21
+
22
+ ### Changed
23
+
24
+ - **ga / Plan B (rego-authoritative, повна централізація):** rego-крок переїхав із `lint-ga.mjs` у `check-ga.mjs::check()` як **перший крок**. Раніше `bun run lint-ga` сам викликав 4 per-workflow conftest + 1 batch для `ga.workflow_common`, а `npx @nitra/cursor check ga` цю частину не робив — тепер вся ga-логіка (rego + JS cross-file) в одному `check-ga.check()`. `lint-ga.mjs::runLintGaCli` спрощено: preflight (shellcheck/uv) → actionlint → zizmor → `await checkGa()`. Видалено: `CONFTEST_TARGETS`, `GA_POLICY_DIR`, `runConftestStep`, `runConftestWorkflowCommon` — і непотрібні імпорти `existsSync`/`readdirSync`/`dirname`/`join`/`fileURLToPath`. `runLintGaCli` тепер `async`; `bin/n-cursor.js` оновлено на `await runLintGaCli()`. Тест `lint-ga.test.mjs` оновлено: `await fn()` замість `fn()`. Тест `check-ga.test.mjs::"exit 1 коли shellcheck відсутній"` переведений на точковий виклик експортованої `checkShellcheckInstalled` (бо `withBinRemovedFromPath('shellcheck')` на macOS заодно видаляв `/opt/homebrew/bin` де conftest, ламаючи hard-fail у `runConftestBatch`).
25
+ - **`check-ga.mjs::checkShellcheckInstalled`:** додано `export` (потрібен для точкового тесту після рефактору).
26
+ - **тестова фікстура `setupCanonicalGaProject` у check-ga.test.mjs:** додано секцію `concurrency` (з канонічними `group` і `cancel-in-progress: true`) у workflow `clean-ga-workflows.yml`, `clean-merged-branch.yml`, `git-ai.yml` — `ga.workflow_common` rego тепер запускається у `check()`, а ці workflow раніше не мали concurrency у фікстурі (правило `lint-ga.yml` уже мало). Це **правильна** реакція: rego-перевірка тепер ловить порушення на тих самих фікстурах, на яких раніше не запускалась.
27
+
28
+ ## [1.8.225] - 2026-05-10
29
+
30
+ ### Added
31
+
32
+ - **utility `runConftestBatch`:** новий `npm/scripts/utils/run-conftest-batch.mjs` — спавнить `conftest test` одним викликом для batched-списку файлів, парсить `--output json`, повертає структуровані `{filename, namespace, message}` порушення. Hard-fail зі install-hint якщо `conftest` не у PATH (узгоджено з рішенням Plan B). Використовується з `check-*.mjs` для делегування пер-документної валідації у Rego-полісі без помітного сповільнення (один спавн на namespace, не на файл).
33
+
34
+ ### Changed
35
+
36
+ - **abie / Plan B (rego-authoritative, pilot):** `npm/scripts/check-abie.mjs` рефакторнуто — пер-документна валідація 4 правил тепер делегується rego через `runConftestBatch`, JS залишає лише cross-file-оркестрацію (walking, path-фільтрацію, парність файлів). Видалені JS-функції-предикати (тепер єдине джерело істини — rego): `abieBaseHttpRouteHostnamesErrors`, `deploymentDocumentHasAbieBasePreemNodeSelector`, `parseCleanMergedIgnoreBranches`, `ignoreBranchesIncludesRequired`, `validateAbieHcPolicy`, плюс хелпери `collectAbieHostnames`, `isAllowedAbieBaseDevHostname`, `isAbiePreemTruthy`, `processBaseHttpRouteDoc`, `httpRouteHasNonEmptyHostnames`, `findHealthCheckPolicyInDocs` і константа `ABIE_REQUIRED_IGNORE_BRANCHES`. Імпорти `flattenWorkflowSteps`, `getStepUses`, `parseWorkflowYaml` (`./utils/gha-workflow.mjs`) теж прибрано — orphan після видалення JS-парсера workflow.
37
+ - **abie.health_check_policy (rego):** виправлено divergence з JS — тепер targetRef.name перевіряється точним match-ем `<hcp.metadata.name>-hl` (з нормалізацією: якщо name вже закінчується на `-hl`, береться як є). До цього rego перевіряло лише суфікс `-hl`, що дозволяло `targetRef.name=bar-hl` для HCP з `name=foo` — це не дзеркалило JS.
38
+ - **`validateAbieHcYaml` → `validateAbieHcModeline`:** export перейменовано — JS-частина перевірки hc.yaml тепер обмежується modeline (`# yaml-language-server: $schema=…`); парсинг YAML і структурна валідація HCP делеговано rego.
39
+ - **`npm/tests/check-abie.test.mjs`:** прибрано тести видалених JS-предикатів (8 тестів) — їх покриття тепер забезпечують `_test.rego` фікстури через `conftest verify`.
40
+ - **`npm/tests/cross-check-rego-abie.test.mjs`:** видалено — після Plan B JS-сторони для крос-чеку немає; `_test.rego` фікстури в кожному abie-пакеті дають аналогічне покриття.
41
+
42
+ ## [1.8.224] - 2026-05-10
43
+
44
+ ### Added
45
+
46
+ - **golden cross-check тести JS↔rego (abie):** додано `npm/tests/cross-check-rego-abie.test.mjs` (25 тестів), який для кожної пари (JS-предикат у `check-abie.mjs` ↔ rego-пакет у `npm/policy/abie/`) подає однаковий вхід у обидва імплементації через `opa eval --format json` і перевіряє інваріант **«обидва бачать порушення або обидва ні»**. Покриває: `deploymentDocumentHasAbieBasePreemNodeSelector` ↔ `abie.base_deployment_preem`; `parseCleanMergedIgnoreBranches`+`ignoreBranchesIncludesRequired` ↔ `abie.clean_merged_ignore_branches`; `abieBaseHttpRouteHostnamesErrors` ↔ `abie.http_route_base`; rego-only golden-фікстури для `abie.health_check_policy` (бо JS-функція `validateAbieHcPolicy` приватна). Тест автоматично пропускається, якщо `opa` не у PATH. Sanity-check ламанням rego навмисно — drift детектується.
47
+
48
+ ## [1.8.223] - 2026-05-10
49
+
50
+ ### Added
51
+
52
+ - **abie / нові rego-пакети:** `npm/policy/abie/base_deployment_preem/` (Deployment у `…/k8s/.../base/...` має `spec.template.spec.nodeSelector.preem` зі значенням, що вважається істинним — boolean `true` або рядок `"true"`); `npm/policy/abie/clean_merged_ignore_branches/` (у workflow `.github/workflows/clean-merged-branch.yml` крок `phpdocker-io/github-actions-delete-abandoned-branches` має `with.ignore_branches` з токенами `dev,ua,ru`, case-insensitive). Реєстрація в `lint-conftest.mjs` TARGETS: walk-pattern для base-resource YAML і single-target для workflow.
53
+ - **abie / `_test.rego` фікстури:** додано юніт-тести для всіх 4 abie-пакетів — нових (`base_deployment_preem_test.rego`, `clean_merged_ignore_branches_test.rego`) і існуючих (`http_route_base_test.rego`, `health_check_policy_test.rego`). 35 тестів покривають happy paths і deny-кейси.
54
+
55
+ ### Changed
56
+
57
+ - **abie.health_check_policy (rego):** виправлено помилковий шлях `spec.config.httpHealthCheck` → правильний `spec.default.config.httpHealthCheck` (узгоджено з `validateAbieHcPolicy` у `check-abie.mjs`). Розширено перевірками: точна `apiVersion: networking.gke.io/v1`, `metadata.name` непорожній, `spec.default.config.type: HTTP`, `targetRef.kind: Service`. Cross-file звірка `<deployment.name>-hl` лишається у JS.
58
+ - **abie.mdc:** додано розділ «Швидкий gate через conftest (Rego)» зі списком rego-пакетів і опису того, що cross-file логіка (парність HCP↔Deployment, обчислений `<name>-hl`, валідація ru/ua-overlay JSON6902 patches, env→cluster DNS, cross-namespace backendRefs) лишається у `check-abie.mjs`.
59
+ - **lint-conftest.mjs TARGETS:** `abie.health_check_policy` і `abie.http_route_base` — `policyDir` уточнено до конкретного підкаталогу (`abie/health_check_policy`, `abie/http_route_base`) замість загального `abie`. Додано шляховий regex `K8S_BASE_RESOURCE_PATH_RE` для базових ресурсних YAML.
60
+
7
61
  ## [1.8.222] - 2026-05-10
8
62
 
9
63
  ### Added
package/bin/n-cursor.js CHANGED
@@ -1327,8 +1327,9 @@ try {
1327
1327
  break
1328
1328
  }
1329
1329
  case 'lint-ga': {
1330
- // Канонічний lint-ga з preflight на shellcheck → actionlint → zizmor (ga.mdc).
1331
- process.exitCode = runLintGaCli()
1330
+ // Канонічний lint-ga з preflight на shellcheck → actionlint → zizmor → check-ga (ga.mdc).
1331
+ // Останній крок (check-ga) async — тому await обов'язковий, інакше process.exitCode буде Promise.
1332
+ process.exitCode = await runLintGaCli()
1332
1333
 
1333
1334
  break
1334
1335
  }
package/mdc/abie.mdc CHANGED
@@ -388,3 +388,16 @@ with:
388
388
  ## Перевірка
389
389
 
390
390
  **`npx @nitra/cursor check abie`**
391
+
392
+ ### Швидкий gate через conftest (Rego)
393
+
394
+ Підмножину пер-документних правил продубльовано як rego-полісі у **`npm/policy/abie/`** (запускається через **`bun run lint-rego`** для `*_test.rego` тестів і **`npx @nitra/cursor lint-conftest`** для прогону по реальних YAML — деталі в **conftest.mdc** / **n-rego.mdc**). JS у **`check-abie.mjs`** authoritative — rego тільки швидкий gate для одиничного маніфеста (зокрема через IDE-розширення `tsandall.opa`).
395
+
396
+ Пакети (директорія в **`npm/policy/abie/`** → namespace → що перевіряє):
397
+
398
+ - **`http_route_base/`** → `abie.http_route_base` — у HTTPRoute під `…/k8s/.../base/...` усі `spec.hostnames` мають бути в домені `aiml.live` (включно з `*.aiml.live` та піддоменами). **Цільові файли:** `…/k8s/.../base/.../hr.yaml`.
399
+ - **`health_check_policy/`** → `abie.health_check_policy` — структура HealthCheckPolicy: `apiVersion: networking.gke.io/v1`, `metadata.name`, `spec.default.config.type: HTTP`, `httpHealthCheck.requestPath` починається з `/`, `port: 8080`, `targetRef.kind: Service`, `targetRef.name` має суфікс `-hl`. **Цільові файли:** `…/k8s/.../hc.yaml`.
400
+ - **`base_deployment_preem/`** → `abie.base_deployment_preem` — Deployment у base/ має `spec.template.spec.nodeSelector.preem` зі значенням `true` (boolean або рядок). **Цільові файли:** ресурсні YAML під `…/k8s/.../base/...`.
401
+ - **`clean_merged_ignore_branches/`** → `abie.clean_merged_ignore_branches` — у workflow `.github/workflows/clean-merged-branch.yml` крок з `uses: phpdocker-io/github-actions-delete-abandoned-branches` має `with.ignore_branches`, що містить токени `dev,ua,ru` (case-insensitive). **Цільові файли:** `.github/workflows/clean-merged-branch.yml`.
402
+
403
+ Cross-file логіка (парність HCP↔Deployment у каталозі, обчислений `<deployment.name>-hl` для `targetRef.name`, валідація ru/ua-overlay JSON6902 patches на Service/HTTPRoute, env→cluster DNS, аналіз cross-namespace backendRefs у пакетах) лишається у **`check-abie.mjs`** — Rego не читає файлову систему й не робить cross-document резолюцію.
package/mdc/ci4.mdc CHANGED
@@ -15,6 +15,14 @@ C4-діаграми проєкту живуть у Markdown поряд із ко
15
15
  тримати знання разом із кодом — версійно, в одному PR і доступно для агентів. Якщо щось
16
16
  важливе про систему існує лише у Confluence/Notion/месенджері — для проєкту цього **немає**.
17
17
 
18
+ ## Розташування
19
+
20
+ C4-діаграми проєкту живуть у теці `docs/ci4/` — це **канонічне місце** для всіх рівнів
21
+ моделі: `01-context.md`, `02-containers.md`, `03-components.md`, `04-code.md`, плюс
22
+ супутні `README.md` (вступ) і `decisions.md` (зведення ADR-впливів на C4). Інших місць
23
+ для C4-схем у репозиторії немає: розпорошення по підтеках сервісів ламає навігацію
24
+ «від системи вглиб» і робить агентський аналіз перед зміною дорожчим.
25
+
18
26
  ## Аналіз перед зміною
19
27
 
20
28
  Перш ніж вносити зміни, агент **читає відповідні C4-файли**: контекст, контейнери, компоненти
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.222",
3
+ "version": "1.8.228",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,56 @@
1
+ # Порт перевірки `deploymentDocumentHasAbieBasePreemNodeSelector` з
2
+ # `npm/scripts/check-abie.mjs` (abie.mdc): кожен `Deployment` у файлах під
3
+ # `…/k8s/.../base/…` має `spec.template.spec.nodeSelector.preem` зі
4
+ # значенням, що вважається істинним (boolean `true` або рядок `"true"`
5
+ # без урахування регістру). Overlays (ua/ru) далі підміняють селектор
6
+ # JSON6902-патчами на `preem: false` / `yandex.cloud/preemptible: false`.
7
+ #
8
+ # Запуск (локально, лише для одного base-YAML з Deployment):
9
+ # conftest test path/to/k8s/base/deployment.yaml \
10
+ # -p npm/policy/abie/base_deployment_preem \
11
+ # --namespace abie.base_deployment_preem
12
+ #
13
+ # JS відбирає файли під `…/k8s/.../base/…` (через `isAbieK8sBaseYamlPath`) і
14
+ # викликає conftest з цією намеспейс. JS authoritative (`check-abie.mjs`:
15
+ # `deploymentDocumentHasAbieBasePreemNodeSelector` + `ensureAbieBaseDeploymentPreemNodeSelector`).
16
+ # Cross-file gating (правило `abie` у `.n-cursor.json`, шлях файла) — у JS.
17
+ #
18
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
19
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
20
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
21
+ package abie.base_deployment_preem
22
+
23
+ import rego.v1
24
+
25
+ deny_msg := concat(" ", [
26
+ "Deployment у base: потрібен spec.template.spec.nodeSelector.preem:",
27
+ "true (або 'true') — abie.mdc",
28
+ ])
29
+
30
+ deny contains deny_msg if {
31
+ input.kind == "Deployment"
32
+ not has_truthy_preem
33
+ }
34
+
35
+ # preem truthy: boolean true або рядок "true" (case-insensitive, з обрізаними пробілами).
36
+ has_truthy_preem if {
37
+ preem := object.get(node_selector, "preem", null)
38
+ is_preem_truthy(preem)
39
+ }
40
+
41
+ is_preem_truthy(true)
42
+
43
+ is_preem_truthy(v) if {
44
+ is_string(v)
45
+ lower(trim_space(v)) == "true"
46
+ }
47
+
48
+ node_selector := object.get(
49
+ object.get(
50
+ object.get(object.get(input, "spec", {}), "template", {}),
51
+ "spec",
52
+ {},
53
+ ),
54
+ "nodeSelector",
55
+ {},
56
+ )
@@ -0,0 +1,60 @@
1
+ # Тести для `abie.base_deployment_preem`. Запуск:
2
+ # conftest verify -p npm/policy/abie/base_deployment_preem
3
+ package abie.base_deployment_preem_test
4
+
5
+ import rego.v1
6
+
7
+ import data.abie.base_deployment_preem
8
+
9
+ mk_deployment(node_selector) := {
10
+ "apiVersion": "apps/v1",
11
+ "kind": "Deployment",
12
+ "metadata": {"name": "api", "namespace": "dev"},
13
+ "spec": {"template": {"spec": object.union(
14
+ {"containers": [{"name": "main", "image": "x"}]},
15
+ {"nodeSelector": node_selector},
16
+ )}},
17
+ }
18
+
19
+ test_deny_no_node_selector if {
20
+ input_doc := {
21
+ "apiVersion": "apps/v1",
22
+ "kind": "Deployment",
23
+ "metadata": {"name": "api"},
24
+ "spec": {"template": {"spec": {"containers": [{"name": "main", "image": "x"}]}}},
25
+ }
26
+ count(base_deployment_preem.deny) > 0 with input as input_doc
27
+ }
28
+
29
+ test_deny_node_selector_without_preem if {
30
+ count(base_deployment_preem.deny) > 0 with input as mk_deployment({"role": "worker"})
31
+ }
32
+
33
+ test_deny_preem_false if {
34
+ count(base_deployment_preem.deny) > 0 with input as mk_deployment({"preem": false})
35
+ }
36
+
37
+ test_deny_preem_string_false if {
38
+ count(base_deployment_preem.deny) > 0 with input as mk_deployment({"preem": "false"})
39
+ }
40
+
41
+ test_allow_preem_boolean_true if {
42
+ count(base_deployment_preem.deny) == 0 with input as mk_deployment({"preem": true})
43
+ }
44
+
45
+ test_allow_preem_string_true if {
46
+ count(base_deployment_preem.deny) == 0 with input as mk_deployment({"preem": "true"})
47
+ }
48
+
49
+ test_allow_preem_string_uppercase_true if {
50
+ count(base_deployment_preem.deny) == 0 with input as mk_deployment({"preem": "TRUE"})
51
+ }
52
+
53
+ # Не Deployment — пакет не діє (дзеркало JS-предиката).
54
+ test_allow_non_deployment if {
55
+ count(base_deployment_preem.deny) == 0 with input as {
56
+ "apiVersion": "v1",
57
+ "kind": "ConfigMap",
58
+ "metadata": {"name": "x"},
59
+ }
60
+ }
@@ -0,0 +1,100 @@
1
+ # Порт перевірки `parseCleanMergedIgnoreBranches` + `ignoreBranchesIncludesRequired`
2
+ # з `npm/scripts/check-abie.mjs` (abie.mdc): у workflow
3
+ # `.github/workflows/clean-merged-branch.yml` крок з
4
+ # `uses: phpdocker-io/github-actions-delete-abandoned-branches` має у
5
+ # `with.ignore_branches` містити усі обовʼязкові токени `dev,ua,ru`
6
+ # (case-insensitive, кома-розділені).
7
+ #
8
+ # Запуск (локально):
9
+ # conftest test .github/workflows/clean-merged-branch.yml \
10
+ # -p npm/policy/abie/clean_merged_ignore_branches \
11
+ # --namespace abie.clean_merged_ignore_branches
12
+ #
13
+ # JS authoritative (`check-abie.mjs`: `checkCleanMergedBranch`,
14
+ # `parseCleanMergedIgnoreBranches`, `ignoreBranchesIncludesRequired`); ця Rego —
15
+ # швидкий gate для одиничного workflow YAML. Cross-file гейтинг (правило
16
+ # `abie` у `.n-cursor.json`) — у JS.
17
+ #
18
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
19
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
20
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
21
+ package abie.clean_merged_ignore_branches
22
+
23
+ import rego.v1
24
+
25
+ # Обовʼязкові гілки в `ignore_branches` (узгоджено з `ABIE_REQUIRED_IGNORE_BRANCHES`).
26
+ required_branches := {"dev", "ua", "ru"}
27
+
28
+ # Префікс `uses:` для GitHub Action, у якого читаємо `with.ignore_branches`.
29
+ target_action_marker := "phpdocker-io/github-actions-delete-abandoned-branches"
30
+
31
+ step_missing_msg := concat(" ", [
32
+ "clean-merged-branch.yml: не знайдено крок з uses: phpdocker-io/github-actions-delete-abandoned-branches",
33
+ "(abie.mdc)",
34
+ ])
35
+
36
+ ignore_branches_missing_msg := concat(" ", [
37
+ "clean-merged-branch.yml: не знайдено with.ignore_branches у кроці",
38
+ "phpdocker-io/github-actions-delete-abandoned-branches (abie.mdc)",
39
+ ])
40
+
41
+ # ── deny: крок не знайдено ────────────────────────────────────────────────
42
+
43
+ deny contains step_missing_msg if {
44
+ count(target_steps) == 0
45
+ }
46
+
47
+ # ── deny: з step нема with.ignore_branches ────────────────────────────────
48
+
49
+ deny contains ignore_branches_missing_msg if {
50
+ count(target_steps) > 0
51
+ not has_ignore_branches_value
52
+ }
53
+
54
+ # ── deny: ignore_branches не містить усіх обов'язкових токенів ────────────
55
+
56
+ deny contains msg if {
57
+ count(target_steps) > 0
58
+ ignore_branches_value != ""
59
+ missing := required_branches - parsed_ignore_tokens(ignore_branches_value)
60
+ count(missing) > 0
61
+ msg := sprintf(
62
+ "clean-merged-branch.yml: ignore_branches має містити %v (зараз: %q; не вистачає: %v) (abie.mdc)",
63
+ [concat(",", sort(required_branches)), ignore_branches_value, concat(",", sort(missing))],
64
+ )
65
+ }
66
+
67
+ # ── helpers ───────────────────────────────────────────────────────────────
68
+
69
+ # Усі steps з усіх jobs у workflow (підтримує jobs.<job>.steps[]).
70
+ target_steps contains step if {
71
+ some job in object.get(input, "jobs", {})
72
+ some step in object.get(job, "steps", [])
73
+ uses := object.get(step, "uses", "")
74
+ is_string(uses)
75
+ contains(uses, target_action_marker)
76
+ }
77
+
78
+ # Чи у знайдених steps хоча б у одного є with.ignore_branches непорожнім рядком.
79
+ has_ignore_branches_value if {
80
+ some step in target_steps
81
+ v := object.get(object.get(step, "with", {}), "ignore_branches", null)
82
+ is_string(v)
83
+ }
84
+
85
+ default ignore_branches_value := ""
86
+
87
+ ignore_branches_value := values[0] if {
88
+ values := [v |
89
+ some step in target_steps
90
+ v := object.get(object.get(step, "with", {}), "ignore_branches", null)
91
+ is_string(v)
92
+ ]
93
+ count(values) > 0
94
+ }
95
+
96
+ # Розбирає `ignore_branches` як `,`-розділений список, нормалізує через trim+lower.
97
+ parsed_ignore_tokens(value) := {lower(trim_space(part)) |
98
+ some part in split(value, ",")
99
+ trim_space(part) != ""
100
+ }
@@ -0,0 +1,48 @@
1
+ # Тести для `abie.clean_merged_ignore_branches`. Запуск:
2
+ # conftest verify -p npm/policy/abie/clean_merged_ignore_branches
3
+ package abie.clean_merged_ignore_branches_test
4
+
5
+ import rego.v1
6
+
7
+ import data.abie.clean_merged_ignore_branches
8
+
9
+ # Каркас workflow з одним job, що містить step із заданим with.
10
+ mk_workflow(step_with) := {"jobs": {"cleanup": {"steps": [{
11
+ "uses": "phpdocker-io/github-actions-delete-abandoned-branches@v2",
12
+ "with": step_with,
13
+ }]}}}
14
+
15
+ other_step_workflow := {"jobs": {"cleanup": {"steps": [{"uses": "actions/checkout@v6"}]}}}
16
+
17
+ # Workflow без потрібного кроку.
18
+ test_deny_step_missing if {
19
+ count(clean_merged_ignore_branches.deny) > 0 with input as other_step_workflow
20
+ }
21
+
22
+ test_deny_ignore_branches_missing if {
23
+ count(clean_merged_ignore_branches.deny) > 0 with input as mk_workflow({})
24
+ }
25
+
26
+ test_deny_missing_required_token if {
27
+ count(clean_merged_ignore_branches.deny) > 0 with input as mk_workflow({"ignore_branches": "dev,ua"})
28
+ }
29
+
30
+ test_deny_completely_wrong_tokens if {
31
+ count(clean_merged_ignore_branches.deny) > 0 with input as mk_workflow({"ignore_branches": "main,develop"})
32
+ }
33
+
34
+ test_allow_all_three_tokens if {
35
+ count(clean_merged_ignore_branches.deny) == 0 with input as mk_workflow({"ignore_branches": "dev,ua,ru"})
36
+ }
37
+
38
+ # Регістронезалежне порівняння і пропуск пробілів.
39
+ test_allow_uppercase_with_spaces if {
40
+ count(clean_merged_ignore_branches.deny) == 0 with input as mk_workflow({"ignore_branches": " DEV , UA , RU "})
41
+ }
42
+
43
+ extra_branches_workflow := mk_workflow({"ignore_branches": "dev,ua,ru,main,release/*"})
44
+
45
+ # Додаткові гілки після обов'язкових — дозволено.
46
+ test_allow_extra_branches if {
47
+ count(clean_merged_ignore_branches.deny) == 0 with input as extra_branches_workflow
48
+ }
@@ -1,22 +1,25 @@
1
- # Порт мінімальної структурної перевірки `HealthCheckPolicy` з
1
+ # Порт структурної перевірки `HealthCheckPolicy` з
2
2
  # `npm/scripts/check-abie.mjs` (abie.mdc).
3
3
  #
4
4
  # Запуск (локально):
5
- # conftest test path/to/k8s/.../hc.yaml -p npm/policy/abie \
5
+ # conftest test path/to/k8s/.../hc.yaml \
6
+ # -p npm/policy/abie/health_check_policy \
6
7
  # --namespace abie.health_check_policy
7
8
  #
8
- # Перевіряє, для документів з `kind: HealthCheckPolicy` (apiVersion
9
- # `networking.gke.io/v1`):
10
- # - `spec.config.httpHealthCheck.requestPath` — непорожній шлях, що починається з `/`;
11
- # - `spec.config.httpHealthCheck.port` (або `spec.targetRef.name` суфікс) — `8080`;
12
- # - `spec.targetRef.name` має закінчуватись на `-hl` (headless backend).
9
+ # Перевіряє для `kind: HealthCheckPolicy`:
10
+ # - `apiVersion: networking.gke.io/v1` (точна відповідність);
11
+ # - `metadata.name` — непорожній рядок;
12
+ # - `spec.default.config.type: HTTP`;
13
+ # - `spec.default.config.httpHealthCheck.requestPath` непорожній і
14
+ # починається з `/`;
15
+ # - `spec.default.config.httpHealthCheck.port: 8080`;
16
+ # - `spec.targetRef.kind: Service`;
17
+ # - `spec.targetRef.name` має суфікс `-hl` (headless backend).
13
18
  #
14
- # Cross-file gating (`abie` правило в `.n-cursor.json`, парність з Deployment-каталогу,
15
- # узгодження з `metadata.name` Deployment) — у JS (`check-abie.mjs`). JS-перевірка
16
- # в `check-abie.mjs` (`validateAbieHcPolicy`) authoritative й тестує ширший набір полів
17
- # (apiVersion, spec.default.config.type=="HTTP", targetRef.kind=="Service",
18
- # обчислений `<name>-hl` суфікс); ця Rego — швидкий gate для одиничного YAML
19
- # (наприклад через IDE).
19
+ # Cross-file gating (правило `abie` у `.n-cursor.json`, парність з Deployment-каталогу,
20
+ # точна звірка `targetRef.name` з обчисленим `<deployment.name>-hl`) — у JS
21
+ # (`check-abie.mjs`: `validateAbieHcPolicy`, `checkHcYamlFiles`). JS authoritative;
22
+ # ця Rego — швидкий gate для одиничного YAML (наприклад через IDE).
20
23
  #
21
24
  # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
22
25
  # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
@@ -25,21 +28,58 @@ package abie.health_check_policy
25
28
 
26
29
  import rego.v1
27
30
 
31
+ expected_api_version := "networking.gke.io/v1"
32
+
28
33
  req_path_starts_with_slash_template := concat(" ", [
29
34
  "HealthCheckPolicy: requestPath має починатись з `/`",
30
35
  "(зараз %q) (abie.mdc)",
31
36
  ])
32
37
 
33
- # ── deny: requestPath ──────────────────────────────────────────────────────
38
+ target_ref_name_template := concat(" ", [
39
+ "HealthCheckPolicy: targetRef.name має посилатися на headless Service",
40
+ "(очікується %q, суфікс -hl) (зараз %q) (abie.mdc)",
41
+ ])
42
+
43
+ # ── deny: apiVersion / kind ───────────────────────────────────────────────
34
44
 
35
45
  deny contains msg if {
46
+ input.kind == "HealthCheckPolicy"
47
+ api_version := object.get(input, "apiVersion", "")
48
+ api_version != expected_api_version
49
+ msg := sprintf(
50
+ "HealthCheckPolicy: apiVersion має бути %q (зараз %q) (abie.mdc)",
51
+ [expected_api_version, api_version],
52
+ )
53
+ }
54
+
55
+ # ── deny: metadata.name ───────────────────────────────────────────────────
56
+
57
+ deny contains "HealthCheckPolicy: metadata.name має бути непорожнім рядком (abie.mdc)" if {
58
+ input.kind == "HealthCheckPolicy"
59
+ startswith(object.get(input, "apiVersion", ""), "networking.gke.io/")
60
+ name := object.get(object.get(input, "metadata", {}), "name", "")
61
+ trim_space(name) == ""
62
+ }
63
+
64
+ # ── deny: spec.default.config.type ────────────────────────────────────────
65
+
66
+ deny contains "HealthCheckPolicy: spec.default.config.type має бути HTTP (abie.mdc)" if {
36
67
  is_health_check_policy
68
+ is_object(default_config)
69
+ object.get(default_config, "type", "") != "HTTP"
70
+ }
71
+
72
+ # ── deny: requestPath ─────────────────────────────────────────────────────
73
+
74
+ deny contains "HealthCheckPolicy: spec.default.config.httpHealthCheck.requestPath має бути непорожнім (abie.mdc)" if {
75
+ is_health_check_policy
76
+ is_object(http_health_check)
37
77
  req_path == ""
38
- msg := "HealthCheckPolicy: spec.config.httpHealthCheck.requestPath має бути непорожнім (abie.mdc)"
39
78
  }
40
79
 
41
80
  deny contains msg if {
42
81
  is_health_check_policy
82
+ is_object(http_health_check)
43
83
  req_path != ""
44
84
  not startswith(req_path, "/")
45
85
  msg := sprintf(req_path_starts_with_slash_template, [req_path])
@@ -49,29 +89,58 @@ deny contains msg if {
49
89
 
50
90
  deny contains msg if {
51
91
  is_health_check_policy
92
+ is_object(http_health_check)
52
93
  port := object.get(http_health_check, "port", null)
53
94
  port != null
54
95
  port != 8080
55
96
  msg := sprintf("HealthCheckPolicy: port має бути 8080 (зараз %v) (abie.mdc)", [port])
56
97
  }
57
98
 
58
- # ── deny: targetRef.name закінчується на `-hl` ────────────────────────────
99
+ # ── deny: targetRef.kind == Service ──────────────────────────────────────
59
100
 
60
101
  deny contains msg if {
61
102
  is_health_check_policy
62
- name := object.get(object.get(input.spec, "targetRef", {}), "name", "")
63
- name != ""
64
- not endswith(name, "-hl")
65
- msg := sprintf("HealthCheckPolicy: targetRef.name має закінчуватись на `-hl` (зараз %q) (abie.mdc)", [name])
103
+ target_ref := object.get(object.get(input, "spec", {}), "targetRef", null)
104
+ is_object(target_ref)
105
+ kind := object.get(target_ref, "kind", "")
106
+ kind != ""
107
+ kind != "Service"
108
+ msg := sprintf("HealthCheckPolicy: targetRef.kind має бути Service (зараз %q) (abie.mdc)", [kind])
66
109
  }
67
110
 
68
- # ── helpers ────────────────────────────────────────────────────────────────
111
+ # ── deny: targetRef.name = `<hcp.metadata.name>-hl` (exact, з нормалізацією)
112
+
113
+ deny contains msg if {
114
+ is_health_check_policy
115
+ hcp_name := object.get(object.get(input, "metadata", {}), "name", "")
116
+ hcp_name != ""
117
+ target_name := object.get(object.get(object.get(input, "spec", {}), "targetRef", {}), "name", "")
118
+ target_name != ""
119
+ expected_hl := expected_target_ref_name(hcp_name)
120
+ target_name != expected_hl
121
+ msg := sprintf(target_ref_name_template, [expected_hl, target_name])
122
+ }
123
+
124
+ # Нормалізація: якщо `metadata.name` уже закінчується на `-hl` — використовуємо
125
+ # як є; інакше додаємо суфікс. Узгоджено з `validateAbieHcPolicy`
126
+ # у `check-abie.mjs`.
127
+ expected_target_ref_name(name) := name if {
128
+ endswith(name, "-hl")
129
+ } else := concat("", [name, "-hl"])
130
+
131
+ # ── helpers ───────────────────────────────────────────────────────────────
69
132
 
70
133
  is_health_check_policy if {
71
134
  input.kind == "HealthCheckPolicy"
72
135
  startswith(object.get(input, "apiVersion", ""), "networking.gke.io/")
73
136
  }
74
137
 
75
- http_health_check := object.get(object.get(object.get(input, "spec", {}), "config", {}), "httpHealthCheck", {})
138
+ default_config := object.get(
139
+ object.get(object.get(input, "spec", {}), "default", {}),
140
+ "config",
141
+ {},
142
+ )
143
+
144
+ http_health_check := object.get(default_config, "httpHealthCheck", {})
76
145
 
77
146
  req_path := object.get(http_health_check, "requestPath", "")