@nitra/cursor 1.8.201 → 1.8.203
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 +19 -0
- package/bin/n-cursor.js +44 -1
- package/mdc/js-lint.mdc +3 -3
- package/mdc/k8s.mdc +2 -2
- package/package.json +1 -1
- package/scripts/check-js-lint.mjs +10 -8
- package/scripts/check-k8s.mjs +111 -35
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.8.203] - 2026-05-07
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `check-k8s.mjs` (автоконверт `image-replace` patches → `images:`): тепер працює і для `patches[i].patch` із **кількома** ops, а не лише з одинокою image-replace op. Сканує всі ops у патчі, конвертує **кожну** `op: replace` на `/spec/template/spec/containers/<N>/image` (target `kind: Deployment`) у запис `images:`; якщо всі ops патча конвертовано — `patches[i]` видаляється повністю; інакше inline `patch:` переписується через `parseDocument` без конвертованих ops зі збереженням block-literal scalar (`|-`) і вихідного порядку решти ops. Реалізовано через нові функції `tryParseJson6902Array` (≥ 1 op, замість `tryParseSingleJson6902Array`) і `rewriteInlinePatchWithoutOps`; `imageReplaceDeploymentPatchInfo` повертає `{ deployName, totalOps, ops: [{ containerIndex, newImage, opIndex }] }` (раніше — одиничний `{ deployName, containerIndex, newImage }` лише за `length === 1`); `applyConversionsToDoc` групує конвертації по індексу патча й вирізає ops або сам патч за потреби. Сортування решти ops після видалення лишається поза цією зміною — за нього відповідає окрема перевірка `kustomizationInlinePatchOpsSortedViolation`.
|
|
12
|
+
- `mdc/k8s.mdc` (v1.26 → v1.27): уточнено крок 1 авто-перевірки в розділі «Зміна image — через `images:`, не через `patches[]`» — тепер описує і випадок, коли в `patches[i].patch` лишаються не-image ops (їх зберігає, у вихідному порядку, без коментарів).
|
|
13
|
+
- `check-js-lint.mjs` + `mdc/js-lint.mdc` (v1.16 → v1.17): мінімум `@nitra/eslint-config` піднято з `^3.8.0` до `^3.9.2`. Обґрунтування: з 3.9.2 у `getConfig` вбудовано ignore для `**/adr/**`, тож ADR-документи не валідуються ESLint, і консьюмерам не треба додавати цей glob у `eslint.config.js` локально. `nitraEslintConfigMeetsMinVersion` тепер повертає `false` для діапазонів `^3.8.x`–`^3.9.1`; `workspace:*` лишається ok без змін. Pass/fail-повідомлення `checkPackageJsonLintDeps` оновлено під новий мінімум; `for...in`-бан з 3.8.0 згадується як накопичена відмінність. Тести `nitraEslintConfigMeetsMinVersion` розширено: `^3.9.2`/`^3.9.10`/`^3.10.0`/`^4.0.0` — ok; `^3.9.1`/`^3.8.0`/`^3.6.12`/`^3.4.3` — ні.
|
|
14
|
+
- `bin/n-cursor.js` (`reexecIfPackageVersionChanged` + `spawnSync`-виклик): `process.env.NITRA_CURSOR_REEXEC` і `...process.env` замінено на `env.NITRA_CURSOR_REEXEC` і `...env` з `node:process` (`import { cwd, env } from 'node:process'`). Підстава: правило `js-run.mdc` забороняє прямий `process.env.*` у Node-коді; `NITRA_CURSOR_REEXEC` — опційна змінна (виставляється лише при re-exec), тож імпорт `env` з `node:process` (а не з `@nitra/check-env`) — канонічна форма для опційних. Поведінка не змінена; раніше `npm/scripts/check-js-run.mjs` помилявся на `bin/n-cursor.js:1136` (правило `process-env`), тепер intergation-test `check-* на реальному репозиторії` проходить.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `tests/check-k8s-images.test.mjs`: нова форма `imageReplaceDeploymentPatchInfo` (`ops`/`totalOps`/`opIndex`); e2e-тести на multi-op patch (image + `add nodeSelector`), три не-image ops + image у hasura-стилі (`add containers/-` + `add volumes` + `replace nodeSelector`), multi-image patch (containers/0 + containers/1 → обидва конвертовано, патч видаляється), mixed patch з digest у одному з image-values (звичайний tag конвертовано, digest op лишається у патчі) і одиничний digest-image (повертає `errors`, патч на диску не змінюється).
|
|
19
|
+
|
|
20
|
+
## [1.8.202] - 2026-05-07
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `bin/n-cursor.js`: новий хелпер `reexecIfPackageVersionChanged(effectivePackageRoot)` і його виклик у `runSync` одразу після `upgradeNitraCursorToLatestAndBunInstall`. Якщо self-upgrade встановив у `node_modules/@nitra/cursor` версію, відмінну від тієї, з якої стартував поточний процес (типово — npx-кеш), CLI спавнить `process.execPath <newBin> <args…>` через `spawnSync` (`stdio: 'inherit'`), додає в env `NITRA_CURSOR_REEXEC=1` і завершується з exit-кодом дочірнього процесу. Обґрунтування: ES-модулі (`RULE_MIGRATIONS`, `detectAutoRulesAndSkills`, списки правил) уже завантажені у V8 і нова логіка з-під свіжо встановленого пакета без re-exec невидима для поточного запуску — `import()` не вирішує цього, бо процес виконується з `bin/` у npx-кеші, а не з `node_modules/`. Захист від нескінченного циклу — раннє повернення при `process.env.NITRA_CURSOR_REEXEC === '1'`; додатково нічого не робить, якщо `effectivePackageRoot === BUNDLED_PACKAGE_ROOT` (реального апгрейду не сталося), якщо `version` не вдалося прочитати з обох `package.json`, або якщо у новому корені відсутній `bin/n-cursor.js`. `runChecks` свідомо не патчиться — він не виконує self-upgrade, тож версія процесу і пакета там завжди узгоджені. Імпорт `spawnSync` із `node:child_process` — єдина нова зовнішня залежність.
|
|
25
|
+
|
|
7
26
|
## [1.8.201] - 2026-05-07
|
|
8
27
|
|
|
9
28
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -49,10 +49,11 @@
|
|
|
49
49
|
* `node_modules/@nitra/cursor`, якщо пакет з’явився після встановлення.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
+
import { spawnSync } from 'node:child_process'
|
|
52
53
|
import { existsSync } from 'node:fs'
|
|
53
54
|
import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'node:fs/promises'
|
|
54
55
|
import { basename, dirname, join } from 'node:path'
|
|
55
|
-
import { cwd } from 'node:process'
|
|
56
|
+
import { cwd, env } from 'node:process'
|
|
56
57
|
import { fileURLToPath } from 'node:url'
|
|
57
58
|
|
|
58
59
|
import { buildAgentsCommandBulletItems } from '../scripts/build-agents-commands.mjs'
|
|
@@ -1122,6 +1123,46 @@ async function readBundledVersionAt(packageRoot) {
|
|
|
1122
1123
|
}
|
|
1123
1124
|
}
|
|
1124
1125
|
|
|
1126
|
+
/**
|
|
1127
|
+
* Якщо `upgradeNitraCursorToLatestAndBunInstall` встановив у `node_modules/@nitra/cursor` версію,
|
|
1128
|
+
* відмінну від тієї, з якої стартував поточний процес (наприклад, з npx-кешу), запускає бінар нової
|
|
1129
|
+
* версії через `spawnSync` і завершує поточний процес із успадкованим exit-кодом. Re-exec потрібен,
|
|
1130
|
+
* бо ES-модулі вже завантажені у V8 (RULE_MIGRATIONS, detectAutoRulesAndSkills тощо) і нова логіка
|
|
1131
|
+
* без повної заміни процесу не підхопиться. Захист від нескінченного циклу — env `NITRA_CURSOR_REEXEC=1`.
|
|
1132
|
+
* @param {string} effectivePackageRoot шлях, повернутий `upgradeNitraCursorToLatestAndBunInstall`
|
|
1133
|
+
* @returns {Promise<void>} повертається лише якщо re-exec не потрібен (інакше викликає `process.exit`)
|
|
1134
|
+
*/
|
|
1135
|
+
async function reexecIfPackageVersionChanged(effectivePackageRoot) {
|
|
1136
|
+
if (env.NITRA_CURSOR_REEXEC === '1') {
|
|
1137
|
+
return
|
|
1138
|
+
}
|
|
1139
|
+
if (effectivePackageRoot === BUNDLED_PACKAGE_ROOT) {
|
|
1140
|
+
return
|
|
1141
|
+
}
|
|
1142
|
+
const currentVersion = await readBundledVersionAt(BUNDLED_PACKAGE_ROOT)
|
|
1143
|
+
const installedVersion = await readBundledVersionAt(effectivePackageRoot)
|
|
1144
|
+
if (!currentVersion || !installedVersion || currentVersion === installedVersion) {
|
|
1145
|
+
return
|
|
1146
|
+
}
|
|
1147
|
+
const newBinPath = join(effectivePackageRoot, 'bin', 'n-cursor.js')
|
|
1148
|
+
if (!existsSync(newBinPath)) {
|
|
1149
|
+
return
|
|
1150
|
+
}
|
|
1151
|
+
console.log(
|
|
1152
|
+
`🔁 Перезапуск ${PACKAGE_NAME}: процес стартував на ${currentVersion}, ` +
|
|
1153
|
+
`після self-upgrade встановлено ${installedVersion}.\n` +
|
|
1154
|
+
` Re-exec свіжого бінаря, щоб підхопити нову логіку (RULE_MIGRATIONS, auto-detect тощо).\n`
|
|
1155
|
+
)
|
|
1156
|
+
const result = spawnSync(process.execPath, [newBinPath, ...process.argv.slice(2)], {
|
|
1157
|
+
stdio: 'inherit',
|
|
1158
|
+
env: { ...env, NITRA_CURSOR_REEXEC: '1' }
|
|
1159
|
+
})
|
|
1160
|
+
if (result.error) {
|
|
1161
|
+
throw result.error
|
|
1162
|
+
}
|
|
1163
|
+
process.exit(typeof result.status === 'number' ? result.status : 1)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1125
1166
|
/**
|
|
1126
1167
|
* Копіює правила з каталогу `mdc/` установленого пакету та синхронізує `.cursor/rules`
|
|
1127
1168
|
* @returns {Promise<void>}
|
|
@@ -1134,6 +1175,8 @@ async function runSync() {
|
|
|
1134
1175
|
upgradeNitraCursorToLatestAndBunInstall(projectRoot, BUNDLED_PACKAGE_ROOT)
|
|
1135
1176
|
)
|
|
1136
1177
|
|
|
1178
|
+
await reexecIfPackageVersionChanged(effectivePackageRoot)
|
|
1179
|
+
|
|
1137
1180
|
const bundledMdcDir = join(effectivePackageRoot, 'mdc')
|
|
1138
1181
|
const bundledSkillsDir = join(effectivePackageRoot, 'skills')
|
|
1139
1182
|
const bundledAgentsTemplatePath = join(effectivePackageRoot, AGENTS_TEMPLATE_FILE)
|
package/mdc/js-lint.mdc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.17'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.
|
|
7
|
+
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.9.2`** (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint); пакет **`@e18e/eslint-plugin`** окремо не додавай. Пакети oxlint/eslint/jscpd не додавай без потреби монорепо.
|
|
8
8
|
|
|
9
9
|
```json title=".vscode/extensions.json"
|
|
10
10
|
{
|
|
@@ -25,7 +25,7 @@ version: '1.16'
|
|
|
25
25
|
"lint-js": "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd ."
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@nitra/eslint-config": "^3.
|
|
28
|
+
"@nitra/eslint-config": "^3.9.2"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
```
|
package/mdc/k8s.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.27'
|
|
4
4
|
globs: "**/k8s/**/*.yaml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -318,7 +318,7 @@ images:
|
|
|
318
318
|
|
|
319
319
|
**`check k8s` автоматично** для кожного `kustomization.yaml`:
|
|
320
320
|
|
|
321
|
-
1. конвертує JSON6902
|
|
321
|
+
1. конвертує кожну JSON6902-операцію `op: replace` на `/spec/template/spec/containers/<N>/image` (target `kind: Deployment`) у запис `images:` (резолвить оригінальний image у base через `resources:` / `bases` / `components` / `crds`). Якщо у `patches[i].patch` після конвертації не залишилось ops — патч прибирається повністю; інакше у `patches[i].patch` залишаються лише не-image ops у вихідному порядку;
|
|
322
322
|
2. чистить існуючий блок `images:` — зрізає `:tag` з `name` і видаляє `newTag`, який збігається з відрізаним тегом.
|
|
323
323
|
|
|
324
324
|
## Ingress → Gateway API (GKE)
|
package/package.json
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Канонічний `lint-js`, flat ESLint з getConfig і ignore для auto-imports, рекомендації VSCode,
|
|
5
5
|
* `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/scripts/utils/oxlint-canonical.json`):
|
|
6
6
|
* plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
|
|
7
|
-
* globals, ignorePatterns. `@nitra/eslint-config` у devDependencies мінімум **3.
|
|
8
|
-
*
|
|
7
|
+
* globals, ignorePatterns. `@nitra/eslint-config` у devDependencies мінімум **3.9.2** (з 3.8.0 правило
|
|
8
|
+
* `no-restricted-syntax` для `ForInStatement` забороняє `for...in`; з 3.9.2 у `getConfig` вбудовано
|
|
9
|
+
* ignore для ADR-каталогів — локально цей glob додавати не потрібно; також тягне транзитивний
|
|
9
10
|
* `@e18e/eslint-plugin` для oxlint), `.jscpd.json` (gitignore, exitCode, reporters, minLines), workflow
|
|
10
11
|
* `lint-js.yml` (checkout@v6, setup-bun-deps, bunx без --fix), без prettier, `engines.node` >= 24,
|
|
11
12
|
* `engines.bun` >= 1.3, `"type": "module"` у кореневому і всіх workspace `package.json`. Дубль перевірки JS у `lint.yml` — заборонено.
|
|
@@ -54,10 +55,11 @@ export function isCanonicalLintJs(script) {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
|
-
* Чи діапазон `@nitra/eslint-config` у `package.json` задовольняє мінімум `>= 3.
|
|
58
|
-
* (заборона `for...in` через `no-restricted-syntax` +
|
|
58
|
+
* Чи діапазон `@nitra/eslint-config` у `package.json` задовольняє мінімум `>= 3.9.2`
|
|
59
|
+
* (заборона `for...in` через `no-restricted-syntax` з 3.8.0 + вбудований ignore для ADR-каталогів
|
|
60
|
+
* у `getConfig` з 3.9.2 + транзитивний `@e18e/eslint-plugin` для oxlint).
|
|
59
61
|
* @param {unknown} versionSpec значення `devDependencies['@nitra/eslint-config']`
|
|
60
|
-
* @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.
|
|
62
|
+
* @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.9.2
|
|
61
63
|
*/
|
|
62
64
|
export function nitraEslintConfigMeetsMinVersion(versionSpec) {
|
|
63
65
|
const s = String(versionSpec).trim()
|
|
@@ -72,7 +74,7 @@ export function nitraEslintConfigMeetsMinVersion(versionSpec) {
|
|
|
72
74
|
if ([major, minor, patch].some(n => Number.isNaN(n))) {
|
|
73
75
|
return false
|
|
74
76
|
}
|
|
75
|
-
return major > 3 || (major === 3 && minor
|
|
77
|
+
return major > 3 || (major === 3 && (minor > 9 || (minor === 9 && patch >= 2)))
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
/**
|
|
@@ -269,11 +271,11 @@ function checkPackageJsonLintDeps(pkg, passFn, failFn) {
|
|
|
269
271
|
passFn('@nitra/eslint-config є в devDependencies')
|
|
270
272
|
if (nitraEslintConfigMeetsMinVersion(nitraEslint)) {
|
|
271
273
|
passFn(
|
|
272
|
-
'@nitra/eslint-config: мінімум 3.
|
|
274
|
+
'@nitra/eslint-config: мінімум 3.9.2 (no-restricted-syntax для ForInStatement з 3.8.0 + вбудований ignore "**/adr/**" з 3.9.2 + @e18e/eslint-plugin транзитивно, js-lint.mdc)'
|
|
273
275
|
)
|
|
274
276
|
} else {
|
|
275
277
|
failFn(
|
|
276
|
-
'@nitra/eslint-config: онови до мінімум "^3.
|
|
278
|
+
'@nitra/eslint-config: онови до мінімум "^3.9.2" — з 3.9.2 у getConfig вбудовано ignore для "**/adr/**" (ADR-документи не валідуються), плюс транзитивний @e18e/eslint-plugin для oxlint і заборона for...in з 3.8.0 (js-lint.mdc)'
|
|
277
279
|
)
|
|
278
280
|
}
|
|
279
281
|
} else {
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -5626,10 +5626,14 @@ function applyNameStripTag(originalLine, parsed) {
|
|
|
5626
5626
|
const KUSTOMIZATION_DEPLOYMENT_CONTAINER_IMAGE_PATH_RE = /^\/spec\/template\/spec\/containers\/(\d+)\/image$/u
|
|
5627
5627
|
|
|
5628
5628
|
/**
|
|
5629
|
-
* Якщо `patchObj` — JSON6902
|
|
5630
|
-
*
|
|
5629
|
+
* Якщо `patchObj` — JSON6902 для `kind: Deployment`, повертає всі image-replace ops
|
|
5630
|
+
* у його `patch:` разом із `opIndex` (позиція в масиві ops) і `totalOps` (загальна довжина).
|
|
5631
5631
|
* @param {unknown} patchObj елемент масиву `patches[]`
|
|
5632
|
-
* @returns {{
|
|
5632
|
+
* @returns {{
|
|
5633
|
+
* deployName: string,
|
|
5634
|
+
* totalOps: number,
|
|
5635
|
+
* ops: Array<{ containerIndex: number, newImage: string, opIndex: number }>
|
|
5636
|
+
* } | null} інформація про image-replace ops у патчі або null
|
|
5633
5637
|
*/
|
|
5634
5638
|
export function imageReplaceDeploymentPatchInfo(patchObj) {
|
|
5635
5639
|
const pr = asPlainObject(patchObj)
|
|
@@ -5638,20 +5642,21 @@ export function imageReplaceDeploymentPatchInfo(patchObj) {
|
|
|
5638
5642
|
if (deployName === null) return null
|
|
5639
5643
|
if (typeof pr.patch !== 'string') return null
|
|
5640
5644
|
|
|
5641
|
-
const parsedArr =
|
|
5645
|
+
const parsedArr = tryParseJson6902Array(pr.patch)
|
|
5642
5646
|
if (parsedArr === null) return null
|
|
5643
|
-
const op = asPlainObject(parsedArr[0])
|
|
5644
|
-
if (op === null) return null
|
|
5645
5647
|
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
containerIndex
|
|
5653
|
-
|
|
5648
|
+
/** @type {Array<{ containerIndex: number, newImage: string, opIndex: number }>} */
|
|
5649
|
+
const ops = []
|
|
5650
|
+
for (let i = 0; i < parsedArr.length; i++) {
|
|
5651
|
+
const op = asPlainObject(parsedArr[i])
|
|
5652
|
+
if (op === null) continue
|
|
5653
|
+
const containerIndex = singleImageReplaceContainerIndex(op)
|
|
5654
|
+
if (containerIndex === null) continue
|
|
5655
|
+
if (typeof op.value !== 'string' || op.value.trim() === '') continue
|
|
5656
|
+
ops.push({ containerIndex, newImage: op.value.trim(), opIndex: i })
|
|
5654
5657
|
}
|
|
5658
|
+
if (ops.length === 0) return null
|
|
5659
|
+
return { deployName, totalOps: parsedArr.length, ops }
|
|
5655
5660
|
}
|
|
5656
5661
|
|
|
5657
5662
|
/**
|
|
@@ -5679,11 +5684,11 @@ function deploymentTargetName(target) {
|
|
|
5679
5684
|
}
|
|
5680
5685
|
|
|
5681
5686
|
/**
|
|
5682
|
-
* Парсить `patch`-рядок як YAML-масив
|
|
5687
|
+
* Парсить `patch`-рядок як YAML-масив JSON6902-операцій (≥ 1 елемент).
|
|
5683
5688
|
* @param {string} patch текст YAML-масиву JSON6902-операцій
|
|
5684
|
-
* @returns {unknown[] | null} масив
|
|
5689
|
+
* @returns {unknown[] | null} масив операцій або null
|
|
5685
5690
|
*/
|
|
5686
|
-
function
|
|
5691
|
+
function tryParseJson6902Array(patch) {
|
|
5687
5692
|
let parsedArr
|
|
5688
5693
|
try {
|
|
5689
5694
|
for (const d of parseAllDocuments(patch.trim())) {
|
|
@@ -5697,7 +5702,7 @@ function tryParseSingleJson6902Array(patch) {
|
|
|
5697
5702
|
} catch {
|
|
5698
5703
|
return null
|
|
5699
5704
|
}
|
|
5700
|
-
return Array.isArray(parsedArr) && parsedArr.length
|
|
5705
|
+
return Array.isArray(parsedArr) && parsedArr.length >= 1 ? parsedArr : null
|
|
5701
5706
|
}
|
|
5702
5707
|
|
|
5703
5708
|
/**
|
|
@@ -5880,11 +5885,17 @@ export async function convertImagePatchesToImagesInKustomization(kustAbs, rootNo
|
|
|
5880
5885
|
|
|
5881
5886
|
/**
|
|
5882
5887
|
* Парсить kustomization.yaml як Document і повертає його разом зі списком кандидатів-патчів
|
|
5883
|
-
* (
|
|
5884
|
-
* розпарсився, не є Kustomization або не має масиву `patches:`.
|
|
5888
|
+
* (по одному кандидату на кожну image-replace op у `patches[i].patch` — патч може містити кілька).
|
|
5889
|
+
* Повертає null, якщо документ не розпарсився, не є Kustomization або не має масиву `patches:`.
|
|
5885
5890
|
* @param {string} raw текст файлу
|
|
5886
|
-
* @returns {{
|
|
5887
|
-
*
|
|
5891
|
+
* @returns {{
|
|
5892
|
+
* doc: ReturnType<typeof parseDocument>,
|
|
5893
|
+
* candidates: Array<{
|
|
5894
|
+
* index: number,
|
|
5895
|
+
* totalOps: number,
|
|
5896
|
+
* info: { deployName: string, containerIndex: number, newImage: string, opIndex: number }
|
|
5897
|
+
* }>
|
|
5898
|
+
* } | null} document і список кандидатів, або null
|
|
5888
5899
|
*/
|
|
5889
5900
|
function parseKustomizationWithPatches(raw) {
|
|
5890
5901
|
let doc
|
|
@@ -5901,11 +5912,23 @@ function parseKustomizationWithPatches(raw) {
|
|
|
5901
5912
|
if (typeof rec.apiVersion !== 'string' || !rec.apiVersion.startsWith(KUSTOMIZE_CONFIG_API_PREFIX)) return null
|
|
5902
5913
|
if (!Array.isArray(rec.patches)) return null
|
|
5903
5914
|
|
|
5904
|
-
/** @type {Array<{ index: number, info: { deployName: string, containerIndex: number, newImage: string } }>} */
|
|
5915
|
+
/** @type {Array<{ index: number, totalOps: number, info: { deployName: string, containerIndex: number, newImage: string, opIndex: number } }>} */
|
|
5905
5916
|
const candidates = []
|
|
5906
5917
|
for (const [i, p] of rec.patches.entries()) {
|
|
5907
5918
|
const info = imageReplaceDeploymentPatchInfo(p)
|
|
5908
|
-
if (info
|
|
5919
|
+
if (info === null) continue
|
|
5920
|
+
for (const op of info.ops) {
|
|
5921
|
+
candidates.push({
|
|
5922
|
+
index: i,
|
|
5923
|
+
totalOps: info.totalOps,
|
|
5924
|
+
info: {
|
|
5925
|
+
deployName: info.deployName,
|
|
5926
|
+
containerIndex: op.containerIndex,
|
|
5927
|
+
newImage: op.newImage,
|
|
5928
|
+
opIndex: op.opIndex
|
|
5929
|
+
}
|
|
5930
|
+
})
|
|
5931
|
+
}
|
|
5909
5932
|
}
|
|
5910
5933
|
return { doc, candidates }
|
|
5911
5934
|
}
|
|
@@ -5915,17 +5938,17 @@ function parseKustomizationWithPatches(raw) {
|
|
|
5915
5938
|
* (або повідомлення про помилку, чому конвертація неможлива).
|
|
5916
5939
|
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
5917
5940
|
* @param {string} rootNorm нормалізований корінь репо
|
|
5918
|
-
* @param {Array<{ index: number, info: { deployName: string, containerIndex: number, newImage: string } }>} candidates кандидати з `patches[]`
|
|
5919
|
-
* @returns {Promise<{ conversions: Array<{ index: number, name: string, newName: string, newTag: string | null }>, errors: string[] }>}
|
|
5941
|
+
* @param {Array<{ index: number, totalOps: number, info: { deployName: string, containerIndex: number, newImage: string, opIndex: number } }>} candidates кандидати з `patches[]`
|
|
5942
|
+
* @returns {Promise<{ conversions: Array<{ index: number, opIndex: number, totalOps: number, name: string, newName: string, newTag: string | null }>, errors: string[] }>}
|
|
5920
5943
|
* результати конвертації та зібрані нефатальні помилки
|
|
5921
5944
|
*/
|
|
5922
5945
|
async function buildPatchToImageConversions(kustAbs, rootNorm, candidates) {
|
|
5923
|
-
/** @type {Array<{ index: number, name: string, newName: string, newTag: string | null }>} */
|
|
5946
|
+
/** @type {Array<{ index: number, opIndex: number, totalOps: number, name: string, newName: string, newTag: string | null }>} */
|
|
5924
5947
|
const conversions = []
|
|
5925
5948
|
/** @type {string[]} */
|
|
5926
5949
|
const errors = []
|
|
5927
5950
|
|
|
5928
|
-
for (const { index, info } of candidates) {
|
|
5951
|
+
for (const { index, totalOps, info } of candidates) {
|
|
5929
5952
|
const baseImage = await walkKustomizationForDeploymentImage(
|
|
5930
5953
|
kustAbs,
|
|
5931
5954
|
rootNorm,
|
|
@@ -5934,7 +5957,7 @@ async function buildPatchToImageConversions(kustAbs, rootNorm, candidates) {
|
|
|
5934
5957
|
new Set()
|
|
5935
5958
|
)
|
|
5936
5959
|
const conversion = buildConversionForCandidate(index, info, baseImage, errors)
|
|
5937
|
-
if (conversion !== null) conversions.push(conversion)
|
|
5960
|
+
if (conversion !== null) conversions.push({ ...conversion, opIndex: info.opIndex, totalOps })
|
|
5938
5961
|
}
|
|
5939
5962
|
|
|
5940
5963
|
return { conversions, errors }
|
|
@@ -5944,7 +5967,7 @@ async function buildPatchToImageConversions(kustAbs, rootNorm, candidates) {
|
|
|
5944
5967
|
* Будує одну конвертацію `patches[index]` → `images[]` запис з відповідним `newTag`.
|
|
5945
5968
|
* Якщо щось не так (немає baseImage, digest у base/new) — додає текст у `errors` і повертає null.
|
|
5946
5969
|
* @param {number} index індекс патча в `patches[]`
|
|
5947
|
-
* @param {{ deployName: string, containerIndex: number, newImage: string }} info
|
|
5970
|
+
* @param {{ deployName: string, containerIndex: number, newImage: string, opIndex: number }} info один із записів `imageReplaceDeploymentPatchInfo().ops` (плюс `deployName` патча)
|
|
5948
5971
|
* @param {string | null} baseImage знайдений базовий image або null
|
|
5949
5972
|
* @param {string[]} errors буфер нефатальних помилок (мутується)
|
|
5950
5973
|
* @returns {{ index: number, name: string, newName: string, newTag: string | null } | null} запис конвертації або null
|
|
@@ -5975,19 +5998,43 @@ function buildConversionForCandidate(index, info, baseImage, errors) {
|
|
|
5975
5998
|
}
|
|
5976
5999
|
|
|
5977
6000
|
/**
|
|
5978
|
-
*
|
|
6001
|
+
* Застосовує конвертації до Document: для кожного `patches[i]` або видаляє патч цілком (коли всі
|
|
6002
|
+
* його ops конвертовано), або переписує inline `patch:`, лишаючи решту ops без коментарів.
|
|
6003
|
+
* Допише `images:` з усіма конвертованими записами.
|
|
5979
6004
|
* @param {ReturnType<typeof parseDocument>} doc документ kustomization.yaml
|
|
5980
|
-
* @param {Array<{ index: number, name: string, newName: string, newTag: string | null }>} conversions конвертації
|
|
6005
|
+
* @param {Array<{ index: number, opIndex: number, totalOps: number, name: string, newName: string, newTag: string | null }>} conversions конвертації
|
|
5981
6006
|
* @returns {boolean} true, якщо мутації відбулися (документ можна серіалізувати)
|
|
5982
6007
|
*/
|
|
5983
6008
|
function applyConversionsToDoc(doc, conversions) {
|
|
5984
6009
|
const patchesNode = doc.get('patches', true)
|
|
5985
6010
|
if (!isSeq(patchesNode)) return false
|
|
5986
6011
|
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
6012
|
+
/** @type {Map<number, { totalOps: number, opIdx: number[] }>} */
|
|
6013
|
+
const byPatch = new Map()
|
|
6014
|
+
for (const c of conversions) {
|
|
6015
|
+
const slot = byPatch.get(c.index) ?? { totalOps: c.totalOps, opIdx: [] }
|
|
6016
|
+
slot.opIdx.push(c.opIndex)
|
|
6017
|
+
byPatch.set(c.index, slot)
|
|
6018
|
+
}
|
|
6019
|
+
|
|
6020
|
+
const sortedIdx = [...byPatch.keys()].sort((a, b) => b - a)
|
|
6021
|
+
for (const i of sortedIdx) {
|
|
6022
|
+
const slot = byPatch.get(i)
|
|
6023
|
+
if (slot === undefined) continue
|
|
6024
|
+
const { totalOps, opIdx } = slot
|
|
6025
|
+
if (opIdx.length === totalOps) {
|
|
6026
|
+
patchesNode.delete(i)
|
|
6027
|
+
continue
|
|
6028
|
+
}
|
|
6029
|
+
const patchEntry = patchesNode.get(i, true)
|
|
6030
|
+
if (patchEntry === undefined || patchEntry === null) continue
|
|
6031
|
+
const patchScalar = patchEntry.get('patch', true)
|
|
6032
|
+
if (patchScalar === undefined || patchScalar === null || typeof patchScalar.value !== 'string') continue
|
|
6033
|
+
const rewritten = rewriteInlinePatchWithoutOps(patchScalar.value, opIdx)
|
|
6034
|
+
if (rewritten === null) continue
|
|
6035
|
+
patchScalar.value = rewritten
|
|
5990
6036
|
}
|
|
6037
|
+
|
|
5991
6038
|
if (patchesNode.items.length === 0) {
|
|
5992
6039
|
doc.delete('patches')
|
|
5993
6040
|
}
|
|
@@ -6004,6 +6051,35 @@ function applyConversionsToDoc(doc, conversions) {
|
|
|
6004
6051
|
return true
|
|
6005
6052
|
}
|
|
6006
6053
|
|
|
6054
|
+
/**
|
|
6055
|
+
* Видаляє ops за списком індексів з inline `patch:` (текст YAML-масиву JSON6902-ops)
|
|
6056
|
+
* і повертає переписаний текст. Зберігає block-style. Повертає null, якщо не вдалося розпарсити
|
|
6057
|
+
* або після видалення не лишилось ops.
|
|
6058
|
+
* @param {string} patchText текст YAML-масиву ops (literal block scalar)
|
|
6059
|
+
* @param {number[]} opIndices індекси ops, які треба видалити
|
|
6060
|
+
* @returns {string | null} переписаний текст або null
|
|
6061
|
+
*/
|
|
6062
|
+
function rewriteInlinePatchWithoutOps(patchText, opIndices) {
|
|
6063
|
+
let inner
|
|
6064
|
+
try {
|
|
6065
|
+
inner = parseDocument(patchText)
|
|
6066
|
+
} catch {
|
|
6067
|
+
return null
|
|
6068
|
+
}
|
|
6069
|
+
if (inner.errors.length > 0) return null
|
|
6070
|
+
const seq = inner.contents
|
|
6071
|
+
if (!isSeq(seq)) return null
|
|
6072
|
+
|
|
6073
|
+
const toRemove = [...new Set(opIndices)].sort((a, b) => b - a)
|
|
6074
|
+
for (const i of toRemove) {
|
|
6075
|
+
if (i < 0 || i >= seq.items.length) return null
|
|
6076
|
+
seq.delete(i)
|
|
6077
|
+
}
|
|
6078
|
+
if (seq.items.length === 0) return null
|
|
6079
|
+
seq.flow = false
|
|
6080
|
+
return inner.toString().replace(/\n+$/u, '')
|
|
6081
|
+
}
|
|
6082
|
+
|
|
6007
6083
|
/**
|
|
6008
6084
|
* Прохід для всіх `kustomization.yaml`: конвертує image-replace patches у `images:`,
|
|
6009
6085
|
* потім чистить `images:` (зрізає теги в `name`, видаляє надлишкові `newTag`).
|