@nitra/cursor 1.8.119 → 1.8.120
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/mdc/abie.mdc +3 -3
- package/mdc/k8s.mdc +6 -6
- package/mdc/nginx-default-tpl.mdc +1 -1
- package/package.json +1 -1
- package/scripts/check-abie.mjs +174 -93
- package/scripts/check-ga.mjs +51 -38
- package/scripts/check-k8s.mjs +680 -404
package/mdc/abie.mdc
CHANGED
|
@@ -118,7 +118,7 @@ spec:
|
|
|
118
118
|
YC ALB (gwin) має баг: якщо HTTPRoute-правило містить одночасно `URLRewrite` (ReplacePrefixMatch) і `upgrade_types: websocket` — ALB не обробляє WebSocket і повертає 404.
|
|
119
119
|
<https://center.yandex.cloud/support/tickets/TX549394>
|
|
120
120
|
|
|
121
|
-
Обхідний варіант: nginx-sidecar у поді, що сам виконує rewrite і
|
|
121
|
+
Обхідний варіант: nginx-sidecar у поді, що сам виконує rewrite і передає WebSocket до Hasura.
|
|
122
122
|
|
|
123
123
|
**Умова:** в дереві `k8s` є Deployment з image `hasura/graphql-engine` (або `newName` на нього через `images:` у kustomization) **і** `ru/kustomization.yaml` містить `HASURA_GRAPHQL_JWT_SECRET` (patch на ConfigMap Hasura з JWT).
|
|
124
124
|
|
|
@@ -161,7 +161,7 @@ data:
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
# WebSocket: ALB не робить rewrite (URLRewrite + upgrade = bug YC ALB)
|
|
164
|
-
# nginx
|
|
164
|
+
# nginx переписує /PREFIX/* → /* і передає до Hasura
|
|
165
165
|
location /PREFIX/ {
|
|
166
166
|
rewrite ^/PREFIX/(.*)$ /$1 break;
|
|
167
167
|
proxy_pass http://127.0.0.1:8080;
|
|
@@ -171,7 +171,7 @@ data:
|
|
|
171
171
|
proxy_set_header Host $host;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
# HTTP: ALB вже зробив prefix_rewrite → /, nginx просто
|
|
174
|
+
# HTTP: ALB вже зробив prefix_rewrite → /, nginx просто передає
|
|
175
175
|
location / {
|
|
176
176
|
proxy_pass http://127.0.0.1:8080;
|
|
177
177
|
proxy_http_version 1.1;
|
package/mdc/k8s.mdc
CHANGED
|
@@ -308,12 +308,12 @@ data:
|
|
|
308
308
|
|
|
309
309
|
### Прод-оверрайди у `kustomization.yaml`
|
|
310
310
|
|
|
311
|
-
Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному**
|
|
311
|
+
Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному** прод-накладенні `kustomization.yaml` у `patches[]` **обов'язково** має бути перевизначення:
|
|
312
312
|
|
|
313
313
|
- для `HorizontalPodAutoscaler`: `spec.minReplicas` **і** `spec.maxReplicas` (щоб у проді вийшло ≥2).
|
|
314
314
|
- для `PodDisruptionBudget`: `spec.minAvailable` (щоб у проді вийшло ≥1).
|
|
315
315
|
|
|
316
|
-
Формат patch — JSON6902 або Strategic Merge; `check k8s` перевіряє **наявність**
|
|
316
|
+
Формат patch — JSON6902 або Strategic Merge; `check k8s` перевіряє **наявність** перевизначення відповідного поля. Конкретне значення має задовольняти прод-мінімуми — це видно у вмісті patch і остаточно матеріалізується під час збірки Kustomize.
|
|
317
317
|
|
|
318
318
|
```yaml title="k8s/prod/kustomization.yaml (фрагмент)"
|
|
319
319
|
patches:
|
|
@@ -323,7 +323,7 @@ patches:
|
|
|
323
323
|
patch: |-
|
|
324
324
|
- op: replace
|
|
325
325
|
path: /spec/minReplicas
|
|
326
|
-
value:
|
|
326
|
+
value: 2
|
|
327
327
|
- op: replace
|
|
328
328
|
path: /spec/maxReplicas
|
|
329
329
|
value: 10
|
|
@@ -349,8 +349,8 @@ spec:
|
|
|
349
349
|
apiVersion: apps/v1
|
|
350
350
|
kind: Deployment
|
|
351
351
|
name: backend-api
|
|
352
|
-
minReplicas: 1 # dev-like;
|
|
353
|
-
maxReplicas: 1 # dev-like;
|
|
352
|
+
minReplicas: 1 # dev-like; прод-накладення перевизначають на ≥ 2
|
|
353
|
+
maxReplicas: 1 # dev-like; прод-накладення перевизначають на ≥ 2
|
|
354
354
|
metrics:
|
|
355
355
|
- type: Resource
|
|
356
356
|
resource:
|
|
@@ -404,7 +404,7 @@ spec:
|
|
|
404
404
|
app: backend-api
|
|
405
405
|
```
|
|
406
406
|
|
|
407
|
-
В overlays для
|
|
407
|
+
В overlays для прод-середовища `minReplicas`, `maxReplicas`, `minAvailable` підіймаєш до прод-мінімумів через **Kustomize patches** або окремі `hpa.yaml` / `pdb.yaml` у каталозі overlay (тоді перевірка спрацьовує за їхнім середовищем).
|
|
408
408
|
|
|
409
409
|
## HorizontalPodAutoscaler: `autoscaling/v2`
|
|
410
410
|
|
|
@@ -3,7 +3,7 @@ description: Правила nginx для статичних файлів
|
|
|
3
3
|
version: '1.2'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
>
|
|
6
|
+
> **Автоматична міграція:** `npx @nitra/cursor check nginx-default-tpl` автоматично перейменовує `default.tpl.conf` → `default.conf.template` (або перезаписує вміст, якщо обидва файли існують). Якщо шаблон відсутній — перевірка пропускається.
|
|
7
7
|
|
|
8
8
|
default.conf.template повинен виглядати так:
|
|
9
9
|
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -1647,7 +1647,7 @@ async function checkHcYamlFiles(root, deploymentDirs, pass, fail) {
|
|
|
1647
1647
|
/**
|
|
1648
1648
|
* Чи Deployment-документ містить контейнер із образом **`hasura/graphql-engine`** (abie.mdc nginx-sidecar).
|
|
1649
1649
|
* @param {unknown} obj корінь YAML-документа
|
|
1650
|
-
* @returns {boolean}
|
|
1650
|
+
* @returns {boolean} true якщо документ є Deployment із hasura/graphql-engine
|
|
1651
1651
|
*/
|
|
1652
1652
|
function deploymentDocHasHasuraImage(obj) {
|
|
1653
1653
|
if (!isDeploymentDoc(obj)) return false
|
|
@@ -1672,7 +1672,7 @@ function deploymentDocHasHasuraImage(obj) {
|
|
|
1672
1672
|
/**
|
|
1673
1673
|
* Чи Kustomization-документ містить у **`images[*].newName`** рядок **`hasura/graphql-engine`** (abie.mdc nginx-sidecar).
|
|
1674
1674
|
* @param {unknown} obj корінь YAML-документа
|
|
1675
|
-
* @returns {boolean}
|
|
1675
|
+
* @returns {boolean} true якщо є images[*].newName із hasura/graphql-engine
|
|
1676
1676
|
*/
|
|
1677
1677
|
function kustomizationDocHasHasuraImageNewName(obj) {
|
|
1678
1678
|
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return false
|
|
@@ -1687,6 +1687,50 @@ function kustomizationDocHasHasuraImageNewName(obj) {
|
|
|
1687
1687
|
return false
|
|
1688
1688
|
}
|
|
1689
1689
|
|
|
1690
|
+
/**
|
|
1691
|
+
* Чи patch-запис у Kustomization має **target.kind === 'Deployment'**.
|
|
1692
|
+
* @param {unknown} patchEntry елемент масиву **patches**
|
|
1693
|
+
* @returns {boolean} true, якщо patch цілить на Deployment
|
|
1694
|
+
*/
|
|
1695
|
+
function isPatchTargetingDeployment(patchEntry) {
|
|
1696
|
+
if (patchEntry === null || typeof patchEntry !== 'object' || Array.isArray(patchEntry)) return false
|
|
1697
|
+
const pr = /** @type {Record<string, unknown>} */ (patchEntry)
|
|
1698
|
+
const target = pr.target
|
|
1699
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) return false
|
|
1700
|
+
return /** @type {Record<string, unknown>} */ (target).kind === 'Deployment'
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* Витягує текст **patch** із запису Kustomization, якщо він непорожній рядок.
|
|
1705
|
+
* @param {unknown} patchEntry елемент масиву **patches**
|
|
1706
|
+
* @returns {string | null} текст patch або null
|
|
1707
|
+
*/
|
|
1708
|
+
function extractPatchString(patchEntry) {
|
|
1709
|
+
const pr = /** @type {Record<string, unknown>} */ (patchEntry)
|
|
1710
|
+
const patchStr = pr.patch
|
|
1711
|
+
if (typeof patchStr === 'string' && patchStr.trim() !== '') return patchStr
|
|
1712
|
+
return null
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* Збирає тексти inline **patch** для **Deployment** з одного YAML-документа Kustomization.
|
|
1717
|
+
* @param {import('yaml').Document} doc YAML-документ
|
|
1718
|
+
* @param {string[]} out масив для збору результатів
|
|
1719
|
+
*/
|
|
1720
|
+
function collectDeploymentPatchTextsFromDoc(doc, out) {
|
|
1721
|
+
if (doc.errors.length > 0) return
|
|
1722
|
+
const root = doc.toJSON()
|
|
1723
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) return
|
|
1724
|
+
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
1725
|
+
if (!Array.isArray(rec.patches)) return
|
|
1726
|
+
for (const p of rec.patches) {
|
|
1727
|
+
if (isPatchTargetingDeployment(p)) {
|
|
1728
|
+
const text = extractPatchString(p)
|
|
1729
|
+
if (text !== null) out.push(text)
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1690
1734
|
/**
|
|
1691
1735
|
* Збирає тексти inline **patch** для **Deployment** з **kustomization.yaml** (усі документи).
|
|
1692
1736
|
* @param {string} raw повний текст файлу
|
|
@@ -1707,25 +1751,22 @@ function collectDeploymentPatchTextsFromKustomization(raw) {
|
|
|
1707
1751
|
/** @type {string[]} */
|
|
1708
1752
|
const out = []
|
|
1709
1753
|
for (const doc of docs) {
|
|
1710
|
-
|
|
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
|
-
}
|
|
1754
|
+
collectDeploymentPatchTextsFromDoc(doc, out)
|
|
1725
1755
|
}
|
|
1726
1756
|
return out
|
|
1727
1757
|
}
|
|
1728
1758
|
|
|
1759
|
+
/**
|
|
1760
|
+
* Чи YAML-документ містить образ Hasura (Deployment або Kustomization images).
|
|
1761
|
+
* @param {import('yaml').Document} doc YAML-документ
|
|
1762
|
+
* @returns {boolean} true, якщо документ посилається на hasura/graphql-engine
|
|
1763
|
+
*/
|
|
1764
|
+
function yamlDocReferencesHasuraImage(doc) {
|
|
1765
|
+
if (doc.errors.length > 0) return false
|
|
1766
|
+
const obj = doc.toJSON()
|
|
1767
|
+
return deploymentDocHasHasuraImage(obj) || kustomizationDocHasHasuraImageNewName(obj)
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1729
1770
|
/**
|
|
1730
1771
|
* Каталоги пакетів, де в дереві **k8s** є **Deployment** з образом **`hasura/graphql-engine`** або
|
|
1731
1772
|
* **Kustomization** з **`images[*].newName`** на нього (abie.mdc nginx-sidecar).
|
|
@@ -1739,19 +1780,126 @@ async function collectHasuraK8sPackageDirs(root, yamlAbs) {
|
|
|
1739
1780
|
for (const abs of yamlAbs) {
|
|
1740
1781
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1741
1782
|
const docs = await readAndParseYamlDocs(abs, rel, silentFail)
|
|
1742
|
-
if (
|
|
1743
|
-
|
|
1744
|
-
if (
|
|
1745
|
-
const obj = doc.toJSON()
|
|
1746
|
-
if (deploymentDocHasHasuraImage(obj) || kustomizationDocHasHasuraImageNewName(obj)) {
|
|
1747
|
-
const pkgDir = abiePackageDirFromK8sYamlRel(root, rel)
|
|
1748
|
-
if (pkgDir) dirs.add(pkgDir)
|
|
1749
|
-
}
|
|
1783
|
+
if (docs && docs.some(doc => yamlDocReferencesHasuraImage(doc))) {
|
|
1784
|
+
const pkgDir = abiePackageDirFromK8sYamlRel(root, rel)
|
|
1785
|
+
if (pkgDir) dirs.add(pkgDir)
|
|
1750
1786
|
}
|
|
1751
1787
|
}
|
|
1752
1788
|
return dirs
|
|
1753
1789
|
}
|
|
1754
1790
|
|
|
1791
|
+
/**
|
|
1792
|
+
* Якщо в дереві **k8s** є Deployment з **`hasura/graphql-engine`** і **`ru/kustomization.yaml`** містить
|
|
1793
|
+
* **`HASURA_GRAPHQL_JWT_SECRET`** — вимагає **nginx-sidecar** (abie.mdc):
|
|
1794
|
+
* **`ru/configmap-nginx.yaml`**, **`resources`** у kustomization, patch **Service -hl** (port 8081),
|
|
1795
|
+
* patch **Deployment** (nginx-sidecar image + containerPort 8081), patch **HTTPRoute** (port 8081).
|
|
1796
|
+
* @param {string} root корінь репозиторію
|
|
1797
|
+
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
1798
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1799
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1800
|
+
* @returns {Promise<void>}
|
|
1801
|
+
*/
|
|
1802
|
+
/**
|
|
1803
|
+
* Перевіряє nginx-sidecar вимоги у **ru/kustomization.yaml** для одного Hasura-пакета.
|
|
1804
|
+
* @param {string} relPkg відносний шлях пакета
|
|
1805
|
+
* @param {string} relRu відносний шлях до ru/kustomization.yaml
|
|
1806
|
+
* @param {string} pkgAbs абсолютний шлях до пакета
|
|
1807
|
+
* @param {string} ruRaw вміст ru/kustomization.yaml
|
|
1808
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1809
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1810
|
+
*/
|
|
1811
|
+
function validateNginxSidecarInRuKustomization(relPkg, relRu, pkgAbs, ruRaw, fail, passFn) {
|
|
1812
|
+
// configmap-nginx.yaml must exist
|
|
1813
|
+
const configmapNginxAbs = join(pkgAbs, 'k8s', 'ru', 'configmap-nginx.yaml')
|
|
1814
|
+
if (!existsSync(configmapNginxAbs)) {
|
|
1815
|
+
fail(`${relPkg}/k8s/ru: потрібен configmap-nginx.yaml з nginx.conf (nginx-sidecar для Hasura WebSocket, abie.mdc)`)
|
|
1816
|
+
return
|
|
1817
|
+
}
|
|
1818
|
+
passFn(`${relPkg}/k8s/ru/configmap-nginx.yaml: існує`)
|
|
1819
|
+
// kustomization resources must include configmap-nginx.yaml
|
|
1820
|
+
if (!RESOURCES_CONFIGMAP_NGINX_RE.test(ruRaw)) {
|
|
1821
|
+
fail(`${relRu}: у resources потрібен configmap-nginx.yaml (nginx-sidecar, abie.mdc)`)
|
|
1822
|
+
return
|
|
1823
|
+
}
|
|
1824
|
+
passFn(`${relRu}: resources містить configmap-nginx.yaml`)
|
|
1825
|
+
validateNginxSidecarPatches(relRu, ruRaw, fail, passFn)
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Перевіряє наявність nginx-sidecar patch (Service -hl, Deployment, HTTPRoute) у **ru/kustomization.yaml**.
|
|
1830
|
+
* @param {string} relRu відносний шлях до ru/kustomization.yaml
|
|
1831
|
+
* @param {string} ruRaw вміст ru/kustomization.yaml
|
|
1832
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1833
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1834
|
+
*/
|
|
1835
|
+
function validateNginxSidecarPatches(relRu, ruRaw, fail, passFn) {
|
|
1836
|
+
// Service -hl patch must include port: 8081 (proxy)
|
|
1837
|
+
const svcPatchByName = collectAbieRuServicePatchTextByTargetNameFromRaw(ruRaw)
|
|
1838
|
+
const hasHlWith8081 = [...svcPatchByName.entries()].some(
|
|
1839
|
+
([name, pt]) => name.endsWith('-hl') && PATCH_PROXY_PORT_8081_RE.test(pt)
|
|
1840
|
+
)
|
|
1841
|
+
if (!hasHlWith8081) {
|
|
1842
|
+
fail(`${relRu}: у patch Service -hl потрібен port: 8081 (proxy) для nginx-sidecar (abie.mdc)`)
|
|
1843
|
+
return
|
|
1844
|
+
}
|
|
1845
|
+
passFn(`${relRu}: Service -hl patch містить port 8081 (nginx-sidecar)`)
|
|
1846
|
+
// Deployment patch must include nginx-sidecar (image nginx:*-alpine + containerPort: 8081)
|
|
1847
|
+
const deployPatches = collectDeploymentPatchTextsFromKustomization(ruRaw)
|
|
1848
|
+
const hasNginxSidecar = deployPatches.some(
|
|
1849
|
+
pt => NGINX_SIDECAR_IMAGE_RE.test(pt) && NGINX_SIDECAR_CONTAINER_PORT_RE.test(pt)
|
|
1850
|
+
)
|
|
1851
|
+
if (!hasNginxSidecar) {
|
|
1852
|
+
fail(`${relRu}: у patch Deployment потрібен nginx-sidecar (image nginx:…-alpine, containerPort: 8081) — abie.mdc`)
|
|
1853
|
+
return
|
|
1854
|
+
}
|
|
1855
|
+
passFn(`${relRu}: Deployment patch містить nginx-sidecar (image + containerPort 8081)`)
|
|
1856
|
+
// HTTPRoute patch must replace a backendRef port to 8081
|
|
1857
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(ruRaw)
|
|
1858
|
+
if (
|
|
1859
|
+
!HTTPROUTE_BACKENDREF_PORT_8081_RE.test(combined) &&
|
|
1860
|
+
!HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE.test(combined)
|
|
1861
|
+
) {
|
|
1862
|
+
fail(
|
|
1863
|
+
`${relRu}: у patch HTTPRoute потрібен JSON6902 з path /spec/rules/…/backendRefs/…/port та value: 8081 (nginx-sidecar, abie.mdc)`
|
|
1864
|
+
)
|
|
1865
|
+
return
|
|
1866
|
+
}
|
|
1867
|
+
passFn(`${relRu}: HTTPRoute patch замінює порт на 8081 (nginx-sidecar)`)
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
/**
|
|
1871
|
+
* Обробляє один Hasura-пакет: читає **ru/kustomization.yaml** та перевіряє nginx-sidecar вимоги.
|
|
1872
|
+
* @param {string} root корінь репозиторію
|
|
1873
|
+
* @param {string} pkgAbs абсолютний шлях до пакета
|
|
1874
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1875
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1876
|
+
* @returns {Promise<void>}
|
|
1877
|
+
*/
|
|
1878
|
+
async function checkNginxSidecarForHasuraPackage(root, pkgAbs, fail, passFn) {
|
|
1879
|
+
const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
|
|
1880
|
+
const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
|
|
1881
|
+
if (!existsSync(ruAbs)) {
|
|
1882
|
+
passFn(`${relPkg}/k8s: є Hasura Deployment, але немає ru/kustomization.yaml — nginx-sidecar не перевіряється`)
|
|
1883
|
+
return
|
|
1884
|
+
}
|
|
1885
|
+
let ruRaw
|
|
1886
|
+
try {
|
|
1887
|
+
ruRaw = await readFile(ruAbs, 'utf8')
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1890
|
+
fail(`${relPkg}/k8s/ru/kustomization.yaml: не вдалося прочитати (${msg})`)
|
|
1891
|
+
return
|
|
1892
|
+
}
|
|
1893
|
+
if (!ruRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)) {
|
|
1894
|
+
passFn(
|
|
1895
|
+
`${relPkg}/k8s/ru/kustomization.yaml: немає ${HASURA_JWT_SECRET_IN_KUSTOMIZATION} — nginx-sidecar не вимагається (abie.mdc)`
|
|
1896
|
+
)
|
|
1897
|
+
return
|
|
1898
|
+
}
|
|
1899
|
+
const relRu = relative(root, ruAbs).replaceAll('\\', '/') || ruAbs
|
|
1900
|
+
validateNginxSidecarInRuKustomization(relPkg, relRu, pkgAbs, ruRaw, fail, passFn)
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1755
1903
|
/**
|
|
1756
1904
|
* Якщо в дереві **k8s** є Deployment з **`hasura/graphql-engine`** і **`ru/kustomization.yaml`** містить
|
|
1757
1905
|
* **`HASURA_GRAPHQL_JWT_SECRET`** — вимагає **nginx-sidecar** (abie.mdc):
|
|
@@ -1770,74 +1918,7 @@ async function ensureAbieNginxSidecarForHasura(root, yamlFilesAbs, fail, passFn)
|
|
|
1770
1918
|
return
|
|
1771
1919
|
}
|
|
1772
1920
|
for (const pkgAbs of [...hasuraPkgDirs].toSorted((a, b) => a.localeCompare(b))) {
|
|
1773
|
-
|
|
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)`)
|
|
1921
|
+
await checkNginxSidecarForHasuraPackage(root, pkgAbs, fail, passFn)
|
|
1841
1922
|
}
|
|
1842
1923
|
}
|
|
1843
1924
|
|
package/scripts/check-ga.mjs
CHANGED
|
@@ -265,6 +265,55 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Перевіряє, чи on.pull_request.types у parsed YAML містить 'closed'.
|
|
270
|
+
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
271
|
+
* @returns {boolean} true, якщо тригер pull_request має тип closed
|
|
272
|
+
*/
|
|
273
|
+
function hasPullRequestClosedTrigger(root) {
|
|
274
|
+
const on = root.on
|
|
275
|
+
if (!on || typeof on !== 'object') return false
|
|
276
|
+
const pr = /** @type {Record<string, unknown>} */ (on)['pull_request']
|
|
277
|
+
if (!pr || typeof pr !== 'object') return false
|
|
278
|
+
const types = /** @type {Record<string, unknown>} */ (pr).types
|
|
279
|
+
return Array.isArray(types) && types.includes('closed')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Перевіряє, чи будь-який job у parsed YAML має if-умову з 'merged'.
|
|
284
|
+
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
285
|
+
* @returns {boolean} true, якщо хоча б один job містить умову merged
|
|
286
|
+
*/
|
|
287
|
+
function hasJobMergedCondition(root) {
|
|
288
|
+
const { jobs } = root
|
|
289
|
+
if (!jobs || typeof jobs !== 'object') return false
|
|
290
|
+
return Object.values(jobs).some(job => {
|
|
291
|
+
if (!job || typeof job !== 'object') return false
|
|
292
|
+
const ifCond = String(/** @type {Record<string, unknown>} */ (job).if ?? '')
|
|
293
|
+
return ifCond.includes('merged')
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Перевіряє parsed YAML git-ai.yml: тригер closed та умова merged.
|
|
299
|
+
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
300
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
301
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
302
|
+
*/
|
|
303
|
+
function validateGitAiParsedYaml(root, passFn, failFn) {
|
|
304
|
+
if (hasPullRequestClosedTrigger(root)) {
|
|
305
|
+
passFn('git-ai.yml: on.pull_request.types містить closed')
|
|
306
|
+
} else {
|
|
307
|
+
failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (hasJobMergedCondition(root)) {
|
|
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
|
+
|
|
268
317
|
/**
|
|
269
318
|
* Перевіряє git-ai.yml: тригер pull_request з types: [closed], умова merged у job, виклик git-ai.
|
|
270
319
|
* @param {string} wfDir директорія workflows
|
|
@@ -278,46 +327,10 @@ async function checkGitAiWorkflow(wfDir, passFn, failFn) {
|
|
|
278
327
|
const root = parseWorkflowYaml(content)
|
|
279
328
|
|
|
280
329
|
if (root) {
|
|
281
|
-
|
|
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
|
-
}
|
|
330
|
+
validateGitAiParsedYaml(root, passFn, failFn)
|
|
315
331
|
}
|
|
316
332
|
|
|
317
|
-
|
|
318
|
-
const hasGitAiRun = root
|
|
319
|
-
? anyRunStepIncludes(root, 'git-ai ci github run')
|
|
320
|
-
: content.includes('git-ai ci github run')
|
|
333
|
+
const hasGitAiRun = root ? anyRunStepIncludes(root, 'git-ai ci github run') : content.includes('git-ai ci github run')
|
|
321
334
|
if (hasGitAiRun) {
|
|
322
335
|
passFn('git-ai.yml: крок виконує git-ai ci github run')
|
|
323
336
|
} else {
|