@7n/rules-ci-azure 1.2.1 → 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 +6 -0
- package/package.json +1 -1
- package/rules/azure-pipelines/main.mdc +14 -4
- package/rules/azure-pipelines/service_deploy_pipeline/concern.json +9 -0
- package/rules/azure-pipelines/service_deploy_pipeline/service_deploy_pipeline.mdc +19 -0
- package/rules/azure-pipelines/service_deploy_pipeline/service_deploy_pipeline.rego +281 -0
- package/rules/azure-pipelines/service_deploy_pipeline/template/deploy-service-pipeline.yml.snippet.yml +77 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.0] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Сервіс-орієнтований CI-канон: концерн azure-pipelines/service_deploy_pipeline — форма per-service pipeline у .azurepipelines/ (plan-гейт `ci plan --azure`, per-domain lint-джоби з dependsOn+condition, термінальні джоби зі Skipped-толерантним condition)
|
|
8
|
+
|
|
3
9
|
## [1.2.1] - 2026-07-16
|
|
4
10
|
|
|
5
11
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules-ci-azure",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "\u041f\u043b\u0430\u0433\u0456\u043d @7n/rules: \u043a\u0430\u043d\u043e\u043d Azure Pipelines (azure-pipelines.yml) \u2014 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430, lint-\u0441\u0442\u0435\u043f, VS Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"azure-pipelines",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Канон Azure Pipelines — структура azure-pipelines.yml, обов'язковий lint-степ n-rules, розширення VS Code
|
|
3
|
-
version: '1.
|
|
4
|
-
globs: "azure-pipelines.yml"
|
|
2
|
+
description: Канон Azure Pipelines — структура azure-pipelines.yml, обов'язковий lint-степ n-rules, сервіс-орієнтовані deploy-pipelines, розширення VS Code
|
|
3
|
+
version: '1.1'
|
|
4
|
+
globs: "azure-pipelines.yml,.azurepipelines/**/*.yml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Правило **azure-pipelines** (плагін `@7n/rules-ci-azure`) перевіряє канонічну структуру `azure-pipelines.yml` у корені
|
|
8
|
+
Правило **azure-pipelines** (плагін `@7n/rules-ci-azure`) перевіряє канонічну структуру `azure-pipelines.yml` у корені репозиторію, форму сервіс-орієнтованих deploy-pipelines у `.azurepipelines/` та налаштування VS Code для роботи з Azure Pipelines. Аналог правила `ga` (плагін `@7n/rules-ci-github`) для репозиторіїв на Azure DevOps.
|
|
9
|
+
|
|
10
|
+
Ролі pipeline-ів: **кореневий `azure-pipelines.yml`** — repo-wide лінт (safety-net `--full`), деплой **не гейтить**; **сервісні pipelines** (`.azurepipelines/**/*.yml` із `trigger.paths.include`) — гейтять деплой свого сервісу через plan → lint/test → deploy (концерн `service_deploy_pipeline`).
|
|
9
11
|
|
|
10
12
|
- `azure-pipelines.yml`:
|
|
11
13
|
|
|
@@ -67,6 +69,14 @@ Substring-перевірки замість exact-match — Azure YAML допу
|
|
|
67
69
|
|
|
68
70
|
Канон: [azure-pipelines-lint.yml.snippet.yml](./lint_pipeline/template/azure-pipelines-lint.yml.snippet.yml)
|
|
69
71
|
|
|
72
|
+
## Сервіс-орієнтовані deploy-pipelines (`.azurepipelines/**/*.yml`, опційно)
|
|
73
|
+
|
|
74
|
+
Rego-пакет: `azure_pipelines.service_deploy_pipeline`
|
|
75
|
+
|
|
76
|
+
**Цільові файли:** `.azurepipelines/**/*.yml` крім `templates/**`; сервісним вважається файл із `trigger.paths.include`.
|
|
77
|
+
|
|
78
|
+
Форма: джоба `plan` (`bunx n-rules ci plan --path <svc> --azure`, крок з `name: plan`) → паралельні `lint_<domain>`-джоби (`dependsOn: plan`, `condition` по `dependencies.plan.outputs['plan.<domain>']`, `lint <domain> --path <svc> --no-fix`) і `run_tests` → термінальні джоби з dependsOn-ланцюгом на всі перевірки та Skipped-толерантним `condition`. Деталі й template-розкладка: [service_deploy_pipeline.mdc](./service_deploy_pipeline/service_deploy_pipeline.mdc); приклад — [deploy-service-pipeline.yml.snippet.yml](./service_deploy_pipeline/template/deploy-service-pipeline.yml.snippet.yml).
|
|
79
|
+
|
|
70
80
|
## `.vscode/extensions.json` — розширення для Azure Pipelines
|
|
71
81
|
|
|
72
82
|
Rego-пакет: `azure_pipelines.vscode_extensions`
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## Rego-gate сервіс-орієнтованих deploy-pipeline (`.azurepipelines/**/*.yml`)
|
|
2
|
+
|
|
3
|
+
Rego-пакет: `azure_pipelines.service_deploy_pipeline`
|
|
4
|
+
|
|
5
|
+
Цільові файли: усі `.azurepipelines/**/*.yml`, крім `templates/**` (walkGlob). Дискримінатор — **`trigger.paths.include`**: файл із paths-фільтром вважається сервісним pipeline і мусить відповідати формі; без нього — концерн мовчить. Кореневий `azure-pipelines.yml` (repo-wide лінт, канон `pipeline_common`) поза глобом — він деплой не гейтить.
|
|
6
|
+
|
|
7
|
+
Канонічна форма per-service pipeline (каталог сервісу — напр. `run/nexus`):
|
|
8
|
+
|
|
9
|
+
1. **Тригер** — `trigger.paths.include` з каталогом сервісу.
|
|
10
|
+
2. **`plan`-джоба** — `checkout: self` із `fetchDepth: 0` → prep (install bun + `bun install --frozen-lockfile`) → крок `bunx n-rules ci plan --path run/<service> --azure` **з `name: plan`** (downstream читає `dependencies.plan.outputs['plan.<domain>']`).
|
|
11
|
+
3. **`lint_<domain>`-джоби** (паралельно) — `dependsOn: plan`, `condition: eq(dependencies.plan.outputs['plan.<domain>'], 'true')` (ключ = домен із `-`→`_`), крок `bunx n-rules lint <domain> --path run/<service> --no-fix` з `--path` ∈ `trigger.paths.include`, prep у кожній джобі (джоби не шарять ФС).
|
|
12
|
+
4. **`run_tests`** — `dependsOn: plan`, `condition` по `plan.any`; тести лише сервісу (python-стек — uv: `curl … astral.sh/uv/install.sh | sh` + `uv sync --locked`).
|
|
13
|
+
5. **Термінальні джоби** (`build_and_push`/`deploy_*`) — dependsOn-ланцюг **транзитивно досягає** `plan` і всі перевірки; джоба з `dependsOn` на умовні lint-джоби має `condition` з `not(canceled())` та `in(dependencies.<j>.result, 'Succeeded', 'Skipped')` — skipped-лінт не блокує деплой, failed — блокує.
|
|
14
|
+
|
|
15
|
+
**Template-розкладка** (efes-стиль `- template:` з параметром-каталогом): законна, rego тоді перевіряє лише видиме у файлі — що параметр template-посилання дорівнює одному з `paths.include`; повнота графа всередині шаблону — відповідальність автора за цим каноном (rego не бачить розгорнутий YAML).
|
|
16
|
+
|
|
17
|
+
Що НЕ перевіряється: добір доменів під вміст сервісу, зміст test/deploy-кроків, uv-prep, шаблони в `templates/**`.
|
|
18
|
+
|
|
19
|
+
Канон-snippet (документаційний приклад): [deploy-service-pipeline.yml.snippet.yml](./template/deploy-service-pipeline.yml.snippet.yml)
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Форма сервіс-орієнтованого deploy-pipeline (`.azurepipelines/**/*.yml`,
|
|
2
|
+
# крім `templates/**`). Дзеркало ga.service_deploy_workflow для Azure Pipelines.
|
|
3
|
+
#
|
|
4
|
+
# Дискримінатор застосовності — НЕ імʼя файлу, а `trigger.paths.include`:
|
|
5
|
+
# файл із paths-фільтром = сервісний pipeline і мусить відповідати формі;
|
|
6
|
+
# файл без нього (repo-wide) — жодних deny. Кореневий azure-pipelines.yml
|
|
7
|
+
# поза walkGlob узагалі (канон pipeline_common).
|
|
8
|
+
#
|
|
9
|
+
# Канон (azure-pipelines.mdc, сніпет template/deploy-service-pipeline.yml.snippet.yml):
|
|
10
|
+
# plan (checkout fetchDepth:0 + bun install + `bunx n-rules ci plan --path <svc> --azure`,
|
|
11
|
+
# крок з `name: plan`)
|
|
12
|
+
# ├─ lint_<domain> × N (dependsOn: plan; condition по dependencies.plan.outputs['plan.<domain>'];
|
|
13
|
+
# │ `bunx n-rules lint <domain> --path <svc> --no-fix`)
|
|
14
|
+
# ├─ run_tests (dependsOn: plan)
|
|
15
|
+
# └─ build_and_push/deploy (dependsOn-ланцюг досягає plan і всі перевірки;
|
|
16
|
+
# condition: not(canceled()) + 'Skipped'-толерантність)
|
|
17
|
+
#
|
|
18
|
+
# Template-розкладка (`- template:` з параметром-каталогом) законна: rego тоді
|
|
19
|
+
# перевіряє лише видиме у файлі — наявність template-посилання з параметром,
|
|
20
|
+
# що дорівнює одному з paths.include; повна перевірка графа — .mdc-канон.
|
|
21
|
+
package azure_pipelines.service_deploy_pipeline
|
|
22
|
+
|
|
23
|
+
import rego.v1
|
|
24
|
+
|
|
25
|
+
# ── extraction ──────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
service_paths contains p if {
|
|
28
|
+
some p in input.trigger.paths.include
|
|
29
|
+
is_string(p)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
is_service_pipeline if count(service_paths) > 0
|
|
33
|
+
|
|
34
|
+
# Джоби на будь-якій глибині (root jobs / stages→jobs).
|
|
35
|
+
all_jobs contains j if {
|
|
36
|
+
walk(input, [_, j])
|
|
37
|
+
is_object(j)
|
|
38
|
+
is_string(object.get(j, "job", 0))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# `- template:`-посилання з параметрами (efes-стиль modulePath).
|
|
42
|
+
template_refs contains t if {
|
|
43
|
+
walk(input, [_, t])
|
|
44
|
+
is_object(t)
|
|
45
|
+
is_string(object.get(t, "template", 0))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
template_covers_service if {
|
|
49
|
+
some t in template_refs
|
|
50
|
+
some v in object.get(t, "parameters", {})
|
|
51
|
+
v in service_paths
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
plan_jobs contains j if {
|
|
55
|
+
some j in all_jobs
|
|
56
|
+
j.job == "plan"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
plan_runs contains r if {
|
|
60
|
+
some j in plan_jobs
|
|
61
|
+
some r in job_cmds(j)
|
|
62
|
+
contains(r, "n-rules ci plan")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Lint-джоби (`n-rules lint <domain> … --path …`) з розібраною командою.
|
|
66
|
+
lint_cmds contains cmd if {
|
|
67
|
+
some j in all_jobs
|
|
68
|
+
j.job != "plan"
|
|
69
|
+
some r in job_cmds(j)
|
|
70
|
+
m := regex.find_all_string_submatch_n(`n-rules lint\s+([a-z0-9-]+)\s+[^\n]*--path\s+(\S+)`, r, 1)
|
|
71
|
+
count(m) > 0
|
|
72
|
+
cmd := {"job": j.job, "spec": j, "domain": m[0][1], "path": m[0][2], "run": r}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
check_jobs contains cmd.job if some cmd in lint_cmds
|
|
76
|
+
|
|
77
|
+
# Граф dependsOn і транзитивна досяжність (ланцюг deploy → build → перевірки).
|
|
78
|
+
needs_graph[name] := depends_of(j) if {
|
|
79
|
+
some j in all_jobs
|
|
80
|
+
name := j.job
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
all_needed contains n if {
|
|
84
|
+
some j in all_jobs
|
|
85
|
+
some n in depends_of(j)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
terminal_jobs contains j.job if {
|
|
89
|
+
some j in all_jobs
|
|
90
|
+
j.job != "plan"
|
|
91
|
+
not j.job in check_jobs
|
|
92
|
+
not j.job in all_needed
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
job_by_name[name] := j if {
|
|
96
|
+
some j in all_jobs
|
|
97
|
+
name := j.job
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ── deny: plan-джоба ────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
deny contains msg if {
|
|
103
|
+
is_service_pipeline
|
|
104
|
+
count(all_jobs) > 0
|
|
105
|
+
count(plan_jobs) == 0
|
|
106
|
+
not template_covers_service
|
|
107
|
+
msg := "service-pipeline: немає job `plan` з кроком `bunx n-rules ci plan --path <svc> --azure` (azure-pipelines.mdc: сервіс-канон)"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
deny contains msg if {
|
|
111
|
+
is_service_pipeline
|
|
112
|
+
count(all_jobs) == 0
|
|
113
|
+
count(template_refs) > 0
|
|
114
|
+
not template_covers_service
|
|
115
|
+
msg := "service-pipeline: `- template:` без параметра-каталогу з trigger.paths.include — гейт не привʼязаний до сервісу (azure-pipelines.mdc)"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
deny contains msg if {
|
|
119
|
+
msg := "service-pipeline: job `plan` без кроку `bunx n-rules ci plan …` (azure-pipelines.mdc: сервіс-канон)"
|
|
120
|
+
is_service_pipeline
|
|
121
|
+
some j in plan_jobs
|
|
122
|
+
count([r | some r in job_cmds(j); contains(r, "n-rules ci plan")]) == 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
deny contains msg if {
|
|
126
|
+
msg := "service-pipeline: `ci plan` без `--azure` — outputs не стануть змінними джоби (azure-pipelines.mdc)"
|
|
127
|
+
is_service_pipeline
|
|
128
|
+
some r in plan_runs
|
|
129
|
+
not contains(r, "--azure")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
deny contains msg if {
|
|
133
|
+
is_service_pipeline
|
|
134
|
+
some r in plan_runs
|
|
135
|
+
m := regex.find_all_string_submatch_n(`n-rules ci plan\s+--path\s+(\S+)`, r, 1)
|
|
136
|
+
count(m) > 0
|
|
137
|
+
not m[0][1] in service_paths
|
|
138
|
+
msg := sprintf("service-pipeline: `ci plan --path %s` ∉ trigger.paths.include — план рахується не для того каталогу (azure-pipelines.mdc)", [m[0][1]])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Outputs адресуються як dependencies.plan.outputs['plan.<key>'] — крок мусить мати name: plan.
|
|
142
|
+
deny contains msg if {
|
|
143
|
+
msg := "service-pipeline: крок `ci plan` без `name: plan` — downstream-condition не знайде outputs (azure-pipelines.mdc)"
|
|
144
|
+
is_service_pipeline
|
|
145
|
+
some j in plan_jobs
|
|
146
|
+
count([s | some s in object.get(j, "steps", []); contains(step_cmd(s), "n-rules ci plan"); s.name == "plan"]) == 0
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# ── deny: lint-джоби ────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
deny contains msg if {
|
|
152
|
+
is_service_pipeline
|
|
153
|
+
some cmd in lint_cmds
|
|
154
|
+
not "plan" in depends_of(cmd.spec)
|
|
155
|
+
msg := sprintf("%s: lint-джоба мусить мати dependsOn: plan (azure-pipelines.mdc: сервіс-канон)", [cmd.job])
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
deny contains msg if {
|
|
159
|
+
is_service_pipeline
|
|
160
|
+
some cmd in lint_cmds
|
|
161
|
+
cond := object.get(cmd.spec, "condition", "")
|
|
162
|
+
not contains(cond, sprintf("dependencies.plan.outputs['plan.%s']", [replace(cmd.domain, "-", "_")]))
|
|
163
|
+
msg := sprintf("%s: lint-джоба без condition по dependencies.plan.outputs['plan.%s'] — крок не скіпатиметься (azure-pipelines.mdc)", [cmd.job, replace(cmd.domain, "-", "_")])
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
deny contains msg if {
|
|
167
|
+
is_service_pipeline
|
|
168
|
+
some cmd in lint_cmds
|
|
169
|
+
not cmd.path in service_paths
|
|
170
|
+
msg := sprintf("%s: `lint %s --path %s` ∉ trigger.paths.include — лінтиться інший каталог (azure-pipelines.mdc)", [cmd.job, cmd.domain, cmd.path])
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
deny contains msg if {
|
|
174
|
+
is_service_pipeline
|
|
175
|
+
some cmd in lint_cmds
|
|
176
|
+
not contains(cmd.run, "--no-fix")
|
|
177
|
+
msg := sprintf("%s: `n-rules lint` у CI мусить мати `--no-fix` (azure-pipelines.mdc)", [cmd.job])
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
deny contains msg if {
|
|
181
|
+
is_service_pipeline
|
|
182
|
+
some cmd in lint_cmds
|
|
183
|
+
not job_has_prep(cmd.spec)
|
|
184
|
+
msg := sprintf("%s: lint-джоба без prep-кроку `bun install --frozen-lockfile` (джоби не шарять ФС) (azure-pipelines.mdc)", [cmd.job])
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# ── deny: fetchDepth для git-дельти ─────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
deny contains msg if {
|
|
190
|
+
msg := "plan: checkout мусить мати fetchDepth: 0 — без історії git-дельта `ci plan` не рахується (azure-pipelines.mdc)"
|
|
191
|
+
is_service_pipeline
|
|
192
|
+
some j in plan_jobs
|
|
193
|
+
count([r | some r in job_cmds(j); contains(r, "n-rules ci plan")]) > 0
|
|
194
|
+
not job_has_full_checkout(j)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
deny contains msg if {
|
|
198
|
+
is_service_pipeline
|
|
199
|
+
some cmd in lint_cmds
|
|
200
|
+
not job_has_full_checkout(cmd.spec)
|
|
201
|
+
msg := sprintf("%s: checkout мусить мати fetchDepth: 0 — без історії `lint --path` не бачить дельти (azure-pipelines.mdc)", [cmd.job])
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# ── deny: термінальні (deploy) джоби ────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
deny contains msg if {
|
|
207
|
+
is_service_pipeline
|
|
208
|
+
count(check_jobs) > 0
|
|
209
|
+
some name in terminal_jobs
|
|
210
|
+
not "plan" in graph.reachable(needs_graph, {name})
|
|
211
|
+
msg := sprintf("%s: термінальна джоба не досягає `plan` через dependsOn-ланцюг (azure-pipelines.mdc: сервіс-канон)", [name])
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
deny contains msg if {
|
|
215
|
+
is_service_pipeline
|
|
216
|
+
some name in terminal_jobs
|
|
217
|
+
some cj in check_jobs
|
|
218
|
+
not cj in graph.reachable(needs_graph, {name})
|
|
219
|
+
msg := sprintf("%s: термінальна джоба не досягає перевірки `%s` через dependsOn-ланцюг — деплой не гейтиться нею (azure-pipelines.mdc)", [name, cj])
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# Skipped-лінт (умовний гейт спрацював) не мусить блокувати деплой, fail — мусить:
|
|
223
|
+
# канон `and(not(canceled()), in(dependencies.<j>.result, 'Succeeded', 'Skipped'), …)`.
|
|
224
|
+
deny contains msg if {
|
|
225
|
+
is_service_pipeline
|
|
226
|
+
some name in terminal_jobs
|
|
227
|
+
needed := depends_of(job_by_name[name])
|
|
228
|
+
count([cj | some cj in check_jobs; cj in needed]) > 0
|
|
229
|
+
cond := object.get(job_by_name[name], "condition", "")
|
|
230
|
+
not skip_tolerant(cond)
|
|
231
|
+
msg := sprintf("%s: джоба з dependsOn на умовні lint-джоби мусить мати condition із not(canceled()) і 'Skipped'-толерантністю — інакше skipped-лінт назавжди блокує деплой (azure-pipelines.mdc)", [name])
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# ── helpers ─────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
# dependsOn: string | array → array.
|
|
237
|
+
depends_of(job) := [n] if {
|
|
238
|
+
n := object.get(job, "dependsOn", [])
|
|
239
|
+
is_string(n)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
depends_of(job) := object.get(job, "dependsOn", []) if is_array(object.get(job, "dependsOn", []))
|
|
243
|
+
|
|
244
|
+
# Команда кроку: `script:` або `bash:`.
|
|
245
|
+
step_cmd(step) := s if {
|
|
246
|
+
s := object.get(step, "script", "")
|
|
247
|
+
s != ""
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
step_cmd(step) := s if {
|
|
251
|
+
object.get(step, "script", "") == ""
|
|
252
|
+
s := object.get(step, "bash", "")
|
|
253
|
+
s != ""
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
step_cmd(step) := "" if {
|
|
257
|
+
object.get(step, "script", "") == ""
|
|
258
|
+
object.get(step, "bash", "") == ""
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
job_cmds(job) := [c |
|
|
262
|
+
some step in object.get(job, "steps", [])
|
|
263
|
+
c := step_cmd(step)
|
|
264
|
+
c != ""
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
job_has_prep(job) if {
|
|
268
|
+
some c in job_cmds(job)
|
|
269
|
+
contains(c, "bun install --frozen-lockfile")
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
job_has_full_checkout(job) if {
|
|
273
|
+
some step in object.get(job, "steps", [])
|
|
274
|
+
object.get(step, "checkout", "") != ""
|
|
275
|
+
step.fetchDepth == 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
skip_tolerant(cond) if {
|
|
279
|
+
contains(cond, "not(canceled())")
|
|
280
|
+
contains(cond, "'Skipped'")
|
|
281
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Документаційний приклад сервіс-орієнтованого deploy-pipeline (azure-pipelines.mdc).
|
|
2
|
+
# Це НЕ deep-subset-канон (перелік сервісів консюмер-специфічний) — форму
|
|
3
|
+
# перевіряє service_deploy_pipeline.rego по всіх `.azurepipelines/**/*.yml`
|
|
4
|
+
# з trigger.paths.include. Плейсхолдер run/<service> заміни на каталог сервісу.
|
|
5
|
+
trigger:
|
|
6
|
+
branches:
|
|
7
|
+
include:
|
|
8
|
+
- dev
|
|
9
|
+
- main
|
|
10
|
+
paths:
|
|
11
|
+
include:
|
|
12
|
+
- run/<service>
|
|
13
|
+
|
|
14
|
+
pool:
|
|
15
|
+
vmImage: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
- job: plan
|
|
19
|
+
steps:
|
|
20
|
+
- checkout: self
|
|
21
|
+
fetchDepth: 0
|
|
22
|
+
- script: |
|
|
23
|
+
curl -fsSL https://bun.sh/install | bash
|
|
24
|
+
echo "##vso[task.prependpath]$HOME/.bun/bin"
|
|
25
|
+
displayName: Install bun
|
|
26
|
+
- script: bun install --frozen-lockfile
|
|
27
|
+
displayName: Install deps
|
|
28
|
+
- script: bunx n-rules ci plan --path run/<service> --azure
|
|
29
|
+
name: plan # ОБОВʼЯЗКОВО: outputs адресуються як dependencies.plan.outputs['plan.<domain>']
|
|
30
|
+
|
|
31
|
+
- job: lint_js
|
|
32
|
+
dependsOn: plan
|
|
33
|
+
condition: eq(dependencies.plan.outputs['plan.js'], 'true')
|
|
34
|
+
steps:
|
|
35
|
+
- checkout: self
|
|
36
|
+
fetchDepth: 0
|
|
37
|
+
- script: |
|
|
38
|
+
curl -fsSL https://bun.sh/install | bash
|
|
39
|
+
echo "##vso[task.prependpath]$HOME/.bun/bin"
|
|
40
|
+
displayName: Install bun
|
|
41
|
+
- script: bun install --frozen-lockfile
|
|
42
|
+
displayName: Install deps
|
|
43
|
+
- script: bunx n-rules lint js --path run/<service> --no-fix
|
|
44
|
+
displayName: Lint js
|
|
45
|
+
|
|
46
|
+
# …lint_<domain> для кожного домену сервісу (python/docker/k8s/security/…)
|
|
47
|
+
# python-стек: перед тестами ще uv (curl -LsSf https://astral.sh/uv/install.sh | sh + uv sync --locked)
|
|
48
|
+
|
|
49
|
+
- job: run_tests
|
|
50
|
+
dependsOn: plan
|
|
51
|
+
condition: eq(dependencies.plan.outputs['plan.any'], 'true')
|
|
52
|
+
steps:
|
|
53
|
+
- checkout: self
|
|
54
|
+
fetchDepth: 0
|
|
55
|
+
- script: |
|
|
56
|
+
curl -fsSL https://bun.sh/install | bash
|
|
57
|
+
echo "##vso[task.prependpath]$HOME/.bun/bin"
|
|
58
|
+
displayName: Install bun
|
|
59
|
+
- script: bun install --frozen-lockfile
|
|
60
|
+
displayName: Install deps
|
|
61
|
+
- script: bun test run/<service>
|
|
62
|
+
displayName: Tests
|
|
63
|
+
|
|
64
|
+
- job: build_and_push
|
|
65
|
+
dependsOn:
|
|
66
|
+
- plan
|
|
67
|
+
- lint_js
|
|
68
|
+
- run_tests
|
|
69
|
+
# Skipped-лінт (гейт ci plan) НЕ блокує деплой; failed — блокує.
|
|
70
|
+
condition: >-
|
|
71
|
+
and(not(canceled()),
|
|
72
|
+
eq(dependencies.plan.result, 'Succeeded'),
|
|
73
|
+
in(dependencies.lint_js.result, 'Succeeded', 'Skipped'),
|
|
74
|
+
in(dependencies.run_tests.result, 'Succeeded', 'Skipped'))
|
|
75
|
+
steps:
|
|
76
|
+
- checkout: self
|
|
77
|
+
# …кроки build/push/deploy сервісу
|