@nitra/cursor 1.8.204 → 1.8.206
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 +23 -1
- package/bin/auto-rules.md +2 -0
- package/mdc/rego.mdc +77 -0
- package/package.json +1 -1
- package/policy/ga/git_ai/git_ai.rego +109 -0
- package/policy/ga/lint_ga/lint_ga.rego +144 -0
- package/scripts/auto-rules.mjs +10 -0
- package/scripts/check-adr.mjs +4 -1
- package/scripts/check-ga.mjs +0 -284
- package/scripts/check-hasura.mjs +3 -3
- package/scripts/check-js-run.mjs +1 -4
- package/scripts/check-k8s.mjs +2 -1
- package/scripts/lint-ga.mjs +10 -0
- package/scripts/lint-rego.mjs +67 -21
- package/scripts/run-shellcheck-text.mjs +1 -4
- package/scripts/utils/depcheck-workflow.mjs +2 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
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.206] - 2026-05-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `mdc/rego.mdc` (нова версія 1.1, з 1.0): VS Code-секція з рекомендованим розширенням `tsandall.opa` (LSP від автора OPA: підсвічування, hover, go-to-definition, format-on-save через `opa fmt`), `.vscode/extensions.json` і `.vscode/settings.json` сніпети для `[rego]` (`editor.defaultFormatter: tsandall.opa`, `formatOnSave: true`); опис кроків `lint-rego` (preflight `opa`+`regal`, далі `opa check --strict` і `regal lint`); `package.json`-сніпет зі скриптом `lint-rego`; install-команди (`brew install opa regal` + universal лінки); приклад `.regal/config.yaml`. Раніше файл містив лише placeholder `npx @nitra/cursor check rego`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- `scripts/lint-rego.mjs`: додано preflight на `opa` (поряд з `regal`) з install-hint `brew install opa` і покликом до VS Code-розширення `tsandall.opa`; до `regal lint` додано попередній крок `opa check --strict <targets>` (типи + строгий режим: мертвий код, неоднозначні правила, незадекларовані змінні) — `opa check` ловить compile-помилки, які `regal` навмисно лишає поза скоупом. Якщо хоч один з `opa`/`regal` відсутній у `PATH` — exit 1 ще до запуску, з підказкою встановлення для обох.
|
|
16
|
+
|
|
17
|
+
## [1.8.205] - 2026-05-08
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `npm/policy/ga/lint_ga/lint_ga.rego` — порт `validateLintGaWorkflowStructure` + `validateLintGaOnTriggers`: `name` / `on.push.branches∋{dev,main}` / `on.pull_request.branches∋{dev,main}` / `on.push.paths∋{.github/actions/**,.github/workflows/**}` / `concurrency` / `jobs.lint-ga.runs-on` / `jobs.lint-ga.permissions.contents=read` / `steps` non-empty / `uses` set містить `actions/checkout@v6`, `./.github/actions/setup-bun-deps`, `astral-sh/setup-uv@v8.0.0` / `run` blob містить `bun run lint-ga`.
|
|
22
|
+
- `npm/policy/ga/git_ai/git_ai.rego` — порт `validateGitAiWorkflowStructure`: `name` / `on.pull_request.types∋closed` / `concurrency` / `jobs.git-ai.if` містить `merged == true` / `permissions.contents=write` / `run` blob містить `curl … usegitai.com … bash` і `git-ai ci github run`.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `scripts/lint-ga.mjs`: `CONFTEST_TARGETS` тепер охоплює всі 4 канонічні GA-workflow — `clean-ga-workflows.yml`, `clean-merged-branch.yml`, `lint-ga.yml`, `git-ai.yml` — кожен зі своїм `--namespace ga.<name>`.
|
|
27
|
+
- `scripts/check-ga.mjs`: видалено `validateLintGaWorkflowStructure`, `validateLintGaOnTriggers`, `validateGitAiWorkflowStructure`, `validateGitAiParsedYaml`, `hasPullRequestClosedTrigger`, `hasJobMergedCondition`, `checkLintGaWorkflow`, `checkGitAiWorkflow`, `checkCanonicalWorkflowsMatchRule`, локальний `isExactString` і відповідні імпорти `anyRunStepIncludes`/`flattenWorkflowSteps`/`getStepRun`/`getStepUses`. Файл скоротився з 1074 → 570 рядків (≈47%) — структурні перевірки канонічних GA-workflow повністю мігрували в conftest. У JS лишилися: file-existence (zizmor.yml, .vscode/settings.json, setup-bun-deps), `package.json` script `lint-ga`, MegaLinter-зачистка, `verifyConcurrencyBlock` для всіх workflow без винятків (включно з не-канонічними), `verifyNoDirectBunOrCache`, `verifyCheckoutBeforeLocalSetupBunDeps`, paths-globs через `git ls-files`, preflight `shellcheck`.
|
|
28
|
+
|
|
7
29
|
## [1.8.204] - 2026-05-07
|
|
8
30
|
|
|
9
31
|
### Changed
|
|
@@ -120,7 +142,7 @@
|
|
|
120
142
|
### Added
|
|
121
143
|
|
|
122
144
|
- `run-shellcheck-text.mjs`: для `lint-text` — перевірка наявності `shellcheck`/`patch`, авто-виправлення через `shellcheck -f diff` + `patch -p1`, фінальний прогін по tracked `*.sh` (git) або `**/*.sh` без `node_modules`.
|
|
123
|
-
- `text` (mdc v1.25 → v1.26): **shellcheck** у ланцюжку `lint-text`, рекомендація **`timonwong.shellcheck`**, тригер workflow
|
|
145
|
+
- `text` (mdc v1.25 → v1.26): **shellcheck** у ланцюжку `lint-text`, рекомендація **`timonwong.shellcheck`**, тригер workflow **`**/\*.sh`**; тести `run-shellcheck-text.test.mjs`.
|
|
124
146
|
|
|
125
147
|
### Changed
|
|
126
148
|
|
package/bin/auto-rules.md
CHANGED
package/mdc/rego.mdc
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Opa, Rego — інструментарій (VS Code, lint-rego)
|
|
3
|
+
version: '1.1'
|
|
4
|
+
globs: "**/*.rego"
|
|
5
|
+
alwaysApply: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Rego (opa)
|
|
9
|
+
|
|
10
|
+
Синтаксичні правила (`rego.v1`, `import rego.v1`, заборона legacy v0) — у `conftest.mdc` (alwaysApply). Цей файл — про **інструментарій**: VS Code, лінтери, форматування.
|
|
11
|
+
|
|
12
|
+
## VS Code
|
|
13
|
+
|
|
14
|
+
Розширення `tsandall.opa` (від автора OPA): підсвічування, hover, go-to-definition, оцінка виразів і `format-on-save` через `opa fmt`. Працює лише за наявності `opa` у `PATH` — встановити нижче.
|
|
15
|
+
|
|
16
|
+
```json title=".vscode/extensions.json"
|
|
17
|
+
{
|
|
18
|
+
"recommendations": ["tsandall.opa"]
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```json title=".vscode/settings.json"
|
|
23
|
+
{
|
|
24
|
+
"[rego]": {
|
|
25
|
+
"editor.defaultFormatter": "tsandall.opa",
|
|
26
|
+
"editor.formatOnSave": true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`opa.checkOnSave` за замовчуванням увімкнено в розширенні — діагностика від `opa check` показується в редакторі, тож синтаксичні/типові помилки видно одразу, без запуску `lint-rego`.
|
|
32
|
+
|
|
33
|
+
## Перевірка
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bun run lint-rego
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Скрипт делегує до `bun ./npm/scripts/lint-rego.mjs`. Послідовність:
|
|
40
|
+
|
|
41
|
+
1. **preflight** — наявність `opa` і `regal` у `PATH`; якщо хоча б одного нема — exit 1 з підказкою встановлення;
|
|
42
|
+
2. `opa check --strict <targets>` — компіляція з типами та `--strict` (мертвий код, неоднозначні правила, незадекларовані змінні);
|
|
43
|
+
3. `regal lint <targets>` — статичний лінтер Rego ([Styra Regal](https://docs.styra.com/regal)): ловить v0-синтаксис, неявні set-rules і відхилення від `rego.v1`, плюс bugs/idiomatic/style-правила.
|
|
44
|
+
|
|
45
|
+
Цілі — `npm/policy/` (де живуть Rego-полісі пакета). Інші *.rego поза деревом додай у `LINT_TARGETS` у `lint-rego.mjs`.
|
|
46
|
+
|
|
47
|
+
### Встановлення інструментів
|
|
48
|
+
|
|
49
|
+
- macOS: `brew install opa regal`
|
|
50
|
+
- Linux/Windows:
|
|
51
|
+
- opa — <https://www.openpolicyagent.org/docs/latest/#1-download-opa>
|
|
52
|
+
- regal — <https://docs.styra.com/regal#installation>
|
|
53
|
+
|
|
54
|
+
Обидва — лише в `PATH`, **не** додавай у `dependencies` / `devDependencies` (як `shellcheck` у `text.mdc`).
|
|
55
|
+
|
|
56
|
+
### `package.json`
|
|
57
|
+
|
|
58
|
+
```json title="package.json"
|
|
59
|
+
{
|
|
60
|
+
"scripts": {
|
|
61
|
+
"lint-rego": "bun ./npm/scripts/lint-rego.mjs"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
У кореневому `lint` (з `text.mdc` і дотичних) включай `bun run lint-rego` — щоб локальний прогін співпадав з CI.
|
|
67
|
+
|
|
68
|
+
## Конфіг regal
|
|
69
|
+
|
|
70
|
+
У корені — `.regal/config.yaml`. Дозволено вимикати окремі правила під специфіку репо (наприклад, conftest-полісі — `deny`-правила як де-факто entrypoint-и):
|
|
71
|
+
|
|
72
|
+
```yaml title=".regal/config.yaml"
|
|
73
|
+
rules:
|
|
74
|
+
idiomatic:
|
|
75
|
+
no-defined-entrypoint:
|
|
76
|
+
level: ignore
|
|
77
|
+
```
|
package/package.json
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Порт перевірки `validateGitAiWorkflowStructure` з `npm/scripts/check-ga.mjs` (ga.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Запуск (локально):
|
|
4
|
+
# conftest test .github/workflows/git-ai.yml \
|
|
5
|
+
# -p npm/policy/ga --namespace ga.git_ai
|
|
6
|
+
#
|
|
7
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
8
|
+
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
9
|
+
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
10
|
+
package ga.git_ai
|
|
11
|
+
|
|
12
|
+
import rego.v1
|
|
13
|
+
|
|
14
|
+
# ── Очікувані значення ─────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
|
|
17
|
+
|
|
18
|
+
expected_name := "Git AI"
|
|
19
|
+
|
|
20
|
+
expected_if_substring := "github.event.pull_request.merged == true"
|
|
21
|
+
|
|
22
|
+
expected_install_substring := "curl -fsSL https://usegitai.com/install.sh | bash"
|
|
23
|
+
|
|
24
|
+
expected_run_substring := "git-ai ci github run"
|
|
25
|
+
|
|
26
|
+
# Шаблон повідомлення про відсутню `concurrency`-секцію — через `concat` для
|
|
27
|
+
# regal style/line-length.
|
|
28
|
+
concurrency_missing_template := concat(" ", [
|
|
29
|
+
"git-ai.yml: відсутня секція concurrency —",
|
|
30
|
+
"додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
34
|
+
#
|
|
35
|
+
# YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
|
|
36
|
+
|
|
37
|
+
gha_on := input["true"]
|
|
38
|
+
|
|
39
|
+
# Job-id містить дефіс — звертаємося через `[…]`. Імʼя `job` (без префіксу пакету)
|
|
40
|
+
# — щоб уникнути regal-правила `rule-name-repeats-package`.
|
|
41
|
+
job := input.jobs["git-ai"]
|
|
42
|
+
|
|
43
|
+
# Усі `run:` зі steps цього job-а, склеєні в один blob — для substring-перевірки.
|
|
44
|
+
job_run_blob := concat("\n", [run |
|
|
45
|
+
run := job.steps[_].run
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
# ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
|
|
49
|
+
|
|
50
|
+
deny contains msg if {
|
|
51
|
+
input.name != expected_name
|
|
52
|
+
msg := sprintf("git-ai.yml: name має бути %q (ga.mdc)", [expected_name])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
deny contains msg if {
|
|
56
|
+
not "closed" in {t | some t in gha_on.pull_request.types}
|
|
57
|
+
msg := "git-ai.yml: on.pull_request.types має містити closed (ga.mdc)"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
deny contains msg if {
|
|
61
|
+
not is_object(input.concurrency)
|
|
62
|
+
msg := sprintf(concurrency_missing_template, [expected_concurrency_group])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
deny contains msg if {
|
|
66
|
+
is_object(input.concurrency)
|
|
67
|
+
input.concurrency.group != expected_concurrency_group
|
|
68
|
+
msg := sprintf("git-ai.yml: concurrency.group має бути %s (ga.mdc)", [expected_concurrency_group])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
deny contains msg if {
|
|
72
|
+
is_object(input.concurrency)
|
|
73
|
+
input.concurrency["cancel-in-progress"] != true
|
|
74
|
+
msg := "git-ai.yml: concurrency.cancel-in-progress має бути true (ga.mdc)"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
deny contains msg if {
|
|
78
|
+
not job
|
|
79
|
+
msg := "git-ai.yml: jobs.git-ai відсутній (ga.mdc)"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
deny contains msg if {
|
|
83
|
+
not contains(job_if_str, expected_if_substring)
|
|
84
|
+
msg := "git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
deny contains msg if {
|
|
88
|
+
job.permissions.contents != "write"
|
|
89
|
+
msg := "git-ai.yml: permissions мають бути contents: write (ga.mdc)"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
deny contains msg if {
|
|
93
|
+
not contains(job_run_blob, expected_install_substring)
|
|
94
|
+
msg := "git-ai.yml: має встановлювати git-ai через curl | bash (ga.mdc)"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
deny contains msg if {
|
|
98
|
+
not contains(job_run_blob, expected_run_substring)
|
|
99
|
+
msg := "git-ai.yml: має виконувати git-ai ci github run (ga.mdc)"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ── helpers ────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
# `if` поле job-а може бути відсутнім — тоді `sprintf` дає невизначене значення
|
|
105
|
+
# і спрацьовує `default`, повертаючи порожній рядок; `contains(…)` нижче дасть
|
|
106
|
+
# false і відповідне `deny`-правило спрацює зі зрозумілим повідомленням.
|
|
107
|
+
default job_if_str := ""
|
|
108
|
+
|
|
109
|
+
job_if_str := sprintf("%v", [job.if])
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Порт перевірок `validateLintGaWorkflowStructure` + `validateLintGaOnTriggers` з
|
|
2
|
+
# `npm/scripts/check-ga.mjs` (ga.mdc).
|
|
3
|
+
#
|
|
4
|
+
# Запуск (локально):
|
|
5
|
+
# conftest test .github/workflows/lint-ga.yml \
|
|
6
|
+
# -p npm/policy/ga --namespace ga.lint_ga
|
|
7
|
+
#
|
|
8
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
9
|
+
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
10
|
+
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
11
|
+
package ga.lint_ga
|
|
12
|
+
|
|
13
|
+
import rego.v1
|
|
14
|
+
|
|
15
|
+
# ── Очікувані значення ─────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
|
|
18
|
+
|
|
19
|
+
expected_name := "Lint GA"
|
|
20
|
+
|
|
21
|
+
expected_branches := {"dev", "main"}
|
|
22
|
+
|
|
23
|
+
expected_push_paths := {".github/actions/**", ".github/workflows/**"}
|
|
24
|
+
|
|
25
|
+
# Шаблон повідомлення про відсутню `concurrency`-секцію — через `concat` для
|
|
26
|
+
# regal style/line-length.
|
|
27
|
+
concurrency_missing_template := concat(" ", [
|
|
28
|
+
"lint-ga.yml: відсутня секція concurrency —",
|
|
29
|
+
"додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
33
|
+
#
|
|
34
|
+
# YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
|
|
35
|
+
|
|
36
|
+
gha_on := input["true"]
|
|
37
|
+
|
|
38
|
+
# Job-id містить дефіс — звертаємося через `[…]`. Імʼя `job` (без префіксу пакету)
|
|
39
|
+
# — щоб уникнути regal-правила `rule-name-repeats-package`.
|
|
40
|
+
job := input.jobs["lint-ga"]
|
|
41
|
+
|
|
42
|
+
# Усі `uses:` зі steps цього job-а — для перевірки членства.
|
|
43
|
+
job_uses_set contains job.steps[_].uses
|
|
44
|
+
|
|
45
|
+
# Усі `run:` зі steps цього job-а, склеєні в один blob — для substring-перевірки.
|
|
46
|
+
job_run_blob := concat("\n", [run |
|
|
47
|
+
run := job.steps[_].run
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
# ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
|
|
51
|
+
|
|
52
|
+
deny contains msg if {
|
|
53
|
+
input.name != expected_name
|
|
54
|
+
msg := sprintf("lint-ga.yml: name має бути %q (ga.mdc)", [expected_name])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
deny contains msg if {
|
|
58
|
+
not push_branches_have_dev_and_main
|
|
59
|
+
msg := "lint-ga.yml: on.push.branches має містити dev і main (ga.mdc)"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
deny contains msg if {
|
|
63
|
+
not pr_branches_have_dev_and_main
|
|
64
|
+
msg := "lint-ga.yml: on.pull_request.branches має містити dev і main (ga.mdc)"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
deny contains msg if {
|
|
68
|
+
not push_paths_have_required
|
|
69
|
+
msg := "lint-ga.yml: on.push.paths має містити .github/actions/** і .github/workflows/** (ga.mdc)"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
deny contains msg if {
|
|
73
|
+
not is_object(input.concurrency)
|
|
74
|
+
msg := sprintf(concurrency_missing_template, [expected_concurrency_group])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
deny contains msg if {
|
|
78
|
+
is_object(input.concurrency)
|
|
79
|
+
input.concurrency.group != expected_concurrency_group
|
|
80
|
+
msg := sprintf("lint-ga.yml: concurrency.group має бути %s (ga.mdc)", [expected_concurrency_group])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
deny contains msg if {
|
|
84
|
+
is_object(input.concurrency)
|
|
85
|
+
input.concurrency["cancel-in-progress"] != true
|
|
86
|
+
msg := "lint-ga.yml: concurrency.cancel-in-progress має бути true (ga.mdc)"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
deny contains msg if {
|
|
90
|
+
not job
|
|
91
|
+
msg := "lint-ga.yml: jobs.lint-ga відсутній (ga.mdc)"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
deny contains msg if {
|
|
95
|
+
job["runs-on"] != "ubuntu-latest"
|
|
96
|
+
msg := "lint-ga.yml: runs-on має бути ubuntu-latest (ga.mdc)"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
deny contains msg if {
|
|
100
|
+
job.permissions.contents != "read"
|
|
101
|
+
msg := "lint-ga.yml: permissions мають бути contents: read (ga.mdc)"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
deny contains msg if {
|
|
105
|
+
count(job.steps) == 0
|
|
106
|
+
msg := "lint-ga.yml: jobs.lint-ga.steps відсутні (ga.mdc)"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
deny contains msg if {
|
|
110
|
+
not "actions/checkout@v6" in job_uses_set
|
|
111
|
+
msg := "lint-ga.yml: має бути uses: actions/checkout@v6 (ga.mdc)"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
deny contains msg if {
|
|
115
|
+
not "./.github/actions/setup-bun-deps" in job_uses_set
|
|
116
|
+
msg := "lint-ga.yml: має бути uses: ./.github/actions/setup-bun-deps (ga.mdc)"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
deny contains msg if {
|
|
120
|
+
not "astral-sh/setup-uv@v8.0.0" in job_uses_set
|
|
121
|
+
msg := "lint-ga.yml: має бути uses: astral-sh/setup-uv@v8.0.0 (ga.mdc)"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
deny contains msg if {
|
|
125
|
+
not contains(job_run_blob, "bun run lint-ga")
|
|
126
|
+
msg := "lint-ga.yml: має бути крок run: bun run lint-ga (ga.mdc)"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# ── helpers ────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
push_branches_have_dev_and_main if {
|
|
132
|
+
branches := gha_on.push.branches
|
|
133
|
+
expected_branches & {b | some b in branches} == expected_branches
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pr_branches_have_dev_and_main if {
|
|
137
|
+
branches := gha_on.pull_request.branches
|
|
138
|
+
expected_branches & {b | some b in branches} == expected_branches
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
push_paths_have_required if {
|
|
142
|
+
paths := gha_on.push.paths
|
|
143
|
+
expected_push_paths & {p | some p in paths} == expected_push_paths
|
|
144
|
+
}
|
package/scripts/auto-rules.mjs
CHANGED
|
@@ -41,6 +41,7 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
41
41
|
'nginx-default-tpl',
|
|
42
42
|
'npm-module',
|
|
43
43
|
'php',
|
|
44
|
+
'rego',
|
|
44
45
|
'style-lint',
|
|
45
46
|
'text',
|
|
46
47
|
'vue'
|
|
@@ -104,6 +105,7 @@ export const AUTO_RULE_DEPENDENCIES = Object.freeze(
|
|
|
104
105
|
const ABIE_REPOSITORY_URL_MARKER = 'https://github.com/abinbevefes/'
|
|
105
106
|
const HASURA_CONFIG_MARKER = 'metadata_directory: metadata'
|
|
106
107
|
const JS_LIKE_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx)$/iu
|
|
108
|
+
const REGO_RE = /\.rego$/iu
|
|
107
109
|
const STYLE_RE = /\.(?:css|vue)$/iu
|
|
108
110
|
const VUE_RE = /\.vue$/iu
|
|
109
111
|
const NGINX_DEFAULT_FILES = new Set(['default.conf.template', 'default.conf', 'nginx.conf'])
|
|
@@ -287,6 +289,7 @@ function updateDirFacts(dirName, facts) {
|
|
|
287
289
|
* hasDockerfile: boolean,
|
|
288
290
|
* hasJsLikeSource: boolean,
|
|
289
291
|
* hasNginxDefaultTplFile: boolean,
|
|
292
|
+
* hasRegoFile: boolean,
|
|
290
293
|
* hasVueOrCssSource: boolean,
|
|
291
294
|
* hasVueSource: boolean
|
|
292
295
|
* }} facts агреговані факти
|
|
@@ -311,6 +314,9 @@ function updateFileFacts(fileName, relPath, facts) {
|
|
|
311
314
|
if (STYLE_RE.test(relPath)) {
|
|
312
315
|
facts.hasVueOrCssSource = true
|
|
313
316
|
}
|
|
317
|
+
if (REGO_RE.test(relPath)) {
|
|
318
|
+
facts.hasRegoFile = true
|
|
319
|
+
}
|
|
314
320
|
}
|
|
315
321
|
|
|
316
322
|
/**
|
|
@@ -401,6 +407,7 @@ async function updateHasuraFactFromFile(absPath, fileName, facts) {
|
|
|
401
407
|
* hasHasuraConfig: boolean,
|
|
402
408
|
* hasJsLikeSource: boolean,
|
|
403
409
|
* hasNginxDefaultTplFile: boolean,
|
|
410
|
+
* hasRegoFile: boolean,
|
|
404
411
|
* hasVueOrCssSource: boolean,
|
|
405
412
|
* hasVueSource: boolean
|
|
406
413
|
* }} facts агреговані факти
|
|
@@ -489,6 +496,7 @@ export function isMonorepoPackage(packageJson) {
|
|
|
489
496
|
* hasJsLikeSource: boolean,
|
|
490
497
|
* hasK8sDir: boolean,
|
|
491
498
|
* hasNginxDefaultTplFile: boolean,
|
|
499
|
+
* hasRegoFile: boolean,
|
|
492
500
|
* hasTempoDir: boolean,
|
|
493
501
|
* hasVueSource: boolean,
|
|
494
502
|
* hasVueOrCssSource: boolean
|
|
@@ -505,6 +513,7 @@ export async function collectAutoRuleFacts(root) {
|
|
|
505
513
|
hasJsLikeSource: false,
|
|
506
514
|
hasK8sDir: false,
|
|
507
515
|
hasNginxDefaultTplFile: false,
|
|
516
|
+
hasRegoFile: false,
|
|
508
517
|
hasTempoDir: false,
|
|
509
518
|
hasVueSource: false,
|
|
510
519
|
hasVueOrCssSource: false
|
|
@@ -650,6 +659,7 @@ export async function detectAutoRulesAndSkills({
|
|
|
650
659
|
{ enabled: facts.hasNginxDefaultTplFile, id: 'nginx-default-tpl' },
|
|
651
660
|
{ enabled: npmDirExists, id: 'npm-module' },
|
|
652
661
|
{ enabled: composerJsonExists, id: 'php' },
|
|
662
|
+
{ enabled: facts.hasRegoFile, id: 'rego' },
|
|
653
663
|
{ enabled: facts.hasVueOrCssSource, id: 'style-lint' }
|
|
654
664
|
]
|
|
655
665
|
for (const item of autoRuleChecks) {
|
package/scripts/check-adr.mjs
CHANGED
|
@@ -68,7 +68,10 @@ async function checkHookScript(reporter) {
|
|
|
68
68
|
fail(`канонічний скрипт у пакеті не знайдено: ${BUNDLED_HOOK_PATH} — перевстанови @nitra/cursor`)
|
|
69
69
|
return
|
|
70
70
|
}
|
|
71
|
-
const [project, bundled] = await Promise.all([
|
|
71
|
+
const [project, bundled] = await Promise.all([
|
|
72
|
+
readFile(PROJECT_HOOK_PATH, 'utf8'),
|
|
73
|
+
readFile(BUNDLED_HOOK_PATH, 'utf8')
|
|
74
|
+
])
|
|
72
75
|
if (project === bundled) {
|
|
73
76
|
pass(`${PROJECT_HOOK_PATH} збігається з канонічним`)
|
|
74
77
|
} else {
|
package/scripts/check-ga.mjs
CHANGED
|
@@ -24,15 +24,11 @@ import { join } from 'node:path'
|
|
|
24
24
|
|
|
25
25
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
26
26
|
import {
|
|
27
|
-
anyRunStepIncludes,
|
|
28
27
|
eventPathsIncludeExact,
|
|
29
28
|
findForbiddenUsesOrRunPatterns,
|
|
30
29
|
findRunStepsWithShellLineContinuationBackslash,
|
|
31
30
|
hasAnyStepUsesContaining,
|
|
32
31
|
hasCheckoutBeforeLocalSetupBunDeps,
|
|
33
|
-
flattenWorkflowSteps,
|
|
34
|
-
getStepRun,
|
|
35
|
-
getStepUses,
|
|
36
32
|
parseWorkflowYaml
|
|
37
33
|
} from './utils/gha-workflow.mjs'
|
|
38
34
|
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
@@ -160,156 +156,6 @@ function getObjKey(obj, key) {
|
|
|
160
156
|
: undefined
|
|
161
157
|
}
|
|
162
158
|
|
|
163
|
-
/**
|
|
164
|
-
* Очікує, що значення є рядком рівно `expected`.
|
|
165
|
-
* @param {unknown} v значення
|
|
166
|
-
* @param {string} expected очікуваний рядок
|
|
167
|
-
* @returns {boolean} true, якщо збігається
|
|
168
|
-
*/
|
|
169
|
-
function isExactString(v, expected) {
|
|
170
|
-
return typeof v === 'string' && v === expected
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Перевіряє тригери `on.push` / `on.pull_request` у `lint-ga.yml`.
|
|
175
|
-
* @param {unknown} on корінь `on:` з YAML
|
|
176
|
-
* @param {(msg: string) => void} failFn fail
|
|
177
|
-
*/
|
|
178
|
-
function validateLintGaOnTriggers(on, failFn) {
|
|
179
|
-
const push = getObjKey(on, 'push')
|
|
180
|
-
const pr = getObjKey(on, 'pull_request')
|
|
181
|
-
const pushBranches = getObjKey(push, 'branches')
|
|
182
|
-
const pushPaths = getObjKey(push, 'paths')
|
|
183
|
-
const prBranches = getObjKey(pr, 'branches')
|
|
184
|
-
|
|
185
|
-
if (!Array.isArray(pushBranches) || !(pushBranches.includes('dev') && pushBranches.includes('main'))) {
|
|
186
|
-
failFn('lint-ga.yml: on.push.branches має містити dev і main (ga.mdc)')
|
|
187
|
-
}
|
|
188
|
-
if (!Array.isArray(prBranches) || !(prBranches.includes('dev') && prBranches.includes('main'))) {
|
|
189
|
-
failFn('lint-ga.yml: on.pull_request.branches має містити dev і main (ga.mdc)')
|
|
190
|
-
}
|
|
191
|
-
if (
|
|
192
|
-
!Array.isArray(pushPaths) ||
|
|
193
|
-
!(pushPaths.includes('.github/actions/**') && pushPaths.includes('.github/workflows/**'))
|
|
194
|
-
) {
|
|
195
|
-
failFn('lint-ga.yml: on.push.paths має містити .github/actions/** і .github/workflows/** (ga.mdc)')
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Перевіряє структуру workflow `lint-ga.yml` (ga.mdc).
|
|
201
|
-
* @param {Record<string, unknown> | null} root parsed YAML
|
|
202
|
-
* @param {(msg: string) => void} passFn pass
|
|
203
|
-
* @param {(msg: string) => void} failFn fail
|
|
204
|
-
*/
|
|
205
|
-
function validateLintGaWorkflowStructure(root, passFn, failFn) {
|
|
206
|
-
if (!root) {
|
|
207
|
-
failFn('lint-ga.yml: YAML не вдалося розібрати (ga.mdc)')
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (!isExactString(root.name, 'Lint GA')) {
|
|
212
|
-
failFn('lint-ga.yml: name має бути "Lint GA" (ga.mdc)')
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
validateLintGaOnTriggers(root.on, failFn)
|
|
216
|
-
|
|
217
|
-
validateConcurrencyOnRoot('lint-ga.yml', root, passFn, failFn)
|
|
218
|
-
|
|
219
|
-
const jobs = getObjKey(root, 'jobs')
|
|
220
|
-
const job = getObjKey(jobs, 'lint-ga')
|
|
221
|
-
if (!job) {
|
|
222
|
-
failFn('lint-ga.yml: jobs.lint-ga відсутній (ga.mdc)')
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (!isExactString(getObjKey(job, 'runs-on'), 'ubuntu-latest')) {
|
|
227
|
-
failFn('lint-ga.yml: runs-on має бути ubuntu-latest (ga.mdc)')
|
|
228
|
-
}
|
|
229
|
-
const perm = getObjKey(job, 'permissions')
|
|
230
|
-
if (getObjKey(perm, 'contents') !== 'read') {
|
|
231
|
-
failFn('lint-ga.yml: permissions мають бути contents: read (ga.mdc)')
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const steps = getObjKey(job, 'steps')
|
|
235
|
-
if (!Array.isArray(steps) || steps.length === 0) {
|
|
236
|
-
failFn('lint-ga.yml: jobs.lint-ga.steps відсутні (ga.mdc)')
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const flat = flattenWorkflowSteps(root)
|
|
241
|
-
const usesList = new Set(flat.map(s => getStepUses(s.step)))
|
|
242
|
-
const runBlob = flat.map(s => getStepRun(s.step)).join('\n')
|
|
243
|
-
|
|
244
|
-
if (!usesList.has('actions/checkout@v6')) {
|
|
245
|
-
failFn('lint-ga.yml: має бути uses: actions/checkout@v6 (ga.mdc)')
|
|
246
|
-
}
|
|
247
|
-
if (!usesList.has('./.github/actions/setup-bun-deps')) {
|
|
248
|
-
failFn('lint-ga.yml: має бути uses: ./.github/actions/setup-bun-deps (ga.mdc)')
|
|
249
|
-
}
|
|
250
|
-
if (!usesList.has('astral-sh/setup-uv@v8.0.0')) {
|
|
251
|
-
failFn('lint-ga.yml: має бути uses: astral-sh/setup-uv@v8.0.0 (ga.mdc)')
|
|
252
|
-
}
|
|
253
|
-
if (runBlob.includes('bun run lint-ga')) {
|
|
254
|
-
passFn('lint-ga.yml: структура jobs/steps OK')
|
|
255
|
-
} else {
|
|
256
|
-
failFn('lint-ga.yml: має бути крок run: bun run lint-ga (ga.mdc)')
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Перевіряє структуру workflow `git-ai.yml` (ga.mdc).
|
|
262
|
-
* @param {Record<string, unknown> | null} root parsed YAML
|
|
263
|
-
* @param {(msg: string) => void} passFn pass
|
|
264
|
-
* @param {(msg: string) => void} failFn fail
|
|
265
|
-
*/
|
|
266
|
-
function validateGitAiWorkflowStructure(root, passFn, failFn) {
|
|
267
|
-
if (!root) {
|
|
268
|
-
failFn('git-ai.yml: YAML не вдалося розібрати (ga.mdc)')
|
|
269
|
-
return
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (!isExactString(root.name, 'Git AI')) {
|
|
273
|
-
failFn('git-ai.yml: name має бути "Git AI" (ga.mdc)')
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const on = root.on
|
|
277
|
-
const pr = getObjKey(on, 'pull_request')
|
|
278
|
-
const types = getObjKey(pr, 'types')
|
|
279
|
-
if (!Array.isArray(types) || !types.includes('closed')) {
|
|
280
|
-
failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
validateConcurrencyOnRoot('git-ai.yml', root, passFn, failFn)
|
|
284
|
-
|
|
285
|
-
const jobs = getObjKey(root, 'jobs')
|
|
286
|
-
const job = getObjKey(jobs, 'git-ai')
|
|
287
|
-
if (!job) {
|
|
288
|
-
failFn('git-ai.yml: jobs.git-ai відсутній (ga.mdc)')
|
|
289
|
-
return
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (!String(getObjKey(job, 'if') ?? '').includes('github.event.pull_request.merged == true')) {
|
|
293
|
-
failFn('git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)')
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const perm = getObjKey(job, 'permissions')
|
|
297
|
-
if (getObjKey(perm, 'contents') !== 'write') {
|
|
298
|
-
failFn('git-ai.yml: permissions мають бути contents: write (ga.mdc)')
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const flat = flattenWorkflowSteps(root)
|
|
302
|
-
const runBlob = flat.map(s => getStepRun(s.step)).join('\n')
|
|
303
|
-
if (!runBlob.includes('curl -fsSL https://usegitai.com/install.sh | bash')) {
|
|
304
|
-
failFn('git-ai.yml: має встановлювати git-ai через curl | bash (ga.mdc)')
|
|
305
|
-
}
|
|
306
|
-
if (runBlob.includes('git-ai ci github run')) {
|
|
307
|
-
passFn('git-ai.yml: структура jobs/steps OK')
|
|
308
|
-
} else {
|
|
309
|
-
failFn('git-ai.yml: має виконувати git-ai ci github run (ga.mdc)')
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
159
|
/**
|
|
314
160
|
* Перевіряє блок `concurrency` на вже розпарсеному корені workflow (ga.mdc).
|
|
315
161
|
*
|
|
@@ -624,33 +470,6 @@ function checkShellcheckInstalled(passFn, failFn) {
|
|
|
624
470
|
)
|
|
625
471
|
}
|
|
626
472
|
|
|
627
|
-
/**
|
|
628
|
-
* Перевіряє lint-ga.yml workflow.
|
|
629
|
-
* @param {string} wfDir директорія workflows
|
|
630
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
631
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
632
|
-
*/
|
|
633
|
-
async function checkLintGaWorkflow(wfDir, passFn, failFn) {
|
|
634
|
-
const lintGaWf = join(wfDir, 'lint-ga.yml')
|
|
635
|
-
if (!existsSync(lintGaWf)) return
|
|
636
|
-
const lgContent = await readFile(lintGaWf, 'utf8')
|
|
637
|
-
const root = parseWorkflowYaml(lgContent)
|
|
638
|
-
const hasBunRun = root ? anyRunStepIncludes(root, 'bun run lint-ga') : lgContent.includes('bun run lint-ga')
|
|
639
|
-
const hasSetupUv = root
|
|
640
|
-
? hasAnyStepUsesContaining(root, ['astral-sh/setup-uv']) || lgContent.includes('astral-sh/setup-uv')
|
|
641
|
-
: lgContent.includes('astral-sh/setup-uv')
|
|
642
|
-
if (hasBunRun) {
|
|
643
|
-
passFn('lint-ga.yml викликає bun run lint-ga')
|
|
644
|
-
} else {
|
|
645
|
-
failFn('lint-ga.yml: крок має містити bun run lint-ga')
|
|
646
|
-
}
|
|
647
|
-
if (hasSetupUv) {
|
|
648
|
-
passFn('lint-ga.yml містить astral-sh/setup-uv')
|
|
649
|
-
} else {
|
|
650
|
-
failFn('lint-ga.yml: додай astral-sh/setup-uv для uvx zizmor (ga.mdc)')
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
473
|
/**
|
|
655
474
|
* Перевіряє розширення workflow-файлів і наявність обов'язкових workflow.
|
|
656
475
|
* @param {string} wfDir шлях до директорії workflows
|
|
@@ -684,105 +503,6 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
|
|
|
684
503
|
}
|
|
685
504
|
}
|
|
686
505
|
|
|
687
|
-
/**
|
|
688
|
-
* Перевіряє, чи on.pull_request.types у parsed YAML містить 'closed'.
|
|
689
|
-
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
690
|
-
* @returns {boolean} true, якщо тригер pull_request має тип closed
|
|
691
|
-
*/
|
|
692
|
-
function hasPullRequestClosedTrigger(root) {
|
|
693
|
-
const on = root.on
|
|
694
|
-
if (!on || typeof on !== 'object') return false
|
|
695
|
-
const pr = /** @type {Record<string, unknown>} */ (on)['pull_request']
|
|
696
|
-
if (!pr || typeof pr !== 'object') return false
|
|
697
|
-
const types = /** @type {Record<string, unknown>} */ (pr).types
|
|
698
|
-
return Array.isArray(types) && types.includes('closed')
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Перевіряє, чи будь-який job у parsed YAML має if-умову з 'merged'.
|
|
703
|
-
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
704
|
-
* @returns {boolean} true, якщо хоча б один job містить умову merged
|
|
705
|
-
*/
|
|
706
|
-
function hasJobMergedCondition(root) {
|
|
707
|
-
const { jobs } = root
|
|
708
|
-
if (!jobs || typeof jobs !== 'object') return false
|
|
709
|
-
return Object.values(jobs).some(job => {
|
|
710
|
-
if (!job || typeof job !== 'object') return false
|
|
711
|
-
const ifCond = String(/** @type {Record<string, unknown>} */ (job).if ?? '')
|
|
712
|
-
return ifCond.includes('merged')
|
|
713
|
-
})
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* Перевіряє parsed YAML git-ai.yml: тригер closed та умова merged.
|
|
718
|
-
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
719
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
720
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
721
|
-
*/
|
|
722
|
-
function validateGitAiParsedYaml(root, passFn, failFn) {
|
|
723
|
-
if (hasPullRequestClosedTrigger(root)) {
|
|
724
|
-
passFn('git-ai.yml: on.pull_request.types містить closed')
|
|
725
|
-
} else {
|
|
726
|
-
failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (hasJobMergedCondition(root)) {
|
|
730
|
-
passFn('git-ai.yml: job має умову merged')
|
|
731
|
-
} else {
|
|
732
|
-
failFn('git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)')
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* Перевіряє git-ai.yml: тригер pull_request з types: [closed], умова merged у job, виклик git-ai.
|
|
738
|
-
* @param {string} wfDir директорія workflows
|
|
739
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
740
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
741
|
-
*/
|
|
742
|
-
async function checkGitAiWorkflow(wfDir, passFn, failFn) {
|
|
743
|
-
const gitAiWf = join(wfDir, 'git-ai.yml')
|
|
744
|
-
if (!existsSync(gitAiWf)) return
|
|
745
|
-
const content = await readFile(gitAiWf, 'utf8')
|
|
746
|
-
const root = parseWorkflowYaml(content)
|
|
747
|
-
|
|
748
|
-
if (root) {
|
|
749
|
-
validateGitAiParsedYaml(root, passFn, failFn)
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const hasGitAiRun = root ? anyRunStepIncludes(root, 'git-ai ci github run') : content.includes('git-ai ci github run')
|
|
753
|
-
if (hasGitAiRun) {
|
|
754
|
-
passFn('git-ai.yml: крок виконує git-ai ci github run')
|
|
755
|
-
} else {
|
|
756
|
-
failFn('git-ai.yml: крок має містити git-ai ci github run (ga.mdc)')
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Перевіряє, що “канонічні” workflows відповідають ga.mdc (структура і значення).
|
|
762
|
-
*
|
|
763
|
-
* Структурні валідатори `clean-ga-workflows.yml` і `clean-merged-branch.yml` мігровано в Rego-полісі
|
|
764
|
-
* під `npm/policy/ga/clean_ga_workflows/` та `npm/policy/ga/clean_merged_branch/` (виконує conftest з
|
|
765
|
-
* `bun run lint-ga`). Тут лишаються `lint-ga.yml` і `git-ai.yml` — їх перенесення в наступних ітераціях.
|
|
766
|
-
* @param {string} wfDir директорія workflows
|
|
767
|
-
* @param {(msg: string) => void} passFn pass
|
|
768
|
-
* @param {(msg: string) => void} failFn fail
|
|
769
|
-
*/
|
|
770
|
-
async function checkCanonicalWorkflowsMatchRule(wfDir, passFn, failFn) {
|
|
771
|
-
const paths = {
|
|
772
|
-
lintGa: join(wfDir, 'lint-ga.yml'),
|
|
773
|
-
gitAi: join(wfDir, 'git-ai.yml')
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (existsSync(paths.lintGa)) {
|
|
777
|
-
const c = await readFile(paths.lintGa, 'utf8')
|
|
778
|
-
validateLintGaWorkflowStructure(parseWorkflowYaml(c), passFn, failFn)
|
|
779
|
-
}
|
|
780
|
-
if (existsSync(paths.gitAi)) {
|
|
781
|
-
const c = await readFile(paths.gitAi, 'utf8')
|
|
782
|
-
validateGitAiWorkflowStructure(parseWorkflowYaml(c), passFn, failFn)
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
506
|
/**
|
|
787
507
|
* Перевіряє відповідність проєкту правилам ga.mdc
|
|
788
508
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -841,12 +561,8 @@ export async function check() {
|
|
|
841
561
|
}
|
|
842
562
|
}
|
|
843
563
|
|
|
844
|
-
await checkCanonicalWorkflowsMatchRule(wfDir, pass, fail)
|
|
845
|
-
|
|
846
564
|
await checkZizmor(pass, fail)
|
|
847
565
|
await checkLintGaScript(pass, fail)
|
|
848
|
-
await checkLintGaWorkflow(wfDir, pass, fail)
|
|
849
|
-
await checkGitAiWorkflow(wfDir, pass, fail)
|
|
850
566
|
checkShellcheckInstalled(pass, fail)
|
|
851
567
|
|
|
852
568
|
return reporter.getExitCode()
|
package/scripts/check-hasura.mjs
CHANGED
|
@@ -45,8 +45,7 @@ const ENV_FILE_RE = /\.env$/u
|
|
|
45
45
|
const HASURA_ENDPOINT_LINE_RE = /^[ \t]*(?:export[ \t]+)?HASURA_GRAPHQL_ENDPOINT[ \t]*=[ \t]*['"]?([^'"\r\n#]+)/mu
|
|
46
46
|
// Дозволяємо два DNS-суфікси кластера: `<name>.internal` (GKE/GCP) і `cluster.local`
|
|
47
47
|
// (стандартний k8s / Yandex Cloud). У YC namespace.yaml + cluster mode дають коротший суфікс.
|
|
48
|
-
const INTERNAL_HASURA_URL_RE =
|
|
49
|
-
/^http:\/\/([^./]+)\.([^./]+)\.svc\.((?:[^./:]+\.internal)|cluster\.local):(\d+)\/?$/u
|
|
48
|
+
const INTERNAL_HASURA_URL_RE = /^http:\/\/([^./]+)\.([^./]+)\.svc\.((?:[^./:]+\.internal)|cluster\.local):(\d+)\/?$/u
|
|
50
49
|
const CLUSTER_LOCAL_SUFFIX = 'cluster.local'
|
|
51
50
|
const INTERNAL_DNS_SUFFIX = '.internal'
|
|
52
51
|
|
|
@@ -151,7 +150,8 @@ async function checkEnvFile(relPath, expected, reporter) {
|
|
|
151
150
|
const parsed = parseInternalHasuraEndpoint(value)
|
|
152
151
|
if (!parsed.ok) {
|
|
153
152
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url, sonarjs/no-clear-text-protocols -- hasura.mdc вимагає саме http:// для кластерного URL (TLS не використовується)
|
|
154
|
-
const example =
|
|
153
|
+
const example =
|
|
154
|
+
'http://<service>.<namespace>.svc.<cluster>.internal:<port> або http://<service>.<namespace>.svc.cluster.local:<port>'
|
|
155
155
|
fail(
|
|
156
156
|
`${relPath}: HASURA_GRAPHQL_ENDPOINT="${value}" — потрібен внутрішній кластерний URL виду ${example} (hasura.mdc)`
|
|
157
157
|
)
|
package/scripts/check-js-run.mjs
CHANGED
|
@@ -47,10 +47,7 @@ import {
|
|
|
47
47
|
resolveConnDirFromPackageJson
|
|
48
48
|
} from './utils/conn-imports-scan.mjs'
|
|
49
49
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
50
|
-
import {
|
|
51
|
-
findPromiseSetTimeoutInText,
|
|
52
|
-
isPromiseSetTimeoutScanSourceFile
|
|
53
|
-
} from './utils/promise-settimeout-scan.mjs'
|
|
50
|
+
import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from './utils/promise-settimeout-scan.mjs'
|
|
54
51
|
import { walkDir } from './utils/walkDir.mjs'
|
|
55
52
|
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
56
53
|
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -524,7 +524,8 @@ function kustomizationPatchSortKey(patchItem) {
|
|
|
524
524
|
const rec = /** @type {Record<string, unknown>} */ (patchItem)
|
|
525
525
|
const t = rec.target
|
|
526
526
|
/** @type {Record<string, unknown>} */
|
|
527
|
-
const target =
|
|
527
|
+
const target =
|
|
528
|
+
t !== null && typeof t === 'object' && !Array.isArray(t) ? /** @type {Record<string, unknown>} */ (t) : {}
|
|
528
529
|
const kind = typeof target.kind === 'string' ? target.kind : ''
|
|
529
530
|
const name = typeof target.name === 'string' ? target.name : ''
|
|
530
531
|
const ns = typeof target.namespace === 'string' ? target.namespace : ''
|
package/scripts/lint-ga.mjs
CHANGED
|
@@ -46,6 +46,16 @@ const CONFTEST_TARGETS = [
|
|
|
46
46
|
workflow: '.github/workflows/clean-merged-branch.yml',
|
|
47
47
|
namespace: 'ga.clean_merged_branch',
|
|
48
48
|
label: 'clean-merged-branch.yml structure'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
workflow: '.github/workflows/lint-ga.yml',
|
|
52
|
+
namespace: 'ga.lint_ga',
|
|
53
|
+
label: 'lint-ga.yml structure'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
workflow: '.github/workflows/git-ai.yml',
|
|
57
|
+
namespace: 'ga.git_ai',
|
|
58
|
+
label: 'git-ai.yml structure'
|
|
49
59
|
}
|
|
50
60
|
]
|
|
51
61
|
|
package/scripts/lint-rego.mjs
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Лінт Rego-полісі (`conftest.mdc` + `rego.mdc`): preflight на `opa` і `regal`,
|
|
3
|
+
* далі послідовно `opa check --strict` і `regal lint`.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Чому два інструменти:
|
|
6
|
+
* - `opa check --strict` — компіляція з типами і строгим режимом (мертвий код, неоднозначні
|
|
7
|
+
* правила, незадекларовані змінні). Ловить помилки, які `regal` навмисно лишає поза скоупом
|
|
8
|
+
* (він — про стиль і ідіоматичність, а не про компіляцію).
|
|
9
|
+
* - `regal lint` (https://docs.styra.com/regal) — статичний лінтер Rego: ловить v0-синтаксис,
|
|
10
|
+
* неявні set-rules та інші відхилення від `rego.v1`, плюс bugs/idiomatic/performance-правила.
|
|
11
|
+
*
|
|
12
|
+
* Без preflight-у на бінарники лінт мовчки злетить з невиразним повідомленням від shell —
|
|
13
|
+
* друкуємо явні install-hints (як це робить `lint-ga.mjs` для shellcheck/uv). `opa` додатково
|
|
14
|
+
* потрібен VS Code-розширенню `tsandall.opa` (LSP, format-on-save через `opa fmt`) — деталі в
|
|
15
|
+
* `mdc/rego.mdc`.
|
|
8
16
|
*
|
|
9
17
|
* Цілі лінту: `npm/policy/` (місце, де поки що живуть Rego-полісі пакета `@nitra/cursor`).
|
|
10
18
|
* Якщо в репозиторії з’являться інші *.rego поза цим деревом, додай шлях у `LINT_TARGETS` —
|
|
11
|
-
*
|
|
19
|
+
* обидва інструменти приймають кілька шляхів і самі рекурсивно обходять директорії.
|
|
12
20
|
*/
|
|
13
21
|
import { spawnSync } from 'node:child_process'
|
|
14
22
|
import { existsSync } from 'node:fs'
|
|
@@ -19,6 +27,24 @@ import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
|
19
27
|
/** Шляхи з Rego-полісі (відносно cwd). Існують не всі на ранніх стадіях — фільтруємо нижче. */
|
|
20
28
|
const LINT_TARGETS = ['npm/policy']
|
|
21
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Друкує підказку зі встановлення `opa` (потрібен для `opa check --strict` і VS Code LSP).
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
function printOpaInstallHints() {
|
|
35
|
+
process.stderr.write(
|
|
36
|
+
[
|
|
37
|
+
'❌ opa не знайдено в PATH.',
|
|
38
|
+
' Без нього не запускається `opa check --strict` (типи + мертвий код у *.rego),',
|
|
39
|
+
' і не працює VS Code-розширення `tsandall.opa` (LSP, format-on-save через opa fmt).',
|
|
40
|
+
' Встанови:',
|
|
41
|
+
' macOS: brew install opa',
|
|
42
|
+
' Universal: https://www.openpolicyagent.org/docs/latest/#1-download-opa',
|
|
43
|
+
''
|
|
44
|
+
].join('\n')
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
/**
|
|
23
49
|
* Друкує підказку зі встановлення `regal`.
|
|
24
50
|
* @returns {void}
|
|
@@ -29,7 +55,7 @@ function printRegalInstallHints() {
|
|
|
29
55
|
'❌ regal не знайдено в PATH.',
|
|
30
56
|
' Без нього не перевіряється rego.v1 синтаксис у *.rego (правило `conftest`).',
|
|
31
57
|
' Встанови:',
|
|
32
|
-
' macOS:
|
|
58
|
+
' macOS: brew install regal',
|
|
33
59
|
' Universal: https://docs.styra.com/regal#installation',
|
|
34
60
|
''
|
|
35
61
|
].join('\n')
|
|
@@ -37,34 +63,54 @@ function printRegalInstallHints() {
|
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
/**
|
|
40
|
-
* Запускає
|
|
66
|
+
* Запускає крок з відображенням команди користувачу. Stdout/stderr передаємо як є
|
|
67
|
+
* (`stdio: 'inherit'`), щоб виглядало як прямий виклик у shell.
|
|
68
|
+
* @param {string} bin абсолютний шлях до бінарника
|
|
69
|
+
* @param {string[]} args аргументи
|
|
70
|
+
* @param {string} cwd робочий каталог
|
|
71
|
+
* @returns {number} код виходу (0 — OK)
|
|
72
|
+
*/
|
|
73
|
+
function runStep(bin, args, cwd) {
|
|
74
|
+
console.log(`▶ ${bin} ${args.join(' ')}`)
|
|
75
|
+
const result = spawnSync(bin, args, { cwd, stdio: 'inherit', env: process.env })
|
|
76
|
+
if (result.error) {
|
|
77
|
+
process.stderr.write(`❌ Не вдалося запустити ${bin}: ${result.error.message}\n`)
|
|
78
|
+
return 1
|
|
79
|
+
}
|
|
80
|
+
return result.status ?? 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Запускає `opa check --strict` і `regal lint` по існуючих цілях. Якщо жодної цілі немає —
|
|
85
|
+
* пропускає лінт із кодом 0. Якщо хоча б один preflight не пройшов — exit 1 ще до запусків.
|
|
41
86
|
* @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
|
|
42
|
-
* @returns {number} 0 — OK або skip; інакше код виходу
|
|
87
|
+
* @returns {number} 0 — OK або skip; інакше код виходу першого кроку, що впав
|
|
43
88
|
*/
|
|
44
89
|
export function runLintRego(cwd = process.cwd()) {
|
|
45
90
|
const root = resolve(cwd)
|
|
91
|
+
const opa = resolveCmd('opa')
|
|
46
92
|
const regal = resolveCmd('regal')
|
|
93
|
+
|
|
94
|
+
let preflightOk = true
|
|
95
|
+
if (!opa) {
|
|
96
|
+
printOpaInstallHints()
|
|
97
|
+
preflightOk = false
|
|
98
|
+
}
|
|
47
99
|
if (!regal) {
|
|
48
100
|
printRegalInstallHints()
|
|
49
|
-
|
|
101
|
+
preflightOk = false
|
|
50
102
|
}
|
|
103
|
+
if (!preflightOk) return 1
|
|
51
104
|
|
|
52
105
|
const targets = LINT_TARGETS.filter(rel => existsSync(resolve(root, rel)))
|
|
53
106
|
if (targets.length === 0) {
|
|
54
107
|
return 0
|
|
55
108
|
}
|
|
56
109
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
env: process.env
|
|
62
|
-
})
|
|
63
|
-
if (result.error) {
|
|
64
|
-
process.stderr.write(`❌ Не вдалося запустити regal: ${result.error.message}\n`)
|
|
65
|
-
return 1
|
|
66
|
-
}
|
|
67
|
-
return result.status ?? 1
|
|
110
|
+
const opaCode = runStep(opa, ['check', '--strict', ...targets], root)
|
|
111
|
+
if (opaCode !== 0) return opaCode
|
|
112
|
+
|
|
113
|
+
return runStep(regal, ['lint', ...targets], root)
|
|
68
114
|
}
|
|
69
115
|
|
|
70
116
|
process.exitCode = runLintRego()
|
|
@@ -85,10 +85,7 @@ export function listShellScriptPaths(cwd) {
|
|
|
85
85
|
|
|
86
86
|
const fromGlob = globSync('**/*.sh', {
|
|
87
87
|
cwd,
|
|
88
|
-
exclude: p =>
|
|
89
|
-
p.includes('node_modules') ||
|
|
90
|
-
p.startsWith(`node_modules/`) ||
|
|
91
|
-
p.split('/').includes('node_modules')
|
|
88
|
+
exclude: p => p.includes('node_modules') || p.startsWith(`node_modules/`) || p.split('/').includes('node_modules')
|
|
92
89
|
})
|
|
93
90
|
return [...new Set(fromGlob.map(p => p.replaceAll('\\', '/')))].sort()
|
|
94
91
|
}
|
|
@@ -19,11 +19,7 @@
|
|
|
19
19
|
import { readdir, readFile } from 'node:fs/promises'
|
|
20
20
|
import { join, relative } from 'node:path'
|
|
21
21
|
|
|
22
|
-
import {
|
|
23
|
-
flattenWorkflowSteps,
|
|
24
|
-
getStepRun,
|
|
25
|
-
parseWorkflowYaml
|
|
26
|
-
} from './gha-workflow.mjs'
|
|
22
|
+
import { flattenWorkflowSteps, getStepRun, parseWorkflowYaml } from './gha-workflow.mjs'
|
|
27
23
|
|
|
28
24
|
const WORKFLOWS_DIR_REL = '.github/workflows'
|
|
29
25
|
const REQUIRED_IGNORES = ['graphql', 'bun']
|
|
@@ -122,7 +118,7 @@ export function evaluateDepcheckStepForPackage(root, pkgRoot) {
|
|
|
122
118
|
// Усі знайдені кроки існують, але жоден не має повного списку обов'язкових ignores —
|
|
123
119
|
// повертаємо missing з першого, щоб дати конкретний фідбек.
|
|
124
120
|
const firstMissing = REQUIRED_IGNORES.filter(
|
|
125
|
-
req => !(
|
|
121
|
+
req => !(parseDepcheckIgnoresArg(stepsForThisPackage[0].args) ?? []).includes(req)
|
|
126
122
|
)
|
|
127
123
|
return { kind: 'missing-ignores', missing: firstMissing }
|
|
128
124
|
}
|