@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 +12 -0
- package/package.json +1 -1
- package/rules/ga/main.mdc +3 -3
- package/rules/ga/service_deploy_workflow/concern.json +1 -1
- package/rules/ga/service_deploy_workflow/service_deploy_workflow.mdc +3 -3
- package/rules/ga/service_deploy_workflow/service_deploy_workflow.rego +60 -16
- package/rules/ga/service_deploy_workflow/template/deploy-service.yml.snippet.yml +7 -4
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
package/rules/ga/main.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила форматів для .github/workflows
|
|
3
|
-
version: '1.
|
|
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 (
|
|
24
|
+
## Сервіс-орієнтовані deploy-workflow (опційно)
|
|
25
25
|
|
|
26
|
-
Монорепо з деплой-сервісами (каталог сервісу, напр. `run/nexus`) оформлює деплой per-service workflow `deploy-<service>.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` тощо) — ті покривають зміни поза сервіс-каталогами.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
## Rego-gate сервіс-орієнтованих deploy-workflow (`.github/workflows
|
|
1
|
+
## Rego-gate сервіс-орієнтованих deploy-workflow (`.github/workflows/*.yml`)
|
|
2
2
|
|
|
3
3
|
Rego-пакет: `ga.service_deploy_workflow`
|
|
4
4
|
|
|
5
|
-
Цільові файли: усі `.github/workflows
|
|
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 (
|
|
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
|
|
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
|
-
#
|
|
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 := "
|
|
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 := "
|
|
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 := "
|
|
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 := "
|
|
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
|
|
111
|
-
msg := sprintf("
|
|
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
|
-
|
|
165
|
+
is_service_workflow
|
|
131
166
|
some cmd in lint_cmds
|
|
132
|
-
not cmd.path
|
|
133
|
-
msg := sprintf("%s: `lint %s --path %s`
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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:
|