@nitra/cursor 1.13.75 → 1.13.76

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,12 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.76] - 2026-05-22
8
+
9
+ ### Added
10
+
11
+ - **`ga.workflow_common` — мінімальні версії marketplace actions у `uses:`:** `actions/checkout` >= major `v6` (`@v6` і `@v6.0.2` дозволені), `Infisical/secrets-action` >= `v1.0.16` (канон у `policy/workflow_common/template/uses-min-versions.snippet.json`; SHA-pin пропускається). `check-ga` передає template через `--data`. Bump `ga.mdc` `1.9` → `1.10`.
12
+
7
13
  ## [1.13.75] - 2026-05-22
8
14
 
9
15
  ### Removed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.75",
3
+ "version": "1.13.76",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -303,10 +303,14 @@ async function runAllGaRego(wfDir, ymlWorkflows, pass, fail) {
303
303
 
304
304
  if (ymlWorkflows.length === 0) return
305
305
  const wfFiles = ymlWorkflows.map(f => join(wfDir, f))
306
+ const workflowCommonDir = join(GA_POLICY_DIR, 'workflow_common')
307
+ const workflowCommonTpl = await loadTemplate(workflowCommonDir)
308
+ const usesMinVersionsSnippet = workflowCommonTpl['uses-min-versions']?.snippet
306
309
  const violations = runConftestBatch({
307
310
  policyDirRel: 'ga/workflow_common',
308
311
  namespace: 'ga.workflow_common',
309
- files: wfFiles
312
+ files: wfFiles,
313
+ templateData: usesMinVersionsSnippet ? { snippet: usesMinVersionsSnippet } : undefined
310
314
  })
311
315
  for (const v of violations) fail(`${v.filename}: ${v.message}`)
312
316
  if (violations.length === 0) {
package/rules/ga/ga.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Правила форматів для .github/workflows
3
- version: '1.9'
3
+ version: '1.10'
4
4
  globs: ".github/workflows/*.yml"
5
5
  alwaysApply: false
6
6
  ---
@@ -35,6 +35,13 @@ concurrency:
35
35
 
36
36
  - Канон: [git-ai.yml.snippet.yml](./policy/git_ai/template/git-ai.yml.snippet.yml)
37
37
 
38
+ **Мінімальні версії marketplace actions** у `uses:` (перевіряє `ga.workflow_common`):
39
+
40
+ - `actions/checkout` — **не нижче major `v6`** (`@v6`, `@v6.0.2` тощо дозволені; `@v5` — ні);
41
+ - `Infisical/secrets-action` — **не нижче `v1.0.16`** (Node 24; нижчі теги лишаються на Node 20, deprecated з червня 2026).
42
+
43
+ Канон: [uses-min-versions.snippet.json](./policy/workflow_common/template/uses-min-versions.snippet.json). SHA-pin (40 hex) semver-політику не застосовує. Для checkout рекомендується **`v6.0.2+`** (Node 24 у action), але мінімум політики — лише major **6**.
44
+
38
45
  **Локальний composite** (`uses: ./.github/actions/setup-bun-deps` або `./npm/github-actions/setup-bun-deps`): **спочатку** обов’язковий крок **`actions/checkout@v6`** (`persist-credentials: false`), інакше runner не знайде `action.yml`. Сам composite: **`actions/setup-node@v6`** (**Node 24**), **Bun**, **`actions/cache@v5`**, **`bun install --frozen-lockfile`**.
39
46
 
40
47
  **ЗАБОРОНЕНО** дублювати кроки встановлення Bun та кешування безпосередньо у workflow файлах. Завжди використовуй локальний composite action.
@@ -0,0 +1,4 @@
1
+ {
2
+ "actions/checkout": "6",
3
+ "Infisical/secrets-action": "1.0.16"
4
+ }
@@ -55,6 +55,11 @@ setup_bun_no_checkout_template := concat(" ", [
55
55
  "інакше runner не знайде action.yml (ga.mdc)",
56
56
  ])
57
57
 
58
+ min_uses_version_template := concat(" ", [
59
+ "jobs.%s.steps[%d]: %s має бути >= v%s (зараз %q) —",
60
+ "онови ref у uses: (ga.mdc)",
61
+ ])
62
+
58
63
  forbidden_run_command_template := concat(" ", [
59
64
  "jobs.%s.steps[%d]: `%s` заборонено у workflow —",
60
65
  "мігровано на knip (js-lint.mdc, ga.mdc)",
@@ -147,6 +152,28 @@ deny contains msg if {
147
152
  msg := "concurrency.cancel-in-progress має бути true (ga.mdc)"
148
153
  }
149
154
 
155
+ # ── deny: мінімальні версії marketplace actions у `uses:` ─────────────────
156
+ #
157
+ # Канон — `template/uses-min-versions.snippet.json` (через --data).
158
+ # Перевіряє semver-подібні теги `vX.Y.Z` / `vN`; SHA-pin (40 hex) пропускаємо.
159
+
160
+ deny contains msg if {
161
+ some entry in all_flat_steps
162
+ uses := object.get(entry.step, "uses", "")
163
+ uses != ""
164
+ some action_slug, min_ver in data.template.snippet
165
+ action_uses_matches(uses, action_slug)
166
+ ref := action_uses_ref(uses)
167
+ not action_ref_meets_min(ref, min_ver)
168
+ msg := sprintf(min_uses_version_template, [
169
+ entry.job_id,
170
+ entry.step_index,
171
+ action_slug,
172
+ min_ver,
173
+ ref,
174
+ ])
175
+ }
176
+
150
177
  # ── helpers ────────────────────────────────────────────────────────────────
151
178
 
152
179
  # Об'єднаний рядок `uses` + `run` для одного кроку — для substring-пошуку
@@ -182,3 +209,65 @@ has_checkout_before(job, before) if {
182
209
  uses := object.get(step, "uses", "")
183
210
  contains(uses, "actions/checkout@")
184
211
  }
212
+
213
+ # `uses:` починається з `owner/repo@` для заданого slug.
214
+ action_uses_matches(uses, slug) if {
215
+ startswith(uses, concat("", [slug, "@"]))
216
+ }
217
+
218
+ # Ref після останнього `@` у `uses:` (owner/repo@ref).
219
+ action_uses_ref(uses) := ref if {
220
+ parts := split(uses, "@")
221
+ count(parts) >= 2
222
+ ref := parts[count(parts) - 1]
223
+ }
224
+
225
+ # SHA-pin — semver-політика не застосовується.
226
+ action_ref_is_sha_pin(ref) if {
227
+ regex.match(`^[0-9a-fA-F]{40}$`, ref)
228
+ }
229
+
230
+ # Semver ref >= min (обидва як X.Y.Z після optional `v`).
231
+ action_ref_meets_min(ref, _) if {
232
+ action_ref_is_sha_pin(ref)
233
+ }
234
+
235
+ action_ref_meets_min(ref, min_ver) if {
236
+ not action_ref_is_sha_pin(ref)
237
+ version_triple_gte(version_triple(ref), version_triple(min_ver))
238
+ }
239
+
240
+ version_triple(raw) := [major, minor, patch] if {
241
+ stripped := trim_prefix(trim_prefix(raw, "v"), "V")
242
+ parts := split_to_numbers(stripped)
243
+ major := version_part(parts, 0)
244
+ minor := version_part(parts, 1)
245
+ patch := version_part(parts, 2)
246
+ }
247
+
248
+ version_part(parts, idx) := parts[idx] if {
249
+ count(parts) > idx
250
+ }
251
+
252
+ else := 0
253
+
254
+ version_triple_gte(a, b) if {
255
+ a[0] > b[0]
256
+ }
257
+
258
+ version_triple_gte(a, b) if {
259
+ a[0] == b[0]
260
+ a[1] > b[1]
261
+ }
262
+
263
+ version_triple_gte(a, b) if {
264
+ a[0] == b[0]
265
+ a[1] == b[1]
266
+ a[2] >= b[2]
267
+ }
268
+
269
+ split_to_numbers(spec) := nums if {
270
+ tokens := regex.split(`\D+`, spec)
271
+ non_empty := [t | some t in tokens; t != ""]
272
+ nums := [n | some t in non_empty; n := to_number(t)]
273
+ }