@7n/rules-ci-github 1.6.2 → 1.7.1

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.1] - 2026-07-19
4
+
5
+ ### Changed
6
+
7
+ - release: @7n/rules@1.27.0, @7n/rules-lang-js@0.3.0, @7n/rules-lang-python@0.5.1, @7n/rules-lang-rust@0.5.1
8
+
9
+ ## [1.7.0] - 2026-07-18
10
+
11
+ ### Changed
12
+
13
+ - service_deploy_workflow: дискримінатор за ЗМІСТОМ замість імені deploy-*.yml (дзеркало ci-azure) — сервісний workflow визначається dir-scoped глобом `on.push.paths` (`npm/**`), імʼя довільне (npm-publish.yml завʼязаний на OIDC trusted publishing — перейменування ламає публікацію); plan-гейт вимагається лише за наявності lint-джоб
14
+
3
15
  ## [1.6.2] - 2026-07-18
4
16
 
5
17
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules-ci-github",
3
- "version": "1.6.2",
3
+ "version": "1.7.1",
4
4
  "description": "Плагін @7n/rules: канон GitHub Actions (.github/workflows) — обов'язкові workflow, zizmor, VS Code",
5
5
  "keywords": [
6
6
  "github-actions",
package/rules/ga/main.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Правила форматів для .github/workflows
3
- version: '1.11'
3
+ version: '1.12'
4
4
  globs: ".github/workflows/*.yml"
5
5
  alwaysApply: false
6
6
  ---
@@ -21,6 +21,6 @@ alwaysApply: false
21
21
 
22
22
  Перевірки без path-підтримки (`scope: full` без glob: knip, jscpd, dep-policy тощо) живуть в окремому обовʼязковому workflow `.github/workflows/lint-repo.yml` із кроком `bunx n-rules lint --repo-wide --no-fix`. Він **не гейтить деплой** — жоден deploy-workflow не має на нього `needs`. Деталі: [lint_repo_yml.mdc](./lint_repo_yml/lint_repo_yml.mdc).
23
23
 
24
- ## Сервіс-орієнтовані deploy-workflow (`deploy-*.yml`, опційно)
24
+ ## Сервіс-орієнтовані deploy-workflow (опційно)
25
25
 
26
- Монорепо з деплой-сервісами (каталог сервісу, напр. `run/nexus`) оформлює деплой per-service workflow `deploy-<service>.yml`: тригер `paths` по каталогу → джоба `plan` (`bunx n-rules ci plan --path <svc> --github`) → паралельні `lint-<domain>`-джоби з гейтом по outputs плану + тести сервісу → deploy з `needs` на всі перевірки (skipped не блокує, failed блокує). Форму перевіряє rego-концерн `service_deploy_workflow`; приклад — [deploy-service.yml.snippet.yml](./service_deploy_workflow/template/deploy-service.yml.snippet.yml), деталі — [service_deploy_workflow.mdc](./service_deploy_workflow/service_deploy_workflow.mdc). Співіснує з file-type-workflow (`lint-js.yml` тощо) — ті покривають зміни поза сервіс-каталогами.
26
+ Монорепо з деплой-сервісами (каталог сервісу, напр. `run/nexus`) оформлює деплой per-service workflow: тригер `paths` по каталогу (dir-scoped glob `run/<service>/**` — саме він, а не імʼя файлу, робить workflow сервісним; імʼя довільне — `npm-publish.yml`, `deploy-<service>.yml`, і **не перейменовуй наявні** — OIDC trusted publishing привʼязаний до імені) → джоба `plan` (`bunx n-rules ci plan --path <svc> --github`) → паралельні `lint-<domain>`-джоби з гейтом по outputs плану + тести сервісу → deploy з `needs` на всі перевірки (skipped не блокує, failed блокує). Форму перевіряє rego-концерн `service_deploy_workflow` — вимоги вмикаються лише за наявності lint-джоб у workflow; приклад — [deploy-service.yml.snippet.yml](./service_deploy_workflow/template/deploy-service.yml.snippet.yml), деталі — [service_deploy_workflow.mdc](./service_deploy_workflow/service_deploy_workflow.mdc). Співіснує з file-type-workflow (`lint-js.yml` тощо) — ті покривають зміни поза сервіс-каталогами.
@@ -3,7 +3,7 @@
3
3
  "fixability": "config",
4
4
  "policy": {
5
5
  "files": {
6
- "walkGlob": [".github/workflows/deploy-*.yml"]
6
+ "walkGlob": [".github/workflows/*.yml"]
7
7
  }
8
8
  }
9
9
  }
