@nitra/cursor 1.8.173 → 1.8.177
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/.claude-template/npm-CLAUDE.md +9 -4
- package/CHANGELOG.md +30 -1
- package/bin/auto-rules.md +2 -0
- package/mdc/changelog.mdc +64 -0
- package/mdc/k8s.mdc +57 -0
- package/mdc/npm-module.mdc +2 -6
- package/package.json +1 -1
- package/scripts/auto-rules.mjs +2 -0
- package/scripts/check-changelog.mjs +402 -0
- package/scripts/check-k8s.mjs +109 -1
- package/scripts/check-npm-module.mjs +0 -55
|
@@ -4,20 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
Path-scoped нагадування для агента: підвантажується автоматично, коли редагуємо файли під `npm/`.
|
|
6
6
|
|
|
7
|
-
## Перед коміт-релевантними змінами в `npm/`
|
|
7
|
+
## Перед PR з коміт-релевантними змінами в `npm/`
|
|
8
8
|
|
|
9
9
|
1. Підвищ `version` у `npm/package.json` (build-bump, не більше одного кроку відносно `HEAD`).
|
|
10
10
|
2. Додай запис у `npm/CHANGELOG.md` форматом Keep a Changelog: `## [версія] - YYYY-MM-DD` + секції `### Added/Changed/Fixed/Removed`.
|
|
11
|
+
3. Переконайся, що `"CHANGELOG.md"` є в масиві `files` у `npm/package.json` (правило `changelog`).
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
Логіка PR-scoped: bump і запис достатньо зробити **один раз — як суму по всьому PR** (порівняння йде з гілкою `dev`), а не на кожен коміт.
|
|
14
|
+
|
|
15
|
+
Без оновленого CHANGELOG `npx @nitra/cursor check changelog` падає, а `Stop` hook блокує завершення ходу.
|
|
13
16
|
|
|
14
17
|
## Перевірка локально
|
|
15
18
|
|
|
16
19
|
```bash
|
|
20
|
+
npx @nitra/cursor check changelog
|
|
17
21
|
npx @nitra/cursor check npm-module
|
|
18
22
|
```
|
|
19
23
|
|
|
20
24
|
## Джерело правил
|
|
21
25
|
|
|
22
|
-
- `.cursor/rules/n-
|
|
23
|
-
-
|
|
26
|
+
- `.cursor/rules/n-changelog.mdc` — правило про CHANGELOG (PR-scoped, для всіх воркспейсів)
|
|
27
|
+
- `.cursor/rules/n-npm-module.mdc` — правило публікації пакета (типи, hk, npm-publish workflow)
|
|
28
|
+
- `npm/scripts/check-changelog.mjs`, `npm/scripts/check-npm-module.mjs` — алгоритми перевірки
|
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,36 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
-
## [1.8.
|
|
7
|
+
## [1.8.177] - 2026-05-05
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `changelog` (mdc v2.0): тепер дві моделі бази порівняння на рівні воркспейсу. **npm-published** (`name` + `files` + не `private: true`) — порівняння з опублікованою версією через `npm view <name> version` (git не задіяний; покриває кейс прямих комітів у `main` поза PR-flow). **local-only** (приватні / без `files`) — PR-scoped через `git merge-base <dev> HEAD`, що коректно обробляє: feature-гілку (видно лише унікальні коміти), `main` після merge `dev → main` (diff порожній → правило мовчить), direct-commit на `main` поза PR (ловиться як зміна, що потребує bump). Якщо реєстр недосяжний (офлайн / пакет не публікувався) — fail-safe pass, щоб локальна розробка не блокувалась.
|
|
12
|
+
- `check-changelog.mjs`: повний рефактор. Експорт `check(opts?)` з опційним `getPublishedVersion` для підстановки в тестах (CLI калить без аргументів — використовується дефолтний `npm view`-виклик з 10s таймаутом). Класифікація воркспейсів через `isNpmPublishable(pkg)`; для published — `checkPublishedWorkspace`, для local-only — окрема `runLocalOnlyChecks` із власною skip-логікою (no-git / on dev / no dev ref / no merge-base) і `resolveMergeBase(baseRef)` через `git merge-base`. Спільна `verifyChangelogEntry` для обох режимів.
|
|
13
|
+
- `n-changelog.mdc` / `mdc/changelog.mdc` (v1.1 → 2.0): переписано під дві моделі з прикладами кейсів.
|
|
14
|
+
- Тести `check-changelog.test.mjs`: 16 кейсів (раніше 11) — npm-mode (sync / out-of-sync / no CHANGELOG / no entry / files без `CHANGELOG.md` / offline), local-only skip-логіка, merge-base сценарії (feature-гілка, `main` після merge `dev → main`, direct-commit на `main`), змішаний режим.
|
|
15
|
+
|
|
16
|
+
## [1.8.176] - 2026-05-05
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `changelog` стало єдиним правилом про CHANGELOG для всіх воркспейсів — включно з `npm/`. У `check-npm-module.mjs` прибрано `checkChangelog()` (і константу `CHANGELOG_PATH`); відповідну секцію `## CHANGELOG` видалено з `mdc/npm-module.mdc` (v1.9). Логіка перевірки `npm/CHANGELOG.md` лишилася незмінна за наповненням, але тепер вона PR-scoped (порівняння з `dev`), тож на feature-гілці bump і запис достатньо зробити **один раз — як суму по PR**, без bump-шуму в проміжних комітах.
|
|
21
|
+
- `check-changelog.mjs`: додано перевірку `files`-масиву — якщо `<ws>/package.json` його оголошує, у ньому має бути `"CHANGELOG.md"` (приватні воркспейси без `files` цей пункт пропускають). Прибрано `SKIP_WORKSPACE = 'npm'` — `npm/` тепер у звичайному циклі. Хелпер `readPackageJsonOrNull` об'єднує читання `package.json` (раніше було два окремі читачі — `version` і `files`).
|
|
22
|
+
- `auto-rules.mjs` / `auto-rules.md`: `changelog` переведено на `AUTO_RULE_DEPENDENCIES = ['bun']` (раніше — пряма умова `packageJsonExists`); тепер послідовно з рештою правил.
|
|
23
|
+
- `npm/.claude-template/npm-CLAUDE.md` (і згенерований `npm/CLAUDE.md`): оновлено — посилається на `n-changelog.mdc`, явно згадує `files: ["CHANGELOG.md"]`, наголошує на PR-scoped логіці.
|
|
24
|
+
- Тести `check-changelog.test.mjs`: кейс `npm/ пропускається` замінено на `npm/ перевіряється з files=["CHANGELOG.md"]`; додано окремий кейс fail при `files` без `CHANGELOG.md`.
|
|
25
|
+
|
|
26
|
+
## [1.8.175] - 2026-05-05
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `k8s.mdc` / `check-k8s.mjs`: у маршрутах Gateway API (**HTTPRoute**, **GRPCRoute**, **TCPRoute**, **TLSRoute**, **UDPRoute**, група `gateway.networking.k8s.io`) забороняється поле `namespace` у `spec.rules[*].backendRefs[*]` (і однини `backendRef`), якщо його значення збігається з `metadata.namespace` самого маршруту. За замовчуванням Gateway API резолвить backend у тому ж namespace, що й маршрут — дублювання у `backendRef` мертве й заважає Kustomize-overlay, що міняє namespace маршруту. Cross-namespace backendRef (з відмінним `namespace`) правило не торкається. Експортовано `collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, routeNs)`; перевіряється усередині існуючого `failIfGatewayRouteUsesNonHeadlessService` (той самий обхід дерева, що й для headless-перевірки). Додано приклад «погано/добре» у `k8s.mdc` і відповідні юніт-тести.
|
|
31
|
+
|
|
32
|
+
## [1.8.174] - 2026-05-05
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Нове правило `changelog` (`mdc/changelog.mdc` + `scripts/check-changelog.mjs`): для «звичайних» Bun-монорепо проєктів вимагає, щоб у кожному workspace, який змінився відносно базової гілки `dev`, у поточному PR було підвищено `version` у `<ws>/package.json` і додано запис `## [version] - YYYY-MM-DD` у `<ws>/CHANGELOG.md` (Keep a Changelog 1.1.0). Перевірка PR-scoped: на самій гілці `dev` пропускається; на feature-гілці bump і запис достатньо зробити **один раз — як суму по всьому PR**, без бамп-шуму в проміжних комітах. Воркспейс `npm/` пропускається — його CHANGELOG покриває окреме правило `npm-module`. У `auto-rules.md` / `auto-rules.mjs` `changelog` додано до автодетекту з умовою «у корені є `package.json`» і до `AUTO_RULE_ORDER` між `capacitor` і `docker`.
|
|
8
37
|
|
|
9
38
|
### Added
|
|
10
39
|
|
package/bin/auto-rules.md
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: CHANGELOG.md в кожному workspace, з двома моделями бази порівняння
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '2.0'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Bun monorepo: у кожному workspace із кореневого `package.json.workspaces` (плюс кореневий пакет, плюс `npm/`) має бути власний **`CHANGELOG.md`**. Спільного на репозиторій змісту змін **не існує** — кожен пакет веде свій. Правило `npm-module` відповідає лише за публікацію типів і workflow, а CHANGELOG — за цим правилом.
|
|
8
|
+
|
|
9
|
+
## Дві моделі бази порівняння
|
|
10
|
+
|
|
11
|
+
Правило за **`<ws>/package.json`** автоматично визначає режим перевірки:
|
|
12
|
+
|
|
13
|
+
### npm-published воркспейс
|
|
14
|
+
|
|
15
|
+
Якщо в `<ws>/package.json` є непорожнє `name`, **не** `private: true` і оголошено масив `files` — workspace публікується в npm. База — **опублікована версія в реєстрі** (`npm view <name> version`):
|
|
16
|
+
|
|
17
|
+
1. Якщо локальна `version` дорівнює опублікованій — ще нічого не зрелізнуто, перевірка мовчить.
|
|
18
|
+
2. Якщо локальна `version` відрізняється від опублікованої — потрібен запис у `<ws>/CHANGELOG.md` для локальної версії (формат `## [версія] - YYYY-MM-DD`) і `"CHANGELOG.md"` у `files`.
|
|
19
|
+
3. Якщо реєстр недосяжний (офлайн / пакет ще не публікувався) — fail-safe pass із поясненням, щоб локальна розробка не блокувалася.
|
|
20
|
+
|
|
21
|
+
Git у цьому режимі не використовується — порівнюється поточний стан робочої копії з тим, що насправді в npm. Це покриває кейс «прямі коміти в `main` поза PR-flow» автоматично, бо неопубліковані зміни одразу видно.
|
|
22
|
+
|
|
23
|
+
### local-only воркспейс
|
|
24
|
+
|
|
25
|
+
Якщо workspace приватний (`private: true`) або без `files` (apps, services, internal-only пакети) — база = **гілка `dev`**, точніше `git merge-base <dev> HEAD`:
|
|
26
|
+
|
|
27
|
+
1. На самій гілці `dev` правило не активне.
|
|
28
|
+
2. На feature-гілці merge-base = точка розгалуження від `dev` → видно лише унікальні коміти цієї гілки. Bump + запис у `CHANGELOG.md` потрібні **раз на весь PR — як сума по гілці**, без bump-шуму в проміжних комітах.
|
|
29
|
+
3. На `main` після merge `dev → main` merge-base = поточний `dev` → diff порожній → правило мовчить.
|
|
30
|
+
4. Direct-commit на `main` поза PR-flow ловиться як зміна, що потребує bump + запис у `CHANGELOG.md`.
|
|
31
|
+
|
|
32
|
+
Якщо не git-репо, або `dev`/`origin/dev` не існує — local-only перевірка пропускається.
|
|
33
|
+
|
|
34
|
+
## Формат CHANGELOG.md
|
|
35
|
+
|
|
36
|
+
[Keep a Changelog 1.1.0](https://keepachangelog.com/uk/1.1.0/), мова — українська, новіші версії зверху.
|
|
37
|
+
|
|
38
|
+
```md title="<ws>/CHANGELOG.md"
|
|
39
|
+
# Changelog
|
|
40
|
+
|
|
41
|
+
Усі помітні зміни цього пакета документуються тут.
|
|
42
|
+
|
|
43
|
+
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
44
|
+
|
|
45
|
+
## [1.2.3] - 2026-05-05
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- ...
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- ...
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- ...
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Секції — підмножина `### Added`, `### Changed`, `### Fixed`, `### Removed` (одна або кілька).
|
|
61
|
+
|
|
62
|
+
## Перевірка
|
|
63
|
+
|
|
64
|
+
`npx @nitra/cursor check changelog`
|
package/mdc/k8s.mdc
CHANGED
|
@@ -214,6 +214,41 @@ spec:
|
|
|
214
214
|
|
|
215
215
|
**Точні умови та повідомлення `fail`** — верхній JSDoc **`npm/scripts/check-k8s.mjs`**.
|
|
216
216
|
|
|
217
|
+
### Gateway API: не дублюй namespace у `backendRef`
|
|
218
|
+
|
|
219
|
+
У маршрутах Gateway API (**HTTPRoute**, **GRPCRoute**, **TCPRoute**, **TLSRoute**, **UDPRoute**) у `spec.rules[*].backendRefs[*]` **не** додавай поле **`namespace`**, якщо його значення збігається з **`metadata.namespace`** самого маршруту. За замовчуванням Gateway API резолвить backend у тому ж namespace, що й маршрут, тож такий рядок — мертвий: він плутає під час перенесень між середовищами і ламається мовчки, якщо overlay змінює namespace маршруту через Kustomize, а в backendRef залишився старий рядок. Прибери поле — поведінка не зміниться. **`check k8s`** падає на такому збігу.
|
|
220
|
+
|
|
221
|
+
```yaml
|
|
222
|
+
# ❌ погано — namespace дублює metadata.namespace маршруту
|
|
223
|
+
apiVersion: gateway.networking.k8s.io/v1
|
|
224
|
+
kind: HTTPRoute
|
|
225
|
+
metadata:
|
|
226
|
+
name: admin-site
|
|
227
|
+
namespace: dev-b2b
|
|
228
|
+
spec:
|
|
229
|
+
rules:
|
|
230
|
+
- backendRefs:
|
|
231
|
+
- name: auth-hl
|
|
232
|
+
namespace: dev-b2b # ← прибери
|
|
233
|
+
port: 8080
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
# ✅ добре — namespace лише в metadata
|
|
238
|
+
apiVersion: gateway.networking.k8s.io/v1
|
|
239
|
+
kind: HTTPRoute
|
|
240
|
+
metadata:
|
|
241
|
+
name: admin-site
|
|
242
|
+
namespace: dev-b2b
|
|
243
|
+
spec:
|
|
244
|
+
rules:
|
|
245
|
+
- backendRefs:
|
|
246
|
+
- name: auth-hl
|
|
247
|
+
port: 8080
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Якщо backend **дійсно** живе в іншому namespace — поле **`namespace`** у `backendRef` обов'язкове (і потрібен `ReferenceGrant` у тому namespace); правило цього випадку не торкається.
|
|
251
|
+
|
|
217
252
|
## ConfigMap: ім'я збігається з Deployment
|
|
218
253
|
|
|
219
254
|
Якщо в `k8s/base/` є **`configmap.yaml`** і **Deployment**, і цей Deployment посилається рівно на **один** ConfigMap — `metadata.name` ConfigMap має збігатися з `metadata.name` Deployment. Точні умови перевірки — **`check-k8s.mjs`**.
|
|
@@ -474,6 +509,28 @@ patches:
|
|
|
474
509
|
preem: "false"
|
|
475
510
|
```
|
|
476
511
|
|
|
512
|
+
### `patches[].target`: лише `kind` і `name`
|
|
513
|
+
|
|
514
|
+
У `patches[].target` залишай **тільки** **`kind`** і **`name`** — поля **`group`** і **`version`** прибирай. Kustomize резолвить ціль за GVK+name; `group`/`version` — звужувальні фільтри, потрібні лише за реальної колізії `kind+name` між різними API-групами або версіями. У межах одного namespace apiserver зберігає об'єкт у єдиному storage-GVK, тож для звичайних маніфестів така колізія неможлива, і `group`/`version` у `target` — мертвий шум, який ламається мовчки під час змін API (наприклад, перехід `v1beta1` → `v1`).
|
|
515
|
+
|
|
516
|
+
```yaml
|
|
517
|
+
# ❌ зайві group / version
|
|
518
|
+
patches:
|
|
519
|
+
- target:
|
|
520
|
+
group: gateway.networking.k8s.io
|
|
521
|
+
version: v1beta1
|
|
522
|
+
kind: Gateway
|
|
523
|
+
name: gw
|
|
524
|
+
|
|
525
|
+
# ✅
|
|
526
|
+
patches:
|
|
527
|
+
- target:
|
|
528
|
+
kind: Gateway
|
|
529
|
+
name: gw
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Виняток:** залишай `group` / `version`, лише якщо в дереві overlay реально співіснують ресурси з однаковими `kind`+`name`, але різними API-групами/версіями (наприклад, дві CRD з одним `kind`). У такому разі вкажи мінімальний набір полів, потрібний для дисамбігуації.
|
|
533
|
+
|
|
477
534
|
## Перевірка
|
|
478
535
|
|
|
479
536
|
**`npx @nitra/cursor check k8s`** — програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
|
package/mdc/npm-module.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Оформлення репозиторію для npm модуля
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.9'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Bun monorepo: workspace **`npm/`**, кореневий **`package.json`**, **`.github/workflows/`**; опційно **`demo/`**.
|
|
@@ -51,11 +51,7 @@ bunx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly
|
|
|
51
51
|
|
|
52
52
|
## CHANGELOG
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/) (новіші версії зверху, мова — українська). Кожен запис починається з рядка `## [версія] - YYYY-MM-DD` і має одну або кілька секцій: `### Added`, `### Changed`, `### Fixed`, `### Removed`.
|
|
57
|
-
|
|
58
|
-
Файл **`CHANGELOG.md`** має бути в масиві **`files`** у **`npm/package.json`**, щоб публікувався разом із пакетом.
|
|
54
|
+
Окреме правило **`changelog`** ([changelog.mdc](changelog.mdc)) вимагає `npm/CHANGELOG.md` із записом для поточної версії (Keep a Changelog) і присутність `"CHANGELOG.md"` у масиві `files` у `npm/package.json`. Логіка — PR-scoped (сума по гілці vs `dev`).
|
|
59
55
|
|
|
60
56
|
## npm publish
|
|
61
57
|
|
package/package.json
CHANGED
package/scripts/auto-rules.mjs
CHANGED
|
@@ -26,6 +26,7 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
26
26
|
'abie',
|
|
27
27
|
'bun',
|
|
28
28
|
'capacitor',
|
|
29
|
+
'changelog',
|
|
29
30
|
'docker',
|
|
30
31
|
'ga',
|
|
31
32
|
'graphql',
|
|
@@ -54,6 +55,7 @@ export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint'])
|
|
|
54
55
|
*/
|
|
55
56
|
export const AUTO_RULE_DEPENDENCIES = Object.freeze(
|
|
56
57
|
/** @type {Record<string, readonly string[]>} */ ({
|
|
58
|
+
changelog: Object.freeze(['bun']),
|
|
57
59
|
image: Object.freeze(['vue'])
|
|
58
60
|
})
|
|
59
61
|
)
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє, що в кожному workspace із незакомічаними/незрелізнутими змінами підвищена `version` у
|
|
3
|
+
* `<ws>/package.json` і в `<ws>/CHANGELOG.md` присутній запис `## [version] - YYYY-MM-DD`
|
|
4
|
+
* (формат Keep a Changelog).
|
|
5
|
+
*
|
|
6
|
+
* Дві моделі визначення «бази для порівняння» — на рівні воркспейсу:
|
|
7
|
+
*
|
|
8
|
+
* 1) **npm-published mode** (`<ws>/package.json` має непорожнє `name`, не `private: true`,
|
|
9
|
+
* і має масив `files`): база = опублікована версія в npm-реєстрі (`npm view <name> version`).
|
|
10
|
+
* Git не задіяний. Якщо локальна версія відрізняється від опублікованої — потрібен запис
|
|
11
|
+
* у CHANGELOG для локальної версії й `"CHANGELOG.md"` у `files`. Якщо `npm view` недосяжний
|
|
12
|
+
* (немає мережі / пакет ще не публікувався) — fail-safe pass із поясненням, щоб локальна
|
|
13
|
+
* розробка офлайн не блокувалась.
|
|
14
|
+
*
|
|
15
|
+
* 2) **local-only mode** (приватні / без `files` воркспейси): PR-scoped перевірка проти `dev`.
|
|
16
|
+
* База = `git merge-base <dev> HEAD` (точка розгалуження поточної гілки від `dev`), щоб:
|
|
17
|
+
* - на feature-гілці бачити лише унікальні коміти цієї гілки;
|
|
18
|
+
* - на `main` після merge `dev → main` diff був порожній (нічого не вимагати);
|
|
19
|
+
* - direct-commit на `main` поза PR-flow ловився як зміна, що потребує bump + CHANGELOG.
|
|
20
|
+
* Якщо не git-репо, поточна гілка = `dev`, або `dev`/`origin/dev` не існує — пропуск.
|
|
21
|
+
*
|
|
22
|
+
* Усі `git` і `npm` виклики — через `execFile`, без shell-інтерполяції.
|
|
23
|
+
*/
|
|
24
|
+
import { execFile } from 'node:child_process'
|
|
25
|
+
import { existsSync } from 'node:fs'
|
|
26
|
+
import { readFile } from 'node:fs/promises'
|
|
27
|
+
import { join } from 'node:path'
|
|
28
|
+
import { promisify } from 'node:util'
|
|
29
|
+
|
|
30
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
31
|
+
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
32
|
+
|
|
33
|
+
const execFileAsync = promisify(execFile)
|
|
34
|
+
|
|
35
|
+
/** Базова гілка PR — фіксована, без конфіга (див. n-changelog.mdc) */
|
|
36
|
+
const BASE_BRANCH = 'dev'
|
|
37
|
+
|
|
38
|
+
/** Таймаут на `npm view <name> version` (мс), щоб не блокуватись на офлайні */
|
|
39
|
+
const NPM_VIEW_TIMEOUT_MS = 10_000
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Тихо запускає `git` і повертає stdout або `null` при будь-якій помилці.
|
|
43
|
+
* @param {string[]} args аргументи `git`
|
|
44
|
+
* @returns {Promise<string | null>}
|
|
45
|
+
*/
|
|
46
|
+
async function gitOrNull(args) {
|
|
47
|
+
try {
|
|
48
|
+
const { stdout } = await execFileAsync('git', args)
|
|
49
|
+
return stdout
|
|
50
|
+
} catch {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Чи робочий каталог — git-репозиторій.
|
|
57
|
+
* @returns {Promise<boolean>}
|
|
58
|
+
*/
|
|
59
|
+
async function isInsideGitRepo() {
|
|
60
|
+
const out = await gitOrNull(['rev-parse', '--is-inside-work-tree'])
|
|
61
|
+
return typeof out === 'string' && out.trim() === 'true'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Назва поточної гілки (або `HEAD` для detached state).
|
|
66
|
+
* @returns {Promise<string | null>}
|
|
67
|
+
*/
|
|
68
|
+
async function currentBranchName() {
|
|
69
|
+
const out = await gitOrNull(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
70
|
+
return typeof out === 'string' ? out.trim() : null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Знаходить ref для базової гілки. Перевага локальному `dev`, далі `origin/dev`. Повертає `null`,
|
|
75
|
+
* якщо жоден не існує.
|
|
76
|
+
* @returns {Promise<string | null>}
|
|
77
|
+
*/
|
|
78
|
+
async function resolveBaseRef() {
|
|
79
|
+
for (const ref of [BASE_BRANCH, `origin/${BASE_BRANCH}`]) {
|
|
80
|
+
const out = await gitOrNull(['rev-parse', '--verify', '--quiet', ref])
|
|
81
|
+
if (typeof out === 'string' && out.trim().length > 0) {
|
|
82
|
+
return ref
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Точка розгалуження поточної гілки від `baseRef`. На feature-гілці = коли вона відгалузилась;
|
|
90
|
+
* на `main` після merge `dev → main` = поточний `dev`. Повертає `null`, якщо merge-base нема.
|
|
91
|
+
* @param {string} baseRef
|
|
92
|
+
* @returns {Promise<string | null>}
|
|
93
|
+
*/
|
|
94
|
+
async function resolveMergeBase(baseRef) {
|
|
95
|
+
const out = await gitOrNull(['merge-base', baseRef, 'HEAD'])
|
|
96
|
+
if (typeof out !== 'string') return null
|
|
97
|
+
const sha = out.trim()
|
|
98
|
+
return sha.length > 0 ? sha : null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Будує pathspec для `git diff` / `ls-files` для воркспейсу.
|
|
103
|
+
*
|
|
104
|
+
* Для кореня `.` — це точка плюс magic-виключення кожного підворкспейсу через `:(exclude)<sub>/`,
|
|
105
|
+
* щоб зміни всередині sub-workspace не вважалися змінами кореня.
|
|
106
|
+
* Для звичайного воркспейсу — просто `<ws>/`.
|
|
107
|
+
* @param {string} ws
|
|
108
|
+
* @param {string[]} subWorkspaces
|
|
109
|
+
* @returns {string[]}
|
|
110
|
+
*/
|
|
111
|
+
function pathspecForWorkspace(ws, subWorkspaces) {
|
|
112
|
+
if (ws !== '.') return [`${ws}/`]
|
|
113
|
+
return ['.', ...subWorkspaces.filter(s => s !== '.').map(s => `:(exclude)${s}/`)]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Чи є зміни (committed або в робочому дереві) у каталозі `<ws>` відносно `baseRef`.
|
|
118
|
+
*
|
|
119
|
+
* `git diff --quiet <baseRef> -- <pathspec>` ловить committed-зміни на цій гілці й незбережені
|
|
120
|
+
* правки tracked-файлів. Untracked-файли — `git ls-files --others --exclude-standard`.
|
|
121
|
+
* @param {string} baseRef SHA або ref-name (зокрема merge-base)
|
|
122
|
+
* @param {string} ws
|
|
123
|
+
* @param {string[]} subWorkspaces
|
|
124
|
+
* @returns {Promise<boolean>}
|
|
125
|
+
*/
|
|
126
|
+
async function workspaceHasChangesAgainstBase(baseRef, ws, subWorkspaces) {
|
|
127
|
+
const pathspec = pathspecForWorkspace(ws, subWorkspaces)
|
|
128
|
+
try {
|
|
129
|
+
await execFileAsync('git', ['diff', '--quiet', baseRef, '--', ...pathspec])
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const code = /** @type {{ code?: number }} */ (err).code
|
|
132
|
+
if (code === 1) return true
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
const untracked = await gitOrNull(['ls-files', '--others', '--exclude-standard', '--', ...pathspec])
|
|
136
|
+
return typeof untracked === 'string' && untracked.trim().length > 0
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Версія з `<ws>/package.json` на `baseRef` або `null`.
|
|
141
|
+
* @param {string} baseRef
|
|
142
|
+
* @param {string} ws
|
|
143
|
+
* @returns {Promise<string | null>}
|
|
144
|
+
*/
|
|
145
|
+
async function readBaseVersion(baseRef, ws) {
|
|
146
|
+
const wsPath = ws === '.' ? 'package.json' : `${ws}/package.json`
|
|
147
|
+
const out = await gitOrNull(['show', `${baseRef}:${wsPath}`])
|
|
148
|
+
if (out === null) return null
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(out)
|
|
151
|
+
return typeof parsed?.version === 'string' ? parsed.version : null
|
|
152
|
+
} catch {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Чи містить текст `CHANGELOG.md` запис `## [version]` (з опційним `- YYYY-MM-DD`).
|
|
159
|
+
* @param {string} text
|
|
160
|
+
* @param {string} version
|
|
161
|
+
* @returns {boolean}
|
|
162
|
+
*/
|
|
163
|
+
function changelogHasVersionEntry(text, version) {
|
|
164
|
+
const escaped = version.replaceAll(/[.+*?^$()[\]{}|\\]/g, String.raw`\$&`)
|
|
165
|
+
const re = new RegExp(String.raw`^##\s+\[${escaped}\]`, 'm')
|
|
166
|
+
return re.test(text)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Зчитує `<ws>/package.json`. `null`, якщо файл відсутній або JSON некоректний.
|
|
171
|
+
* @param {string} ws
|
|
172
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
173
|
+
*/
|
|
174
|
+
async function readPackageJsonOrNull(ws) {
|
|
175
|
+
const path = join(ws, 'package.json')
|
|
176
|
+
if (!existsSync(path)) return null
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'))
|
|
179
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
180
|
+
? /** @type {Record<string, unknown>} */ (parsed)
|
|
181
|
+
: null
|
|
182
|
+
} catch {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Воркспейс публікується в npm: має непорожній `name`, не `private: true`, і має масив `files`.
|
|
189
|
+
* @param {Record<string, unknown> | null} pkg
|
|
190
|
+
* @returns {boolean}
|
|
191
|
+
*/
|
|
192
|
+
function isNpmPublishable(pkg) {
|
|
193
|
+
if (!pkg) return false
|
|
194
|
+
if (typeof pkg.name !== 'string' || pkg.name.length === 0) return false
|
|
195
|
+
if (pkg.private === true) return false
|
|
196
|
+
return Array.isArray(pkg.files)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Опублікована версія пакета в npm-реєстрі. `null` — пакет не знайдено / нема мережі / помилка.
|
|
201
|
+
* Дефолтна імплементація — `npm view <name> version` із таймаутом, щоб не блокуватись офлайн.
|
|
202
|
+
* @param {string} name
|
|
203
|
+
* @returns {Promise<string | null>}
|
|
204
|
+
*/
|
|
205
|
+
async function defaultGetPublishedVersion(name) {
|
|
206
|
+
try {
|
|
207
|
+
const { stdout } = await execFileAsync('npm', ['view', name, 'version'], { timeout: NPM_VIEW_TIMEOUT_MS })
|
|
208
|
+
const v = stdout.trim()
|
|
209
|
+
return v.length > 0 ? v : null
|
|
210
|
+
} catch {
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Перевіряє масив `files` у `<ws>/package.json`: якщо оголошено — має містити `"CHANGELOG.md"`.
|
|
217
|
+
* @param {Record<string, unknown> | null} pkg
|
|
218
|
+
* @param {string} ws
|
|
219
|
+
* @param {(msg: string) => void} pass
|
|
220
|
+
* @param {(msg: string) => void} fail
|
|
221
|
+
*/
|
|
222
|
+
function checkFilesArrayContainsChangelog(pkg, ws, pass, fail) {
|
|
223
|
+
if (!pkg || !Array.isArray(pkg.files)) return
|
|
224
|
+
const pkgPath = join(ws, 'package.json')
|
|
225
|
+
if (pkg.files.includes('CHANGELOG.md')) {
|
|
226
|
+
pass(`${pkgPath}: files містить "CHANGELOG.md"`)
|
|
227
|
+
} else {
|
|
228
|
+
fail(`${pkgPath}: масив files має містити "CHANGELOG.md", щоб публікувати changelog із пакетом`)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Перевіряє наявність запису у `<ws>/CHANGELOG.md` для версії `version`.
|
|
234
|
+
* @param {string} ws
|
|
235
|
+
* @param {string} version
|
|
236
|
+
* @param {(msg: string) => void} pass
|
|
237
|
+
* @param {(msg: string) => void} fail
|
|
238
|
+
* @returns {Promise<boolean>} `false`, якщо файл відсутній або немає запису
|
|
239
|
+
*/
|
|
240
|
+
async function verifyChangelogEntry(ws, version, pass, fail) {
|
|
241
|
+
const label = ws === '.' ? '<root>' : ws
|
|
242
|
+
const changelogPath = join(ws, 'CHANGELOG.md')
|
|
243
|
+
if (!existsSync(changelogPath)) {
|
|
244
|
+
fail(`${label}: відсутній ${changelogPath} (Keep a Changelog, див. n-changelog.mdc)`)
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
const text = await readFile(changelogPath, 'utf8')
|
|
248
|
+
if (changelogHasVersionEntry(text, version)) {
|
|
249
|
+
pass(`${changelogPath}: знайдено запис для версії ${version}`)
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
fail(`${changelogPath}: відсутній запис для ${version} (формат "## [${version}] - YYYY-MM-DD")`)
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* npm-published режим: порівнює локальну `version` з опублікованою в реєстрі. Якщо вони
|
|
258
|
+
* відрізняються — вимагає запис у CHANGELOG і `"CHANGELOG.md"` у `files`. Якщо реєстр недосяжний,
|
|
259
|
+
* правило fail-safe пасує (щоб офлайн-розробка не блокувалась).
|
|
260
|
+
* @param {string} ws
|
|
261
|
+
* @param {Record<string, unknown>} pkg
|
|
262
|
+
* @param {(name: string) => Promise<string | null>} getPublishedVersion
|
|
263
|
+
* @param {(msg: string) => void} pass
|
|
264
|
+
* @param {(msg: string) => void} fail
|
|
265
|
+
*/
|
|
266
|
+
async function checkPublishedWorkspace(ws, pkg, getPublishedVersion, pass, fail) {
|
|
267
|
+
const label = ws === '.' ? '<root>' : ws
|
|
268
|
+
const Vcurrent = typeof pkg.version === 'string' ? pkg.version : null
|
|
269
|
+
if (!Vcurrent) {
|
|
270
|
+
fail(`${label}: у package.json відсутнє поле version (npm-published воркспейс)`)
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
const name = /** @type {string} */ (pkg.name)
|
|
274
|
+
const Vpublished = await getPublishedVersion(name)
|
|
275
|
+
if (Vpublished === null) {
|
|
276
|
+
pass(`${label}: ${name} — опублікована версія недоступна (мережа/реєстр), перевірку пропущено`)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
if (Vpublished === Vcurrent) {
|
|
280
|
+
pass(`${label}: ${name}@${Vcurrent} вже опубліковано — змін до релізу немає`)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
pass(`${label}: ${name} — нова локальна версія (${Vpublished} → ${Vcurrent})`)
|
|
284
|
+
await verifyChangelogEntry(ws, Vcurrent, pass, fail)
|
|
285
|
+
checkFilesArrayContainsChangelog(pkg, ws, pass, fail)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* local-only режим: PR-scoped перевірка проти `dev` через `git merge-base`. Викликається лише
|
|
290
|
+
* для воркспейсів, де є реальні зміни щодо merge-base.
|
|
291
|
+
* @param {string} mergeBase SHA точки розгалуження
|
|
292
|
+
* @param {string} ws
|
|
293
|
+
* @param {Record<string, unknown> | null} pkg
|
|
294
|
+
* @param {(msg: string) => void} pass
|
|
295
|
+
* @param {(msg: string) => void} fail
|
|
296
|
+
*/
|
|
297
|
+
async function checkLocalOnlyChangedWorkspace(mergeBase, ws, pkg, pass, fail) {
|
|
298
|
+
const label = ws === '.' ? '<root>' : ws
|
|
299
|
+
const Vcurrent = typeof pkg?.version === 'string' ? pkg.version : null
|
|
300
|
+
if (!Vcurrent) {
|
|
301
|
+
fail(`${label}: у package.json відсутнє поле version (потрібне для запису в CHANGELOG)`)
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
const Vbase = await readBaseVersion(mergeBase, ws)
|
|
305
|
+
if (Vbase !== null && Vbase === Vcurrent) {
|
|
306
|
+
fail(
|
|
307
|
+
`${label}: у цій гілці є зміни, але version у ${join(ws, 'package.json')} не підвищено (на ${BASE_BRANCH} — ${Vbase}). Bump + запис у CHANGELOG.md обов'язкові на PR`
|
|
308
|
+
)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
pass(`${label}: version підвищено (${Vbase ?? '∅'} → ${Vcurrent})`)
|
|
312
|
+
if (!(await verifyChangelogEntry(ws, Vcurrent, pass, fail))) return
|
|
313
|
+
checkFilesArrayContainsChangelog(pkg, ws, pass, fail)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Виконує local-only перевірку для всіх workspace-ів, у яких немає npm-published режиму.
|
|
318
|
+
* @param {string[]} localOnlyWorkspaces
|
|
319
|
+
* @param {Map<string, Record<string, unknown> | null>} pkgByWs
|
|
320
|
+
* @param {string[]} subWorkspaces
|
|
321
|
+
* @param {(msg: string) => void} pass
|
|
322
|
+
* @param {(msg: string) => void} fail
|
|
323
|
+
* @returns {Promise<void>}
|
|
324
|
+
*/
|
|
325
|
+
async function runLocalOnlyChecks(localOnlyWorkspaces, pkgByWs, subWorkspaces, pass, fail) {
|
|
326
|
+
if (localOnlyWorkspaces.length === 0) return
|
|
327
|
+
|
|
328
|
+
if (!(await isInsideGitRepo())) {
|
|
329
|
+
pass('changelog: не git-репозиторій — local-only перевірку пропущено')
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
const branch = await currentBranchName()
|
|
333
|
+
if (branch === BASE_BRANCH) {
|
|
334
|
+
pass(`changelog: поточна гілка = ${BASE_BRANCH} — local-only перевірку пропущено`)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
const baseRef = await resolveBaseRef()
|
|
338
|
+
if (!baseRef) {
|
|
339
|
+
pass(`changelog: ref ${BASE_BRANCH} (та origin/${BASE_BRANCH}) не знайдено — local-only перевірку пропущено`)
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
const mergeBase = await resolveMergeBase(baseRef)
|
|
343
|
+
if (!mergeBase) {
|
|
344
|
+
pass(`changelog: merge-base з ${baseRef} не знайдено — local-only перевірку пропущено`)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let checkedAny = false
|
|
349
|
+
for (const ws of localOnlyWorkspaces) {
|
|
350
|
+
if (!(await workspaceHasChangesAgainstBase(mergeBase, ws, subWorkspaces))) continue
|
|
351
|
+
checkedAny = true
|
|
352
|
+
await checkLocalOnlyChangedWorkspace(mergeBase, ws, pkgByWs.get(ws) ?? null, pass, fail)
|
|
353
|
+
}
|
|
354
|
+
if (!checkedAny) {
|
|
355
|
+
pass(`changelog: local-only воркспейси без змін відносно merge-base(${baseRef})`)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Перевіряє відповідність проєкту правилу changelog.mdc.
|
|
361
|
+
* @param {object} [opts]
|
|
362
|
+
* @param {(name: string) => Promise<string | null>} [opts.getPublishedVersion] перевизначення для тестів
|
|
363
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
364
|
+
*/
|
|
365
|
+
export async function check(opts = {}) {
|
|
366
|
+
const reporter = createCheckReporter()
|
|
367
|
+
const { pass, fail } = reporter
|
|
368
|
+
const getPublishedVersion = opts.getPublishedVersion ?? defaultGetPublishedVersion
|
|
369
|
+
|
|
370
|
+
const workspaces = await getMonorepoPackageRootDirs(process.cwd())
|
|
371
|
+
const subWorkspaces = workspaces.filter(w => w !== '.')
|
|
372
|
+
|
|
373
|
+
/** @type {Map<string, Record<string, unknown> | null>} */
|
|
374
|
+
const pkgByWs = new Map()
|
|
375
|
+
/** @type {string[]} */
|
|
376
|
+
const publishedWorkspaces = []
|
|
377
|
+
/** @type {string[]} */
|
|
378
|
+
const localOnlyWorkspaces = []
|
|
379
|
+
for (const ws of workspaces) {
|
|
380
|
+
const pkg = await readPackageJsonOrNull(ws)
|
|
381
|
+
pkgByWs.set(ws, pkg)
|
|
382
|
+
if (isNpmPublishable(pkg)) {
|
|
383
|
+
publishedWorkspaces.push(ws)
|
|
384
|
+
} else {
|
|
385
|
+
localOnlyWorkspaces.push(ws)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const ws of publishedWorkspaces) {
|
|
390
|
+
await checkPublishedWorkspace(
|
|
391
|
+
ws,
|
|
392
|
+
/** @type {Record<string, unknown>} */ (pkgByWs.get(ws)),
|
|
393
|
+
getPublishedVersion,
|
|
394
|
+
pass,
|
|
395
|
+
fail
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
await runLocalOnlyChecks(localOnlyWorkspaces, pkgByWs, subWorkspaces, pass, fail)
|
|
400
|
+
|
|
401
|
+
return reporter.getExitCode()
|
|
402
|
+
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
* кожен **Service** — **`spec.clusterIP: None`** та ім’я на **`-hl`**. У маршрутах **Gateway API**
|
|
39
39
|
* (**`HTTPRoute`**, **`GRPCRoute`**, **`TCPRoute`**, **`TLSRoute`**, **`UDPRoute`**, група **`gateway.networking.k8s.io`**)
|
|
40
40
|
* посилання **`backendRefs` / `backendRef`** на **Service** мають вказувати лише сервіси з суфіксом **`-hl`** у **`name`**.
|
|
41
|
+
* Поле **`namespace`** у **`backendRef`**, що збігається з **`metadata.namespace`** самого маршруту, — надлишкове:
|
|
42
|
+
* прибери його, бо за замовчуванням Gateway API резолвить backend у тому ж namespace, що й маршрут (див. k8s.mdc).
|
|
41
43
|
* **HealthCheckPolicy** (**`networking.gke.io/v1`**, GKE): **`spec.targetRef`** на **Service** — **`name`** з суфіксом **`-hl`** (див. k8s.mdc).
|
|
42
44
|
* Якщо **`kustomization.yaml`** посилається на **`svc.yaml`** (**`resources`**, **`bases`**, **`components`**, **`crds`**,
|
|
43
45
|
* **`patches[].path`**, **`patchesStrategicMerge`**), у **тому ж** файлі має бути посилання на відповідний **`svc-hl.yaml`**
|
|
@@ -55,6 +57,11 @@
|
|
|
55
57
|
* Для **`patchesStrategicMerge`** і для **`patches[].path`** без **`target`** і без inline **`patch`** (зовнішній strategic-merge)
|
|
56
58
|
* кожен YAML-документ з кореневим **`kind`** і **`metadata.name`** також звіряється з цим каталогом.
|
|
57
59
|
*
|
|
60
|
+
* **Зайві `group` / `version` у `patches[].target` / `patchesJson6902[].target`:** якщо в інвентарі **`resources`** /
|
|
61
|
+
* **`bases`** / **`components`** / **`crds`** (рекурсивно) за **`kind`** + **`name`** немає колізії між різними
|
|
62
|
+
* API-групами/версіями, поля **`group`** і **`version`** у **`target`** треба прибрати — Kustomize резолвить ціль
|
|
63
|
+
* за **GVK + name**, а зайві поля ламаються мовчки під час змін API (k8s.mdc «patches[].target: лише kind і name»).
|
|
64
|
+
*
|
|
58
65
|
* Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
|
|
59
66
|
* **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
|
|
60
67
|
* компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
|
|
@@ -1323,6 +1330,52 @@ function failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail) {
|
|
|
1323
1330
|
}
|
|
1324
1331
|
}
|
|
1325
1332
|
|
|
1333
|
+
/**
|
|
1334
|
+
* Зайві **`group`** / **`version`** у **`patches[].target`** / **`patchesJson6902[].target`**: якщо в інвентарі за **`kind`** + **`name`** немає колізії між різними API-групами/версіями, ці поля треба прибрати (k8s.mdc «patches[].target: лише kind і name»).
|
|
1335
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
1336
|
+
* @param {Record<string, unknown>} first корінь Kustomization
|
|
1337
|
+
* @param {KustomizeResourceDescriptor[]} catalog інвентар resources/bases/…
|
|
1338
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
1339
|
+
* @returns {void}
|
|
1340
|
+
*/
|
|
1341
|
+
function failIfExplicitPatchTargetsHaveRedundantGroupVersion(rel, first, catalog, fail) {
|
|
1342
|
+
for (const { section, index, target } of extractExplicitPatchTargetsFromKustomization(first)) {
|
|
1343
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
1344
|
+
continue
|
|
1345
|
+
}
|
|
1346
|
+
const t = /** @type {Record<string, unknown>} */ (target)
|
|
1347
|
+
const kind = typeof t.kind === 'string' ? t.kind.trim() : ''
|
|
1348
|
+
const name = typeof t.name === 'string' ? t.name.trim() : ''
|
|
1349
|
+
if (kind === '' || name === '') {
|
|
1350
|
+
continue
|
|
1351
|
+
}
|
|
1352
|
+
if (patchTargetUsesSelector(t)) {
|
|
1353
|
+
continue
|
|
1354
|
+
}
|
|
1355
|
+
const tgtGroup = typeof t.group === 'string' ? t.group.trim() : ''
|
|
1356
|
+
const tgtVersion = typeof t.version === 'string' ? t.version.trim() : ''
|
|
1357
|
+
if (tgtGroup === '' && tgtVersion === '') {
|
|
1358
|
+
continue
|
|
1359
|
+
}
|
|
1360
|
+
const matchingByKindName = catalog.filter(r => r.kind === kind && r.name === name)
|
|
1361
|
+
const distinctGvk = new Set(matchingByKindName.map(r => `${r.group}/${r.version}`))
|
|
1362
|
+
if (distinctGvk.size > 1) {
|
|
1363
|
+
continue
|
|
1364
|
+
}
|
|
1365
|
+
/** @type {string[]} */
|
|
1366
|
+
const redundant = []
|
|
1367
|
+
if (tgtGroup !== '') {
|
|
1368
|
+
redundant.push('group')
|
|
1369
|
+
}
|
|
1370
|
+
if (tgtVersion !== '') {
|
|
1371
|
+
redundant.push('version')
|
|
1372
|
+
}
|
|
1373
|
+
fail(
|
|
1374
|
+
`${rel}: ${section}[${index}].target — прибери зайві поля ${redundant.join(', ')}; для kind=${kind}, name=${name} в інвентарі немає колізії між різними API-групами/версіями (див. k8s.mdc «patches[].target: лише kind і name»)`
|
|
1375
|
+
)
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1326
1379
|
/**
|
|
1327
1380
|
* Документи з YAML-файлу мають мати дескриптор у **catalog** (інвентар resources).
|
|
1328
1381
|
* @param {string} rel відносний шлях до kustomization.yaml
|
|
@@ -1525,6 +1578,7 @@ async function validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm,
|
|
|
1525
1578
|
const kustDir = dirname(resolve(kustAbs))
|
|
1526
1579
|
const kustNs = typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
|
|
1527
1580
|
failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail)
|
|
1581
|
+
failIfExplicitPatchTargetsHaveRedundantGroupVersion(rel, first, catalog, fail)
|
|
1528
1582
|
await failIfPathOnlyPatchesNotInCatalog(rel, rec.patches, kustDir, rootNorm, root, catalog, kustNs, fail)
|
|
1529
1583
|
await failIfStrategicMergePatchesNotInCatalog(
|
|
1530
1584
|
rel,
|
|
@@ -2880,7 +2934,49 @@ export function collectGatewayApiRouteBackendServiceNames(spec) {
|
|
|
2880
2934
|
}
|
|
2881
2935
|
|
|
2882
2936
|
/**
|
|
2883
|
-
*
|
|
2937
|
+
* Збирає **`backendRef`** до **Service** з полем **`namespace`**, що збігається з namespace маршруту.
|
|
2938
|
+
*
|
|
2939
|
+
* Поле **`namespace`** у такому **`backendRef`** надлишкове: за замовчуванням Gateway API резолвить backend
|
|
2940
|
+
* у тому ж namespace, що й сам маршрут (див. k8s.mdc). Зайві поля у YAML — джерело розсинхрону між середовищами.
|
|
2941
|
+
* @param {unknown} spec значення **`spec`** маршруту
|
|
2942
|
+
* @param {string} routeNs **`metadata.namespace`** маршруту (непорожній рядок)
|
|
2943
|
+
* @returns {string[]} імена backend-сервісів з надлишковим **`namespace`** (можливі дублікати)
|
|
2944
|
+
*/
|
|
2945
|
+
export function collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, routeNs) {
|
|
2946
|
+
/** @type {string[]} */
|
|
2947
|
+
const out = []
|
|
2948
|
+
|
|
2949
|
+
/**
|
|
2950
|
+
* @param {unknown} node вузол для обходу
|
|
2951
|
+
* @returns {void}
|
|
2952
|
+
*/
|
|
2953
|
+
function walk(node) {
|
|
2954
|
+
if (node === null || node === undefined) return
|
|
2955
|
+
if (Array.isArray(node)) {
|
|
2956
|
+
for (const x of node) {
|
|
2957
|
+
walk(x)
|
|
2958
|
+
}
|
|
2959
|
+
return
|
|
2960
|
+
}
|
|
2961
|
+
if (typeof node !== 'object') return
|
|
2962
|
+
if (isGatewayApiBackendRefToService(node)) {
|
|
2963
|
+
const o = /** @type {Record<string, unknown>} */ (node)
|
|
2964
|
+
if (typeof o.namespace === 'string' && o.namespace === routeNs) {
|
|
2965
|
+
out.push(String(o.name))
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
for (const v of Object.values(node)) {
|
|
2969
|
+
walk(v)
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
walk(spec)
|
|
2974
|
+
return out
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
/**
|
|
2978
|
+
* Один документ: маршрут Gateway API має посилатися на **Service** з суфіксом **`-hl`**;
|
|
2979
|
+
* у **`backendRef`** не має дублюватися **`namespace`**, що збігається з **`metadata.namespace`** маршруту.
|
|
2884
2980
|
* @param {string} rel відносний шлях до файлу
|
|
2885
2981
|
* @param {number} docIndex 1-based індекс документа
|
|
2886
2982
|
* @param {Record<string, unknown>} rec корінь маніфесту
|
|
@@ -2906,6 +3002,18 @@ function failIfGatewayRouteUsesNonHeadlessService(rel, docIndex, rec, fail) {
|
|
|
2906
3002
|
)
|
|
2907
3003
|
}
|
|
2908
3004
|
}
|
|
3005
|
+
const meta = rec.metadata
|
|
3006
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
3007
|
+
const routeNs = /** @type {Record<string, unknown>} */ (meta).namespace
|
|
3008
|
+
if (typeof routeNs === 'string' && routeNs !== '') {
|
|
3009
|
+
const redundant = collectGatewayApiRouteBackendRefsWithRedundantNamespace(rec.spec, routeNs)
|
|
3010
|
+
for (const svcName of redundant) {
|
|
3011
|
+
fail(
|
|
3012
|
+
`${rel}: Gateway API ${kind} (документ ${docIndex}): backendRef «${svcName}» має namespace «${routeNs}», що збігається з metadata.namespace маршруту — прибери поле namespace з backendRef (див. k8s.mdc)`
|
|
3013
|
+
)
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
2909
3017
|
}
|
|
2910
3018
|
|
|
2911
3019
|
/**
|
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
* Якщо таких файлів немає — layout через `npm/tsconfig.emit-types.json`: поле `types` має вказувати на існуючий
|
|
10
10
|
* файл під `./types/…`, у hk — `tsc -p tsconfig.emit-types.json`, у JSON-конфігу — потрібні compilerOptions для emit.
|
|
11
11
|
*
|
|
12
|
-
* Окремо перевіряється `npm/CHANGELOG.md`: файл існує, є в `files` у `npm/package.json` і містить запис
|
|
13
|
-
* для поточної версії (формат `## [X.Y.Z] - YYYY-MM-DD`, Keep a Changelog).
|
|
14
|
-
*
|
|
15
12
|
* Поля workflow перевіряються після **YAML parse**, щоб не плутати з коментарями.
|
|
16
13
|
*/
|
|
17
14
|
import { existsSync } from 'node:fs'
|
|
@@ -37,9 +34,6 @@ const TYPES_INDEX = './types/index.d.ts'
|
|
|
37
34
|
/** Файл проєкту TypeScript для emit без каталогу `src` (див. npm-module.mdc) */
|
|
38
35
|
const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
|
|
39
36
|
|
|
40
|
-
/** Шлях до `CHANGELOG.md` в каталозі npm-модуля */
|
|
41
|
-
const CHANGELOG_PATH = 'npm/CHANGELOG.md'
|
|
42
|
-
|
|
43
37
|
/**
|
|
44
38
|
* Чи є під `npm/src` хоча б один `.js` (рекурсивно).
|
|
45
39
|
* @returns {Promise<boolean>} `true`, якщо знайдено хоча б один `.js`
|
|
@@ -205,53 +199,6 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
|
|
|
205
199
|
}
|
|
206
200
|
}
|
|
207
201
|
|
|
208
|
-
/**
|
|
209
|
-
* Чи містить текст `CHANGELOG.md` запис заголовка для конкретної версії
|
|
210
|
-
* у форматі Keep a Changelog: `## [X.Y.Z]` (з опційним `- YYYY-MM-DD`).
|
|
211
|
-
* @param {string} text вміст `CHANGELOG.md`
|
|
212
|
-
* @param {string} version номер версії з `npm/package.json`
|
|
213
|
-
* @returns {boolean} `true`, якщо знайдено заголовок версії
|
|
214
|
-
*/
|
|
215
|
-
function changelogHasVersionEntry(text, version) {
|
|
216
|
-
const escaped = version.replaceAll(/[.+*?^$()[\]{}|\\]/g, String.raw`\$&`)
|
|
217
|
-
const re = new RegExp(String.raw`^##\s+\[${escaped}\]`, 'm')
|
|
218
|
-
return re.test(text)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Перевіряє наявність і вміст `npm/CHANGELOG.md`, а також що він є в `files` у `npm/package.json`.
|
|
223
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
224
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
225
|
-
*/
|
|
226
|
-
async function checkChangelog(passFn, failFn) {
|
|
227
|
-
if (!existsSync(CHANGELOG_PATH)) {
|
|
228
|
-
failFn(`Відсутній ${CHANGELOG_PATH} (npm-module.mdc: CHANGELOG)`)
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
|
-
passFn(`${CHANGELOG_PATH} існує`)
|
|
232
|
-
|
|
233
|
-
if (existsSync('npm/package.json')) {
|
|
234
|
-
const npmPkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
|
|
235
|
-
if (Array.isArray(npmPkg.files) && npmPkg.files.includes('CHANGELOG.md')) {
|
|
236
|
-
passFn('npm/package.json: files містить "CHANGELOG.md"')
|
|
237
|
-
} else {
|
|
238
|
-
failFn('npm/package.json: масив files має містити "CHANGELOG.md"')
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const version = typeof npmPkg.version === 'string' ? npmPkg.version : null
|
|
242
|
-
if (version) {
|
|
243
|
-
const text = await readFile(CHANGELOG_PATH, 'utf8')
|
|
244
|
-
if (changelogHasVersionEntry(text, version)) {
|
|
245
|
-
passFn(`${CHANGELOG_PATH}: знайдено запис для версії ${version}`)
|
|
246
|
-
} else {
|
|
247
|
-
failFn(
|
|
248
|
-
`${CHANGELOG_PATH}: відсутній запис для поточної версії ${version} (формат Keep a Changelog: "## [${version}] - YYYY-MM-DD")`
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
202
|
/**
|
|
256
203
|
* Перевіряє npm/tsconfig.emit-types.json.
|
|
257
204
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
@@ -397,8 +344,6 @@ export async function check() {
|
|
|
397
344
|
|
|
398
345
|
await checkNpmPackageJson(useSrcJsLayout, pass, fail)
|
|
399
346
|
|
|
400
|
-
await checkChangelog(pass, fail)
|
|
401
|
-
|
|
402
347
|
if (!useSrcJsLayout) {
|
|
403
348
|
await checkEmitTypesConfig(pass, fail)
|
|
404
349
|
}
|