@nitra/cursor 1.8.112 → 1.8.113
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/bin/n-cursor.js +81 -5
- package/mdc/ga.mdc +32 -1
- package/mdc/js-pino.mdc +0 -2
- package/mdc/k8s.mdc +4 -0
- package/mdc/nginx-default-tpl.mdc +1 -0
- package/package.json +1 -1
- package/scripts/check-ga.mjs +62 -1
- package/scripts/check-js-pino.mjs +3 -1
- package/scripts/check-k8s.mjs +148 -1
package/bin/n-cursor.js
CHANGED
|
@@ -752,6 +752,76 @@ async function removeOrphanManagedCommandFiles(commandsDir, configSkills) {
|
|
|
752
752
|
return removed.toSorted((a, b) => a.localeCompare(b))
|
|
753
753
|
}
|
|
754
754
|
|
|
755
|
+
/**
|
|
756
|
+
* Синхронізує .claude/commands/{dirName}.md для всіх локальних скілів з .cursor/skills/
|
|
757
|
+
* що не керуються пакетом (відсутні в configSkills).
|
|
758
|
+
* @param {string[]} configSkills id керованих skills (вже оброблені syncCommands)
|
|
759
|
+
* @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих записів
|
|
760
|
+
*/
|
|
761
|
+
async function syncLocalOnlySkillCommands(configSkills) {
|
|
762
|
+
const skillsRoot = join(cwd(), SKILLS_DIR)
|
|
763
|
+
if (!existsSync(skillsRoot)) return { success: 0, fail: 0 }
|
|
764
|
+
|
|
765
|
+
const commandsDir = join(cwd(), COMMANDS_DIR)
|
|
766
|
+
await mkdir(commandsDir, { recursive: true })
|
|
767
|
+
|
|
768
|
+
const managedDirNames = new Set(configSkills.map(s => managedSkillDirName(s)))
|
|
769
|
+
const allDirNames = await listProjectSkillDirNames()
|
|
770
|
+
const localOnly = allDirNames.filter(d => !managedDirNames.has(d))
|
|
771
|
+
|
|
772
|
+
let success = 0
|
|
773
|
+
let fail = 0
|
|
774
|
+
|
|
775
|
+
for (const dirName of localOnly) {
|
|
776
|
+
const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
|
|
777
|
+
const destFile = join(commandsDir, `${dirName}.md`)
|
|
778
|
+
|
|
779
|
+
process.stdout.write(` ⬇ ${dirName} → ${COMMANDS_DIR}/${dirName}.md ... `)
|
|
780
|
+
try {
|
|
781
|
+
let desc = ''
|
|
782
|
+
if (existsSync(skillMdPath)) {
|
|
783
|
+
const raw = await readFile(skillMdPath, 'utf8')
|
|
784
|
+
const parsed = extractSkillDescription(raw)
|
|
785
|
+
if (parsed) desc = skillDescriptionSafeForMarkdownInline(parsed)
|
|
786
|
+
}
|
|
787
|
+
const header = desc ? `# ${dirName} — ${desc}\n\n` : ''
|
|
788
|
+
const body = `${header}Виконай інструкції зі скілу \`${SKILLS_DIR}/${dirName}/SKILL.md\`.\n`
|
|
789
|
+
await writeFile(destFile, body, 'utf8')
|
|
790
|
+
console.log(`✅`)
|
|
791
|
+
success++
|
|
792
|
+
} catch (error) {
|
|
793
|
+
console.log(`❌`)
|
|
794
|
+
console.error(` Помилка: ${errorMessage(error)}`)
|
|
795
|
+
fail++
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return { success, fail }
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Видаляє .claude/commands/{dirName}.md файли локальних скілів, яких більше немає в .cursor/skills/
|
|
803
|
+
* @param {string} commandsDir абсолютний шлях до .claude/commands
|
|
804
|
+
* @param {string[]} configSkills id керованих skills
|
|
805
|
+
* @returns {Promise<string[]>} імена видалених файлів
|
|
806
|
+
*/
|
|
807
|
+
async function removeOrphanLocalSkillCommandFiles(commandsDir, configSkills) {
|
|
808
|
+
if (!existsSync(commandsDir)) return []
|
|
809
|
+
|
|
810
|
+
const managedDirNames = new Set(configSkills.map(s => managedSkillDirName(s)))
|
|
811
|
+
const allDirNames = new Set(await listProjectSkillDirNames())
|
|
812
|
+
const names = await readdir(commandsDir)
|
|
813
|
+
const removed = []
|
|
814
|
+
|
|
815
|
+
for (const name of names.filter(n => n.endsWith('.md') && !n.startsWith(RULE_PREFIX))) {
|
|
816
|
+
const dirName = name.slice(0, -3)
|
|
817
|
+
if (!managedDirNames.has(dirName) && !allDirNames.has(dirName)) {
|
|
818
|
+
await unlink(join(commandsDir, name))
|
|
819
|
+
removed.push(name)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return removed.toSorted((a, b) => a.localeCompare(b))
|
|
823
|
+
}
|
|
824
|
+
|
|
755
825
|
/**
|
|
756
826
|
* Людинозрозумілий текст винятку для логів.
|
|
757
827
|
* @param {unknown} error виняток із catch
|
|
@@ -1031,13 +1101,19 @@ async function runSync() {
|
|
|
1031
1101
|
|
|
1032
1102
|
await runSyncStep('❌ Commands: ', async () => {
|
|
1033
1103
|
const { success: cmdOk, fail: cmdFail } = await syncCommands(skills, bundledSkillsDir)
|
|
1034
|
-
|
|
1035
|
-
|
|
1104
|
+
const { success: localOk, fail: localFail } = await syncLocalOnlySkillCommands(skills)
|
|
1105
|
+
const totalOk = cmdOk + localOk
|
|
1106
|
+
const totalFail = cmdFail + localFail
|
|
1107
|
+
if (totalOk + totalFail > 0) {
|
|
1108
|
+
console.log(`\n⌨️ Commands: ${totalOk} скопійовано, ${totalFail} з помилками`)
|
|
1036
1109
|
}
|
|
1037
|
-
const
|
|
1110
|
+
const commandsDir = join(cwd(), COMMANDS_DIR)
|
|
1111
|
+
const removedCmds = await removeOrphanManagedCommandFiles(commandsDir, skills)
|
|
1038
1112
|
logRemovedManagedItems('commands', COMMANDS_DIR, removedCmds)
|
|
1039
|
-
|
|
1040
|
-
|
|
1113
|
+
const removedLocalCmds = await removeOrphanLocalSkillCommandFiles(commandsDir, skills)
|
|
1114
|
+
logRemovedManagedItems('commands (local)', COMMANDS_DIR, removedLocalCmds)
|
|
1115
|
+
if (totalFail > 0) {
|
|
1116
|
+
throw new Error(`Не вдалося скопіювати ${totalFail} commands`)
|
|
1041
1117
|
}
|
|
1042
1118
|
})
|
|
1043
1119
|
|
package/mdc/ga.mdc
CHANGED
|
@@ -4,7 +4,7 @@ alwaysApply: true
|
|
|
4
4
|
version: '1.3'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
У `.github/workflows/` лише **`.yml`**. Мають бути **`clean-ga-workflows.yml`**, **`clean-merged-branch.yml`**, **`lint-ga.yml`**. Якщо є **`apply-k8s.yml`** / **`apply-nats-consumer.yml`** — paths у тригері як у фрагментах.
|
|
7
|
+
У `.github/workflows/` лише **`.yml`**. Мають бути **`clean-ga-workflows.yml`**, **`clean-merged-branch.yml`**, **`lint-ga.yml`**, **`git-ai.yml`**. Якщо є **`apply-k8s.yml`** / **`apply-nats-consumer.yml`** — paths у тригері як у фрагментах.
|
|
8
8
|
|
|
9
9
|
Повинен бути файл .github/workflows/clean-ga-workflows.yml, зі змістом:
|
|
10
10
|
|
|
@@ -110,6 +110,37 @@ jobs:
|
|
|
110
110
|
run: bun run lint-ga
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
Повинен бути файл .github/workflows/git-ai.yml, зі змістом:
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
name: Git AI
|
|
117
|
+
|
|
118
|
+
on:
|
|
119
|
+
pull_request:
|
|
120
|
+
types: [closed]
|
|
121
|
+
|
|
122
|
+
jobs:
|
|
123
|
+
git-ai:
|
|
124
|
+
if: github.event.pull_request.merged == true
|
|
125
|
+
runs-on: ubuntu-latest
|
|
126
|
+
permissions:
|
|
127
|
+
contents: write
|
|
128
|
+
|
|
129
|
+
steps:
|
|
130
|
+
- name: Install git-ai
|
|
131
|
+
run: |
|
|
132
|
+
curl -fsSL https://usegitai.com/install.sh | bash
|
|
133
|
+
echo "$HOME/.git-ai/bin" >> $GITHUB_PATH
|
|
134
|
+
- name: Run git-ai
|
|
135
|
+
id: run-git-ai
|
|
136
|
+
env:
|
|
137
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
138
|
+
run: |
|
|
139
|
+
git config --global user.name "github-actions[bot]"
|
|
140
|
+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
141
|
+
git-ai ci github run
|
|
142
|
+
```
|
|
143
|
+
|
|
113
144
|
**Локальний composite** (`uses: ./.github/actions/setup-bun-deps` або `./npm/github-actions/setup-bun-deps`): **спочатку** обов’язковий крок **`actions/checkout@v6`** (`persist-credentials: false`), інакше runner не знайде `action.yml`. Сам composite: **`actions/setup-node@v6`** (**Node 24**), **Bun**, **`actions/cache@v5`**, **`bun install --frozen-lockfile`**.
|
|
114
145
|
|
|
115
146
|
```json title=".vscode/extensions.json"
|
package/mdc/js-pino.mdc
CHANGED
|
@@ -10,8 +10,6 @@ version: '1.1'
|
|
|
10
10
|
В **/k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
|
|
11
11
|
а в директоріях з kustomize повинні бути перевизначені значення OTEL_RESOURCE_ATTRIBUTES і в них service.namespace повинен відповідати namespace, в якому знаходиться дана директорія.
|
|
12
12
|
|
|
13
|
-
Configmap повинен бути з тією самою назвою, що і deployment, якщо у deployment тільки один configmap.
|
|
14
|
-
|
|
15
13
|
## Перевірка
|
|
16
14
|
|
|
17
15
|
`npx @nitra/cursor check js-pino`
|
package/mdc/k8s.mdc
CHANGED
|
@@ -210,6 +210,10 @@ spec:
|
|
|
210
210
|
|
|
211
211
|
**Точні умови та повідомлення `fail`** — верхній JSDoc **`npm/scripts/check-k8s.mjs`**.
|
|
212
212
|
|
|
213
|
+
## ConfigMap: ім'я збігається з Deployment
|
|
214
|
+
|
|
215
|
+
Якщо в `k8s/base/` є **`configmap.yaml`** і **Deployment**, і цей Deployment посилається рівно на **один** ConfigMap — `metadata.name` ConfigMap має збігатися з `metadata.name` Deployment. Точні умови перевірки — **`check-k8s.mjs`**.
|
|
216
|
+
|
|
213
217
|
## Kustomize: структура каталогів (`base` / overlays)
|
|
214
218
|
|
|
215
219
|
Трансформуй дерева **`**/k8s`**, щоб **винести спільне** через [Kustomize](https://kustomize.io/): один канонічний **`base`** і тонкі **overlays** для інших середовищ.
|
|
@@ -3,6 +3,7 @@ description: Правила nginx для статичних файлів
|
|
|
3
3
|
version: '1.2'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
> **Автоміграція:** `npx @nitra/cursor check nginx-default-tpl` автоматично перейменовує `default.tpl.conf` → `default.conf.template` (або перезаписує вміст, якщо обидва файли існують). Якщо шаблон відсутній — перевірка пропускається.
|
|
6
7
|
|
|
7
8
|
default.conf.template повинен виглядати так:
|
|
8
9
|
|
package/package.json
CHANGED
package/scripts/check-ga.mjs
CHANGED
|
@@ -256,7 +256,7 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
|
|
|
256
256
|
pass('Всі workflows мають розширення .yml')
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
for (const f of ['clean-ga-workflows.yml', 'clean-merged-branch.yml', 'lint-ga.yml']) {
|
|
259
|
+
for (const f of ['clean-ga-workflows.yml', 'clean-merged-branch.yml', 'lint-ga.yml', 'git-ai.yml']) {
|
|
260
260
|
if (files.includes(f)) {
|
|
261
261
|
pass(`${f} існує`)
|
|
262
262
|
} else {
|
|
@@ -265,6 +265,66 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Перевіряє git-ai.yml: тригер pull_request з types: [closed], умова merged у job, виклик git-ai.
|
|
270
|
+
* @param {string} wfDir директорія workflows
|
|
271
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
272
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
273
|
+
*/
|
|
274
|
+
async function checkGitAiWorkflow(wfDir, passFn, failFn) {
|
|
275
|
+
const gitAiWf = join(wfDir, 'git-ai.yml')
|
|
276
|
+
if (!existsSync(gitAiWf)) return
|
|
277
|
+
const content = await readFile(gitAiWf, 'utf8')
|
|
278
|
+
const root = parseWorkflowYaml(content)
|
|
279
|
+
|
|
280
|
+
if (root) {
|
|
281
|
+
// on.pull_request.types має містити 'closed'
|
|
282
|
+
const on = root.on
|
|
283
|
+
let hasPrClosed = false
|
|
284
|
+
if (on && typeof on === 'object') {
|
|
285
|
+
const pr = /** @type {Record<string, unknown>} */ (on)['pull_request']
|
|
286
|
+
if (pr && typeof pr === 'object') {
|
|
287
|
+
const types = /** @type {Record<string, unknown>} */ (pr).types
|
|
288
|
+
hasPrClosed = Array.isArray(types) && types.includes('closed')
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (hasPrClosed) {
|
|
292
|
+
passFn('git-ai.yml: on.pull_request.types містить closed')
|
|
293
|
+
} else {
|
|
294
|
+
failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Job if-умова: запускати лише при злитті PR
|
|
298
|
+
const jobs = root.jobs
|
|
299
|
+
let hasMergedCondition = false
|
|
300
|
+
if (jobs && typeof jobs === 'object') {
|
|
301
|
+
for (const job of Object.values(jobs)) {
|
|
302
|
+
if (job && typeof job === 'object') {
|
|
303
|
+
const ifCond = String(/** @type {Record<string, unknown>} */ (job).if ?? '')
|
|
304
|
+
if (ifCond.includes('merged')) {
|
|
305
|
+
hasMergedCondition = true
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (hasMergedCondition) {
|
|
311
|
+
passFn('git-ai.yml: job має умову merged')
|
|
312
|
+
} else {
|
|
313
|
+
failFn('git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)')
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Крок викликає git-ai ci github run
|
|
318
|
+
const hasGitAiRun = root
|
|
319
|
+
? anyRunStepIncludes(root, 'git-ai ci github run')
|
|
320
|
+
: content.includes('git-ai ci github run')
|
|
321
|
+
if (hasGitAiRun) {
|
|
322
|
+
passFn('git-ai.yml: крок виконує git-ai ci github run')
|
|
323
|
+
} else {
|
|
324
|
+
failFn('git-ai.yml: крок має містити git-ai ci github run (ga.mdc)')
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
268
328
|
/**
|
|
269
329
|
* Перевіряє відповідність проєкту правилам ga.mdc
|
|
270
330
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -318,6 +378,7 @@ export async function check() {
|
|
|
318
378
|
await checkZizmor(pass, fail)
|
|
319
379
|
await checkLintGaScript(pass, fail)
|
|
320
380
|
await checkLintGaWorkflow(wfDir, pass, fail)
|
|
381
|
+
await checkGitAiWorkflow(wfDir, pass, fail)
|
|
321
382
|
|
|
322
383
|
return reporter.getExitCode()
|
|
323
384
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* (`import` / `require` / динамічний `import()`); наявність `OTEL_RESOURCE_ATTRIBUTES`
|
|
6
6
|
* у `k8s/base/configmap.yaml`, якщо такий файл існує.
|
|
7
7
|
*
|
|
8
|
+
* Перевірка відповідності імені ConfigMap імені Deployment — у `check-k8s.mjs` (k8s.mdc).
|
|
9
|
+
*
|
|
8
10
|
* Імпорти в джерелах сканує AST через `oxc-parser` (див. `utils/bunyan-imports.mjs`),
|
|
9
11
|
* щоб виявити випадки на кшталт `import log from '@nitra/bunyan'`, які лишаються в коді
|
|
10
12
|
* після підміни залежності.
|
|
@@ -78,7 +80,7 @@ async function checkWorkspacePackage(rootDir, fail, passFn) {
|
|
|
78
80
|
passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
const configmapPath = join(rootDir, 'k8s
|
|
83
|
+
const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
|
|
82
84
|
if (existsSync(configmapPath)) {
|
|
83
85
|
const content = await readFile(configmapPath, 'utf8')
|
|
84
86
|
if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
* **Структура `HTTPRoute` для Hasura-Deployment:** звіряється канон 4 правил у **`spec.rules`** (редиректи **`<prefix>/ql`** і **`<prefix>/ql/`** на **`<prefix>/ql/console`** 302, **`PathPrefix <prefix>/ql`** + **URLRewrite** на **`/`**, окреме WebSocket-правило з **`RequestHeaderModifier`** remove **`Authorization`**). **Префікс параметризовано** (рядок перед **`/ql`** у першому Hasura-правилі). **Прив'язка** — за **`metadata.name`** у тому ж каталозі, що й **Deployment** з образом **`hasura/graphql-engine`** (див. k8s.mdc). **Додаткові правила** поверх канону дозволені.
|
|
59
59
|
*/
|
|
60
60
|
import { existsSync } from 'node:fs'
|
|
61
|
-
import { readFile, stat, unlink } from 'node:fs/promises'
|
|
61
|
+
import { readFile, readdir, stat, unlink } from 'node:fs/promises'
|
|
62
62
|
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
63
63
|
|
|
64
64
|
import { parseAllDocuments } from 'yaml'
|
|
@@ -1959,6 +1959,88 @@ export function isHasuraDeploymentManifest(manifest) {
|
|
|
1959
1959
|
return containerListHasHasuraImage(p.containers) || containerListHasHasuraImage(p.initContainers)
|
|
1960
1960
|
}
|
|
1961
1961
|
|
|
1962
|
+
const K8S_YAML_EXT_RE = /\.ya?ml$/iu
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* Знаходить перший документ **Deployment** серед YAML-файлів каталогу (для перевірки імені ConfigMap, js-pino.mdc).
|
|
1966
|
+
* @param {string} dirPath абсолютний шлях до каталогу
|
|
1967
|
+
* @returns {Promise<Record<string, unknown> | null>} об'єкт Deployment або null
|
|
1968
|
+
*/
|
|
1969
|
+
export async function findDeploymentDocInDir(dirPath) {
|
|
1970
|
+
let entries
|
|
1971
|
+
try {
|
|
1972
|
+
entries = await readdir(dirPath)
|
|
1973
|
+
} catch {
|
|
1974
|
+
return null
|
|
1975
|
+
}
|
|
1976
|
+
for (const entry of entries) {
|
|
1977
|
+
if (!K8S_YAML_EXT_RE.test(entry)) continue
|
|
1978
|
+
let raw
|
|
1979
|
+
try {
|
|
1980
|
+
raw = await readFile(join(dirPath, entry), 'utf8')
|
|
1981
|
+
} catch {
|
|
1982
|
+
continue
|
|
1983
|
+
}
|
|
1984
|
+
let docs
|
|
1985
|
+
try {
|
|
1986
|
+
docs = parseAllDocuments(raw)
|
|
1987
|
+
} catch {
|
|
1988
|
+
continue
|
|
1989
|
+
}
|
|
1990
|
+
for (const doc of docs) {
|
|
1991
|
+
if (doc.errors.length > 0) continue
|
|
1992
|
+
const obj = doc.toJSON()
|
|
1993
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1994
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1995
|
+
if (rec.kind === 'Deployment') return rec
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
return null
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
/**
|
|
2003
|
+
* Збирає унікальні імена **ConfigMap**, на які посилається **Deployment**
|
|
2004
|
+
* через `spec.template.spec.containers[*].envFrom[*].configMapRef.name`
|
|
2005
|
+
* та `spec.template.spec.volumes[*].configMap.name` (для перевірки js-pino.mdc).
|
|
2006
|
+
* @param {Record<string, unknown>} deployment об'єкт Deployment
|
|
2007
|
+
* @returns {Set<string>} унікальні імена ConfigMap
|
|
2008
|
+
*/
|
|
2009
|
+
export function collectDeploymentConfigMapRefs(deployment) {
|
|
2010
|
+
/** @type {Set<string>} */
|
|
2011
|
+
const names = new Set()
|
|
2012
|
+
const spec = deployment.spec
|
|
2013
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return names
|
|
2014
|
+
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
2015
|
+
if (template === null || typeof template !== 'object' || Array.isArray(template)) return names
|
|
2016
|
+
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
2017
|
+
if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) return names
|
|
2018
|
+
const ps = /** @type {Record<string, unknown>} */ (podSpec)
|
|
2019
|
+
for (const c of Array.isArray(ps.containers) ? /** @type {unknown[]} */ (ps.containers) : []) {
|
|
2020
|
+
if (c === null || typeof c !== 'object' || Array.isArray(c)) continue
|
|
2021
|
+
const envFrom = /** @type {Record<string, unknown>} */ (c).envFrom
|
|
2022
|
+
for (const ef of Array.isArray(envFrom) ? /** @type {unknown[]} */ (envFrom) : []) {
|
|
2023
|
+
if (ef !== null && typeof ef === 'object' && !Array.isArray(ef)) {
|
|
2024
|
+
const cmr = /** @type {Record<string, unknown>} */ (ef).configMapRef
|
|
2025
|
+
if (cmr !== null && typeof cmr === 'object' && !Array.isArray(cmr)) {
|
|
2026
|
+
const n = /** @type {Record<string, unknown>} */ (cmr).name
|
|
2027
|
+
if (typeof n === 'string' && n.trim() !== '') names.add(n)
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
for (const v of Array.isArray(ps.volumes) ? /** @type {unknown[]} */ (ps.volumes) : []) {
|
|
2033
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
2034
|
+
const cm = /** @type {Record<string, unknown>} */ (v).configMap
|
|
2035
|
+
if (cm !== null && typeof cm === 'object' && !Array.isArray(cm)) {
|
|
2036
|
+
const n = /** @type {Record<string, unknown>} */ (cm).name
|
|
2037
|
+
if (typeof n === 'string' && n.trim() !== '') names.add(n)
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return names
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1962
2044
|
/**
|
|
1963
2045
|
* Чи **Service** містить заборонені анотації GKE у **`metadata.annotations`** (k8s.mdc).
|
|
1964
2046
|
* @param {unknown} manifest корінь YAML-документа
|
|
@@ -3160,6 +3242,69 @@ async function ensureBaseKustomizationHasNamespace(root, yamlFiles, fail) {
|
|
|
3160
3242
|
}
|
|
3161
3243
|
}
|
|
3162
3244
|
|
|
3245
|
+
const CONFIGMAP_BASE_PATH_RE = /\/k8s\/base\/configmap\.yaml$/u
|
|
3246
|
+
|
|
3247
|
+
/**
|
|
3248
|
+
* Якщо в `k8s/base/` є `configmap.yaml` і Deployment посилається рівно на один ConfigMap —
|
|
3249
|
+
* `metadata.name` ConfigMap має збігатися з `metadata.name` Deployment (k8s.mdc).
|
|
3250
|
+
* @param {string} root корінь репозиторію
|
|
3251
|
+
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
3252
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
3253
|
+
* @param {(msg: string) => void} passFn callback при успіху
|
|
3254
|
+
*/
|
|
3255
|
+
async function validateConfigMapNameMatchesDeployment(root, yamlFilesAbs, fail, passFn) {
|
|
3256
|
+
const cmFiles = yamlFilesAbs.filter(abs => {
|
|
3257
|
+
const rel = relative(root, abs).replaceAll('\\', '/')
|
|
3258
|
+
return CONFIGMAP_BASE_PATH_RE.test(`/${rel}`) || rel === 'k8s/base/configmap.yaml'
|
|
3259
|
+
})
|
|
3260
|
+
for (const cmAbs of cmFiles) {
|
|
3261
|
+
const rel = relative(root, cmAbs).replaceAll('\\', '/') || cmAbs
|
|
3262
|
+
let raw
|
|
3263
|
+
try {
|
|
3264
|
+
raw = await readFile(cmAbs, 'utf8')
|
|
3265
|
+
} catch {
|
|
3266
|
+
continue
|
|
3267
|
+
}
|
|
3268
|
+
let cmName = null
|
|
3269
|
+
try {
|
|
3270
|
+
for (const doc of parseAllDocuments(raw)) {
|
|
3271
|
+
if (doc.errors.length > 0) continue
|
|
3272
|
+
const obj = doc.toJSON()
|
|
3273
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
3274
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
3275
|
+
if (rec.kind === 'ConfigMap') {
|
|
3276
|
+
const meta = rec.metadata
|
|
3277
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
3278
|
+
const n = /** @type {Record<string, unknown>} */ (meta).name
|
|
3279
|
+
if (typeof n === 'string' && n.trim() !== '') cmName = n
|
|
3280
|
+
}
|
|
3281
|
+
break
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
} catch {
|
|
3286
|
+
continue
|
|
3287
|
+
}
|
|
3288
|
+
if (cmName === null) continue
|
|
3289
|
+
const deployment = await findDeploymentDocInDir(dirname(cmAbs))
|
|
3290
|
+
if (deployment === null) continue
|
|
3291
|
+
const meta = deployment.metadata
|
|
3292
|
+
const deployName =
|
|
3293
|
+
meta !== null && typeof meta === 'object' && !Array.isArray(meta)
|
|
3294
|
+
? /** @type {Record<string, unknown>} */ (meta).name
|
|
3295
|
+
: null
|
|
3296
|
+
const cmRefs = collectDeploymentConfigMapRefs(deployment)
|
|
3297
|
+
if (cmRefs.size !== 1 || typeof deployName !== 'string') continue
|
|
3298
|
+
if (cmName === deployName) {
|
|
3299
|
+
passFn(`${rel}: metadata.name '${cmName}' збігається з Deployment (k8s.mdc)`)
|
|
3300
|
+
} else {
|
|
3301
|
+
fail(
|
|
3302
|
+
`${rel}: metadata.name '${cmName}' має збігатися з назвою Deployment '${deployName}' — Deployment посилається рівно на один ConfigMap (k8s.mdc)`
|
|
3303
|
+
)
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3163
3308
|
/**
|
|
3164
3309
|
* Перевіряє відповідність проєкту правилам k8s.mdc.
|
|
3165
3310
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -3201,5 +3346,7 @@ export async function check() {
|
|
|
3201
3346
|
|
|
3202
3347
|
await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
|
|
3203
3348
|
|
|
3349
|
+
await validateConfigMapNameMatchesDeployment(root, yamlFiles, fail, pass)
|
|
3350
|
+
|
|
3204
3351
|
return reporter.getExitCode()
|
|
3205
3352
|
}
|