@@ -1,10 +1,10 @@
1
- ## Rego-gate сервіс-орієнтованих deploy-workflow (`.github/workflows/deploy-*.yml`)
1
+ ## Rego-gate сервіс-орієнтованих deploy-workflow (`.github/workflows/*.yml`)
2
2
 
3
3
  Rego-пакет: `ga.service_deploy_workflow`
4
4
 
5
- Цільові файли: усі `.github/workflows/deploy-*.yml` (walkGlob). Немає жодногоконцерн мовчить: сервіс-канон опційний, перелік сервісів консюмер-специфічний (реєстру немає).
5
+ Цільові файли: усі `.github/workflows/*.yml` (walkGlob). Дискримінатор **зміст, не імʼя файлу** (дзеркало ci-azure): сервісним вважається workflow із dir-scoped глобом у `on.push.paths` (`npm/**`, `run/<service>/**`). Імʼя довільне `npm-publish.yml`, `deploy-<service>.yml` тощо; **перейменування часто неможливе**, бо OIDC trusted publishing привʼязаний до імені workflow-файлу. Глоби за типом файлу (`**/*.js` у lint-*.yml) і workflow без `paths` — поза каноном. **Plan-гейт вимагається лише коли у workflow є lint-джоби** (`n-rules lint … --path`): publish/deploy-workflow без сервісного лінт-гейта валідний as-is, перехід на гейт — свідоме рішення консюмера (додати lint-джоби → rego і автофікс доведуть форму).
6
6
 
7
- Канонічна форма per-service workflow (`deploy-<service>.yml`, каталог сервісу — напр. `run/nexus`):
7
+ Канонічна форма per-service workflow (каталог сервісу — напр. `run/nexus`):
8
8
 
9
9
  1. **Тригер** — `on.push.paths` із глобом каталогу сервісу (`run/<service>/**`).
10
10
  2. **`plan`-джоба** — checkout `fetch-depth: 0` → prep (`./.github/actions/setup-bun-deps`) → `bunx n-rules ci plan --path run/<service> --github` (крок з `id: plan`, outputs прокинуті в `jobs.plan.outputs`).
@@ -1,4 +1,13 @@
1
- # Форма сервіс-орієнтованого deploy-workflow (`.github/workflows/deploy-*.yml`).
1
+ # Форма сервіс-орієнтованого deploy-workflow (`.github/workflows/*.yml`).
2
+ #
3
+ # Дискримінатор застосовності — НЕ імʼя файлу, а ЗМІСТ (дзеркало ci-azure):
4
+ # сервісний workflow = `on.push.paths` має dir-scoped glob (`npm/**`,
5
+ # `run/nexus/**`). Імʼя довільне (`npm-publish.yml`, `deploy-*.yml`, …) —
6
+ # перейменування часто неможливе: OIDC trusted publishing привʼязаний до
7
+ # імені workflow-файлу. Глоби за типом файлу (`**/*.js`, lint-*.yml) і
8
+ # workflow без paths — поза каноном. Plan-гейт вимагається лише коли у
9
+ # workflow Є lint-джоби (`n-rules lint … --path`) — publish/deploy-workflow
10
+ # без сервісних lint-джоб валідний as-is (перехід на гейт — свідоме рішення).
2
11
  #
