@nitra/cursor 1.8.110 → 1.8.112
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 +23 -4
- package/mdc/abie.mdc +142 -0
- package/package.json +1 -1
- package/schemas/n-cursor.json +8 -0
- package/scripts/check-abie.mjs +217 -0
- package/scripts/check-k8s.mjs +5 -8
- package/scripts/utils/bunyan-imports.mjs +1 -1
package/bin/n-cursor.js
CHANGED
|
@@ -221,6 +221,10 @@ async function readConfig(paths = {}) {
|
|
|
221
221
|
if ('skills' in parsedConfig && !Array.isArray(parsedConfig.skills)) {
|
|
222
222
|
throw new Error(`У ${CONFIG_FILE} поле "skills" має бути масивом рядків`)
|
|
223
223
|
}
|
|
224
|
+
const { ignore } = parsedConfig
|
|
225
|
+
if (ignore !== undefined && (!Array.isArray(ignore) || ignore.some(p => typeof p !== 'string'))) {
|
|
226
|
+
throw new Error(`У ${CONFIG_FILE} поле "ignore" має бути масивом рядків (шляхів до директорій)`)
|
|
227
|
+
}
|
|
224
228
|
|
|
225
229
|
const rootPkg = await readRootPackageJsonSafe()
|
|
226
230
|
const disableRules = normalizeIdList(parsedConfig['disable-rules'])
|
|
@@ -571,10 +575,23 @@ async function buildClaudeSkillsSectionLines() {
|
|
|
571
575
|
* Завдяки цьому Claude Code автоматично завантажує вміст кожного правила при старті.
|
|
572
576
|
* @returns {Promise<void>}
|
|
573
577
|
*/
|
|
574
|
-
|
|
578
|
+
/**
|
|
579
|
+
* @param {string[]} [ignore] директорії заборонені для редагування
|
|
580
|
+
*/
|
|
581
|
+
async function syncClaudeMd(ignore) {
|
|
575
582
|
const lines = [`<!-- Цей файл генерується автоматично через \`npx ${PACKAGE_NAME}\`. Не редагуй вручну. -->`, '']
|
|
576
|
-
const mdcFiles = await listProjectRulesMdcFiles()
|
|
577
583
|
|
|
584
|
+
if (Array.isArray(ignore) && ignore.length > 0) {
|
|
585
|
+
lines.push('## Захищені директорії', '', 'Ніколи не змінюй, не видаляй і не створюй файли у цих директоріях:')
|
|
586
|
+
for (const dir of ignore) {
|
|
587
|
+
let d = dir
|
|
588
|
+
while (d.endsWith('/')) d = d.slice(0, -1)
|
|
589
|
+
lines.push(`- \`${d}/\``)
|
|
590
|
+
}
|
|
591
|
+
lines.push('')
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const mdcFiles = await listProjectRulesMdcFiles()
|
|
578
595
|
for (const mdcFile of mdcFiles) {
|
|
579
596
|
lines.push(`@${RULES_DIR}/${mdcFile}`)
|
|
580
597
|
}
|
|
@@ -971,7 +988,7 @@ async function runSync() {
|
|
|
971
988
|
|
|
972
989
|
const config = await runSyncStep('❌ ', () => readConfig({ bundledMdcDir, bundledSkillsDir }))
|
|
973
990
|
|
|
974
|
-
const { rules, skills, version } = config
|
|
991
|
+
const { rules, skills, version, ignore } = config
|
|
975
992
|
const bundledVer = await readBundledVersionAt(effectivePackageRoot)
|
|
976
993
|
if (bundledVer) {
|
|
977
994
|
const line =
|
|
@@ -1025,7 +1042,9 @@ async function runSync() {
|
|
|
1025
1042
|
})
|
|
1026
1043
|
|
|
1027
1044
|
await runSyncStep(`❌ Не вдалося оновити ${AGENTS_FILE}: `, () => syncAgentsMd(bundledAgentsTemplatePath))
|
|
1028
|
-
await runSyncStep('❌ Не вдалося оновити CLAUDE.md: ', () =>
|
|
1045
|
+
await runSyncStep('❌ Не вдалося оновити CLAUDE.md: ', () =>
|
|
1046
|
+
syncClaudeMd(/** @type {string[] | undefined} */ (ignore))
|
|
1047
|
+
)
|
|
1029
1048
|
|
|
1030
1049
|
console.log(`\n✨ Готово: ${successCount} завантажено, ${failCount} з помилками\n`)
|
|
1031
1050
|
if (failCount > 0) {
|
package/mdc/abie.mdc
CHANGED
|
@@ -113,6 +113,148 @@ spec:
|
|
|
113
113
|
gwin.yandex.cloud/rules.http.upgradeTypes: "websocket"
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
## k8s: overlay **ru** і nginx-sidecar для WebSocket (Hasura)
|
|
117
|
+
|
|
118
|
+
YC ALB (gwin) має баг: якщо HTTPRoute-правило містить одночасно `URLRewrite` (ReplacePrefixMatch) і `upgrade_types: websocket` — ALB не обробляє WebSocket і повертає 404.
|
|
119
|
+
<https://center.yandex.cloud/support/tickets/TX549394>
|
|
120
|
+
|
|
121
|
+
Обхідний варіант: nginx-sidecar у поді, що сам виконує rewrite і проксіює WebSocket до Hasura.
|
|
122
|
+
|
|
123
|
+
**Умова:** в дереві `k8s` є Deployment з image `hasura/graphql-engine` (або `newName` на нього через `images:` у kustomization) **і** `ru/kustomization.yaml` містить `HASURA_GRAPHQL_JWT_SECRET` (patch на ConfigMap Hasura з JWT).
|
|
124
|
+
|
|
125
|
+
### Що потрібно зробити
|
|
126
|
+
|
|
127
|
+
**1. `ru/configmap-nginx.yaml`** — ConfigMap з nginx.conf:
|
|
128
|
+
|
|
129
|
+
```yaml title="…/k8s/ru/configmap-nginx.yaml"
|
|
130
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/configmap-v1.json
|
|
131
|
+
apiVersion: v1
|
|
132
|
+
kind: ConfigMap
|
|
133
|
+
metadata:
|
|
134
|
+
name: СЕРВІС-nginx
|
|
135
|
+
namespace: ru-NAMESPACE
|
|
136
|
+
data:
|
|
137
|
+
nginx.conf: |
|
|
138
|
+
user nginx;
|
|
139
|
+
worker_processes auto;
|
|
140
|
+
error_log /dev/null;
|
|
141
|
+
|
|
142
|
+
events {
|
|
143
|
+
worker_connections 1024;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
http {
|
|
147
|
+
access_log off;
|
|
148
|
+
|
|
149
|
+
map $http_upgrade $connection_upgrade {
|
|
150
|
+
default upgrade;
|
|
151
|
+
'' close;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
server {
|
|
155
|
+
listen 8081;
|
|
156
|
+
server_name _;
|
|
157
|
+
|
|
158
|
+
location /healthz {
|
|
159
|
+
add_header Content-Type text/plain;
|
|
160
|
+
return 200 "healthy";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# WebSocket: ALB не робить rewrite (URLRewrite + upgrade = bug YC ALB)
|
|
164
|
+
# nginx реврайтить /PREFIX/* → /* і проксіює до Hasura
|
|
165
|
+
location /PREFIX/ {
|
|
166
|
+
rewrite ^/PREFIX/(.*)$ /$1 break;
|
|
167
|
+
proxy_pass http://127.0.0.1:8080;
|
|
168
|
+
proxy_http_version 1.1;
|
|
169
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
170
|
+
proxy_set_header Connection $connection_upgrade;
|
|
171
|
+
proxy_set_header Host $host;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# HTTP: ALB вже зробив prefix_rewrite → /, nginx просто проксіює
|
|
175
|
+
location / {
|
|
176
|
+
proxy_pass http://127.0.0.1:8080;
|
|
177
|
+
proxy_http_version 1.1;
|
|
178
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
179
|
+
proxy_set_header Connection $connection_upgrade;
|
|
180
|
+
proxy_set_header Host $host;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**2. `ru/kustomization.yaml`** — додати до `resources` та чотири patch-блоки:
|
|
187
|
+
|
|
188
|
+
```yaml title="…/k8s/ru/kustomization.yaml (фрагмент)"
|
|
189
|
+
resources:
|
|
190
|
+
- ../base
|
|
191
|
+
- configmap-nginx.yaml # ← додати
|
|
192
|
+
|
|
193
|
+
patches:
|
|
194
|
+
# Headless Service: замінити ports на два іменованих (hasura:8080 + proxy:8081)
|
|
195
|
+
- target:
|
|
196
|
+
kind: Service
|
|
197
|
+
name: СЕРВІС-hl
|
|
198
|
+
patch: |-
|
|
199
|
+
- op: replace
|
|
200
|
+
path: /spec/type
|
|
201
|
+
value: NodePort
|
|
202
|
+
- op: remove
|
|
203
|
+
path: /spec/clusterIP
|
|
204
|
+
- op: replace
|
|
205
|
+
path: /spec/ports
|
|
206
|
+
value:
|
|
207
|
+
- name: hasura
|
|
208
|
+
protocol: TCP
|
|
209
|
+
port: 8080
|
|
210
|
+
- name: proxy
|
|
211
|
+
protocol: TCP
|
|
212
|
+
port: 8081
|
|
213
|
+
|
|
214
|
+
# Deployment: додати nginx-sidecar контейнер та volume
|
|
215
|
+
- target:
|
|
216
|
+
kind: Deployment
|
|
217
|
+
name: СЕРВІС
|
|
218
|
+
patch: |-
|
|
219
|
+
- op: add
|
|
220
|
+
path: /spec/template/spec/containers/-
|
|
221
|
+
value:
|
|
222
|
+
name: СЕРВІС-p
|
|
223
|
+
image: nginx:1.27-alpine
|
|
224
|
+
ports:
|
|
225
|
+
- containerPort: 8081
|
|
226
|
+
protocol: TCP
|
|
227
|
+
volumeMounts:
|
|
228
|
+
- name: nginx-conf
|
|
229
|
+
mountPath: /etc/nginx/nginx.conf
|
|
230
|
+
subPath: nginx.conf
|
|
231
|
+
resources: {}
|
|
232
|
+
- op: add
|
|
233
|
+
path: /spec/template/spec/volumes
|
|
234
|
+
value:
|
|
235
|
+
- name: nginx-conf
|
|
236
|
+
configMap:
|
|
237
|
+
name: СЕРВІС-nginx
|
|
238
|
+
|
|
239
|
+
# HTTPRoute: прибрати URLRewrite-фільтри з PathPrefix-правила,
|
|
240
|
+
# направити трафік на nginx:8081, видалити мертве WebSocket-правило
|
|
241
|
+
- target:
|
|
242
|
+
kind: HTTPRoute
|
|
243
|
+
name: СЕРВІС
|
|
244
|
+
patch: |-
|
|
245
|
+
- op: remove
|
|
246
|
+
path: /spec/rules/INDEX_PATHPREFIX/filters
|
|
247
|
+
- op: replace
|
|
248
|
+
path: /spec/rules/INDEX_PATHPREFIX/backendRefs/0/port
|
|
249
|
+
value: 8081
|
|
250
|
+
- op: remove
|
|
251
|
+
path: /spec/rules/INDEX_WEBSOCKET
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
> `INDEX_PATHPREFIX` і `INDEX_WEBSOCKET` — індекси правил у `base/hr.yaml`. Після видалення filters ALB більше не додає `prefix_rewrite`, тому nginx-sidecar отримує оригінальний шлях і сам виконує rewrite.
|
|
255
|
+
|
|
256
|
+
**Примітка щодо Service:** якщо в base у `-hl` Service два порти задаються вперше, `spec.ports[*].name` є обов'язковим — без нього `kubectl apply` поверне помилку `Required value`.
|
|
257
|
+
|
|
116
258
|
## k8s: overlay **ru** і **Service** (у т. ч. headless → NodePort)
|
|
117
259
|
|
|
118
260
|
Для кожного **Service** в YAML під **`…/k8s/…`**, де шлях файлу **не** містить **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару; у т. ч. **headless** з **`spec.clusterIP: None`** і **`-hl`**), якщо ще не **`spec.type: NodePort`** / **`LoadBalancer`** / **`ExternalName`**, у **`k8s/ru/kustomization.yaml`** того ж пакета (overlay **ru**) додай **inline** **JSON6902** у **`patches`**: **`target.kind: Service`**, **`target.name`** як у маніфеста, **`path: /spec/type`**, **`value: NodePort`**. Якщо в base було **`spec.clusterIP: None`**, у тому ж **patch** додай **`op: remove`** для **`/spec/clusterIP`**. Якщо в base **явно** задано **`spec.clusterIPs`** (зокрема **`['None']`**), додай **`op: remove`** і для **`/spec/clusterIPs`** — інакше **API** може відхилити **NodePort** (*`spec.clusterIPs[0]: Invalid value: "None"`*). **Не** додавай **`remove`** на **`/spec/clusterIPs`**, якщо ключа **немає** в base: **`kubectl kustomize`** тоді падає (*Unable to remove nonexistent key*). Якщо в base лише **`clusterIP: None`**, а помилка лишається після **`kubectl apply -k`** (злиття з уже існуючим **Service** у кластері), тимчасово **видали** **`Service`** у **`ru`** і застосуй знову, або додай у base поле **`clusterIPs`**, щоб **`remove`** у patch був валідний для **kustomize**. Деталі — **`check-abie.mjs`**.
|
package/package.json
CHANGED
package/schemas/n-cursor.json
CHANGED
|
@@ -43,6 +43,14 @@
|
|
|
43
43
|
"minLength": 1
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
|
+
"ignore": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"description": "Директорії, у яких заборонено будь-які модифікації файлів (AI не редагує, не видаляє, не створює файли). Шляхи відносно кореня репозиторію. Наприклад: [\"packages/vendor\", \"packages/generated\"].",
|
|
49
|
+
"items": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"minLength": 1
|
|
52
|
+
}
|
|
53
|
+
},
|
|
46
54
|
"version": {
|
|
47
55
|
"type": "string",
|
|
48
56
|
"description": "Застаріле поле, ігнорується CLI. Правила завжди копіюються з каталогу mdc/ установленого пакету (node_modules або кеш npx); змініть версію через оновлення залежності."
|
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-k8s.mjs
CHANGED
|
@@ -2344,12 +2344,7 @@ function findHasuraCanonStart(rules) {
|
|
|
2344
2344
|
} else {
|
|
2345
2345
|
const m = asPlainRecord(matches[0])
|
|
2346
2346
|
const p = m === null || m.headers !== undefined ? null : asPlainRecord(m.path)
|
|
2347
|
-
if (
|
|
2348
|
-
p !== null &&
|
|
2349
|
-
p.type === 'Exact' &&
|
|
2350
|
-
typeof p.value === 'string' &&
|
|
2351
|
-
p.value.endsWith('/ql')
|
|
2352
|
-
) {
|
|
2347
|
+
if (p !== null && p.type === 'Exact' && typeof p.value === 'string' && p.value.endsWith('/ql')) {
|
|
2353
2348
|
return { prefix: p.value.slice(0, -'/ql'.length), startIndex: i }
|
|
2354
2349
|
}
|
|
2355
2350
|
}
|
|
@@ -2641,7 +2636,7 @@ async function indexOneK8sYamlForHasuraCanon(abs, hasuraByDir, httpRoutes) {
|
|
|
2641
2636
|
* Якщо документ — Hasura-Deployment із непорожнім `metadata.name`, додає ім'я до індексу за каталогом.
|
|
2642
2637
|
* @param {Record<string, unknown>} rec корінь YAML-документа
|
|
2643
2638
|
* @param {string} dir абсолютний шлях до каталогу файлу
|
|
2644
|
-
* @param {Map<string, Set<string>>} hasuraByDir індекс Hasura Deployment-ів за каталогом (
|
|
2639
|
+
* @param {Map<string, Set<string>>} hasuraByDir індекс Hasura Deployment-ів за каталогом (під час обходу в нього додаються імена)
|
|
2645
2640
|
* @returns {void}
|
|
2646
2641
|
*/
|
|
2647
2642
|
function recordHasuraDeploymentName(rec, dir, hasuraByDir) {
|
|
@@ -2677,7 +2672,9 @@ async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
|
|
|
2677
2672
|
const v = httpRouteHasuraCanonViolation(hr.obj)
|
|
2678
2673
|
if (v !== null) {
|
|
2679
2674
|
const rel = (relative(root, hr.abs) || hr.abs).replaceAll('\\', '/')
|
|
2680
|
-
fail(
|
|
2675
|
+
fail(
|
|
2676
|
+
`${rel}: HTTPRoute «${name}» (документ ${hr.docIndex}; прив'язано до Hasura-Deployment у тому ж каталозі): ${v}`
|
|
2677
|
+
)
|
|
2681
2678
|
}
|
|
2682
2679
|
}
|
|
2683
2680
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Семантика береться з **oxc-parser** (`module.staticImports`) — без regex по тілу файлу.
|
|
6
6
|
* Додатково по AST програми ловимо `require('@nitra/bunyan')` і динамічний `import('@nitra/bunyan')`,
|
|
7
|
-
* щоб правило працювало й у CommonJS
|
|
7
|
+
* щоб правило працювало й у CommonJS і при динамічному import у межах одного файлу.
|
|
8
8
|
*
|
|
9
9
|
* Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається порожній
|
|
10
10
|
* результат — спочатку треба полагодити синтаксис, потім перезапустити перевірку.
|