@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 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 і проксіює WebSocket до Hasura.
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 реврайтить /PREFIX/* → /* і проксіює до Hasura
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`), у **кожному** прод-оверлеї `kustomization.yaml` у `patches[]` **обов'язково** має бути перевизначення:
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` перевіряє **наявність** оверрайду відповідного поля. Конкретне значення має задовольняти прод-мінімуми — це видно у вмісті patch і остаточно матеріалізується під час збірки Kustomize.
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: 3
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; прод-оверлеї перевизначають на ≥ 2
353
- maxReplicas: 1 # dev-like; прод-оверлеї перевизначають на ≥ 2
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 для проду `minReplicas`, `maxReplicas`, `minAvailable` підіймаєш до прод-мінімумів через **Kustomize patches** або окремі `hpa.yaml` / `pdb.yaml` у каталозі overlay (тоді перевірка спрацьовує за їхнім середовищем).
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
- > **Автоміграція:** `npx @nitra/cursor check nginx-default-tpl` автоматично перейменовує `default.tpl.conf` → `default.conf.template` (або перезаписує вміст, якщо обидва файли існують). Якщо шаблон відсутній — перевірка пропускається.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.119",
3
+ "version": "1.8.120",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -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
- 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
- }
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 (!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
- }
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
- 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)`)
1921
+ await checkNginxSidecarForHasuraPackage(root, pkgAbs, fail, passFn)
1841
1922
  }
1842
1923
  }
1843
1924
 
@@ -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
- // 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
- }
330
+ validateGitAiParsedYaml(root, passFn, failFn)
315
331
  }
316
332
 
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')
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 {