3
12
  # Канон (ga.mdc, сніпет template/deploy-service.yml.snippet.yml):
4
13
  # plan (checkout fetch-depth:0 + prep + `bunx n-rules ci plan --path <svc> --github`)
@@ -8,9 +17,8 @@
8
17
  # └─ deploy (needs-ланцюг досягає plan і ВСІ lint-джоби;
9
18
  # if: !cancelled() + !contains(needs.*.result, 'failure'))
10
19
  #
11
- # Перелік сервісів консюмер-специфічний — концерн перевіряє ФОРМУ кожного
12
- # знайденого deploy-*.yml, не реєстр. Ланцюги deploy (build → deploy) законні:
13
- # досяжність рахується транзитивно через graph.reachable.
20
+ # Перелік сервісів консюмер-специфічний — концерн перевіряє ФОРМУ, не реєстр.
21
+ # Ланцюги deploy (build → deploy) законні: досяжність — graph.reachable.
14
22
  #
15
23
  # Не перевіряється тут (лишається .mdc-каноном): вибір набору доменів під
16
24
  # сервіс, зміст test/deploy-кроків, outputs-мапінг plan-джоби, uv-prep для
@@ -34,6 +42,15 @@ on_block := object.union_n([
34
42
 
35
43
  push_paths contains p if some p in object.get(object.get(on_block, "push", {}), "paths", [])
36
44
 
45
+ # Dir-scoped глоби тригера (`npm/**`, `run/nexus/**`) — дискримінатор сервісного
46
+ # workflow; глоби за типом файлу (`**/*.js`) чи точкові шляхи — ні.
47
+ service_dir_globs contains p if {
48
+ some p in push_paths
49
+ regex.match(`^[^*]+/\*\*$`, p)
50
+ }
51
+
52
+ is_service_workflow if count(service_dir_globs) > 0
53
+
37
54
  plan_runs contains r if {
38
55
  some step in object.get(object.get(jobs, "plan", {}), "steps", [])
39
56
  r := object.get(step, "run", "")
@@ -61,6 +78,16 @@ lint_cmds contains cmd if {
61
78
 
62
79
  check_jobs contains cmd.job if some cmd in lint_cmds
63
80
 
81
+ # Наявність lint-джоб (domain-style або легасі `lint --path` без домену) —
82
+ # лише тоді вимагаємо plan-гейт.
83
+ has_lint_jobs if count(lint_cmds) > 0
84
+
85
+ has_lint_jobs if {
86
+ some job in jobs
87
+ some step in object.get(job, "steps", [])
88
+ regex.match(`n-rules lint\s+--path\s+\S+`, object.get(step, "run", ""))
89
+ }
90
+
64
91
  # Граф залежностей і транзитивна досяжність (ланцюг deploy → build → lint).
65
92
  needs_graph[name] := needs_of(job) if some name, job in jobs
66
93
 
@@ -80,25 +107,30 @@ terminal_jobs contains name if {
80
107
  # ── deny: plan-джоба ────────────────────────────────────────────────────
81
108
 
82
109
  deny contains msg if {
110
+ is_service_workflow
111
+ has_lint_jobs
83
112
  count(jobs) > 0
84
113
  not jobs.plan
85
- msg := "deploy-workflow: немає job `plan` з кроком `bunx n-rules ci plan --path <svc> --github` (ga.mdc: сервіс-канон)"
114
+ msg := "service-workflow: немає job `plan` з кроком `bunx n-rules ci plan --path <svc> --github` (ga.mdc: сервіс-канон)"
86
115
  }
87
116
 
88
117
  deny contains msg if {
118
+ is_service_workflow
89
119
  jobs.plan
90
120
  count(plan_runs) == 0
91
- msg := "deploy-workflow: job `plan` без кроку `bunx n-rules ci plan …` (ga.mdc: сервіс-канон)"
121
+ msg := "service-workflow: job `plan` без кроку `bunx n-rules ci plan …` (ga.mdc: сервіс-канон)"
92
122
  }
93
123
 
94
124
  deny contains msg if {
95
- msg := "deploy-workflow: `ci plan` без `--path <serviceDir>` (ga.mdc: сервіс-канон)"
125
+ msg := "service-workflow: `ci plan` без `--path <serviceDir>` (ga.mdc: сервіс-канон)"
126
+ is_service_workflow
96
127
  some r in plan_runs
97
128
  not regex.match(`n-rules ci plan\s+--path\s+\S+`, r)
98
129
  }
99
130
 
100
131
  deny contains msg if {
101
- msg := "deploy-workflow: `ci plan` без `--github` — outputs не потраплять у $GITHUB_OUTPUT (ga.mdc)"
132
+ msg := "service-workflow: `ci plan` без `--github` — outputs не потраплять у $GITHUB_OUTPUT (ga.mdc)"
133
+ is_service_workflow
102
134
  some r in plan_runs
103
135
  not contains(r, "--github")
104
136
  }
@@ -106,20 +138,23 @@ deny contains msg if {
106
138
  # ── deny: тригер paths ↔ сервісний каталог ─────────────────────────────
107
139
 
108
140
  deny contains msg if {
141
+ is_service_workflow
109
142
  some sp in service_paths
110
- not trigger_covers(sp)
111
- msg := sprintf("deploy-workflow: on.push.paths не містить glob для сервісного каталогу %s (ga.mdc)", [sp])
143
+ not glob_covers(sp)
144
+ msg := sprintf("service-workflow: on.push.paths не містить glob для сервісного каталогу %s (ga.mdc)", [sp])
112
145
  }
113
146
 
114
147
  # ── deny: lint-джоби ────────────────────────────────────────────────────
115
148
 
116
149
  deny contains msg if {
150
+ is_service_workflow
117
151
  some cmd in lint_cmds
118
152
  not "plan" in needs_of(jobs[cmd.job])
119
153
  msg := sprintf("%s: lint-джоба мусить мати needs: plan (ga.mdc: сервіс-канон)", [cmd.job])
120
154
  }
121
155
 
122
156
  deny contains msg if {
157
+ is_service_workflow
123
158
  some cmd in lint_cmds
124
159
  cond := object.get(jobs[cmd.job], "if", "")
125
160
  not contains(cond, sprintf("needs.plan.outputs.%s", [replace(cmd.domain, "-", "_")]))
@@ -127,19 +162,21 @@ deny contains msg if {
127
162
  }
128
163
 
129
164
  deny contains msg if {
130
- count(service_paths) > 0
165
+ is_service_workflow
131
166
  some cmd in lint_cmds
132
- not cmd.path in service_paths
133
- msg := sprintf("%s: `lint %s --path %s` `--path` plan-джоби — лінтиться інший каталог (ga.mdc)", [cmd.job, cmd.domain, cmd.path])
167
+ not glob_covers(cmd.path)
168
+ msg := sprintf("%s: `lint %s --path %s` on.push.paths — лінтиться інший каталог (ga.mdc)", [cmd.job, cmd.domain, cmd.path])
134
169
  }
135
170
 
136
171
  deny contains msg if {
172
+ is_service_workflow
137
173
  some cmd in lint_cmds
138
174
  not contains(cmd.run, "--no-fix")
139
175
  msg := sprintf("%s: `n-rules lint` у CI мусить мати `--no-fix` (ga.mdc)", [cmd.job])
140
176
  }
141
177
 
142
178
  deny contains msg if {
179
+ is_service_workflow
143
180
  some cmd in lint_cmds
144
181
  not job_has_prep(jobs[cmd.job])
145
182
  msg := sprintf("%s: lint-джоба без prep-кроку `uses: ./.github/actions/setup-bun-deps` (джоби не шарять ФС) (ga.mdc)", [cmd.job])
@@ -148,12 +185,14 @@ deny contains msg if {
148
185
  # ── deny: fetch-depth для git-дельти ────────────────────────────────────
149
186
 
150
187
  deny contains msg if {
188
+ is_service_workflow
151
189
  count(plan_runs) > 0
152
190
  not job_has_full_checkout(jobs.plan)
153
191
  msg := "plan: checkout мусить мати fetch-depth: 0 — без історії git-дельта `ci plan` не рахується (ga.mdc)"
154
192
  }
155
193
 
156
194
  deny contains msg if {
195
+ is_service_workflow
157
196
  some cmd in lint_cmds
158
197
  not job_has_full_checkout(jobs[cmd.job])
159
198
  msg := sprintf("%s: checkout мусить мати fetch-depth: 0 — без історії `lint --path` не бачить дельти (ga.mdc)", [cmd.job])
@@ -162,6 +201,7 @@ deny contains msg if {
162
201
  # ── deny: термінальні (deploy) джоби ────────────────────────────────────
163
202
 
164
203
  deny contains msg if {
204
+ is_service_workflow
165
205
  count(check_jobs) > 0
166
206
  some name in terminal_jobs
167
207
  not "plan" in graph.reachable(needs_graph, {name})
@@ -169,6 +209,7 @@ deny contains msg if {
169
209
  }
170
210
 
171
211
  deny contains msg if {
212
+ is_service_workflow
172
213
  some name in terminal_jobs
173
214
  some cj in check_jobs
174
215
  not cj in graph.reachable(needs_graph, {name})
@@ -178,6 +219,7 @@ deny contains msg if {
178
219
  # Skipped-лінт (умовний гейт спрацював) не мусить блокувати деплой, а fail —
179
220
  # мусить: канонічний if = `!cancelled() && !contains(needs.*.result, 'failure')`.
180
221
  deny contains msg if {
222
+ is_service_workflow
181
223
  some name in terminal_jobs
182
224
  needed := needs_of(jobs[name])
183
225
  count([cj | some cj in check_jobs; cj in needed]) > 0
@@ -196,9 +238,11 @@ needs_of(job) := [n] if {
196
238
 
197
239
  needs_of(job) := object.get(job, "needs", []) if is_array(object.get(job, "needs", []))
198
240
 
199
- trigger_covers(sp) if {
200
- some p in push_paths
201
- startswith(p, sp)
241
+ # Каталог `--path` покривається dir-scoped глобом тригера
242
+ # (`npm/**` покриває `npm`; точний збіг теж).
243
+ glob_covers(sp) if {
244
+ some g in service_dir_globs
245
+ startswith(g, sp)
202
246
  }
203
247
 
204
248
  job_has_prep(job) if {
@@ -1,8 +1,11 @@
1
1
  # Документаційний приклад сервіс-орієнтованого deploy-workflow (ga.mdc).
2
- # Це НЕ deep-subset-канон (перелік сервісів консюмер-специфічний, назва файлу
3
- # deploy-<service>.yml) — форму перевіряє service_deploy_workflow.rego.
4
- # Плейсхолдер run/<service> заміни на каталог сервісу; набір lint-<domain>
5
- # джоб добери під стеки сервісу (зайва джоба нешкідлива — ci plan її скіпне).
2
+ # Це НЕ deep-subset-канон (перелік сервісів консюмер-специфічний) форму
3
+ # перевіряє service_deploy_workflow.rego за ЗМІСТОМ: сервісний workflow =
4
+ # dir-scoped glob у on.push.paths. Імʼя файлу довільне (npm-publish.yml,
5
+ # deploy-<service>.yml, …) не перейменовуй наявні: OIDC trusted publishing
6
+ # привʼязаний до імені workflow. Плейсхолдер run/<service> заміни на каталог
7
+ # сервісу; набір lint-<domain> джоб добери під стеки сервісу (зайва джоба
8
+ # нешкідлива — ci plan її скіпне).
6
9
  name: Deploy <service>
7
10
 
8
11
  on: