@nitra/cursor 1.13.8 → 1.13.12
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 +60 -0
- package/package.json +1 -1
- package/rules/ga/fix/workflows/check.mjs +12 -3
- package/rules/ga/ga.mdc +18 -152
- package/rules/ga/policy/clean_ga_workflows/clean_ga_workflows.rego +38 -49
- package/rules/ga/policy/clean_ga_workflows/template/clean-ga-workflows.yml.snippet.yml +26 -0
- package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +55 -57
- package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +37 -0
- package/rules/ga/policy/git_ai/git_ai.rego +28 -35
- package/rules/ga/policy/git_ai/template/git-ai.yml.snippet.yml +30 -0
- package/rules/ga/policy/lint_ga/lint_ga.rego +46 -55
- package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +35 -0
- package/rules/ga/policy/package_json/package_json.rego +9 -13
- package/rules/ga/policy/package_json/template/package.json.contains.json +1 -0
- package/rules/ga/policy/vscode_extensions/template/extensions.json.snippet.json +1 -0
- package/rules/ga/policy/vscode_extensions/vscode_extensions.rego +6 -4
- package/rules/ga/policy/vscode_settings/template/settings.json.snippet.json +1 -0
- package/rules/ga/policy/vscode_settings/vscode_settings.rego +11 -13
- package/rules/ga/policy/zizmor_yml/template/zizmor.yml.snippet.yml +5 -0
- package/rules/ga/policy/zizmor_yml/zizmor_yml.rego +16 -6
- package/rules/rego/lint/lint.mjs +5 -4
- package/rules/rego/policy/package_json/package_json.rego +8 -29
- package/rules/rego/policy/package_json/template/package.json.snippet.json +1 -0
- package/rules/rego/policy/vscode_extensions/template/extensions.json.snippet.json +1 -0
- package/rules/rego/policy/vscode_extensions/vscode_extensions.rego +7 -11
- package/rules/rego/policy/vscode_settings/template/settings.json.snippet.json +6 -0
- package/rules/rego/policy/vscode_settings/vscode_settings.rego +19 -27
- package/rules/rego/rego.mdc +10 -8
- package/rules/security/todo.MD +27 -0
|
@@ -1,44 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Перевірка `.github/workflows/clean-merged-branch.yml` (ga.mdc).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# -p npm/policy/ga --namespace ga.clean_merged_branch
|
|
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).
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/clean-merged-branch.yml.snippet.yml.
|
|
10
5
|
package ga.clean_merged_branch
|
|
11
6
|
|
|
12
7
|
import rego.v1
|
|
13
8
|
|
|
14
|
-
# ──
|
|
15
|
-
#
|
|
16
|
-
# Шаблонні токени GitHub Actions (`${{ … }}`) збираємо з фрагментів через
|
|
17
|
-
# `concat`, бо `{{` у Rego починає string interpolation.
|
|
18
|
-
|
|
19
|
-
expected_github_token := concat("", ["$", "{{ github.token }}"])
|
|
9
|
+
# ── Аліаси ─────────────────────────────────────────────────────────────────
|
|
20
10
|
|
|
21
|
-
|
|
11
|
+
gha_on := input["true"]
|
|
22
12
|
|
|
23
|
-
|
|
13
|
+
steps := input.jobs.cleanup_old_branches.steps
|
|
24
14
|
|
|
25
|
-
|
|
15
|
+
step0 := steps[0]
|
|
26
16
|
|
|
27
|
-
|
|
17
|
+
step1 := steps[1]
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
# YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
|
|
19
|
+
expected_name := data.template.snippet.name
|
|
32
20
|
|
|
33
|
-
|
|
21
|
+
expected_cron := data.template.snippet.on.schedule[0].cron
|
|
34
22
|
|
|
35
|
-
|
|
23
|
+
expected_step0 := data.template.snippet.jobs.cleanup_old_branches.steps[0]
|
|
36
24
|
|
|
37
|
-
|
|
25
|
+
expected_step1 := data.template.snippet.jobs.cleanup_old_branches.steps[1]
|
|
38
26
|
|
|
39
|
-
|
|
27
|
+
expected_perms := data.template.snippet.jobs.cleanup_old_branches.permissions
|
|
40
28
|
|
|
41
|
-
# ── deny rules
|
|
29
|
+
# ── deny rules ─────────────────────────────────────────────────────────────
|
|
42
30
|
|
|
43
31
|
deny contains msg if {
|
|
44
32
|
input.name != expected_name
|
|
@@ -51,7 +39,7 @@ deny contains msg if {
|
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
deny contains msg if {
|
|
54
|
-
not
|
|
42
|
+
not is_object(object.get(gha_on, "workflow_dispatch", null))
|
|
55
43
|
msg := "clean-merged-branch.yml: має бути workflow_dispatch: {} (ga.mdc)"
|
|
56
44
|
}
|
|
57
45
|
|
|
@@ -61,65 +49,73 @@ deny contains msg if {
|
|
|
61
49
|
}
|
|
62
50
|
|
|
63
51
|
deny contains msg if {
|
|
64
|
-
input.jobs.cleanup_old_branches.permissions.contents !=
|
|
65
|
-
msg := "clean-merged-branch.yml: permissions
|
|
52
|
+
input.jobs.cleanup_old_branches.permissions.contents != expected_perms.contents
|
|
53
|
+
msg := sprintf("clean-merged-branch.yml: permissions.contents має бути %s (ga.mdc)", [expected_perms.contents])
|
|
66
54
|
}
|
|
67
55
|
|
|
68
56
|
deny contains msg if {
|
|
69
57
|
count(steps) < 2
|
|
70
|
-
msg := "clean-merged-branch.yml: steps має містити 2 кроки як у ga.mdc"
|
|
58
|
+
msg := "clean-merged-branch.yml: steps має містити 2 кроки як у template (ga.mdc)"
|
|
71
59
|
}
|
|
72
60
|
|
|
73
61
|
# ── Step 0 (delete_stuff) ──────────────────────────────────────────────────
|
|
74
62
|
|
|
75
63
|
deny contains msg if {
|
|
76
|
-
step0.id !=
|
|
77
|
-
msg := "clean-merged-branch.yml: перший крок має id:
|
|
64
|
+
step0.id != expected_step0.id
|
|
65
|
+
msg := sprintf("clean-merged-branch.yml: перший крок має id: %s (ga.mdc)", [expected_step0.id])
|
|
78
66
|
}
|
|
79
67
|
|
|
80
68
|
deny contains msg if {
|
|
81
|
-
step0.uses !=
|
|
82
|
-
msg := "clean-merged-branch.yml: перший крок має uses
|
|
69
|
+
step0.uses != expected_step0.uses
|
|
70
|
+
msg := sprintf("clean-merged-branch.yml: перший крок має uses: %s (ga.mdc)", [expected_step0.uses])
|
|
83
71
|
}
|
|
84
72
|
|
|
85
73
|
deny contains msg if {
|
|
86
|
-
step0.with.github_token !=
|
|
87
|
-
msg := sprintf(
|
|
74
|
+
step0.with.github_token != expected_step0.with.github_token
|
|
75
|
+
msg := sprintf(
|
|
76
|
+
"clean-merged-branch.yml: with.github_token має бути %s (ga.mdc)",
|
|
77
|
+
[expected_step0.with.github_token],
|
|
78
|
+
)
|
|
88
79
|
}
|
|
89
80
|
|
|
90
81
|
deny contains msg if {
|
|
91
|
-
step0.with.last_commit_age_days !=
|
|
92
|
-
msg :=
|
|
82
|
+
step0.with.last_commit_age_days != expected_step0.with.last_commit_age_days
|
|
83
|
+
msg := sprintf(
|
|
84
|
+
"clean-merged-branch.yml: with.last_commit_age_days має бути %d (ga.mdc)",
|
|
85
|
+
[expected_step0.with.last_commit_age_days],
|
|
86
|
+
)
|
|
93
87
|
}
|
|
94
88
|
|
|
95
89
|
deny contains msg if {
|
|
96
|
-
not
|
|
97
|
-
msg :=
|
|
90
|
+
not ignore_branches_subset
|
|
91
|
+
msg := sprintf(
|
|
92
|
+
"clean-merged-branch.yml: with.ignore_branches має містити %s (ga.mdc)",
|
|
93
|
+
[expected_step0.with.ignore_branches],
|
|
94
|
+
)
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
# `dry_run: no`
|
|
101
|
-
#
|
|
102
|
-
# (Якщо комусь схочеться явного `"no"` — треба буде брати in quotes у YAML.)
|
|
97
|
+
# YAML 1.1 quirk: `dry_run: no` парситься як boolean false у Go-yaml (conftest).
|
|
98
|
+
# Template (від `yaml` npm) теж нормалізовано до false у фікстурі.
|
|
103
99
|
deny contains msg if {
|
|
104
|
-
step0.with.dry_run !=
|
|
100
|
+
step0.with.dry_run != expected_step0.with.dry_run # noqa: rules-style-no-equality-with-false
|
|
105
101
|
msg := "clean-merged-branch.yml: with.dry_run має бути no (ga.mdc)"
|
|
106
102
|
}
|
|
107
103
|
|
|
108
104
|
# ── Step 1 (Get output) ────────────────────────────────────────────────────
|
|
109
105
|
|
|
110
106
|
deny contains msg if {
|
|
111
|
-
step1.name !=
|
|
112
|
-
msg := "clean-merged-branch.yml: другий крок має name:
|
|
107
|
+
step1.name != expected_step1.name
|
|
108
|
+
msg := sprintf("clean-merged-branch.yml: другий крок має name: %s (ga.mdc)", [expected_step1.name])
|
|
113
109
|
}
|
|
114
110
|
|
|
115
111
|
deny contains msg if {
|
|
116
|
-
step1.env.DELETED_BRANCHES !=
|
|
117
|
-
msg := "clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у ga.mdc"
|
|
112
|
+
step1.env.DELETED_BRANCHES != expected_step1.env.DELETED_BRANCHES
|
|
113
|
+
msg := "clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у template (ga.mdc)"
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
deny contains msg if {
|
|
121
117
|
not echo_deleted_branches
|
|
122
|
-
msg := "clean-merged-branch.yml: run має echo Deleted branches як у ga.mdc"
|
|
118
|
+
msg := "clean-merged-branch.yml: run має echo Deleted branches як у template (ga.mdc)"
|
|
123
119
|
}
|
|
124
120
|
|
|
125
121
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -128,15 +124,17 @@ has_expected_cron if {
|
|
|
128
124
|
gha_on.schedule[_].cron == expected_cron
|
|
129
125
|
}
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
# Кожна гілка з template-літералу (через кому) має бути присутня у actual.
|
|
128
|
+
ignore_branches_subset if {
|
|
129
|
+
required_branches := split(expected_step0.with.ignore_branches, ",")
|
|
130
|
+
actual := step0.with.ignore_branches
|
|
131
|
+
every b in required_branches {
|
|
132
|
+
contains(actual, trim_space(b))
|
|
133
|
+
}
|
|
138
134
|
}
|
|
139
135
|
|
|
140
136
|
echo_deleted_branches if {
|
|
141
|
-
|
|
137
|
+
# Звіряємо substring "echo "Deleted branches: …${DELETED_BRANCHES}…"" — формується з template run.
|
|
138
|
+
contains(step1.run, "Deleted branches:")
|
|
139
|
+
contains(step1.run, "${DELETED_BRANCHES}")
|
|
142
140
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Clean abandoned branches
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
# Run daily at midnight
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: '0 1 15 * *'
|
|
7
|
+
|
|
8
|
+
# Allow workflow to be manually run from the GitHub UI
|
|
9
|
+
workflow_dispatch: {}
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
cleanup_old_branches:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
permissions:
|
|
19
|
+
contents: write
|
|
20
|
+
steps:
|
|
21
|
+
- id: delete_stuff
|
|
22
|
+
name: Delete those pesky dead branches
|
|
23
|
+
uses: phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3
|
|
24
|
+
with:
|
|
25
|
+
github_token: ${{ github.token }}
|
|
26
|
+
last_commit_age_days: 90
|
|
27
|
+
ignore_branches: main,dev
|
|
28
|
+
# `no` у workflow → Go-yaml парсить як bool false; тут пишемо явно false,
|
|
29
|
+
# бо template читає `yaml` (YAML 1.2), де `no` лишається рядком — а нам
|
|
30
|
+
# потрібна семантична відповідність runtime-парсингу conftest.
|
|
31
|
+
dry_run: false
|
|
32
|
+
|
|
33
|
+
- name: Get output
|
|
34
|
+
env:
|
|
35
|
+
DELETED_BRANCHES: ${{ steps.delete_stuff.outputs.deleted_branches }}
|
|
36
|
+
run: |
|
|
37
|
+
echo "Deleted branches: ${DELETED_BRANCHES}"
|
|
@@ -1,42 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Перевірка `.github/workflows/git-ai.yml` (ga.mdc).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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).
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/git-ai.yml.snippet.yml.
|
|
5
|
+
# Substring-перевірки (`if:`, `run:` блоки) — на основі ключових фраз з template
|
|
6
|
+
# steps, бо повний multi-line `run:` дуже крихкий для exact-match.
|
|
10
7
|
package ga.git_ai
|
|
11
8
|
|
|
12
9
|
import rego.v1
|
|
13
10
|
|
|
14
|
-
# ──
|
|
15
|
-
|
|
16
|
-
expected_name := "Git AI"
|
|
17
|
-
|
|
18
|
-
expected_if_substring := "github.event.pull_request.merged == true"
|
|
19
|
-
|
|
20
|
-
expected_install_substring := "curl -fsSL https://usegitai.com/install.sh | bash"
|
|
21
|
-
|
|
22
|
-
expected_run_substring := "git-ai ci github run"
|
|
23
|
-
|
|
24
|
-
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
25
|
-
#
|
|
26
|
-
# YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
|
|
11
|
+
# ── Аліаси ─────────────────────────────────────────────────────────────────
|
|
27
12
|
|
|
28
13
|
gha_on := input["true"]
|
|
29
14
|
|
|
30
|
-
# Job-id містить дефіс — звертаємося через `[…]`. Імʼя `job` (без префіксу пакету)
|
|
31
|
-
# — щоб уникнути regal-правила `rule-name-repeats-package`.
|
|
32
15
|
job := input.jobs["git-ai"]
|
|
33
16
|
|
|
34
|
-
# Усі `run:` зі steps цього job-а, склеєні в один blob — для substring-перевірки.
|
|
35
17
|
job_run_blob := concat("\n", [run |
|
|
36
18
|
run := job.steps[_].run
|
|
37
19
|
])
|
|
38
20
|
|
|
39
|
-
|
|
21
|
+
expected_name := data.template.snippet.name
|
|
22
|
+
|
|
23
|
+
expected_types := {t | some t in data.template.snippet.on.pull_request.types}
|
|
24
|
+
|
|
25
|
+
expected_if := data.template.snippet.jobs["git-ai"].if
|
|
26
|
+
|
|
27
|
+
expected_perms := data.template.snippet.jobs["git-ai"].permissions
|
|
28
|
+
|
|
29
|
+
# Substring-маркери з template `run:` блоків — ключові команди, наявність яких
|
|
30
|
+
# гарантує що workflow робить очікувані дії. Конкретний multi-line — не порівнюємо.
|
|
31
|
+
install_substring := "https://usegitai.com/install.sh"
|
|
32
|
+
|
|
33
|
+
run_substring := "git-ai ci github run"
|
|
34
|
+
|
|
35
|
+
# ── deny rules ─────────────────────────────────────────────────────────────
|
|
40
36
|
|
|
41
37
|
deny contains msg if {
|
|
42
38
|
input.name != expected_name
|
|
@@ -54,30 +50,27 @@ deny contains msg if {
|
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
deny contains msg if {
|
|
57
|
-
not contains(job_if_str,
|
|
58
|
-
msg := "git-ai.yml: job має містити if:
|
|
53
|
+
not contains(job_if_str, expected_if)
|
|
54
|
+
msg := sprintf("git-ai.yml: job має містити if: %s (ga.mdc)", [expected_if])
|
|
59
55
|
}
|
|
60
56
|
|
|
61
57
|
deny contains msg if {
|
|
62
|
-
job.permissions.contents !=
|
|
63
|
-
msg := "git-ai.yml: permissions
|
|
58
|
+
job.permissions.contents != expected_perms.contents
|
|
59
|
+
msg := sprintf("git-ai.yml: permissions.contents має бути %s (ga.mdc)", [expected_perms.contents])
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
deny contains msg if {
|
|
67
|
-
not contains(job_run_blob,
|
|
63
|
+
not contains(job_run_blob, install_substring)
|
|
68
64
|
msg := "git-ai.yml: має встановлювати git-ai через curl | bash (ga.mdc)"
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
deny contains msg if {
|
|
72
|
-
not contains(job_run_blob,
|
|
73
|
-
msg := "git-ai.yml: має виконувати
|
|
68
|
+
not contains(job_run_blob, run_substring)
|
|
69
|
+
msg := sprintf("git-ai.yml: має виконувати %s (ga.mdc)", [run_substring])
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
77
73
|
|
|
78
|
-
# `if` поле job-а може бути відсутнім — тоді `sprintf` дає невизначене значення
|
|
79
|
-
# і спрацьовує `default`, повертаючи порожній рядок; `contains(…)` нижче дасть
|
|
80
|
-
# false і відповідне `deny`-правило спрацює зі зрозумілим повідомленням.
|
|
81
74
|
default job_if_str := ""
|
|
82
75
|
|
|
83
76
|
job_if_str := sprintf("%v", [job.if])
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Git AI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [closed]
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
git-ai:
|
|
13
|
+
if: github.event.pull_request.merged == true
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Install git-ai
|
|
20
|
+
run: |
|
|
21
|
+
curl -fsSL https://usegitai.com/install.sh | bash
|
|
22
|
+
echo "$HOME/.git-ai/bin" >> $GITHUB_PATH
|
|
23
|
+
- name: Run git-ai
|
|
24
|
+
id: run-git-ai
|
|
25
|
+
env:
|
|
26
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
27
|
+
run: |
|
|
28
|
+
git config --global user.name "github-actions[bot]"
|
|
29
|
+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
30
|
+
git-ai ci github run
|
|
@@ -1,44 +1,50 @@
|
|
|
1
|
-
#
|
|
2
|
-
# `npm/scripts/check-ga.mjs` (ga.mdc).
|
|
1
|
+
# Перевірка `.github/workflows/lint-ga.yml` (ga.mdc).
|
|
3
2
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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).
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/lint-ga.yml.snippet.yml.
|
|
11
5
|
package ga.lint_ga
|
|
12
6
|
|
|
13
7
|
import rego.v1
|
|
14
8
|
|
|
15
|
-
# ──
|
|
16
|
-
|
|
17
|
-
expected_name := "Lint GA"
|
|
18
|
-
|
|
19
|
-
expected_branches := {"dev", "main"}
|
|
20
|
-
|
|
21
|
-
expected_push_paths := {".github/actions/**", ".github/workflows/**"}
|
|
22
|
-
|
|
23
|
-
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
24
|
-
#
|
|
25
|
-
# YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
|
|
9
|
+
# ── Аліаси ─────────────────────────────────────────────────────────────────
|
|
26
10
|
|
|
27
11
|
gha_on := input["true"]
|
|
28
12
|
|
|
29
|
-
# Job-id містить дефіс — звертаємося через `[…]`. Імʼя `job` (без префіксу пакету)
|
|
30
|
-
# — щоб уникнути regal-правила `rule-name-repeats-package`.
|
|
31
13
|
job := input.jobs["lint-ga"]
|
|
32
14
|
|
|
33
|
-
# Усі `uses:` зі steps цього job-а — для перевірки членства.
|
|
34
15
|
job_uses_set contains job.steps[_].uses
|
|
35
16
|
|
|
36
|
-
# Усі `run:` зі steps цього job-а, склеєні в один blob — для substring-перевірки.
|
|
37
17
|
job_run_blob := concat("\n", [run |
|
|
38
18
|
run := job.steps[_].run
|
|
39
19
|
])
|
|
40
20
|
|
|
41
|
-
|
|
21
|
+
expected_name := data.template.snippet.name
|
|
22
|
+
|
|
23
|
+
expected_push_branches := {b | some b in data.template.snippet.on.push.branches}
|
|
24
|
+
|
|
25
|
+
expected_pr_branches := {b | some b in data.template.snippet.on.pull_request.branches}
|
|
26
|
+
|
|
27
|
+
expected_push_paths := {p | some p in data.template.snippet.on.push.paths}
|
|
28
|
+
|
|
29
|
+
expected_runs_on := data.template.snippet.jobs["lint-ga"]["runs-on"]
|
|
30
|
+
|
|
31
|
+
expected_perms := data.template.snippet.jobs["lint-ga"].permissions
|
|
32
|
+
|
|
33
|
+
# Required `uses:` зі template — фільтруємо тільки кроки що мають `uses`.
|
|
34
|
+
expected_uses_set contains u if {
|
|
35
|
+
some step in data.template.snippet.jobs["lint-ga"].steps
|
|
36
|
+
u := object.get(step, "uses", "")
|
|
37
|
+
u != ""
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Required `run:` substrings — collected from steps with `run`.
|
|
41
|
+
expected_run_blob := concat("\n", [r |
|
|
42
|
+
some step in data.template.snippet.jobs["lint-ga"].steps
|
|
43
|
+
r := object.get(step, "run", "")
|
|
44
|
+
r != ""
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
# ── deny rules ─────────────────────────────────────────────────────────────
|
|
42
48
|
|
|
43
49
|
deny contains msg if {
|
|
44
50
|
input.name != expected_name
|
|
@@ -46,17 +52,17 @@ deny contains msg if {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
deny contains msg if {
|
|
49
|
-
not
|
|
55
|
+
not branches_superset_of(gha_on.push.branches, expected_push_branches)
|
|
50
56
|
msg := "lint-ga.yml: on.push.branches має містити dev і main (ga.mdc)"
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
deny contains msg if {
|
|
54
|
-
not
|
|
60
|
+
not branches_superset_of(gha_on.pull_request.branches, expected_pr_branches)
|
|
55
61
|
msg := "lint-ga.yml: on.pull_request.branches має містити dev і main (ga.mdc)"
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
deny contains msg if {
|
|
59
|
-
not
|
|
65
|
+
not paths_superset_of(gha_on.push.paths, expected_push_paths)
|
|
60
66
|
msg := "lint-ga.yml: on.push.paths має містити .github/actions/** і .github/workflows/** (ga.mdc)"
|
|
61
67
|
}
|
|
62
68
|
|
|
@@ -66,13 +72,13 @@ deny contains msg if {
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
deny contains msg if {
|
|
69
|
-
job["runs-on"] !=
|
|
70
|
-
msg := "lint-ga.yml: runs-on має бути
|
|
75
|
+
job["runs-on"] != expected_runs_on
|
|
76
|
+
msg := sprintf("lint-ga.yml: runs-on має бути %s (ga.mdc)", [expected_runs_on])
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
deny contains msg if {
|
|
74
|
-
job.permissions.contents !=
|
|
75
|
-
msg := "lint-ga.yml: permissions
|
|
80
|
+
job.permissions.contents != expected_perms.contents
|
|
81
|
+
msg := sprintf("lint-ga.yml: permissions.contents має бути %s (ga.mdc)", [expected_perms.contents])
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
deny contains msg if {
|
|
@@ -81,38 +87,23 @@ deny contains msg if {
|
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
deny contains msg if {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
deny contains msg if {
|
|
89
|
-
not "./.github/actions/setup-bun-deps" in job_uses_set
|
|
90
|
-
msg := "lint-ga.yml: має бути uses: ./.github/actions/setup-bun-deps (ga.mdc)"
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
deny contains msg if {
|
|
94
|
-
not "astral-sh/setup-uv@v8.0.0" in job_uses_set
|
|
95
|
-
msg := "lint-ga.yml: має бути uses: astral-sh/setup-uv@v8.0.0 (ga.mdc)"
|
|
90
|
+
some required_use in expected_uses_set
|
|
91
|
+
not required_use in job_uses_set
|
|
92
|
+
msg := sprintf("lint-ga.yml: має бути uses: %s (ga.mdc)", [required_use])
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
deny contains msg if {
|
|
96
|
+
expected_run_blob != ""
|
|
99
97
|
not contains(job_run_blob, "bun run lint-ga")
|
|
100
98
|
msg := "lint-ga.yml: має бути крок run: bun run lint-ga (ga.mdc)"
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
expected_branches & {b | some b in branches} == expected_branches
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
pr_branches_have_dev_and_main if {
|
|
111
|
-
branches := gha_on.pull_request.branches
|
|
112
|
-
expected_branches & {b | some b in branches} == expected_branches
|
|
103
|
+
branches_superset_of(actual, expected) if {
|
|
104
|
+
expected & {b | some b in actual} == expected
|
|
113
105
|
}
|
|
114
106
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expected_push_paths & {p | some p in paths} == expected_push_paths
|
|
107
|
+
paths_superset_of(actual, expected) if {
|
|
108
|
+
expected & {p | some p in actual} == expected
|
|
118
109
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Lint GA
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- dev
|
|
7
|
+
- main
|
|
8
|
+
paths:
|
|
9
|
+
- '.github/actions/**'
|
|
10
|
+
- '.github/workflows/**'
|
|
11
|
+
pull_request:
|
|
12
|
+
branches:
|
|
13
|
+
- dev
|
|
14
|
+
- main
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
lint-ga:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v6
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
|
|
30
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
31
|
+
|
|
32
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
33
|
+
|
|
34
|
+
- name: Lint GA
|
|
35
|
+
run: bun run lint-ga
|
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
# Перевірка кореневого `package.json` для GitHub Actions tooling (ga.mdc).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# перевірки `on.*.paths` через `git ls-files`.
|
|
3
|
+
# Канон надходить через --data: { "template": { "contains": ... } }
|
|
4
|
+
# Структура --data сформована з template/package.json.contains.json.
|
|
6
5
|
#
|
|
7
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
8
6
|
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
9
7
|
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
10
8
|
package ga.package_json
|
|
11
9
|
|
|
12
10
|
import rego.v1
|
|
13
11
|
|
|
12
|
+
# Кожне рядкове поле з contains має містити кожен substring.
|
|
13
|
+
# Відсутність ключа → `""` → contains() = false → deny.
|
|
14
14
|
deny contains msg if {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
lint_ga := object.get(object.get(input, "scripts", {}), "lint-ga", "")
|
|
21
|
-
is_string(lint_ga)
|
|
22
|
-
not regex.match(`\bn-cursor\s+lint-ga\b`, lint_ga)
|
|
23
|
-
msg := "lint-ga має делегувати CLI `n-cursor lint-ga` (ga.mdc)"
|
|
15
|
+
some script_name, needles in data.template.contains.scripts
|
|
16
|
+
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
17
|
+
some needle in needles
|
|
18
|
+
not contains(actual, needle)
|
|
19
|
+
msg := sprintf("package.json: scripts.%s має містити %q (ga.mdc)", [script_name, needle])
|
|
24
20
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "scripts": { "lint-ga": ["n-cursor lint-ga"] } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "recommendations": ["github.vscode-github-actions"] }
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Перевірка `.vscode/extensions.json` для GitHub Actions (ga.mdc).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/extensions.json.snippet.json.
|
|
5
|
+
# `recommendations` — subset-of: кожна рекомендація з template має бути у input.
|
|
4
6
|
# Додаткові рекомендації від інших правил дозволені.
|
|
5
7
|
#
|
|
6
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
7
8
|
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
8
9
|
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
9
10
|
package ga.vscode_extensions
|
|
@@ -11,6 +12,7 @@ package ga.vscode_extensions
|
|
|
11
12
|
import rego.v1
|
|
12
13
|
|
|
13
14
|
deny contains msg if {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
some rec in data.template.snippet.recommendations
|
|
16
|
+
not rec in {r | some r in object.get(input, "recommendations", [])}
|
|
17
|
+
msg := sprintf(".vscode/extensions.json: recommendations має містити %q (ga.mdc)", [rec])
|
|
16
18
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "[github-actions-workflow]": { "editor.defaultFormatter": "oxc.oxc-vscode" } }
|