@7n/rules-ci-github 1.2.2 → 1.3.0

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
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0] - 2026-07-17
4
+
5
+ ### Changed
6
+
7
+ - ga/workflow_common: дозволено режим release-серіалізації concurrency — статичний group + cancel-in-progress: false
8
+
3
9
  ## [1.2.2] - 2026-07-16
4
10
 
5
11
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules-ci-github",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "\u041f\u043b\u0430\u0433\u0456\u043d @7n/rules: \u043a\u0430\u043d\u043e\u043d GitHub Actions (.github/workflows) \u2014 \u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u0456 workflow, zizmor, VS Code",
5
5
  "keywords": [
6
6
  "github-actions",
@@ -6,7 +6,10 @@ Rego-пакет: `ga.workflow_common`
6
6
 
7
7
  ### Що перевіряється
8
8
 
9
- - **concurrency** — обов'язкова наявність блоку; `group` = `${{ github.ref }}-${{ github.workflow }}`; `cancel-in-progress: true`
9
+ - **concurrency** — обов'язкова наявність блоку; два допустимі режими:
10
+ - канонічний CI: `group` = `${{ github.ref }}-${{ github.workflow }}` + `cancel-in-progress: true`
11
+ - release-серіалізація: статичний `group` (без `${{ }}`-виразів, глобальний lock) + `cancel-in-progress: false` — для workflow, що піднімають версію/тег/CHANGELOG і не можуть бути скасовані на півдорозі; відсутній `cancel-in-progress` = `false` (семантика GitHub)
12
+ - змішані комбінації (статичний `group` + `true`, per-ref `group` + `false`) — заборонені
10
13
  - **Заборонені кроки** — `oven-sh/setup-bun`, `actions/cache`, `bun install` у будь-якому `uses:`/`run:` (мають бути інкапсульовані у composite `setup-bun-deps`)
11
14
  - **Порядок кроків** — якщо є `setup-bun-deps`, перед ним обов'язковий `actions/checkout@`
12
15
  - **Shell-продовження** — `\` перед переносом рядка у `run:` заборонено; треба `run: >-`
@@ -18,6 +18,10 @@ import rego.v1
18
18
 
19
19
  expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
20
20
 
21
+ # Маркер GitHub-виразу `${{` — через concat (та сама причина, що й у
22
+ # expected_concurrency_group): без сирого літерала в rego-джерелі.
23
+ gh_expr_marker := concat("", ["$", "{{"])
24
+
21
25
  # Локальні composite setup-bun-deps (ga.mdc) — два варіанти шляху:
22
26
  # `.github/actions/...` (cursor-репо) і `npm/github-actions/...` (npm-пакет dev-локально).
23
27
  local_setup_bun_markers := {
@@ -45,6 +49,16 @@ concurrency_missing_template := concat(" ", [
45
49
  "додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
46
50
  ])
47
51
 
52
+ concurrency_group_template := concat(" ", [
53
+ "concurrency.group має бути %s,",
54
+ "або статичний рядок-lock для release-серіалізації (ga.mdc)",
55
+ ])
56
+
57
+ concurrency_release_cancel_template := concat(" ", [
58
+ "статичний concurrency.group — release-серіалізація: скасування на півдорозі",
59
+ "лишає репо в неконсистентному стані, тому cancel-in-progress: false (ga.mdc)",
60
+ ])
61
+
48
62
  shell_continuation_template := concat(" ", [
49
63
  "jobs.%s.steps[%d]: у run заборонено продовження рядків через зворотний сліш;",
50
64
  "оформи як folded block (run: >-) (ga.mdc)",
@@ -147,6 +161,14 @@ deny contains msg if {
147
161
  # Дублює окремі per-workflow перевірки для clean-ga-workflows / clean-merged-branch /
148
162
  # lint-ga / git-ai, але вкриває й решту workflow-файлів (apply-k8s, lint-js, …),
149
163
  # для яких поки немає виділеної polysi.
164
+ #
165
+ # Допустимі рівно два когерентні режими:
166
+ # 1. канонічний CI: per-ref group + cancel-in-progress: true;
167
+ # 2. release-серіалізація: статичний group (глобальний lock, без `${{ }}`-виразів)
168
+ # + cancel-in-progress: false — для workflow, що піднімають версію/тег і не
169
+ # можуть бути скасовані на півдорозі (per-ref lock не захищає від двох
170
+ # одночасних push-подій, які обидві піднімають версію).
171
+ # Змішані комбінації (статичний group + true, per-ref + false) — заборонені.
150
172
 
151
173
  deny contains msg if {
152
174
  # `object.get(…, default)` повертає `false` коли ключа немає — інакше `not is_object(…)`
@@ -157,16 +179,25 @@ deny contains msg if {
157
179
 
158
180
  deny contains msg if {
159
181
  is_object(object.get(input, "concurrency", false))
160
- input.concurrency.group != expected_concurrency_group
161
- msg := sprintf("concurrency.group має бути %s (ga.mdc)", [expected_concurrency_group])
182
+ group := object.get(input.concurrency, "group", "")
183
+ group != expected_concurrency_group
184
+ not is_release_lock_group(group)
185
+ msg := sprintf(concurrency_group_template, [expected_concurrency_group])
162
186
  }
163
187
 
164
188
  deny contains msg if {
165
189
  is_object(object.get(input, "concurrency", false))
166
- input.concurrency["cancel-in-progress"] != true
190
+ input.concurrency.group == expected_concurrency_group
191
+ object.get(input.concurrency, "cancel-in-progress", false) != true
167
192
  msg := "concurrency.cancel-in-progress має бути true (ga.mdc)"
168
193
  }
169
194
 
195
+ deny contains concurrency_release_cancel_template if {
196
+ is_object(object.get(input, "concurrency", false))
197
+ is_release_lock_group(object.get(input.concurrency, "group", ""))
198
+ object.get(input.concurrency, "cancel-in-progress", false) != false
199
+ }
200
+
170
201
  # ── deny: мінімальні версії marketplace actions у `uses:` ─────────────────
171
202
  #
172
203
  # Канон — `template/uses-min-versions.snippet.json` (через --data).
@@ -191,6 +222,14 @@ deny contains msg if {
191
222
 
192
223
  # ── helpers ────────────────────────────────────────────────────────────────
193
224
 
225
+ # Статичний непорожній group без `${{ }}`-виразів — глобальний lock
226
+ # (режим release-серіалізації concurrency).
227
+ is_release_lock_group(group) if {
228
+ is_string(group)
229
+ group != ""
230
+ not contains(group, gh_expr_marker)
231
+ }
232
+
194
233
  # Об'єднаний рядок `uses` + `run` для одного кроку — для substring-пошуку
195
234
  # заборонених патернів. `run` може бути рядком або масивом рядків (YAML).
196
235
  step_uses_or_run_blob(step) := blob if {