@7n/rules-ci-azure 1.4.0 → 1.4.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 +6 -0
- package/package.json +1 -1
- package/rules/azure-pipelines/service_deploy_pipeline/docs/fix-service_deploy_pipeline.md +1 -1
- package/rules/azure-pipelines/service_deploy_pipeline/fix-service_deploy_pipeline.mjs +31 -5
- package/rules/azure-pipelines/service_deploy_pipeline/service_deploy_pipeline.rego +35 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.1] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- service_deploy_pipeline під реальну efes-форму: template-параметр покриває сервіс за префіксом glob-а (`run/nexus/**` ↔ modulePath `run/nexus`, було — хибний deny на всіх extends-пайплайнах); plan-гейт вимагається лише за наявності lint-джоб (утилітарні service-scoped pipelines типу gen:schema — поза каноном); команди читаються і з кроків `task: Bash@3` (`inputs.script`) — у rego і в автомігратора
|
|
8
|
+
|
|
3
9
|
## [1.4.0] - 2026-07-18
|
|
4
10
|
|
|
5
11
|
### Added
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: fix-service_deploy_pipeline.mjs
|
|
4
4
|
resource: plugins/ci-azure/rules/azure-pipelines/service_deploy_pipeline/fix-service_deploy_pipeline.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 6d6df549
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
8
|
tier: cloud-min
|
|
9
9
|
score: 100
|
|
@@ -33,6 +33,7 @@ import { collectPathScopedFiles } from '@7n/rules/scripts/lib/lint-surface/path-
|
|
|
33
33
|
import { domainKey } from '@7n/rules/scripts/lib/lint-surface/ci-plan.mjs'
|
|
34
34
|
|
|
35
35
|
const WS_RE = /\s+/u
|
|
36
|
+
const GLOB_SUFFIX_RE = /\/\*+$/u
|
|
36
37
|
|
|
37
38
|
/** Канонічні prep-кроки, якщо в pipeline немає власного зразка. */
|
|
38
39
|
const CANONICAL_PREP = [
|
|
@@ -45,7 +46,8 @@ const CANONICAL_PREP = [
|
|
|
45
46
|
]
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
* Текст команди кроку (`script:` або `
|
|
49
|
+
* Текст команди кроку (`script:`, `bash:` або task-форма `inputs.script`),
|
|
50
|
+
* '' якщо крок не командний.
|
|
49
51
|
* @param {unknown} step крок джоби (plain JS)
|
|
50
52
|
* @returns {string} команда або ''
|
|
51
53
|
*/
|
|
@@ -54,9 +56,29 @@ function stepCmd(step) {
|
|
|
54
56
|
const s = /** @type {Record<string, unknown>} */ (step)
|
|
55
57
|
if (typeof s.script === 'string') return s.script
|
|
56
58
|
if (typeof s.bash === 'string') return s.bash
|
|
59
|
+
const inputs = s.inputs
|
|
60
|
+
if (
|
|
61
|
+
inputs &&
|
|
62
|
+
typeof inputs === 'object' &&
|
|
63
|
+
typeof (/** @type {Record<string, unknown>} */ (inputs).script) === 'string'
|
|
64
|
+
) {
|
|
65
|
+
return /** @type {string} */ (/** @type {Record<string, unknown>} */ (inputs).script)
|
|
66
|
+
}
|
|
57
67
|
return ''
|
|
58
68
|
}
|
|
59
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Шлях-суфікс до тексту команди всередині кроку (для `setIn`): `['script']`,
|
|
72
|
+
* `['bash']` або `['inputs', 'script']` (task-форма).
|
|
73
|
+
* @param {Record<string, unknown>} step крок джоби (plain JS)
|
|
74
|
+
* @returns {string[]} суфікс шляху
|
|
75
|
+
*/
|
|
76
|
+
function stepCmdPath(step) {
|
|
77
|
+
if (typeof step.script === 'string') return ['script']
|
|
78
|
+
if (typeof step.bash === 'string') return ['bash']
|
|
79
|
+
return ['inputs', 'script']
|
|
80
|
+
}
|
|
81
|
+
|
|
60
82
|
/**
|
|
61
83
|
* Розбирає команду `n-rules lint …`/`n-rules ci plan …` токенами (без regex —
|
|
62
84
|
* стійко до багаторядкових script-ів і без бектрекінгу).
|
|
@@ -272,8 +294,7 @@ function patchDomainLintJob(doc, base, j, found) {
|
|
|
272
294
|
}
|
|
273
295
|
const cmd = stepCmd(steps[found.stepIndex])
|
|
274
296
|
if (!cmd.includes('--no-fix')) {
|
|
275
|
-
|
|
276
|
-
doc.setIn([...base, 'steps', found.stepIndex, key], `${cmd.trimEnd()} --no-fix`)
|
|
297
|
+
doc.setIn([...base, 'steps', found.stepIndex, ...stepCmdPath(steps[found.stepIndex])], `${cmd.trimEnd()} --no-fix`)
|
|
277
298
|
changed = true
|
|
278
299
|
}
|
|
279
300
|
const checkoutIdx = steps.findIndex(s => s && typeof s === 'object' && 'checkout' in s)
|
|
@@ -385,11 +406,16 @@ function analyzePipeline(doc) {
|
|
|
385
406
|
.map(x => ({ seq: x.seq, job: x.job, path: x.found.path }))
|
|
386
407
|
)
|
|
387
408
|
|
|
388
|
-
// Сервісний каталог: із plan-джоби → з легасі lint-джоби → перший paths.include
|
|
409
|
+
// Сервісний каталог: із plan-джоби → з легасі lint-джоби → перший paths.include
|
|
410
|
+
// (glob-суфікс /**, /* зрізається: `run/nexus/**` → `run/nexus`).
|
|
389
411
|
const planPath = (Array.isArray(planJob?.steps) ? planJob.steps : [])
|
|
390
412
|
.map(s => parseNRulesCmd(stepCmd(s), 'n-rules ci plan')?.path)
|
|
391
413
|
.find(Boolean)
|
|
392
|
-
const
|
|
414
|
+
const firstTriggerPath = servicePaths
|
|
415
|
+
.filter(p => typeof p === 'string')
|
|
416
|
+
.map(p => p.replace(GLOB_SUFFIX_RE, ''))
|
|
417
|
+
.find(p => p !== '' && !p.includes('*'))
|
|
418
|
+
const servicePath = planPath ?? legacy[0]?.path ?? firstTriggerPath
|
|
393
419
|
if (typeof servicePath !== 'string') return null
|
|
394
420
|
|
|
395
421
|
return { seqs, allJobs, planJob, legacy, servicePath }
|
|
@@ -45,10 +45,14 @@ template_refs contains t if {
|
|
|
45
45
|
is_string(object.get(t, "template", 0))
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
# Параметр-каталог покриває сервіс: точний збіг АБО glob paths.include
|
|
49
|
+
# починається з нього (`run/nexus/**` ↔ modulePath `run/nexus`).
|
|
48
50
|
template_covers_service if {
|
|
49
51
|
some t in template_refs
|
|
50
52
|
some v in object.get(t, "parameters", {})
|
|
51
|
-
v
|
|
53
|
+
is_string(v)
|
|
54
|
+
some p in service_paths
|
|
55
|
+
startswith(p, v)
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
plan_jobs contains j if {
|
|
@@ -74,6 +78,17 @@ lint_cmds contains cmd if {
|
|
|
74
78
|
|
|
75
79
|
check_jobs contains cmd.job if some cmd in lint_cmds
|
|
76
80
|
|
|
81
|
+
# Чи є в pipeline lint-гейт узагалі (domain-style або легасі `lint --path` без
|
|
82
|
+
# домену) — утилітарні service-scoped pipelines (gen:schema тощо) без lint-джоб
|
|
83
|
+
# каноном plan-гейта не зобовʼязані.
|
|
84
|
+
has_lint_jobs if count(lint_cmds) > 0
|
|
85
|
+
|
|
86
|
+
has_lint_jobs if {
|
|
87
|
+
some j in all_jobs
|
|
88
|
+
some r in job_cmds(j)
|
|
89
|
+
regex.match(`n-rules lint\s+--path\s`, r)
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
# Граф dependsOn і транзитивна досяжність (ланцюг deploy → build → перевірки).
|
|
78
93
|
needs_graph[name] := depends_of(j) if {
|
|
79
94
|
some j in all_jobs
|
|
@@ -102,6 +117,7 @@ job_by_name[name] := j if {
|
|
|
102
117
|
deny contains msg if {
|
|
103
118
|
is_service_pipeline
|
|
104
119
|
count(all_jobs) > 0
|
|
120
|
+
has_lint_jobs
|
|
105
121
|
count(plan_jobs) == 0
|
|
106
122
|
not template_covers_service
|
|
107
123
|
msg := "service-pipeline: немає job `plan` з кроком `bunx n-rules ci plan --path <svc> --azure` (azure-pipelines.mdc: сервіс-канон)"
|
|
@@ -134,7 +150,7 @@ deny contains msg if {
|
|
|
134
150
|
some r in plan_runs
|
|
135
151
|
m := regex.find_all_string_submatch_n(`n-rules ci plan\s+--path\s+(\S+)`, r, 1)
|
|
136
152
|
count(m) > 0
|
|
137
|
-
not m[0][1]
|
|
153
|
+
not path_covers_service(m[0][1])
|
|
138
154
|
msg := sprintf("service-pipeline: `ci plan --path %s` ∉ trigger.paths.include — план рахується не для того каталогу (azure-pipelines.mdc)", [m[0][1]])
|
|
139
155
|
}
|
|
140
156
|
|
|
@@ -166,7 +182,7 @@ deny contains msg if {
|
|
|
166
182
|
deny contains msg if {
|
|
167
183
|
is_service_pipeline
|
|
168
184
|
some cmd in lint_cmds
|
|
169
|
-
not cmd.path
|
|
185
|
+
not path_covers_service(cmd.path)
|
|
170
186
|
msg := sprintf("%s: `lint %s --path %s` ∉ trigger.paths.include — лінтиться інший каталог (azure-pipelines.mdc)", [cmd.job, cmd.domain, cmd.path])
|
|
171
187
|
}
|
|
172
188
|
|
|
@@ -241,7 +257,14 @@ depends_of(job) := [n] if {
|
|
|
241
257
|
|
|
242
258
|
depends_of(job) := object.get(job, "dependsOn", []) if is_array(object.get(job, "dependsOn", []))
|
|
243
259
|
|
|
244
|
-
#
|
|
260
|
+
# Каталог `--path` покривається trigger.paths.include: точний збіг або glob із
|
|
261
|
+
# суфіксом (`run/nexus/**` покриває `run/nexus`).
|
|
262
|
+
path_covers_service(p) if {
|
|
263
|
+
some sp in service_paths
|
|
264
|
+
startswith(sp, p)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# Команда кроку: `script:`, `bash:` або task-форма (`task: Bash@3` → `inputs.script`).
|
|
245
268
|
step_cmd(step) := s if {
|
|
246
269
|
s := object.get(step, "script", "")
|
|
247
270
|
s != ""
|
|
@@ -253,9 +276,17 @@ step_cmd(step) := s if {
|
|
|
253
276
|
s != ""
|
|
254
277
|
}
|
|
255
278
|
|
|
279
|
+
step_cmd(step) := s if {
|
|
280
|
+
object.get(step, "script", "") == ""
|
|
281
|
+
object.get(step, "bash", "") == ""
|
|
282
|
+
s := object.get(object.get(step, "inputs", {}), "script", "")
|
|
283
|
+
s != ""
|
|
284
|
+
}
|
|
285
|
+
|
|
256
286
|
step_cmd(step) := "" if {
|
|
257
287
|
object.get(step, "script", "") == ""
|
|
258
288
|
object.get(step, "bash", "") == ""
|
|
289
|
+
object.get(object.get(step, "inputs", {}), "script", "") == ""
|
|
259
290
|
}
|
|
260
291
|
|
|
261
292
|
job_cmds(job) := [c |
|