@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules-ci-azure",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Плагін @7n/rules: канон Azure Pipelines (azure-pipelines.yml) — структура, lint-степ, VS Code",
5
5
  "keywords": [
6
6
  "azure-pipelines",
@@ -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: 7841b82d
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, statSync } from 'node:fs'
27
- import { join, resolve } from 'node:path'
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 { loadEnabledLintRules, computeActiveDomains } from '@7n/rules/scripts/lib/lint-surface/run-detectors.mjs'
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 WS_RE = /\s+/u
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:` або `bash:`), '' якщо крок не командний.
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
- * Розбирає команду `n-rules lint …`/`n-rules ci plan …` токенами (без regex
62
- * стійко до багаторядкових script-ів і без бектрекінгу).
63
- * @param {string} cmd повний текст команди кроку
64
- * @param {string} marker підрядок-початок (`n-rules lint` | `n-rules ci plan`)
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 parseNRulesCmd(cmd, marker) {
68
- const at = cmd.indexOf(marker)
69
- if (at === -1) return null
70
- const tokens = cmd
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
- const key = typeof steps[found.stepIndex].script === 'string' ? 'script' : 'bash'
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 servicePath = planPath ?? legacy[0]?.path ?? servicePaths.find(p => typeof p === 'string')
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 in service_paths
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] in service_paths
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 in service_paths
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
- # Команда кроку: `script:` або `bash:`.
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 |