@nitra/cursor 1.8.111 → 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/abie.mdc +1 -1
- 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-abie.mjs +217 -0
- 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/abie.mdc
CHANGED
|
@@ -116,7 +116,7 @@ spec:
|
|
|
116
116
|
## k8s: overlay **ru** і nginx-sidecar для WebSocket (Hasura)
|
|
117
117
|
|
|
118
118
|
YC ALB (gwin) має баг: якщо HTTPRoute-правило містить одночасно `URLRewrite` (ReplacePrefixMatch) і `upgrade_types: websocket` — ALB не обробляє WebSocket і повертає 404.
|
|
119
|
-
https://center.yandex.cloud/support/tickets/TX549394
|
|
119
|
+
<https://center.yandex.cloud/support/tickets/TX549394>
|
|
120
120
|
|
|
121
121
|
Обхідний варіант: nginx-sidecar у поді, що сам виконує rewrite і проксіює WebSocket до Hasura.
|
|
122
122
|
|
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-abie.mjs
CHANGED
|
@@ -89,6 +89,23 @@ const REMOVE_CLUSTER_IP_BEFORE_OP_RE = /path:\s*\/spec\/clusterIP\b[\s\S]{0,200}
|
|
|
89
89
|
const REMOVE_CLUSTER_IPS_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIPs\b/mu
|
|
90
90
|
const REMOVE_CLUSTER_IPS_BEFORE_OP_RE = /path:\s*\/spec\/clusterIPs\b[\s\S]{0,200}?op:\s*remove\b/mu
|
|
91
91
|
|
|
92
|
+
/** Підрядок образу Hasura у контейнері Deployment (abie.mdc nginx-sidecar). */
|
|
93
|
+
const HASURA_IMAGE_MARKER = 'hasura/graphql-engine'
|
|
94
|
+
/** Nginx-sidecar image (abie.mdc): nginx:*-alpine. */
|
|
95
|
+
const NGINX_SIDECAR_IMAGE_RE = /image:\s*nginx:\S*-alpine/u
|
|
96
|
+
/** containerPort: 8081 у patch Deployment (abie.mdc). */
|
|
97
|
+
const NGINX_SIDECAR_CONTAINER_PORT_RE = /containerPort:\s*8081\b/u
|
|
98
|
+
/** port: 8081 у patch Service -hl (proxy порт, abie.mdc). */
|
|
99
|
+
const PATCH_PROXY_PORT_8081_RE = /\bport:\s*8081\b/u
|
|
100
|
+
/** configmap-nginx.yaml у resources kustomization (abie.mdc). */
|
|
101
|
+
const RESOURCES_CONFIGMAP_NGINX_RE = /configmap-nginx\.yaml/u
|
|
102
|
+
/** path /spec/rules/{i}/backendRefs/{j}/port … value: 8081 у patch HTTPRoute (path→value, abie.mdc). */
|
|
103
|
+
const HTTPROUTE_BACKENDREF_PORT_8081_RE =
|
|
104
|
+
/path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/port\b[\s\S]{0,200}?value:\s*8081\b/mu
|
|
105
|
+
/** Те саме, value→path. */
|
|
106
|
+
const HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE =
|
|
107
|
+
/value:\s*8081\b[\s\S]{0,200}?path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/port\b/mu
|
|
108
|
+
|
|
92
109
|
/** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
|
|
93
110
|
export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
|
|
94
111
|
|
|
@@ -1627,6 +1644,203 @@ async function checkHcYamlFiles(root, deploymentDirs, pass, fail) {
|
|
|
1627
1644
|
}
|
|
1628
1645
|
}
|
|
1629
1646
|
|
|
1647
|
+
/**
|
|
1648
|
+
* Чи Deployment-документ містить контейнер із образом **`hasura/graphql-engine`** (abie.mdc nginx-sidecar).
|
|
1649
|
+
* @param {unknown} obj корінь YAML-документа
|
|
1650
|
+
* @returns {boolean}
|
|
1651
|
+
*/
|
|
1652
|
+
function deploymentDocHasHasuraImage(obj) {
|
|
1653
|
+
if (!isDeploymentDoc(obj)) return false
|
|
1654
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1655
|
+
const spec = rec.spec
|
|
1656
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return false
|
|
1657
|
+
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
1658
|
+
if (template === null || typeof template !== 'object' || Array.isArray(template)) return false
|
|
1659
|
+
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
1660
|
+
if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) return false
|
|
1661
|
+
const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
|
|
1662
|
+
if (!Array.isArray(containers)) return false
|
|
1663
|
+
for (const c of containers) {
|
|
1664
|
+
if (c !== null && typeof c === 'object' && !Array.isArray(c)) {
|
|
1665
|
+
const img = /** @type {Record<string, unknown>} */ (c).image
|
|
1666
|
+
if (typeof img === 'string' && img.includes(HASURA_IMAGE_MARKER)) return true
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return false
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Чи Kustomization-документ містить у **`images[*].newName`** рядок **`hasura/graphql-engine`** (abie.mdc nginx-sidecar).
|
|
1674
|
+
* @param {unknown} obj корінь YAML-документа
|
|
1675
|
+
* @returns {boolean}
|
|
1676
|
+
*/
|
|
1677
|
+
function kustomizationDocHasHasuraImageNewName(obj) {
|
|
1678
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return false
|
|
1679
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1680
|
+
if (!Array.isArray(rec.images)) return false
|
|
1681
|
+
for (const img of rec.images) {
|
|
1682
|
+
if (img !== null && typeof img === 'object' && !Array.isArray(img)) {
|
|
1683
|
+
const newName = /** @type {Record<string, unknown>} */ (img).newName
|
|
1684
|
+
if (typeof newName === 'string' && newName.includes(HASURA_IMAGE_MARKER)) return true
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return false
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Збирає тексти inline **patch** для **Deployment** з **kustomization.yaml** (усі документи).
|
|
1692
|
+
* @param {string} raw повний текст файлу
|
|
1693
|
+
* @returns {string[]} рядки patch
|
|
1694
|
+
*/
|
|
1695
|
+
function collectDeploymentPatchTextsFromKustomization(raw) {
|
|
1696
|
+
const body = stripBom(raw)
|
|
1697
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
1698
|
+
const first = lines[0] ?? ''
|
|
1699
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
1700
|
+
/** @type {import('yaml').Document[]} */
|
|
1701
|
+
let docs
|
|
1702
|
+
try {
|
|
1703
|
+
docs = parseAllDocuments(rest)
|
|
1704
|
+
} catch {
|
|
1705
|
+
return []
|
|
1706
|
+
}
|
|
1707
|
+
/** @type {string[]} */
|
|
1708
|
+
const out = []
|
|
1709
|
+
for (const doc of docs) {
|
|
1710
|
+
if (doc.errors.length > 0) continue
|
|
1711
|
+
const root = doc.toJSON()
|
|
1712
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) continue
|
|
1713
|
+
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
1714
|
+
if (!Array.isArray(rec.patches)) continue
|
|
1715
|
+
for (const p of rec.patches) {
|
|
1716
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) continue
|
|
1717
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
1718
|
+
const target = pr.target
|
|
1719
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) continue
|
|
1720
|
+
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
1721
|
+
if (tg.kind !== 'Deployment') continue
|
|
1722
|
+
const patchStr = pr.patch
|
|
1723
|
+
if (typeof patchStr === 'string' && patchStr.trim() !== '') out.push(patchStr)
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return out
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* Каталоги пакетів, де в дереві **k8s** є **Deployment** з образом **`hasura/graphql-engine`** або
|
|
1731
|
+
* **Kustomization** з **`images[*].newName`** на нього (abie.mdc nginx-sidecar).
|
|
1732
|
+
* @param {string} root корінь репозиторію
|
|
1733
|
+
* @param {string[]} yamlAbs абсолютні шляхи yaml під k8s
|
|
1734
|
+
* @returns {Promise<Set<string>>} абсолютні шляхи каталогів пакетів
|
|
1735
|
+
*/
|
|
1736
|
+
async function collectHasuraK8sPackageDirs(root, yamlAbs) {
|
|
1737
|
+
/** @type {Set<string>} */
|
|
1738
|
+
const dirs = new Set()
|
|
1739
|
+
for (const abs of yamlAbs) {
|
|
1740
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1741
|
+
const docs = await readAndParseYamlDocs(abs, rel, silentFail)
|
|
1742
|
+
if (!docs) continue
|
|
1743
|
+
for (const doc of docs) {
|
|
1744
|
+
if (doc.errors.length > 0) continue
|
|
1745
|
+
const obj = doc.toJSON()
|
|
1746
|
+
if (deploymentDocHasHasuraImage(obj) || kustomizationDocHasHasuraImageNewName(obj)) {
|
|
1747
|
+
const pkgDir = abiePackageDirFromK8sYamlRel(root, rel)
|
|
1748
|
+
if (pkgDir) dirs.add(pkgDir)
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return dirs
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* Якщо в дереві **k8s** є Deployment з **`hasura/graphql-engine`** і **`ru/kustomization.yaml`** містить
|
|
1757
|
+
* **`HASURA_GRAPHQL_JWT_SECRET`** — вимагає **nginx-sidecar** (abie.mdc):
|
|
1758
|
+
* **`ru/configmap-nginx.yaml`**, **`resources`** у kustomization, patch **Service -hl** (port 8081),
|
|
1759
|
+
* patch **Deployment** (nginx-sidecar image + containerPort 8081), patch **HTTPRoute** (port 8081).
|
|
1760
|
+
* @param {string} root корінь репозиторію
|
|
1761
|
+
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
1762
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1763
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1764
|
+
* @returns {Promise<void>}
|
|
1765
|
+
*/
|
|
1766
|
+
async function ensureAbieNginxSidecarForHasura(root, yamlFilesAbs, fail, passFn) {
|
|
1767
|
+
const hasuraPkgDirs = await collectHasuraK8sPackageDirs(root, yamlFilesAbs)
|
|
1768
|
+
if (hasuraPkgDirs.size === 0) {
|
|
1769
|
+
passFn('Немає Deployment із hasura/graphql-engine у дереві k8s — nginx-sidecar не вимагається (abie.mdc)')
|
|
1770
|
+
return
|
|
1771
|
+
}
|
|
1772
|
+
for (const pkgAbs of [...hasuraPkgDirs].toSorted((a, b) => a.localeCompare(b))) {
|
|
1773
|
+
const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
|
|
1774
|
+
const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
|
|
1775
|
+
if (!existsSync(ruAbs)) {
|
|
1776
|
+
passFn(`${relPkg}/k8s: є Hasura Deployment, але немає ru/kustomization.yaml — nginx-sidecar не перевіряється`)
|
|
1777
|
+
continue
|
|
1778
|
+
}
|
|
1779
|
+
let ruRaw
|
|
1780
|
+
try {
|
|
1781
|
+
ruRaw = await readFile(ruAbs, 'utf8')
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1784
|
+
fail(`${relPkg}/k8s/ru/kustomization.yaml: не вдалося прочитати (${msg})`)
|
|
1785
|
+
return
|
|
1786
|
+
}
|
|
1787
|
+
if (!ruRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)) {
|
|
1788
|
+
passFn(
|
|
1789
|
+
`${relPkg}/k8s/ru/kustomization.yaml: немає ${HASURA_JWT_SECRET_IN_KUSTOMIZATION} — nginx-sidecar не вимагається (abie.mdc)`
|
|
1790
|
+
)
|
|
1791
|
+
continue
|
|
1792
|
+
}
|
|
1793
|
+
const relRu = relative(root, ruAbs).replaceAll('\\', '/') || ruAbs
|
|
1794
|
+
// configmap-nginx.yaml must exist
|
|
1795
|
+
const configmapNginxAbs = join(pkgAbs, 'k8s', 'ru', 'configmap-nginx.yaml')
|
|
1796
|
+
if (!existsSync(configmapNginxAbs)) {
|
|
1797
|
+
fail(`${relPkg}/k8s/ru: потрібен configmap-nginx.yaml з nginx.conf (nginx-sidecar для Hasura WebSocket, abie.mdc)`)
|
|
1798
|
+
return
|
|
1799
|
+
}
|
|
1800
|
+
passFn(`${relPkg}/k8s/ru/configmap-nginx.yaml: існує`)
|
|
1801
|
+
// kustomization resources must include configmap-nginx.yaml
|
|
1802
|
+
if (!RESOURCES_CONFIGMAP_NGINX_RE.test(ruRaw)) {
|
|
1803
|
+
fail(`${relRu}: у resources потрібен configmap-nginx.yaml (nginx-sidecar, abie.mdc)`)
|
|
1804
|
+
return
|
|
1805
|
+
}
|
|
1806
|
+
passFn(`${relRu}: resources містить configmap-nginx.yaml`)
|
|
1807
|
+
// Service -hl patch must include port: 8081 (proxy)
|
|
1808
|
+
const svcPatchByName = collectAbieRuServicePatchTextByTargetNameFromRaw(ruRaw)
|
|
1809
|
+
const hasHlWith8081 = [...svcPatchByName.entries()].some(
|
|
1810
|
+
([name, pt]) => name.endsWith('-hl') && PATCH_PROXY_PORT_8081_RE.test(pt)
|
|
1811
|
+
)
|
|
1812
|
+
if (!hasHlWith8081) {
|
|
1813
|
+
fail(`${relRu}: у patch Service -hl потрібен port: 8081 (proxy) для nginx-sidecar (abie.mdc)`)
|
|
1814
|
+
return
|
|
1815
|
+
}
|
|
1816
|
+
passFn(`${relRu}: Service -hl patch містить port 8081 (nginx-sidecar)`)
|
|
1817
|
+
// Deployment patch must include nginx-sidecar (image nginx:*-alpine + containerPort: 8081)
|
|
1818
|
+
const deployPatches = collectDeploymentPatchTextsFromKustomization(ruRaw)
|
|
1819
|
+
const hasNginxSidecar = deployPatches.some(
|
|
1820
|
+
pt => NGINX_SIDECAR_IMAGE_RE.test(pt) && NGINX_SIDECAR_CONTAINER_PORT_RE.test(pt)
|
|
1821
|
+
)
|
|
1822
|
+
if (!hasNginxSidecar) {
|
|
1823
|
+
fail(
|
|
1824
|
+
`${relRu}: у patch Deployment потрібен nginx-sidecar (image nginx:…-alpine, containerPort: 8081) — abie.mdc`
|
|
1825
|
+
)
|
|
1826
|
+
return
|
|
1827
|
+
}
|
|
1828
|
+
passFn(`${relRu}: Deployment patch містить nginx-sidecar (image + containerPort 8081)`)
|
|
1829
|
+
// HTTPRoute patch must replace a backendRef port to 8081
|
|
1830
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(ruRaw)
|
|
1831
|
+
if (
|
|
1832
|
+
!HTTPROUTE_BACKENDREF_PORT_8081_RE.test(combined) &&
|
|
1833
|
+
!HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE.test(combined)
|
|
1834
|
+
) {
|
|
1835
|
+
fail(
|
|
1836
|
+
`${relRu}: у patch HTTPRoute потрібен JSON6902 з path /spec/rules/…/backendRefs/…/port та value: 8081 (nginx-sidecar, abie.mdc)`
|
|
1837
|
+
)
|
|
1838
|
+
return
|
|
1839
|
+
}
|
|
1840
|
+
passFn(`${relRu}: HTTPRoute patch замінює порт на 8081 (nginx-sidecar)`)
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1630
1844
|
/**
|
|
1631
1845
|
* Перевіряє відповідність проєкту правилам abie.mdc.
|
|
1632
1846
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -1671,5 +1885,8 @@ export async function check() {
|
|
|
1671
1885
|
await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
|
|
1672
1886
|
}
|
|
1673
1887
|
|
|
1888
|
+
pass('Перевіряємо nginx-sidecar для Hasura WebSocket у ru (abie.mdc)')
|
|
1889
|
+
await ensureAbieNginxSidecarForHasura(root, yamlFiles, fail, pass)
|
|
1890
|
+
|
|
1674
1891
|
return reporter.getExitCode()
|
|
1675
1892
|
}
|
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
|
}
|