@7n/rules-ci-github 1.3.1 → 1.4.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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0] - 2026-07-18
4
+
5
+ ### Added
6
+
7
+ - сервіс-орієнтований CI-канон: концерн ga/service_deploy_workflow (форма `deploy-<service>.yml`: plan-гейт ci plan, per-domain lint-джоби з needs+if, deploy dependsOn всі перевірки) і ga/lint_repo_yml (окремий lint-repo.yml для repo-wide перевірок, що не гейтять деплой)
8
+
9
+ ### Fixed
10
+
11
+ - ga/workflows: додано пропущені `await` для `runConftestBatch` (стала async у ядрі @7n/rules 1.14) — детектор падав із «violations is not iterable»
12
+
3
13
  ## [1.3.1] - 2026-07-18
4
14
 
5
15
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules-ci-github",
3
- "version": "1.3.1",
3
+ "version": "1.4.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",
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "fixability": "config",
4
+ "policy": {
5
+ "files": {
6
+ "single": ".github/workflows/lint-repo.yml",
7
+ "required": true
8
+ },
9
+ "check": "template",
10
+ "missingMessage": ".github/workflows/lint-repo.yml не існує — repo-wide перевірки без path-підтримки (knip/jscpd/dep-policy) живуть в окремому workflow, що не гейтить деплой; створи за каноном ga.mdc (mixin @7n/rules-ci-github)"
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ ## Deep-subset `.github/workflows/lint-repo.yml` (repo-wide перевірки)
2
+
3
+ Цільовий файл: `.github/workflows/lint-repo.yml` — **обовʼязковий** для github-консюмерів.
4
+
5
+ Repo-wide перевірки без path-підтримки (`scope: full` без glob: knip, jscpd, dep-policy, utils_imports тощо) не вміють скоупитись на каталог сервісу, тому живуть в окремому workflow з кроком `bunx n-rules lint --repo-wide --no-fix` і **не гейтять деплой**: жоден `deploy-*.yml` не має `needs` на цей workflow (сервіс-канон — `service_deploy_workflow`).
6
+
7
+ Перевірка — чистий deep-subset без rego (`"check": "template"`): фактичний файл мусить структурно містити сніпет (тригер push/PR на dev/main без paths-фільтра, checkout з `persist-credentials: false`, prep `./.github/actions/setup-bun-deps`, крок `lint --repo-wide --no-fix`). Додаткові кроки/поля дозволені.
8
+
9
+ Канон-snippet: [lint-repo.yml.snippet.yml](./template/lint-repo.yml.snippet.yml)
@@ -0,0 +1,31 @@
1
+ name: Lint repo-wide
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - dev
7
+ - main
8
+
9
+ pull_request:
10
+ branches:
11
+ - dev
12
+ - main
13
+
14
+ concurrency:
15
+ group: ${{ github.ref }}-${{ github.workflow }}
16
+ cancel-in-progress: true
17
+
18
+ jobs:
19
+ lint-repo:
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ steps:
24
+ - uses: actions/checkout@v6
25
+ with:
26
+ persist-credentials: false
27
+
28
+ - uses: ./.github/actions/setup-bun-deps
29
+
30
+ - name: Repo-wide lint
31
+ run: bunx n-rules lint --repo-wide --no-fix
package/rules/ga/main.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Правила форматів для .github/workflows
3
- version: '1.10'
3
+ version: '1.11'
4
4
  globs: ".github/workflows/*.yml"
5
5
  alwaysApply: false
6
6
  ---
@@ -15,3 +15,12 @@ alwaysApply: false
15
15
  - [clean-merged-branch.yml.snippet.yml](./clean_merged_branch/template/clean-merged-branch.yml.snippet.yml)
16
16
  - [extensions.json.snippet.json](./vscode_extensions/template/extensions.json.snippet.json)
17
17
  - [uses-min-versions.snippet.json](./workflow_common/template/uses-min-versions.snippet.json)
18
+ - [lint-repo.yml.snippet.yml](./lint_repo_yml/template/lint-repo.yml.snippet.yml)
19
+
20
+ ## Repo-wide лінти (`lint-repo.yml`, обовʼязковий)
21
+
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
+
24
+ ## Сервіс-орієнтовані deploy-workflow (`deploy-*.yml`, опційно)
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` тощо) — ті покривають зміни поза сервіс-каталогами.
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "fixability": "config",
4
+ "policy": {
5
+ "files": {
6
+ "walkGlob": [".github/workflows/deploy-*.yml"]
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,21 @@
1
+ ## Rego-gate сервіс-орієнтованих deploy-workflow (`.github/workflows/deploy-*.yml`)
2
+
3
+ Rego-пакет: `ga.service_deploy_workflow`
4
+
5
+ Цільові файли: усі `.github/workflows/deploy-*.yml` (walkGlob). Немає жодного — концерн мовчить: сервіс-канон опційний, перелік сервісів консюмер-специфічний (реєстру немає).
6
+
7
+ Канонічна форма per-service workflow (`deploy-<service>.yml`, каталог сервісу — напр. `run/nexus`):
8
+
9
+ 1. **Тригер** — `on.push.paths` із глобом каталогу сервісу (`run/<service>/**`).
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`).
11
+ 3. **`lint-<domain>`-джоби** (паралельно, по одній на домен сервісу) — `needs: plan`, гейт `if: needs.plan.outputs.<domain> == 'true'` (ключ = домен із `-`→`_`), крок `bunx n-rules lint <domain> --path run/<service> --no-fix` з тим самим `--path`, що в plan. Зайва домен-джоба нешкідлива (ci plan її скіпне), відсутня потрібна — дірка в гейті.
12
+ 4. **`test`-джоба** — `needs: plan`, `if: needs.plan.outputs.any == 'true'`; тести лише сервісу (`bun test run/<service>`; python-стек — `astral-sh/setup-uv` + `uv sync --locked` + `pytest run/<service>`).
13
+ 5. **Деплой** — needs-ланцюг (можна через проміжний `build`) **транзитивно досягає** `plan` і всі lint/test-джоби; кожна джоба з `needs` на умовні lint-джоби має `if` з `!cancelled()` та `!contains(needs.*.result, 'failure')` — skipped-лінт не блокує деплой, failed — блокує.
14
+
15
+ Що перевіряє rego: наявність plan-джоби з `ci plan --path … --github`; збіг `on.push.paths` ↔ `--path`; для кожної lint-джоби — `needs: plan`, гейт по outputs, той самий `--path`, `--no-fix`, prep-крок, `fetch-depth: 0`; для термінальних джоб — транзитивну досяжність plan і всіх перевірок та skip-толерантний `if`.
16
+
17
+ Що НЕ перевіряється (справа автора): добір доменів під вміст сервісу, зміст test/deploy-кроків, секрети/environment, uv-prep. Спільні workflow-інваріанти (persist-credentials, min-версії uses, існування paths-глобів) покривають `workflow_common` і `workflows`.
18
+
19
+ Repo-wide перевірки без path-підтримки (knip, jscpd, dep-policy) сюди НЕ входять — вони в окремому `lint-repo.yml` (концерн `lint_repo_yml`) і деплой не гейтять.
20
+
21
+ Канон-snippet (документаційний приклад): [deploy-service.yml.snippet.yml](./template/deploy-service.yml.snippet.yml)
@@ -0,0 +1,213 @@
1
+ # Форма сервіс-орієнтованого deploy-workflow (`.github/workflows/deploy-*.yml`).
2
+ #
3
+ # Канон (ga.mdc, сніпет template/deploy-service.yml.snippet.yml):
4
+ # plan (checkout fetch-depth:0 + prep + `bunx n-rules ci plan --path <svc> --github`)
5
+ # ├─ lint-<domain> × N (needs: plan; if: needs.plan.outputs.<domain> == 'true';
6
+ # │ `bunx n-rules lint <domain> --path <svc> --no-fix`)
7
+ # ├─ test (needs: plan)
8
+ # └─ deploy (needs-ланцюг досягає plan і ВСІ lint-джоби;
9
+ # if: !cancelled() + !contains(needs.*.result, 'failure'))
10
+ #
11
+ # Перелік сервісів консюмер-специфічний — концерн перевіряє ФОРМУ кожного
12
+ # знайденого deploy-*.yml, не реєстр. Ланцюги deploy (build → deploy) законні:
13
+ # досяжність рахується транзитивно через graph.reachable.
14
+ #
15
+ # Не перевіряється тут (лишається .mdc-каноном): вибір набору доменів під
16
+ # сервіс, зміст test/deploy-кроків, outputs-мапінг plan-джоби, uv-prep для
17
+ # python-стеку. Спільні workflow-перевірки (persist-credentials, min-версії
18
+ # uses) — у ga.workflow_common; існування paths-глобів — у ga/workflows.
19
+ package ga.service_deploy_workflow
20
+
21
+ import rego.v1
22
+
23
+ # ── extraction ──────────────────────────────────────────────────────────
24
+
25
+ jobs := object.get(input, "jobs", {})
26
+
27
+ # YAML-парсери розходяться щодо `on:`: YAML 1.2 → ключ "on", YAML 1.1 → bool true.
28
+ on_block := object.union(object.get(input, "on", {}), object.get(input, true, {}))
29
+
30
+ push_paths contains p if some p in object.get(object.get(on_block, "push", {}), "paths", [])
31
+
32
+ plan_runs contains r if {
33
+ some step in object.get(object.get(jobs, "plan", {}), "steps", [])
34
+ r := object.get(step, "run", "")
35
+ contains(r, "n-rules ci plan")
36
+ }
37
+
38
+ # Сервісний каталог із команди plan-джоби (`--path <svc>`).
39
+ service_paths contains p if {
40
+ some r in plan_runs
41
+ m := regex.find_all_string_submatch_n(`n-rules ci plan\s+--path\s+(\S+)`, r, 1)
42
+ count(m) > 0
43
+ p := m[0][1]
44
+ }
45
+
46
+ # Lint-джоби (крок `n-rules lint <domain> … --path …`) з розібраною командою.
47
+ lint_cmds contains cmd if {
48
+ some name, job in jobs
49
+ name != "plan"
50
+ some step in object.get(job, "steps", [])
51
+ r := object.get(step, "run", "")
52
+ m := regex.find_all_string_submatch_n(`n-rules lint\s+([a-z0-9-]+)\s+[^\n]*--path\s+(\S+)`, r, 1)
53
+ count(m) > 0
54
+ cmd := {"job": name, "domain": m[0][1], "path": m[0][2], "run": r}
55
+ }
56
+
57
+ check_jobs contains cmd.job if some cmd in lint_cmds
58
+
59
+ # Граф залежностей і транзитивна досяжність (ланцюг deploy → build → lint).
60
+ needs_graph[name] := needs_of(job) if some name, job in jobs
61
+
62
+ all_needed contains n if {
63
+ some job in jobs
64
+ some n in needs_of(job)
65
+ }
66
+
67
+ # Термінальні джоби: ніхто від них не залежить (крім plan/lint-джоб).
68
+ terminal_jobs contains name if {
69
+ some name in object.keys(jobs)
70
+ name != "plan"
71
+ not name in check_jobs
72
+ not name in all_needed
73
+ }
74
+
75
+ # ── deny: plan-джоба ────────────────────────────────────────────────────
76
+
77
+ deny contains msg if {
78
+ count(jobs) > 0
79
+ not jobs.plan
80
+ msg := "deploy-workflow: немає job `plan` з кроком `bunx n-rules ci plan --path <svc> --github` (ga.mdc: сервіс-канон)"
81
+ }
82
+
83
+ deny contains msg if {
84
+ jobs.plan
85
+ count(plan_runs) == 0
86
+ msg := "deploy-workflow: job `plan` без кроку `bunx n-rules ci plan …` (ga.mdc: сервіс-канон)"
87
+ }
88
+
89
+ deny contains msg if {
90
+ msg := "deploy-workflow: `ci plan` без `--path <serviceDir>` (ga.mdc: сервіс-канон)"
91
+ some r in plan_runs
92
+ not regex.match(`n-rules ci plan\s+--path\s+\S+`, r)
93
+ }
94
+
95
+ deny contains msg if {
96
+ msg := "deploy-workflow: `ci plan` без `--github` — outputs не потраплять у $GITHUB_OUTPUT (ga.mdc)"
97
+ some r in plan_runs
98
+ not contains(r, "--github")
99
+ }
100
+
101
+ # ── deny: тригер paths ↔ сервісний каталог ─────────────────────────────
102
+
103
+ deny contains msg if {
104
+ some sp in service_paths
105
+ not trigger_covers(sp)
106
+ msg := sprintf("deploy-workflow: on.push.paths не містить glob для сервісного каталогу %s (ga.mdc)", [sp])
107
+ }
108
+
109
+ # ── deny: lint-джоби ────────────────────────────────────────────────────
110
+
111
+ deny contains msg if {
112
+ some cmd in lint_cmds
113
+ not "plan" in needs_of(jobs[cmd.job])
114
+ msg := sprintf("%s: lint-джоба мусить мати needs: plan (ga.mdc: сервіс-канон)", [cmd.job])
115
+ }
116
+
117
+ deny contains msg if {
118
+ some cmd in lint_cmds
119
+ cond := object.get(jobs[cmd.job], "if", "")
120
+ not contains(cond, sprintf("needs.plan.outputs.%s", [replace(cmd.domain, "-", "_")]))
121
+ msg := sprintf("%s: lint-джоба без гейта `if: needs.plan.outputs.%s == 'true'` — крок не скіпатиметься (ga.mdc)", [cmd.job, replace(cmd.domain, "-", "_")])
122
+ }
123
+
124
+ deny contains msg if {
125
+ count(service_paths) > 0
126
+ some cmd in lint_cmds
127
+ not cmd.path in service_paths
128
+ msg := sprintf("%s: `lint %s --path %s` ≠ `--path` plan-джоби — лінтиться інший каталог (ga.mdc)", [cmd.job, cmd.domain, cmd.path])
129
+ }
130
+
131
+ deny contains msg if {
132
+ some cmd in lint_cmds
133
+ not contains(cmd.run, "--no-fix")
134
+ msg := sprintf("%s: `n-rules lint` у CI мусить мати `--no-fix` (ga.mdc)", [cmd.job])
135
+ }
136
+
137
+ deny contains msg if {
138
+ some cmd in lint_cmds
139
+ not job_has_prep(jobs[cmd.job])
140
+ msg := sprintf("%s: lint-джоба без prep-кроку `uses: ./.github/actions/setup-bun-deps` (джоби не шарять ФС) (ga.mdc)", [cmd.job])
141
+ }
142
+
143
+ # ── deny: fetch-depth для git-дельти ────────────────────────────────────
144
+
145
+ deny contains msg if {
146
+ count(plan_runs) > 0
147
+ not job_has_full_checkout(jobs.plan)
148
+ msg := "plan: checkout мусить мати fetch-depth: 0 — без історії git-дельта `ci plan` не рахується (ga.mdc)"
149
+ }
150
+
151
+ deny contains msg if {
152
+ some cmd in lint_cmds
153
+ not job_has_full_checkout(jobs[cmd.job])
154
+ msg := sprintf("%s: checkout мусить мати fetch-depth: 0 — без історії `lint --path` не бачить дельти (ga.mdc)", [cmd.job])
155
+ }
156
+
157
+ # ── deny: термінальні (deploy) джоби ────────────────────────────────────
158
+
159
+ deny contains msg if {
160
+ count(check_jobs) > 0
161
+ some name in terminal_jobs
162
+ not "plan" in graph.reachable(needs_graph, {name})
163
+ msg := sprintf("%s: термінальна джоба не досягає `plan` через needs-ланцюг (ga.mdc: сервіс-канон)", [name])
164
+ }
165
+
166
+ deny contains msg if {
167
+ some name in terminal_jobs
168
+ some cj in check_jobs
169
+ not cj in graph.reachable(needs_graph, {name})
170
+ msg := sprintf("%s: термінальна джоба не досягає перевірки `%s` через needs-ланцюг — деплой не гейтиться нею (ga.mdc)", [name, cj])
171
+ }
172
+
173
+ # Skipped-лінт (умовний гейт спрацював) не мусить блокувати деплой, а fail —
174
+ # мусить: канонічний if = `!cancelled() && !contains(needs.*.result, 'failure')`.
175
+ deny contains msg if {
176
+ some name in terminal_jobs
177
+ needed := needs_of(jobs[name])
178
+ count([cj | some cj in check_jobs; cj in needed]) > 0
179
+ cond := object.get(jobs[name], "if", "")
180
+ not skip_tolerant(cond)
181
+ msg := sprintf("%s: джоба з needs на умовні lint-джоби мусить мати if із `!cancelled()` і `!contains(needs.*.result, 'failure')` — інакше skipped-лінт назавжди блокує деплой (ga.mdc)", [name])
182
+ }
183
+
184
+ # ── helpers ─────────────────────────────────────────────────────────────
185
+
186
+ # needs: string | array → array.
187
+ needs_of(job) := [n] if {
188
+ n := object.get(job, "needs", [])
189
+ is_string(n)
190
+ }
191
+
192
+ needs_of(job) := object.get(job, "needs", []) if is_array(object.get(job, "needs", []))
193
+
194
+ trigger_covers(sp) if {
195
+ some p in push_paths
196
+ startswith(p, sp)
197
+ }
198
+
199
+ job_has_prep(job) if {
200
+ some step in object.get(job, "steps", [])
201
+ step.uses == "./.github/actions/setup-bun-deps"
202
+ }
203
+
204
+ job_has_full_checkout(job) if {
205
+ some step in object.get(job, "steps", [])
206
+ startswith(object.get(step, "uses", ""), "actions/checkout@")
207
+ step.with["fetch-depth"] == 0
208
+ }
209
+
210
+ skip_tolerant(cond) if {
211
+ contains(cond, "!cancelled()")
212
+ contains(cond, "needs.*.result")
213
+ }
@@ -0,0 +1,88 @@
1
+ # Документаційний приклад сервіс-орієнтованого deploy-workflow (ga.mdc).
2
+ # Це НЕ deep-subset-канон (перелік сервісів консюмер-специфічний, назва файлу
3
+ # deploy-<service>.yml) — форму перевіряє service_deploy_workflow.rego.
4
+ # Плейсхолдер run/<service> заміни на каталог сервісу; набір lint-<domain>
5
+ # джоб добери під стеки сервісу (зайва джоба нешкідлива — ci plan її скіпне).
6
+ name: Deploy <service>
7
+
8
+ on:
9
+ push:
10
+ branches:
11
+ - dev
12
+ - main
13
+ paths:
14
+ - 'run/<service>/**'
15
+
16
+ concurrency:
17
+ group: ${{ github.ref }}-${{ github.workflow }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ plan:
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ contents: read
25
+ outputs:
26
+ js: ${{ steps.plan.outputs.js }}
27
+ python: ${{ steps.plan.outputs.python }}
28
+ any: ${{ steps.plan.outputs.any }}
29
+ steps:
30
+ - uses: actions/checkout@v6
31
+ with:
32
+ persist-credentials: false
33
+ fetch-depth: 0
34
+
35
+ - uses: ./.github/actions/setup-bun-deps
36
+
37
+ - id: plan
38
+ run: bunx n-rules ci plan --path run/<service> --github
39
+
40
+ lint-js:
41
+ needs: plan
42
+ if: needs.plan.outputs.js == 'true'
43
+ runs-on: ubuntu-latest
44
+ permissions:
45
+ contents: read
46
+ steps:
47
+ - uses: actions/checkout@v6
48
+ with:
49
+ persist-credentials: false
50
+ fetch-depth: 0
51
+
52
+ - uses: ./.github/actions/setup-bun-deps
53
+
54
+ - run: bunx n-rules lint js --path run/<service> --no-fix
55
+
56
+ # …lint-<domain> для кожного домену сервісу (python/docker/k8s/security/…)
57
+
58
+ test:
59
+ needs: plan
60
+ if: needs.plan.outputs.any == 'true'
61
+ runs-on: ubuntu-latest
62
+ permissions:
63
+ contents: read
64
+ steps:
65
+ - uses: actions/checkout@v6
66
+ with:
67
+ persist-credentials: false
68
+ fetch-depth: 0
69
+
70
+ - uses: ./.github/actions/setup-bun-deps
71
+
72
+ # python-стек: перед тестами ще uv (uses: astral-sh/setup-uv@v7 + uv sync --locked)
73
+ - run: bun test run/<service>
74
+
75
+ deploy:
76
+ needs:
77
+ - plan
78
+ - lint-js
79
+ - test
80
+ # Skipped-лінт (гейт ci plan) НЕ блокує деплой; failed — блокує.
81
+ if: ${{ !cancelled() && needs.plan.result == 'success' && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
82
+ runs-on: ubuntu-latest
83
+ steps:
84
+ - uses: actions/checkout@v6
85
+ with:
86
+ persist-credentials: false
87
+
88
+ # …кроки деплою сервісу (build/push/apply)