@7n/rules-ci-azure 1.4.0 → 1.4.2
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/azure-pipelines/service_deploy_pipeline/docs/fix-service_deploy_pipeline.md +1 -2
- package/rules/azure-pipelines/service_deploy_pipeline/fix-service_deploy_pipeline.mjs +30 -56
- package/rules/azure-pipelines/service_deploy_pipeline/service_deploy_pipeline.rego +35 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.2] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- fix-service_deploy_pipeline: `parseNRulesCmd`/`relevantDomains` перенесено в спільний `@7n/rules/scripts/lib/lint-surface/ci-plan.mjs` (jscpd-дублікат із ci-github), без зміни поведінки
|
|
8
|
+
|
|
9
|
+
## [1.4.1] - 2026-07-18
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- 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 і в автомігратора
|
|
14
|
+
|
|
3
15
|
## [1.4.0] - 2026-07-18
|
|
4
16
|
|
|
5
17
|
### Added
|
package/package.json
CHANGED
|
@@ -3,9 +3,8 @@ 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: 0080ec86
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
|
-
tier: cloud-min
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.99
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -23,16 +23,14 @@
|
|
|
23
23
|
* Template-розкладка (`- template:`) не мігрується — фіксеру не видно
|
|
24
24
|
* розгорнутих джоб.
|
|
25
25
|
*/
|
|
26
|
-
import { readFileSync, writeFileSync, existsSync
|
|
27
|
-
import { join
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
27
|
+
import { join } from 'node:path'
|
|
28
28
|
|
|
29
29
|
import { parseDocument } from 'yaml'
|
|
30
30
|
|
|
31
|
-
import {
|
|
32
|
-
import { collectPathScopedFiles } from '@7n/rules/scripts/lib/lint-surface/path-scope.mjs'
|
|
33
|
-
import { domainKey } from '@7n/rules/scripts/lib/lint-surface/ci-plan.mjs'
|
|
31
|
+
import { domainKey, parseNRulesCmd, relevantDomains } from '@7n/rules/scripts/lib/lint-surface/ci-plan.mjs'
|
|
34
32
|
|
|
35
|
-
const
|
|
33
|
+
const GLOB_SUFFIX_RE = /\/\*+$/u
|
|
36
34
|
|
|
37
35
|
/** Канонічні prep-кроки, якщо в pipeline немає власного зразка. */
|
|
38
36
|
const CANONICAL_PREP = [
|
|
@@ -45,7 +43,8 @@ const CANONICAL_PREP = [
|
|
|
45
43
|
]
|
|
46
44
|
|
|
47
45
|
/**
|
|
48
|
-
* Текст команди кроку (`script:` або `
|
|
46
|
+
* Текст команди кроку (`script:`, `bash:` або task-форма `inputs.script`),
|
|
47
|
+
* '' якщо крок не командний.
|
|
49
48
|
* @param {unknown} step крок джоби (plain JS)
|
|
50
49
|
* @returns {string} команда або ''
|
|
51
50
|
*/
|
|
@@ -54,35 +53,27 @@ function stepCmd(step) {
|
|
|
54
53
|
const s = /** @type {Record<string, unknown>} */ (step)
|
|
55
54
|
if (typeof s.script === 'string') return s.script
|
|
56
55
|
if (typeof s.bash === 'string') return s.bash
|
|
56
|
+
const inputs = s.inputs
|
|
57
|
+
if (
|
|
58
|
+
inputs &&
|
|
59
|
+
typeof inputs === 'object' &&
|
|
60
|
+
typeof (/** @type {Record<string, unknown>} */ (inputs).script) === 'string'
|
|
61
|
+
) {
|
|
62
|
+
return /** @type {string} */ (/** @type {Record<string, unknown>} */ (inputs).script)
|
|
63
|
+
}
|
|
57
64
|
return ''
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @param {string}
|
|
64
|
-
* @
|
|
65
|
-
* @returns {{ domain: string|null, path: string|null }|null} розбір або null, якщо маркера немає
|
|
68
|
+
* Шлях-суфікс до тексту команди всередині кроку (для `setIn`): `['script']`,
|
|
69
|
+
* `['bash']` або `['inputs', 'script']` (task-форма).
|
|
70
|
+
* @param {Record<string, unknown>} step крок джоби (plain JS)
|
|
71
|
+
* @returns {string[]} суфікс шляху
|
|
66
72
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
.slice(at + marker.length)
|
|
72
|
-
.split(WS_RE)
|
|
73
|
-
.filter(Boolean)
|
|
74
|
-
let domain = null
|
|
75
|
-
let path = null
|
|
76
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
77
|
-
const t = tokens[i]
|
|
78
|
-
if (t === '--path') {
|
|
79
|
-
path = tokens[i + 1] ?? null
|
|
80
|
-
i++
|
|
81
|
-
continue
|
|
82
|
-
}
|
|
83
|
-
if (!t.startsWith('-') && domain === null && path === null) domain = t
|
|
84
|
-
}
|
|
85
|
-
return { domain, path }
|
|
73
|
+
function stepCmdPath(step) {
|
|
74
|
+
if (typeof step.script === 'string') return ['script']
|
|
75
|
+
if (typeof step.bash === 'string') return ['bash']
|
|
76
|
+
return ['inputs', 'script']
|
|
86
77
|
}
|
|
87
78
|
|
|
88
79
|
/**
|
|
@@ -169,27 +160,6 @@ function buildLintJob(domain, servicePath, prep) {
|
|
|
169
160
|
}
|
|
170
161
|
}
|
|
171
162
|
|
|
172
|
-
/**
|
|
173
|
-
* Релевантні домени сервісу: enabled-правила, чиї per-file glob-и матчать
|
|
174
|
-
* бодай один файл піддерева (та сама таблиця, що `ci plan` по не-дельті).
|
|
175
|
-
* @param {string} cwd корінь consumer-репо
|
|
176
|
-
* @param {string} servicePath каталог сервісу
|
|
177
|
-
* @returns {Promise<string[]>} відсортовані rule-id
|
|
178
|
-
*/
|
|
179
|
-
async function relevantDomains(cwd, servicePath) {
|
|
180
|
-
const abs = resolve(cwd, servicePath)
|
|
181
|
-
if (!existsSync(abs) || !statSync(abs).isDirectory()) return []
|
|
182
|
-
const files = await collectPathScopedFiles(cwd, servicePath)
|
|
183
|
-
const { byRule, enabledSet } = await loadEnabledLintRules({ cwd })
|
|
184
|
-
const active = computeActiveDomains(byRule, enabledSet, files)
|
|
185
|
-
return active
|
|
186
|
-
.entries()
|
|
187
|
-
.filter(([, st]) => st.triggered)
|
|
188
|
-
.map(([id]) => id)
|
|
189
|
-
.toArray()
|
|
190
|
-
.toSorted((a, b) => a.localeCompare(b))
|
|
191
|
-
}
|
|
192
|
-
|
|
193
163
|
/**
|
|
194
164
|
* Розбір lint-команди джоби: `{ legacy, domain, path, stepIndex }` або null.
|
|
195
165
|
* @param {Array<Record<string, unknown>>} steps кроки джоби (plain JS)
|
|
@@ -272,8 +242,7 @@ function patchDomainLintJob(doc, base, j, found) {
|
|
|
272
242
|
}
|
|
273
243
|
const cmd = stepCmd(steps[found.stepIndex])
|
|
274
244
|
if (!cmd.includes('--no-fix')) {
|
|
275
|
-
|
|
276
|
-
doc.setIn([...base, 'steps', found.stepIndex, key], `${cmd.trimEnd()} --no-fix`)
|
|
245
|
+
doc.setIn([...base, 'steps', found.stepIndex, ...stepCmdPath(steps[found.stepIndex])], `${cmd.trimEnd()} --no-fix`)
|
|
277
246
|
changed = true
|
|
278
247
|
}
|
|
279
248
|
const checkoutIdx = steps.findIndex(s => s && typeof s === 'object' && 'checkout' in s)
|
|
@@ -385,11 +354,16 @@ function analyzePipeline(doc) {
|
|
|
385
354
|
.map(x => ({ seq: x.seq, job: x.job, path: x.found.path }))
|
|
386
355
|
)
|
|
387
356
|
|
|
388
|
-
// Сервісний каталог: із plan-джоби → з легасі lint-джоби → перший paths.include
|
|
357
|
+
// Сервісний каталог: із plan-джоби → з легасі lint-джоби → перший paths.include
|
|
358
|
+
// (glob-суфікс /**, /* зрізається: `run/nexus/**` → `run/nexus`).
|
|
389
359
|
const planPath = (Array.isArray(planJob?.steps) ? planJob.steps : [])
|
|
390
360
|
.map(s => parseNRulesCmd(stepCmd(s), 'n-rules ci plan')?.path)
|
|
391
361
|
.find(Boolean)
|
|
392
|
-
const
|
|
362
|
+
const firstTriggerPath = servicePaths
|
|
363
|
+
.filter(p => typeof p === 'string')
|
|
364
|
+
.map(p => p.replace(GLOB_SUFFIX_RE, ''))
|
|
365
|
+
.find(p => p !== '' && !p.includes('*'))
|
|
366
|
+
const servicePath = planPath ?? legacy[0]?.path ?? firstTriggerPath
|
|
393
367
|
if (typeof servicePath !== 'string') return null
|
|
394
368
|
|
|
395
369
|
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 |
|