@nitra/cursor 1.8.203 → 1.8.204
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,18 @@
|
|
|
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.204] - 2026-05-07
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Реструктурував `npm/policy/ga/` під namespaced sub-packages, які проходять regal: `ga/clean_ga_workflows/clean_ga_workflows.rego` та новий `ga/clean_merged_branch/clean_merged_branch.rego` (порт `validateCleanMergedBranch` з check-ga.mjs — `name` / `cron 0 1 15 * *` / `workflow_dispatch` / `concurrency` / `jobs.cleanup_old_branches` / step0 `phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3` з token / age=90 / ignore_branches main,dev / `dry_run: false` (YAML 1.1) / step1 `Get output` + `DELETED_BRANCHES` env + echo).
|
|
12
|
+
- `scripts/lint-ga.mjs`: `CONFTEST_TARGETS` тепер містить `clean-ga-workflows.yml` і `clean-merged-branch.yml`, conftest викликаємо з `--namespace ga.<name>` для ізоляції правил між workflow.
|
|
13
|
+
- `scripts/check-ga.mjs`: видалено `validateCleanGaWorkflows*` і `validateCleanMergedBranch*` — їх повністю покриває conftest у `lint-ga`. `checkCanonicalWorkflowsMatchRule` тепер валідує лише `lint-ga.yml` і `git-ai.yml` (наступні кандидати на міграцію).
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `.regal/config.yaml` у корені — вимикає `idiomatic.no-defined-entrypoint` (для conftest-полісі `deny`-правила є де-факто entrypoint-ами, формальна анотація не несе семантики).
|
|
18
|
+
|
|
7
19
|
## [1.8.203] - 2026-05-07
|
|
8
20
|
|
|
9
21
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Порт перевірки `validateCleanGaWorkflows` з `npm/scripts/check-ga.mjs` (ga.mdc).
|
|
2
2
|
#
|
|
3
3
|
# Запуск (локально):
|
|
4
|
-
# conftest test .github/workflows/clean-ga-workflows.yml
|
|
4
|
+
# conftest test .github/workflows/clean-ga-workflows.yml \
|
|
5
|
+
# -p npm/policy/ga --namespace ga.clean_ga_workflows
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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).
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
package
|
|
11
|
+
# Усі `deny`-правила йдуть контигно (regal: messy-rule); helpers і константи —
|
|
12
|
+
# секціями вище та нижче.
|
|
13
|
+
package ga.clean_ga_workflows
|
|
12
14
|
|
|
13
15
|
import rego.v1
|
|
14
16
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# `
|
|
18
|
-
#
|
|
19
|
-
|
|
17
|
+
# ── Очікувані значення ─────────────────────────────────────────────────────
|
|
18
|
+
#
|
|
19
|
+
# `${{ … }}` — шаблонний синтаксис GitHub Actions; `{{` у Rego починає string
|
|
20
|
+
# interpolation. Збираємо очікувані рядки з фрагментів через `concat`, як це
|
|
21
|
+
# зроблено в check-ga.mjs, щоб і Rego-парсер, і людина-читач не плуталися.
|
|
20
22
|
|
|
21
|
-
# `${{ … }}` — це шаблонний синтаксис GitHub Actions, але `{{` у Rego починає
|
|
22
|
-
# string interpolation. Збираємо очікувані рядки з фрагментів, як це зроблено в
|
|
23
|
-
# check-ga.mjs, щоб і Rego-парсер, і людина-читач не плуталися.
|
|
24
23
|
expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
|
|
25
24
|
|
|
26
25
|
expected_github_token := concat("", ["$", "{{ github.token }}"])
|
|
@@ -29,43 +28,43 @@ expected_name := "Clean action for removing completed workflow runs"
|
|
|
29
28
|
|
|
30
29
|
expected_cron := "0 1 16 * *"
|
|
31
30
|
|
|
32
|
-
#
|
|
31
|
+
# Шаблон повідомлення про відсутню `concurrency`-секцію — винесено через `concat`,
|
|
32
|
+
# щоб дотриматися regal style/line-length.
|
|
33
|
+
concurrency_missing_template := concat(" ", [
|
|
34
|
+
"clean-ga-workflows.yml: відсутня секція concurrency —",
|
|
35
|
+
"додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
39
|
+
#
|
|
40
|
+
# GHA YAML quirk: ключ `on:` — YAML 1.1 boolean `true`, конфтест серіалізує його
|
|
41
|
+
# як рядковий ключ "true". Ані `input.on`, ані `input["on"]`, ані `input[true]`
|
|
42
|
+
# не працюють — лише `input["true"]`.
|
|
43
|
+
|
|
44
|
+
gha_on := input["true"]
|
|
45
|
+
|
|
46
|
+
step0 := input.jobs.cleanup_old_workflows.steps[0]
|
|
47
|
+
|
|
48
|
+
# ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
|
|
33
49
|
|
|
34
50
|
deny contains msg if {
|
|
35
51
|
input.name != expected_name
|
|
36
52
|
msg := sprintf("clean-ga-workflows.yml: name має бути %q (ga.mdc)", [expected_name])
|
|
37
53
|
}
|
|
38
54
|
|
|
39
|
-
# --- on.schedule.cron --------------------------------------------------------
|
|
40
|
-
|
|
41
55
|
deny contains msg if {
|
|
42
56
|
not has_expected_cron
|
|
43
57
|
msg := sprintf("clean-ga-workflows.yml: on.schedule має містити cron: '%s' (ga.mdc)", [expected_cron])
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
has_expected_cron if {
|
|
47
|
-
gha_on.schedule[_].cron == expected_cron
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
# --- on.workflow_dispatch ----------------------------------------------------
|
|
51
|
-
|
|
52
60
|
deny contains msg if {
|
|
53
61
|
not has_workflow_dispatch
|
|
54
62
|
msg := "clean-ga-workflows.yml: має бути workflow_dispatch: {} (ga.mdc)"
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
has_workflow_dispatch if {
|
|
58
|
-
is_object(gha_on.workflow_dispatch)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
# --- concurrency -------------------------------------------------------------
|
|
62
|
-
|
|
63
65
|
deny contains msg if {
|
|
64
66
|
not is_object(input.concurrency)
|
|
65
|
-
msg := sprintf(
|
|
66
|
-
"clean-ga-workflows.yml: відсутня секція concurrency — додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
|
|
67
|
-
[expected_concurrency_group],
|
|
68
|
-
)
|
|
67
|
+
msg := sprintf(concurrency_missing_template, [expected_concurrency_group])
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
deny contains msg if {
|
|
@@ -80,8 +79,6 @@ deny contains msg if {
|
|
|
80
79
|
msg := "clean-ga-workflows.yml: concurrency.cancel-in-progress має бути true (ga.mdc)"
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
# --- jobs.cleanup_old_workflows ---------------------------------------------
|
|
84
|
-
|
|
85
82
|
deny contains msg if {
|
|
86
83
|
not input.jobs.cleanup_old_workflows
|
|
87
84
|
msg := "clean-ga-workflows.yml: jobs.cleanup_old_workflows відсутній (ga.mdc)"
|
|
@@ -99,15 +96,6 @@ deny contains msg if {
|
|
|
99
96
|
msg := "clean-ga-workflows.yml: permissions мають бути actions: write, contents: read (ga.mdc)"
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
actions_write_contents_read(perms) if {
|
|
103
|
-
perms.actions == "write"
|
|
104
|
-
perms.contents == "read"
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
# --- jobs.cleanup_old_workflows.steps[0] ------------------------------------
|
|
108
|
-
|
|
109
|
-
step0 := input.jobs.cleanup_old_workflows.steps[0]
|
|
110
|
-
|
|
111
99
|
deny contains msg if {
|
|
112
100
|
step0.name != "Delete workflow runs"
|
|
113
101
|
msg := "clean-ga-workflows.yml: перший крок має мати name: Delete workflow runs (ga.mdc)"
|
|
@@ -120,12 +108,27 @@ deny contains msg if {
|
|
|
120
108
|
|
|
121
109
|
# Триплет полів `with`: token (gh-токен), save_period=31, save_min_runs_number=0.
|
|
122
110
|
# В JS-перевірці помилка спільна для всіх трьох — лишаємо такий самий формат, щоб
|
|
123
|
-
# повідомлення збігалися.
|
|
111
|
+
# повідомлення збігалися.
|
|
124
112
|
deny contains msg if {
|
|
125
113
|
not step0_with_canonical
|
|
126
114
|
msg := "clean-ga-workflows.yml: with має містити token/save_period/save_min_runs_number як у ga.mdc"
|
|
127
115
|
}
|
|
128
116
|
|
|
117
|
+
# ── helpers ────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
has_expected_cron if {
|
|
120
|
+
gha_on.schedule[_].cron == expected_cron
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
has_workflow_dispatch if {
|
|
124
|
+
is_object(gha_on.workflow_dispatch)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
actions_write_contents_read(perms) if {
|
|
128
|
+
perms.actions == "write"
|
|
129
|
+
perms.contents == "read"
|
|
130
|
+
}
|
|
131
|
+
|
|
129
132
|
step0_with_canonical if {
|
|
130
133
|
step0.with.token == expected_github_token
|
|
131
134
|
step0.with.save_period == 31
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Порт перевірки `validateCleanMergedBranch` з `npm/scripts/check-ga.mjs` (ga.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Запуск (локально):
|
|
4
|
+
# conftest test .github/workflows/clean-merged-branch.yml \
|
|
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).
|
|
10
|
+
package ga.clean_merged_branch
|
|
11
|
+
|
|
12
|
+
import rego.v1
|
|
13
|
+
|
|
14
|
+
# ── Очікувані значення ─────────────────────────────────────────────────────
|
|
15
|
+
#
|
|
16
|
+
# Шаблонні токени GitHub Actions (`${{ … }}`) збираємо з фрагментів через
|
|
17
|
+
# `concat`, бо `{{` у Rego починає string interpolation.
|
|
18
|
+
|
|
19
|
+
expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
|
|
20
|
+
|
|
21
|
+
expected_github_token := concat("", ["$", "{{ github.token }}"])
|
|
22
|
+
|
|
23
|
+
expected_deleted_branches_expr := concat("", ["$", "{{ steps.delete_stuff.outputs.deleted_branches }}"])
|
|
24
|
+
|
|
25
|
+
expected_echo_substring := concat("", ["echo \"Deleted branches: $", "{DELETED_BRANCHES}\""])
|
|
26
|
+
|
|
27
|
+
expected_name := "Clean abandoned branches"
|
|
28
|
+
|
|
29
|
+
expected_cron := "0 1 15 * *"
|
|
30
|
+
|
|
31
|
+
# Шаблони повідомлень — через `concat` для regal style/line-length.
|
|
32
|
+
concurrency_missing_template := concat(" ", [
|
|
33
|
+
"clean-merged-branch.yml: відсутня секція concurrency —",
|
|
34
|
+
"додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
38
|
+
#
|
|
39
|
+
# YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
|
|
40
|
+
|
|
41
|
+
gha_on := input["true"]
|
|
42
|
+
|
|
43
|
+
steps := input.jobs.cleanup_old_branches.steps
|
|
44
|
+
|
|
45
|
+
step0 := steps[0]
|
|
46
|
+
|
|
47
|
+
step1 := steps[1]
|
|
48
|
+
|
|
49
|
+
# ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
|
|
50
|
+
|
|
51
|
+
deny contains msg if {
|
|
52
|
+
input.name != expected_name
|
|
53
|
+
msg := sprintf("clean-merged-branch.yml: name має бути %q (ga.mdc)", [expected_name])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
deny contains msg if {
|
|
57
|
+
not has_expected_cron
|
|
58
|
+
msg := sprintf("clean-merged-branch.yml: on.schedule має містити cron: '%s' (ga.mdc)", [expected_cron])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
deny contains msg if {
|
|
62
|
+
not has_workflow_dispatch
|
|
63
|
+
msg := "clean-merged-branch.yml: має бути workflow_dispatch: {} (ga.mdc)"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
deny contains msg if {
|
|
67
|
+
not is_object(input.concurrency)
|
|
68
|
+
msg := sprintf(concurrency_missing_template, [expected_concurrency_group])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
deny contains msg if {
|
|
72
|
+
is_object(input.concurrency)
|
|
73
|
+
input.concurrency.group != expected_concurrency_group
|
|
74
|
+
msg := sprintf("clean-merged-branch.yml: concurrency.group має бути %s (ga.mdc)", [expected_concurrency_group])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
deny contains msg if {
|
|
78
|
+
is_object(input.concurrency)
|
|
79
|
+
input.concurrency["cancel-in-progress"] != true
|
|
80
|
+
msg := "clean-merged-branch.yml: concurrency.cancel-in-progress має бути true (ga.mdc)"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
deny contains msg if {
|
|
84
|
+
not input.jobs.cleanup_old_branches
|
|
85
|
+
msg := "clean-merged-branch.yml: jobs.cleanup_old_branches відсутній (ga.mdc)"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
deny contains msg if {
|
|
89
|
+
input.jobs.cleanup_old_branches.permissions.contents != "write"
|
|
90
|
+
msg := "clean-merged-branch.yml: permissions мають бути contents: write (ga.mdc)"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
deny contains msg if {
|
|
94
|
+
count(steps) < 2
|
|
95
|
+
msg := "clean-merged-branch.yml: steps має містити 2 кроки як у ga.mdc"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# ── Step 0 (delete_stuff) ──────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
deny contains msg if {
|
|
101
|
+
step0.id != "delete_stuff"
|
|
102
|
+
msg := "clean-merged-branch.yml: перший крок має id: delete_stuff (ga.mdc)"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
deny contains msg if {
|
|
106
|
+
step0.uses != "phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3"
|
|
107
|
+
msg := "clean-merged-branch.yml: перший крок має uses як у ga.mdc"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
deny contains msg if {
|
|
111
|
+
step0.with.github_token != expected_github_token
|
|
112
|
+
msg := sprintf("clean-merged-branch.yml: with.github_token має бути %s (ga.mdc)", [expected_github_token])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
deny contains msg if {
|
|
116
|
+
step0.with.last_commit_age_days != 90
|
|
117
|
+
msg := "clean-merged-branch.yml: with.last_commit_age_days має бути 90 (ga.mdc)"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
deny contains msg if {
|
|
121
|
+
not ignore_branches_has_main_and_dev
|
|
122
|
+
msg := "clean-merged-branch.yml: with.ignore_branches має містити main,dev (ga.mdc)"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# `dry_run: no` у YAML парситься як boolean `false`. JS-перевірка порівнює зі
|
|
126
|
+
# рядком "no", але в нас input уже Go-yaml-парсений — тому очікуємо `false`.
|
|
127
|
+
# (Якщо комусь схочеться явного `"no"` — треба буде брати in quotes у YAML.)
|
|
128
|
+
deny contains msg if {
|
|
129
|
+
step0.with.dry_run != false # noqa: rules-style-no-equality-with-false
|
|
130
|
+
msg := "clean-merged-branch.yml: with.dry_run має бути no (ga.mdc)"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# ── Step 1 (Get output) ────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
deny contains msg if {
|
|
136
|
+
step1.name != "Get output"
|
|
137
|
+
msg := "clean-merged-branch.yml: другий крок має name: Get output (ga.mdc)"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
deny contains msg if {
|
|
141
|
+
step1.env.DELETED_BRANCHES != expected_deleted_branches_expr
|
|
142
|
+
msg := "clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у ga.mdc"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
deny contains msg if {
|
|
146
|
+
not echo_deleted_branches
|
|
147
|
+
msg := "clean-merged-branch.yml: run має echo Deleted branches як у ga.mdc"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# ── helpers ────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
has_expected_cron if {
|
|
153
|
+
gha_on.schedule[_].cron == expected_cron
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
has_workflow_dispatch if {
|
|
157
|
+
is_object(gha_on.workflow_dispatch)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
ignore_branches_has_main_and_dev if {
|
|
161
|
+
contains(step0.with.ignore_branches, "main")
|
|
162
|
+
contains(step0.with.ignore_branches, "dev")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
echo_deleted_branches if {
|
|
166
|
+
contains(step1.run, expected_echo_substring)
|
|
167
|
+
}
|
package/scripts/check-ga.mjs
CHANGED
|
@@ -170,220 +170,6 @@ function isExactString(v, expected) {
|
|
|
170
170
|
return typeof v === 'string' && v === expected
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
/**
|
|
174
|
-
* Перевіряє крок dmvict/clean-workflow-runs@v1 у `clean-ga-workflows.yml`.
|
|
175
|
-
* @param {unknown} step0 перший крок workflow
|
|
176
|
-
* @param {(msg: string) => void} passFn pass
|
|
177
|
-
* @param {(msg: string) => void} failFn fail
|
|
178
|
-
*/
|
|
179
|
-
function validateCleanGaWorkflowsStep0(step0, passFn, failFn) {
|
|
180
|
-
if (!isExactString(getObjKey(step0, 'name'), 'Delete workflow runs')) {
|
|
181
|
-
failFn('clean-ga-workflows.yml: перший крок має мати name: Delete workflow runs (ga.mdc)')
|
|
182
|
-
}
|
|
183
|
-
if (!isExactString(getObjKey(step0, 'uses'), 'dmvict/clean-workflow-runs@v1')) {
|
|
184
|
-
failFn('clean-ga-workflows.yml: перший крок має uses: dmvict/clean-workflow-runs@v1 (ga.mdc)')
|
|
185
|
-
}
|
|
186
|
-
const withObj = getObjKey(step0, 'with')
|
|
187
|
-
const githubToken = ['$', '{{ github.token }}'].join('')
|
|
188
|
-
if (
|
|
189
|
-
getObjKey(withObj, 'token') === githubToken &&
|
|
190
|
-
getObjKey(withObj, 'save_period') === 31 &&
|
|
191
|
-
getObjKey(withObj, 'save_min_runs_number') === 0
|
|
192
|
-
) {
|
|
193
|
-
passFn('clean-ga-workflows.yml: jobs/steps OK')
|
|
194
|
-
} else {
|
|
195
|
-
failFn('clean-ga-workflows.yml: with має містити token/save_period/save_min_runs_number як у ga.mdc')
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Перевіряє структуру workflow `clean-ga-workflows.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 validateCleanGaWorkflows(root, passFn, failFn) {
|
|
206
|
-
if (!root) {
|
|
207
|
-
failFn('clean-ga-workflows.yml: YAML не вдалося розібрати (ga.mdc)')
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (isExactString(root.name, 'Clean action for removing completed workflow runs')) {
|
|
212
|
-
passFn('clean-ga-workflows.yml: name OK')
|
|
213
|
-
} else {
|
|
214
|
-
failFn('clean-ga-workflows.yml: name має бути "Clean action for removing completed workflow runs" (ga.mdc)')
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const on = root.on
|
|
218
|
-
const schedule = getObjKey(on, 'schedule')
|
|
219
|
-
const wfDispatch = getObjKey(on, 'workflow_dispatch')
|
|
220
|
-
|
|
221
|
-
const hasCron =
|
|
222
|
-
Array.isArray(schedule) &&
|
|
223
|
-
schedule.some(v => v && typeof v === 'object' && /** @type {Record<string, unknown>} */ (v).cron === '0 1 16 * *')
|
|
224
|
-
|
|
225
|
-
if (hasCron) {
|
|
226
|
-
passFn('clean-ga-workflows.yml: cron OK')
|
|
227
|
-
} else {
|
|
228
|
-
failFn("clean-ga-workflows.yml: on.schedule має містити cron: '0 1 16 * *' (ga.mdc)")
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (!wfDispatch || typeof wfDispatch !== 'object') {
|
|
232
|
-
failFn('clean-ga-workflows.yml: має бути workflow_dispatch: {} (ga.mdc)')
|
|
233
|
-
} else {
|
|
234
|
-
passFn('clean-ga-workflows.yml: workflow_dispatch OK')
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
validateConcurrencyOnRoot('clean-ga-workflows.yml', root, passFn, failFn)
|
|
238
|
-
|
|
239
|
-
const jobs = getObjKey(root, 'jobs')
|
|
240
|
-
const job = getObjKey(jobs, 'cleanup_old_workflows')
|
|
241
|
-
if (!job) {
|
|
242
|
-
failFn('clean-ga-workflows.yml: jobs.cleanup_old_workflows відсутній (ga.mdc)')
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (!isExactString(getObjKey(job, 'runs-on'), 'ubuntu-latest')) {
|
|
247
|
-
failFn('clean-ga-workflows.yml: runs-on має бути ubuntu-latest (ga.mdc)')
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const perm = getObjKey(job, 'permissions')
|
|
251
|
-
if (!(getObjKey(perm, 'actions') === 'write' && getObjKey(perm, 'contents') === 'read')) {
|
|
252
|
-
failFn('clean-ga-workflows.yml: permissions мають бути actions: write, contents: read (ga.mdc)')
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const steps = getObjKey(job, 'steps')
|
|
256
|
-
const step0 = Array.isArray(steps) ? steps[0] : null
|
|
257
|
-
if (!step0 || typeof step0 !== 'object') {
|
|
258
|
-
failFn('clean-ga-workflows.yml: steps має містити крок з dmvict/clean-workflow-runs@v1 (ga.mdc)')
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
validateCleanGaWorkflowsStep0(step0, passFn, failFn)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Перевіряє крок `phpdocker-io/github-actions-delete-abandoned-branches` у `clean-merged-branch.yml`.
|
|
267
|
-
* @param {unknown} step0 перший крок workflow
|
|
268
|
-
* @param {(msg: string) => void} failFn fail
|
|
269
|
-
*/
|
|
270
|
-
function validateCleanMergedBranchStep0(step0, failFn) {
|
|
271
|
-
if (!isExactString(getObjKey(step0, 'id'), 'delete_stuff')) {
|
|
272
|
-
failFn('clean-merged-branch.yml: перший крок має id: delete_stuff (ga.mdc)')
|
|
273
|
-
}
|
|
274
|
-
if (!isExactString(getObjKey(step0, 'uses'), 'phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3')) {
|
|
275
|
-
failFn('clean-merged-branch.yml: перший крок має uses як у ga.mdc')
|
|
276
|
-
}
|
|
277
|
-
const withObj = getObjKey(step0, 'with')
|
|
278
|
-
const ghToken = ['$', '{{ github.token }}'].join('')
|
|
279
|
-
if (getObjKey(withObj, 'github_token') !== ghToken) {
|
|
280
|
-
failFn(['clean-merged-branch.yml: with.github_token має бути $', '{{ github.token }} (ga.mdc)'].join(''))
|
|
281
|
-
}
|
|
282
|
-
if (getObjKey(withObj, 'last_commit_age_days') !== 90) {
|
|
283
|
-
failFn('clean-merged-branch.yml: with.last_commit_age_days має бути 90 (ga.mdc)')
|
|
284
|
-
}
|
|
285
|
-
const ignoreBranches = String(getObjKey(withObj, 'ignore_branches') ?? '')
|
|
286
|
-
if (!(ignoreBranches.includes('main') && ignoreBranches.includes('dev'))) {
|
|
287
|
-
failFn('clean-merged-branch.yml: with.ignore_branches має містити main,dev (ga.mdc)')
|
|
288
|
-
}
|
|
289
|
-
if (getObjKey(withObj, 'dry_run') !== 'no') {
|
|
290
|
-
failFn('clean-merged-branch.yml: with.dry_run має бути no (ga.mdc)')
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Перевіряє крок виводу в `clean-merged-branch.yml`.
|
|
296
|
-
* @param {unknown} step1 другий крок workflow
|
|
297
|
-
* @param {(msg: string) => void} passFn pass
|
|
298
|
-
* @param {(msg: string) => void} failFn fail
|
|
299
|
-
*/
|
|
300
|
-
function validateCleanMergedBranchStep1(step1, passFn, failFn) {
|
|
301
|
-
if (!isExactString(getObjKey(step1, 'name'), 'Get output')) {
|
|
302
|
-
failFn('clean-merged-branch.yml: другий крок має name: Get output (ga.mdc)')
|
|
303
|
-
}
|
|
304
|
-
const env = getObjKey(step1, 'env')
|
|
305
|
-
const deletedBranchesExpr = ['$', '{{ steps.delete_stuff.outputs.deleted_branches }}'].join('')
|
|
306
|
-
if (getObjKey(env, 'DELETED_BRANCHES') !== deletedBranchesExpr) {
|
|
307
|
-
failFn('clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у ga.mdc')
|
|
308
|
-
}
|
|
309
|
-
const echoDeletedBranches = ['echo "Deleted branches: $', '{DELETED_BRANCHES}"'].join('')
|
|
310
|
-
if (String(getObjKey(step1, 'run') ?? '').includes(echoDeletedBranches)) {
|
|
311
|
-
passFn('clean-merged-branch.yml: jobs/steps OK')
|
|
312
|
-
} else {
|
|
313
|
-
failFn('clean-merged-branch.yml: run має echo Deleted branches як у ga.mdc')
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Перевіряє структуру workflow `clean-merged-branch.yml` (ga.mdc).
|
|
319
|
-
* @param {Record<string, unknown> | null} root parsed YAML
|
|
320
|
-
* @param {(msg: string) => void} passFn pass
|
|
321
|
-
* @param {(msg: string) => void} failFn fail
|
|
322
|
-
*/
|
|
323
|
-
function validateCleanMergedBranch(root, passFn, failFn) {
|
|
324
|
-
if (!root) {
|
|
325
|
-
failFn('clean-merged-branch.yml: YAML не вдалося розібрати (ga.mdc)')
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (isExactString(root.name, 'Clean abandoned branches')) {
|
|
330
|
-
passFn('clean-merged-branch.yml: name OK')
|
|
331
|
-
} else {
|
|
332
|
-
failFn('clean-merged-branch.yml: name має бути "Clean abandoned branches" (ga.mdc)')
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const on = root.on
|
|
336
|
-
const schedule = getObjKey(on, 'schedule')
|
|
337
|
-
const wfDispatch = getObjKey(on, 'workflow_dispatch')
|
|
338
|
-
const hasCron =
|
|
339
|
-
Array.isArray(schedule) &&
|
|
340
|
-
schedule.some(v => v && typeof v === 'object' && /** @type {Record<string, unknown>} */ (v).cron === '0 1 15 * *')
|
|
341
|
-
|
|
342
|
-
if (hasCron) {
|
|
343
|
-
passFn('clean-merged-branch.yml: cron OK')
|
|
344
|
-
} else {
|
|
345
|
-
failFn("clean-merged-branch.yml: on.schedule має містити cron: '0 1 15 * *' (ga.mdc)")
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (!wfDispatch || typeof wfDispatch !== 'object') {
|
|
349
|
-
failFn('clean-merged-branch.yml: має бути workflow_dispatch: {} (ga.mdc)')
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
validateConcurrencyOnRoot('clean-merged-branch.yml', root, passFn, failFn)
|
|
353
|
-
|
|
354
|
-
const jobs = getObjKey(root, 'jobs')
|
|
355
|
-
const job = getObjKey(jobs, 'cleanup_old_branches')
|
|
356
|
-
if (!job) {
|
|
357
|
-
failFn('clean-merged-branch.yml: jobs.cleanup_old_branches відсутній (ga.mdc)')
|
|
358
|
-
return
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const perm = getObjKey(job, 'permissions')
|
|
362
|
-
if (getObjKey(perm, 'contents') !== 'write') {
|
|
363
|
-
failFn('clean-merged-branch.yml: permissions мають бути contents: write (ga.mdc)')
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const steps = getObjKey(job, 'steps')
|
|
367
|
-
if (!Array.isArray(steps) || steps.length < 2) {
|
|
368
|
-
failFn('clean-merged-branch.yml: steps має містити 2 кроки як у ga.mdc')
|
|
369
|
-
return
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const step0 = steps[0]
|
|
373
|
-
if (!step0 || typeof step0 !== 'object') {
|
|
374
|
-
failFn('clean-merged-branch.yml: перший крок невалідний (ga.mdc)')
|
|
375
|
-
return
|
|
376
|
-
}
|
|
377
|
-
validateCleanMergedBranchStep0(step0, failFn)
|
|
378
|
-
|
|
379
|
-
const step1 = steps[1]
|
|
380
|
-
if (!step1 || typeof step1 !== 'object') {
|
|
381
|
-
failFn('clean-merged-branch.yml: другий крок невалідний (ga.mdc)')
|
|
382
|
-
return
|
|
383
|
-
}
|
|
384
|
-
validateCleanMergedBranchStep1(step1, passFn, failFn)
|
|
385
|
-
}
|
|
386
|
-
|
|
387
173
|
/**
|
|
388
174
|
* Перевіряє тригери `on.push` / `on.pull_request` у `lint-ga.yml`.
|
|
389
175
|
* @param {unknown} on корінь `on:` з YAML
|
|
@@ -973,26 +759,20 @@ async function checkGitAiWorkflow(wfDir, passFn, failFn) {
|
|
|
973
759
|
|
|
974
760
|
/**
|
|
975
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` — їх перенесення в наступних ітераціях.
|
|
976
766
|
* @param {string} wfDir директорія workflows
|
|
977
767
|
* @param {(msg: string) => void} passFn pass
|
|
978
768
|
* @param {(msg: string) => void} failFn fail
|
|
979
769
|
*/
|
|
980
770
|
async function checkCanonicalWorkflowsMatchRule(wfDir, passFn, failFn) {
|
|
981
771
|
const paths = {
|
|
982
|
-
cleanGa: join(wfDir, 'clean-ga-workflows.yml'),
|
|
983
|
-
cleanMerged: join(wfDir, 'clean-merged-branch.yml'),
|
|
984
772
|
lintGa: join(wfDir, 'lint-ga.yml'),
|
|
985
773
|
gitAi: join(wfDir, 'git-ai.yml')
|
|
986
774
|
}
|
|
987
775
|
|
|
988
|
-
if (existsSync(paths.cleanGa)) {
|
|
989
|
-
const c = await readFile(paths.cleanGa, 'utf8')
|
|
990
|
-
validateCleanGaWorkflows(parseWorkflowYaml(c), passFn, failFn)
|
|
991
|
-
}
|
|
992
|
-
if (existsSync(paths.cleanMerged)) {
|
|
993
|
-
const c = await readFile(paths.cleanMerged, 'utf8')
|
|
994
|
-
validateCleanMergedBranch(parseWorkflowYaml(c), passFn, failFn)
|
|
995
|
-
}
|
|
996
776
|
if (existsSync(paths.lintGa)) {
|
|
997
777
|
const c = await readFile(paths.lintGa, 'utf8')
|
|
998
778
|
validateLintGaWorkflowStructure(parseWorkflowYaml(c), passFn, failFn)
|
package/scripts/lint-ga.mjs
CHANGED
|
@@ -27,16 +27,26 @@ import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
|
27
27
|
/** Каталог пакету `@nitra/cursor`, від якого ресолвимо вшиту директорію policy/. */
|
|
28
28
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
29
29
|
|
|
30
|
-
/** Шлях до Rego-полісі
|
|
30
|
+
/** Шлях до кореня Rego-полісі для GA. У npm-tarball публікується через `files: ["policy"]` у package.json. */
|
|
31
31
|
const GA_POLICY_DIR = join(PACKAGE_ROOT, 'policy', 'ga')
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Workflow-файли, для яких маємо відповідну Rego-полісі.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Workflow-файли, для яких маємо відповідну Rego-полісі. Кожен таргет посилається на під-пакет
|
|
35
|
+
* `ga.<name>` у `policy/ga/<name>/<name>.rego`; conftest викликаємо з `--namespace`, щоб правила
|
|
36
|
+
* іншого workflow не застосовувалися до чужого файлу.
|
|
37
|
+
* @type {Array<{ workflow: string, namespace: string, label: string }>}
|
|
37
38
|
*/
|
|
38
39
|
const CONFTEST_TARGETS = [
|
|
39
|
-
{
|
|
40
|
+
{
|
|
41
|
+
workflow: '.github/workflows/clean-ga-workflows.yml',
|
|
42
|
+
namespace: 'ga.clean_ga_workflows',
|
|
43
|
+
label: 'clean-ga-workflows.yml structure'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
workflow: '.github/workflows/clean-merged-branch.yml',
|
|
47
|
+
namespace: 'ga.clean_merged_branch',
|
|
48
|
+
label: 'clean-merged-branch.yml structure'
|
|
49
|
+
}
|
|
40
50
|
]
|
|
41
51
|
|
|
42
52
|
/**
|
|
@@ -218,6 +228,8 @@ function runConftestStep() {
|
|
|
218
228
|
target.workflow,
|
|
219
229
|
'-p',
|
|
220
230
|
GA_POLICY_DIR,
|
|
231
|
+
'--namespace',
|
|
232
|
+
target.namespace,
|
|
221
233
|
'--no-color'
|
|
222
234
|
])
|
|
223
235
|
if (code !== 0) return code
|