@nitra/cursor 1.8.208 → 1.8.209
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/CHANGELOG.md +84 -0
- package/package.json +1 -1
- package/policy/abie/health_check_policy/health_check_policy.rego +5 -1
- package/policy/abie/http_route_base/http_route_base.rego +2 -1
- package/policy/hasura/svc_hl/svc_hl.rego +2 -1
- package/policy/k8s/manifest/manifest.rego +2 -0
- package/scripts/check-adr.mjs +10 -88
- package/scripts/check-ga.mjs +14 -192
- package/scripts/check-js-lint.mjs +14 -115
- package/scripts/check-npm-module.mjs +17 -155
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,90 @@
|
|
|
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.209] - 2026-05-08
|
|
8
|
+
|
|
9
|
+
### Removed
|
|
10
|
+
|
|
11
|
+
- Дедуплікація JS-перевірок, що вже покриті Rego-полісі (запускаються через
|
|
12
|
+
`bun run lint-conftest`):
|
|
13
|
+
- `npm/scripts/check-bun.mjs` — без `checkBunfigHoisted`, `checkDevDependencies`,
|
|
14
|
+
`checkLintAggregate`, перевірок `pkg.packageManager` і кореневого
|
|
15
|
+
`pkg.dependencies`. Лишилася FS-existence (`bun.lock`, `bunfig.toml`,
|
|
16
|
+
`package.json`, заборонені lockfile, директорія `.yarn/`) і cross-file гейт
|
|
17
|
+
`lint-docker` / `lint-k8s` від `.n-cursor.json:rules`.
|
|
18
|
+
- `npm/scripts/check-php.mjs` — без перевірок `lint-php` скрипта і `run` у
|
|
19
|
+
`lint-php.yml`. Лишилися FS-existence для `composer.json`, `package.json`,
|
|
20
|
+
`lint-php.yml`.
|
|
21
|
+
- `npm/scripts/check-style-lint.mjs` — без перевірок `lint-style` через
|
|
22
|
+
`npx stylelint`, `@nitra/stylelint-config` у `devDependencies`,
|
|
23
|
+
`stylelint.extends`, `npx stylelint` у `lint-style.yml`. Лишилися VSCode-
|
|
24
|
+
конфіги, `.stylelintignore`, FS-existence workflow і альтернатива зовнішнього
|
|
25
|
+
конфіг-файлу `stylelint`.
|
|
26
|
+
- `npm/scripts/check-graphql.mjs` — без `checkPackageDumpSchemaScript` (структура
|
|
27
|
+
`scripts.dump-schema`); решта логіки (gql AST-скан, `.graphqlrc.yml`,
|
|
28
|
+
VSCode-розширення) лишилася.
|
|
29
|
+
- `npm/scripts/check-image-compress.mjs` — без `checkLintImageScript`,
|
|
30
|
+
`checkLintAggregateIncludesImage`, `checkMinifyImageNotInDeps`. Лишилися
|
|
31
|
+
`.n-minify-image.tsv` НЕ в `.gitignore` і видалення застарілого
|
|
32
|
+
`.minify-image-cache.tsv`.
|
|
33
|
+
- `npm/scripts/check-js-bun-db.mjs` — без `checkForbiddenDependencies`
|
|
34
|
+
(`pg`/`pg-format`/`mysql2`); AST-скан коду (`new SQL(...)` всередині функції,
|
|
35
|
+
`unsafe()` без маркера, динамічні `IN (…)`) лишився.
|
|
36
|
+
- `npm/scripts/check-text.mjs` — без `checkOxfmtRc`, `checkCspellConfig`,
|
|
37
|
+
`checkCspellJsonDictImports`, `checkMarkdownlintConfig`, `prettier`/`@nitra/cspell-dict`/`markdownlint-cli2`/`@nitra/*` гейт у
|
|
38
|
+
`checkPackageJsonTextDepsUsage`. Лишилися VSCode-конфіги, `.v8rignore`,
|
|
39
|
+
Prettier-файли в корені, абзац про український апостроф у `.mdc`,
|
|
40
|
+
складна валідація скрипта `lint-text` і виклик `bun run lint-text` у
|
|
41
|
+
workflow.
|
|
42
|
+
- `npm/scripts/check-vue.mjs` — без `checkViteVersion` (vite ≥ 8). AST-скан коду
|
|
43
|
+
і vite-config-перевірки лишилися.
|
|
44
|
+
- `npm/scripts/check-npm-module.mjs` — без `checkNpmTypesField`,
|
|
45
|
+
`emitTypesConfigIssues`, перевірок полів `npm-publish.yml`,
|
|
46
|
+
`workspaces ∋ "npm"` у кореневому `package.json`. Лишилися FS-existence,
|
|
47
|
+
наявність файлу зі шляху `types`, hk.pkl-перевірки, CHANGELOG-version-match,
|
|
48
|
+
git-dirty-bump.
|
|
49
|
+
- `npm/scripts/check-js-lint.mjs` — без `checkPackageJsonLintDeps`
|
|
50
|
+
(prettier-залежність, `@nitra/eslint-config ≥ 3.9.2`),
|
|
51
|
+
`checkPackageJsonTypeModule` для root, `checkEnginesNode/Bun` для root,
|
|
52
|
+
канонічний `lint-js`-скрипт, валідація `lint-js.yml` (`verifyLintJsWorkflowStructure`
|
|
53
|
+
+ fallback). Лишилися — `.oxlintrc.json` canonical-snapshot, VSCode-розширення,
|
|
54
|
+
workspace-ітерація для `type: "module"` і engines, дубль JS-кроків у `lint.yml`,
|
|
55
|
+
`.jscpd.json`. Прибрано непотрібні імпорти `parseWorkflowYaml`,
|
|
56
|
+
`verifyLintJsWorkflowStructure` і `OXLINT_FIX_RE`.
|
|
57
|
+
- `npm/scripts/check-js-run.mjs` — без перевірок `bunyan` / `@nitra/bunyan` у
|
|
58
|
+
залежностях, canonical `jsconfig.json` через `deepEqualJson`,
|
|
59
|
+
`OTEL_RESOURCE_ATTRIBUTES` у `configmap.yaml`. Лишилися AST-скан коду
|
|
60
|
+
(bunyan, conn-aliases, process.env, setTimeout) і FS-existence для
|
|
61
|
+
`jsconfig.json` / `configmap.yaml`. Прибрано `CANONICAL_BACKEND_JSCONFIG`,
|
|
62
|
+
`deepEqualJson`.
|
|
63
|
+
- `npm/scripts/check-adr.mjs` — без `settingsHaveAdrHookGroup`,
|
|
64
|
+
`checkProjectSettings` структурного порівняння і
|
|
65
|
+
`checkLocalSettingsNoDuplicate`. Лишилися hash-порівняння bash-скрипта,
|
|
66
|
+
`.gitignore`-патерн, LLM CLI у PATH, FS-existence settings.json.
|
|
67
|
+
Прибрано `HOOK_COMMAND_MARKER`, `PROJECT_LOCAL_SETTINGS_PATH` (для
|
|
68
|
+
settings.local — Rego policy gating).
|
|
69
|
+
- `npm/scripts/check-ga.mjs` — без `verifyConcurrencyBlock`,
|
|
70
|
+
`verifyNoDirectBunOrCache`, `verifyNoRunShellLineContinuationBackslash`,
|
|
71
|
+
`verifyCheckoutBeforeLocalSetupBunDeps`, `validateConcurrencyOnRoot`. Тепер
|
|
72
|
+
усі workflow-структурні перевірки виконуються через conftest у `lint-ga.mjs`
|
|
73
|
+
(`ga.workflow_common`); лишилася лише git-залежна перевірка `on.*.paths`
|
|
74
|
+
glob-ів через `git ls-files :(glob)`. Прибрано константи
|
|
75
|
+
`SETUP_BUN_PATTERNS`, `FORBIDDEN_BUN_PATTERNS`, `EXPECTED_CONCURRENCY_GROUP`
|
|
76
|
+
і непотрібні імпорти з `gha-workflow.mjs`.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- Тести `check-bun.test.mjs`, `check-image-compress.test.mjs`,
|
|
81
|
+
`check-js-bun-db.test.mjs`, `check-js-run-fixture.test.mjs`,
|
|
82
|
+
`check-adr.test.mjs` — прибрано / `skip` тести, що дублювали Rego-полісі;
|
|
83
|
+
лишилися лише FS / cross-file сценарії.
|
|
84
|
+
- `npm/policy/{capacitor,js_mssql,abie,k8s,hasura}/**/*.rego` — у заголовках
|
|
85
|
+
policy-файлів додано позначку, що JS-чек у відповідному `check-*.mjs`
|
|
86
|
+
лишається authoritative (повна semver-семантика з OR-діапазонами для
|
|
87
|
+
`capacitor`/`js-mssql`; ширший набір полів і cross-file Kustomize-контекст
|
|
88
|
+
для `abie`/`k8s`; cross-file env-DNS-резолюція для `hasura`). Rego там — швидкий
|
|
89
|
+
гейт для одиничного файлу (наприклад через IDE).
|
|
90
|
+
|
|
7
91
|
## [1.8.208] - 2026-05-08
|
|
8
92
|
|
|
9
93
|
### Added
|
package/package.json
CHANGED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
# - `spec.targetRef.name` має закінчуватись на `-hl` (headless backend).
|
|
13
13
|
#
|
|
14
14
|
# Cross-file gating (`abie` правило в `.n-cursor.json`, парність з Deployment-каталогу,
|
|
15
|
-
# узгодження з `metadata.name` Deployment) — у JS (`check-abie.mjs`).
|
|
15
|
+
# узгодження з `metadata.name` Deployment) — у JS (`check-abie.mjs`). JS-перевірка
|
|
16
|
+
# в `check-abie.mjs` (`validateAbieHcPolicy`) authoritative й тестує ширший набір полів
|
|
17
|
+
# (apiVersion, spec.default.config.type=="HTTP", targetRef.kind=="Service",
|
|
18
|
+
# обчислений `<name>-hl` суфікс); ця Rego — швидкий gate для одиничного YAML
|
|
19
|
+
# (наприклад через IDE).
|
|
16
20
|
#
|
|
17
21
|
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
18
22
|
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Cross-file gating (саме шлях `…/base/…` визначає, чи застосовувати правило)
|
|
10
10
|
# — у JS: conftest викликаємо лише на YAML-ах з base/. Тут — лише валідація вмісту
|
|
11
|
-
# `spec.hostnames`.
|
|
11
|
+
# `spec.hostnames`. JS authoritative (`check-abie.mjs`) — ця Rego гейт для
|
|
12
|
+
# одиничного YAML.
|
|
12
13
|
#
|
|
13
14
|
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
14
15
|
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
# Решта логіки `check-hasura.mjs` (звірення `HASURA_GRAPHQL_ENDPOINT` в `.env`-файлах
|
|
10
10
|
# з `<service>.<namespace>.svc.<cluster>` через regex по всьому дереву репо, gating
|
|
11
11
|
# на `repository` у кореневому `package.json`) — у JS: вона потребує текстового
|
|
12
|
-
# парсингу `.env`-файлів, обходу дерева й cross-file resolution.
|
|
12
|
+
# парсингу `.env`-файлів, обходу дерева й cross-file resolution. JS authoritative;
|
|
13
|
+
# ця Rego — додатковий gate (JS неявно перевіряє суфікс через звірку URL).
|
|
13
14
|
#
|
|
14
15
|
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
15
16
|
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
# HPA/PDB/topologySpreadConstraints за каталогом, BackendConfig-сепарація,
|
|
21
21
|
# yaml-language-server schema modeline, namespace-перевірки за деревом
|
|
22
22
|
# `…/k8s/base/`) лишається у `check-k8s.mjs`: вона потребує файлової системи.
|
|
23
|
+
# JS authoritative (`check-k8s.mjs` робить ці ж пер-документні перевірки в ширшому
|
|
24
|
+
# контексті); ця Rego — швидкий gate для одиничного маніфеста.
|
|
23
25
|
#
|
|
24
26
|
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
25
27
|
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
package/scripts/check-adr.mjs
CHANGED
|
@@ -23,9 +23,7 @@ import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
|
23
23
|
|
|
24
24
|
const PROJECT_HOOK_PATH = '.claude/hooks/capture-decisions.sh'
|
|
25
25
|
const PROJECT_SETTINGS_PATH = '.claude/settings.json'
|
|
26
|
-
const PROJECT_LOCAL_SETTINGS_PATH = '.claude/settings.local.json'
|
|
27
26
|
const PROJECT_LOG_PATH = '.claude/hooks/capture-decisions.log'
|
|
28
|
-
const HOOK_COMMAND_MARKER = '.claude/hooks/capture-decisions.sh'
|
|
29
27
|
const EOL_RE = /\r?\n/u
|
|
30
28
|
|
|
31
29
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
@@ -81,93 +79,18 @@ async function checkHookScript(reporter) {
|
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
82
|
+
* FS-existence для project-shared `.claude/settings.json` і
|
|
83
|
+
* `.claude/settings.local.json`. Структуру (`hooks.Stop[]` містить групу з
|
|
84
|
+
* `capture-decisions.sh`; `settings.local.json` не дублює) валідують
|
|
85
|
+
* `npm/policy/adr/settings_json/` і `npm/policy/adr/settings_local_json/`.
|
|
86
|
+
* @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер
|
|
87
87
|
*/
|
|
88
|
-
function
|
|
89
|
-
if (!settings || typeof settings !== 'object') {
|
|
90
|
-
return false
|
|
91
|
-
}
|
|
92
|
-
const hooks = /** @type {Record<string, unknown>} */ (settings).hooks
|
|
93
|
-
if (!hooks || typeof hooks !== 'object') {
|
|
94
|
-
return false
|
|
95
|
-
}
|
|
96
|
-
const stopGroups = /** @type {Record<string, unknown>} */ (hooks).Stop
|
|
97
|
-
if (!Array.isArray(stopGroups)) {
|
|
98
|
-
return false
|
|
99
|
-
}
|
|
100
|
-
return stopGroups.some(group => {
|
|
101
|
-
const inner = group && typeof group === 'object' ? /** @type {Record<string, unknown>} */ (group).hooks : null
|
|
102
|
-
if (!Array.isArray(inner)) {
|
|
103
|
-
return false
|
|
104
|
-
}
|
|
105
|
-
return inner.some(h => {
|
|
106
|
-
const cmd = h && typeof h === 'object' ? /** @type {Record<string, unknown>} */ (h).command : null
|
|
107
|
-
return typeof cmd === 'string' && cmd.includes(HOOK_COMMAND_MARKER)
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Зчитує JSON-файл або повертає `undefined`, якщо файл відсутній чи невалідний.
|
|
114
|
-
* @param {string} path відносний шлях до JSON-файлу
|
|
115
|
-
* @returns {Promise<unknown | undefined>} розпарсений вміст або `undefined`
|
|
116
|
-
*/
|
|
117
|
-
async function readJsonOrUndefined(path) {
|
|
118
|
-
if (!existsSync(path)) {
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
try {
|
|
122
|
-
return JSON.parse(await readFile(path, 'utf8'))
|
|
123
|
-
} catch {
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Перевіряє project-shared `.claude/settings.json` на наявність ADR Stop-hook'а.
|
|
130
|
-
* @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
|
|
131
|
-
* @returns {Promise<void>}
|
|
132
|
-
*/
|
|
133
|
-
async function checkProjectSettings(reporter) {
|
|
88
|
+
function checkProjectSettings(reporter) {
|
|
134
89
|
const { pass, fail } = reporter
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
fail(`${PROJECT_SETTINGS_PATH} не існує або невалідний — запусти \`npx @nitra/cursor\``)
|
|
138
|
-
return
|
|
139
|
-
}
|
|
140
|
-
if (settingsHaveAdrHookGroup(settings)) {
|
|
141
|
-
pass(`${PROJECT_SETTINGS_PATH} містить ADR Stop-hook (capture-decisions.sh)`)
|
|
142
|
-
} else {
|
|
143
|
-
fail(
|
|
144
|
-
`${PROJECT_SETTINGS_PATH}: у hooks.Stop немає групи з \`${HOOK_COMMAND_MARKER}\` — переконайся, що "adr" у rules і запусти \`npx @nitra/cursor\``
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Перевіряє, що `.claude/settings.local.json` не дублює ADR Stop-hook (project-shared — джерело правди).
|
|
151
|
-
* @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
|
|
152
|
-
* @returns {Promise<void>}
|
|
153
|
-
*/
|
|
154
|
-
async function checkLocalSettingsNoDuplicate(reporter) {
|
|
155
|
-
const { pass, fail } = reporter
|
|
156
|
-
if (!existsSync(PROJECT_LOCAL_SETTINGS_PATH)) {
|
|
157
|
-
pass(`${PROJECT_LOCAL_SETTINGS_PATH} відсутній — дубля немає`)
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
const local = await readJsonOrUndefined(PROJECT_LOCAL_SETTINGS_PATH)
|
|
161
|
-
if (local === undefined) {
|
|
162
|
-
pass(`${PROJECT_LOCAL_SETTINGS_PATH} нечитабельний — дубля немає`)
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
if (settingsHaveAdrHookGroup(local)) {
|
|
166
|
-
fail(
|
|
167
|
-
`${PROJECT_LOCAL_SETTINGS_PATH} містить дубль ADR Stop-hook (capture-decisions.sh) — прибери, бо project-shared settings.json уже керує цим`
|
|
168
|
-
)
|
|
90
|
+
if (existsSync(PROJECT_SETTINGS_PATH)) {
|
|
91
|
+
pass(`${PROJECT_SETTINGS_PATH} є (Stop-hook перевіряє bun run lint-conftest → adr.settings_json)`)
|
|
169
92
|
} else {
|
|
170
|
-
|
|
93
|
+
fail(`${PROJECT_SETTINGS_PATH} не існує — запусти \`npx @nitra/cursor\``)
|
|
171
94
|
}
|
|
172
95
|
}
|
|
173
96
|
|
|
@@ -244,8 +167,7 @@ function checkLlmCliAvailable(reporter) {
|
|
|
244
167
|
export async function check() {
|
|
245
168
|
const reporter = createCheckReporter()
|
|
246
169
|
await checkHookScript(reporter)
|
|
247
|
-
|
|
248
|
-
await checkLocalSettingsNoDuplicate(reporter)
|
|
170
|
+
checkProjectSettings(reporter)
|
|
249
171
|
await checkGitignore(reporter)
|
|
250
172
|
checkLlmCliAvailable(reporter)
|
|
251
173
|
return reporter.getExitCode()
|
package/scripts/check-ga.mjs
CHANGED
|
@@ -4,18 +4,15 @@
|
|
|
4
4
|
* Workflows лише з розширенням `.yml`, наявність clean/lint workflow, конфіг zizmor з ref-pin,
|
|
5
5
|
* відсутність MegaLinter, коректний скрипт `lint-ga` у `package.json`, виклик у `lint-ga.yml`,
|
|
6
6
|
* наявність composite `.github/actions/setup-bun-deps/action.yml` (його записує npx `\@nitra/cursor`),
|
|
7
|
-
* `\.vscode/settings.json` — `editor.defaultFormatter` **oxc** для `[github-actions-workflow]
|
|
8
|
-
* перед `uses: ./…/setup-bun-deps` у workflow — `actions/checkout` (runner інакше не бачить локальний action).
|
|
7
|
+
* `\.vscode/settings.json` — `editor.defaultFormatter` **oxc** для `[github-actions-workflow]`.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* (
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* У `run:` заборонено shell-продовження рядків через `\\` перед переносом; довгі команди — через folded block `>-`.
|
|
9
|
+
* Структурні поля 4 канонічних workflow (`clean-ga-workflows.yml`, `clean-merged-branch.yml`,
|
|
10
|
+
* `lint-ga.yml`, `git-ai.yml`) і УНІВЕРСАЛЬНІ перевірки для всіх `.github/workflows/*.yml`
|
|
11
|
+
* (`concurrency`, заборонені `oven-sh/setup-bun` / `actions/cache` / `bun install` у `uses`/`run`,
|
|
12
|
+
* shell-продовження `\` у `run`, обов'язковий `actions/checkout@v6` перед локальним
|
|
13
|
+
* `setup-bun-deps`) — у Rego-полісі під `npm/policy/ga/` і запускаються через
|
|
14
|
+
* `bun run lint-ga` (`runConftestStep` у `lint-ga.mjs`). Тут лишилася лише git-залежна
|
|
15
|
+
* перевірка `on.*.paths` glob-ів через `git ls-files :(glob)`.
|
|
19
16
|
*/
|
|
20
17
|
import { existsSync } from 'node:fs'
|
|
21
18
|
import { readdir, readFile } from 'node:fs/promises'
|
|
@@ -23,14 +20,7 @@ import { execFileSync } from 'node:child_process'
|
|
|
23
20
|
import { join } from 'node:path'
|
|
24
21
|
|
|
25
22
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
26
|
-
import {
|
|
27
|
-
eventPathsIncludeExact,
|
|
28
|
-
findForbiddenUsesOrRunPatterns,
|
|
29
|
-
findRunStepsWithShellLineContinuationBackslash,
|
|
30
|
-
hasAnyStepUsesContaining,
|
|
31
|
-
hasCheckoutBeforeLocalSetupBunDeps,
|
|
32
|
-
parseWorkflowYaml
|
|
33
|
-
} from './utils/gha-workflow.mjs'
|
|
23
|
+
import { eventPathsIncludeExact, parseWorkflowYaml } from './utils/gha-workflow.mjs'
|
|
34
24
|
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
35
25
|
|
|
36
26
|
/** Шаблони наявності MegaLinter у вмісті workflow */
|
|
@@ -41,22 +31,9 @@ const MEGALINTER_CONFIG_NAMES = ['.mega-linter.yml', '.megalinter.yaml', '.mega-
|
|
|
41
31
|
|
|
42
32
|
const N_CURSOR_LINT_GA_RE = /\bn-cursor\s+lint-ga\b/
|
|
43
33
|
|
|
44
|
-
/** Локальні composite setup-bun-deps (ga.mdc). */
|
|
45
|
-
const SETUP_BUN_PATTERNS = ['./.github/actions/setup-bun-deps', './npm/github-actions/setup-bun-deps']
|
|
46
|
-
|
|
47
|
-
/** Заборонені підрядки лише в кроках uses/run. */
|
|
48
|
-
const FORBIDDEN_BUN_PATTERNS = [
|
|
49
|
-
{ pattern: 'oven-sh/setup-bun', msg: 'використовуй .github/actions/setup-bun-deps замість oven-sh/setup-bun' },
|
|
50
|
-
{ pattern: 'actions/cache', msg: 'використовуй .github/actions/setup-bun-deps замість actions/cache' },
|
|
51
|
-
{ pattern: 'bun install', msg: 'використовуй .github/actions/setup-bun-deps замість bun install' }
|
|
52
|
-
]
|
|
53
|
-
|
|
54
34
|
/** Обовʼязкові workflow-файли (ga.mdc). */
|
|
55
35
|
const REQUIRED_WORKFLOWS = ['clean-ga-workflows.yml', 'clean-merged-branch.yml', 'lint-ga.yml', 'git-ai.yml']
|
|
56
36
|
|
|
57
|
-
/** Канонічне значення `concurrency.group` (ga.mdc). Збирається з фрагментів, щоб не плодити expression-токени в коді. */
|
|
58
|
-
const EXPECTED_CONCURRENCY_GROUP = ['$', '{{ github.ref }}-$', '{{ github.workflow }}'].join('')
|
|
59
|
-
|
|
60
37
|
/**
|
|
61
38
|
* Повертає true, якщо glob у GitHub Actions `on.*.paths` матчитсья хоча б на один tracked файл у репозиторії.
|
|
62
39
|
*
|
|
@@ -156,162 +133,6 @@ function getObjKey(obj, key) {
|
|
|
156
133
|
: undefined
|
|
157
134
|
}
|
|
158
135
|
|
|
159
|
-
/**
|
|
160
|
-
* Перевіряє блок `concurrency` на вже розпарсеному корені workflow (ga.mdc).
|
|
161
|
-
*
|
|
162
|
-
* Використовується в канонічних структурних валідаторах (clean-ga-workflows, clean-merged-branch,
|
|
163
|
-
* lint-ga, git-ai), де root уже отримано через `parseWorkflowYaml`. Логіка ідентична
|
|
164
|
-
* `verifyConcurrencyBlock`, але без повторного парсингу.
|
|
165
|
-
* @param {string} relPath шлях для повідомлень
|
|
166
|
-
* @param {Record<string, unknown>} root parsed YAML workflow
|
|
167
|
-
* @param {(msg: string) => void} passFn pass
|
|
168
|
-
* @param {(msg: string) => void} failFn fail
|
|
169
|
-
* @returns {void}
|
|
170
|
-
*/
|
|
171
|
-
function validateConcurrencyOnRoot(relPath, root, passFn, failFn) {
|
|
172
|
-
const conc = getObjKey(root, 'concurrency')
|
|
173
|
-
if (!conc || typeof conc !== 'object') {
|
|
174
|
-
failFn(
|
|
175
|
-
`${relPath}: відсутня секція concurrency — додай concurrency.group: ${EXPECTED_CONCURRENCY_GROUP} і cancel-in-progress: true (ga.mdc)`
|
|
176
|
-
)
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
const group = getObjKey(conc, 'group')
|
|
180
|
-
const cancel = getObjKey(conc, 'cancel-in-progress')
|
|
181
|
-
if (group !== EXPECTED_CONCURRENCY_GROUP) {
|
|
182
|
-
failFn(`${relPath}: concurrency.group має бути ${EXPECTED_CONCURRENCY_GROUP} (ga.mdc)`)
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
if (cancel !== true) {
|
|
186
|
-
failFn(`${relPath}: concurrency.cancel-in-progress має бути true (ga.mdc)`)
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
passFn(`${relPath}: concurrency блок OK`)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Перевіряє, що workflow містить блок `concurrency` з канонічними `group` і `cancel-in-progress: true` (ga.mdc).
|
|
194
|
-
*
|
|
195
|
-
* Без винятків — застосовується до всіх workflow у `.github/workflows/*.yml`, включно з scheduled cleanup,
|
|
196
|
-
* `pull_request: types: [closed]` та publish-воркфлоу. Делегує логіку `validateConcurrencyOnRoot`,
|
|
197
|
-
* додаючи лише крок парсингу YAML; якщо парсинг провалився — мовчки виходить (синтаксичні проблеми
|
|
198
|
-
* ловлять інші перевірки).
|
|
199
|
-
* @param {string} relPath шлях для повідомлень
|
|
200
|
-
* @param {string} content вміст YAML
|
|
201
|
-
* @param {(msg: string) => void} failFn реєструє порушення (exit 1)
|
|
202
|
-
* @param {(msg: string) => void} passFn реєструє успішну перевірку
|
|
203
|
-
* @returns {void}
|
|
204
|
-
*/
|
|
205
|
-
function verifyConcurrencyBlock(relPath, content, failFn, passFn) {
|
|
206
|
-
const root = parseWorkflowYaml(content)
|
|
207
|
-
if (!root) {
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
validateConcurrencyOnRoot(relPath, root, passFn, failFn)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Якщо workflow викликає локальний setup-bun-deps, раніше у файлі має бути `actions/checkout@v…` (ga.mdc).
|
|
215
|
-
* Fallback: сирий текст, якщо YAML не вдається розібрати.
|
|
216
|
-
* @param {string} relPath шлях для повідомлень
|
|
217
|
-
* @param {string} content вміст YAML
|
|
218
|
-
* @param {(msg: string) => void} failFn реєструє порушення (exit 1)
|
|
219
|
-
* @param {(msg: string) => void} passFn реєструє успішну перевірку
|
|
220
|
-
* @returns {void}
|
|
221
|
-
*/
|
|
222
|
-
function verifyCheckoutBeforeLocalSetupBunDeps(relPath, content, failFn, passFn) {
|
|
223
|
-
const root = parseWorkflowYaml(content)
|
|
224
|
-
if (root) {
|
|
225
|
-
if (!hasAnyStepUsesContaining(root, SETUP_BUN_PATTERNS)) {
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
if (!hasCheckoutBeforeLocalSetupBunDeps(root, SETUP_BUN_PATTERNS)) {
|
|
229
|
-
failFn(
|
|
230
|
-
`${relPath}: перед локальним setup-bun-deps потрібен крок actions/checkout@v6 — інакше runner не знайде action.yml (ga.mdc)`
|
|
231
|
-
)
|
|
232
|
-
return
|
|
233
|
-
}
|
|
234
|
-
passFn(`${relPath}: перед setup-bun-deps є checkout`)
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
let idxSetup = -1
|
|
238
|
-
for (const p of SETUP_BUN_PATTERNS) {
|
|
239
|
-
const i = content.indexOf(p)
|
|
240
|
-
if (i !== -1 && (idxSetup === -1 || i < idxSetup)) {
|
|
241
|
-
idxSetup = i
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
if (idxSetup === -1) {
|
|
245
|
-
return
|
|
246
|
-
}
|
|
247
|
-
const idxCheckout = content.indexOf('actions/checkout@v')
|
|
248
|
-
if (idxCheckout === -1 || idxCheckout > idxSetup) {
|
|
249
|
-
failFn(
|
|
250
|
-
`${relPath}: перед локальним setup-bun-deps потрібен крок actions/checkout@v6 — інакше runner не знайде action.yml (ga.mdc)`
|
|
251
|
-
)
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
passFn(`${relPath}: перед setup-bun-deps є checkout`)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Перевіряє заборонені кроки Bun/cache/install у `uses` та `run`.
|
|
259
|
-
* @param {string} relPath шлях для повідомлень
|
|
260
|
-
* @param {string} content вміст YAML
|
|
261
|
-
* @param {(msg: string) => void} failFn реєструє порушення (exit 1)
|
|
262
|
-
* @param {(msg: string) => void} passFn реєструє успішну перевірку
|
|
263
|
-
* @returns {void}
|
|
264
|
-
*/
|
|
265
|
-
function verifyNoDirectBunOrCache(relPath, content, failFn, passFn) {
|
|
266
|
-
const root = parseWorkflowYaml(content)
|
|
267
|
-
if (root) {
|
|
268
|
-
const hits = findForbiddenUsesOrRunPatterns(root, FORBIDDEN_BUN_PATTERNS)
|
|
269
|
-
if (hits.length === 0) {
|
|
270
|
-
passFn(`${relPath}: не містить заборонених кроків setup-bun/cache/install`)
|
|
271
|
-
} else {
|
|
272
|
-
for (const h of hits) {
|
|
273
|
-
failFn(`${relPath}: ${h.msg} (ga.mdc)`)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
let foundForbidden = false
|
|
279
|
-
for (const { pattern, msg } of FORBIDDEN_BUN_PATTERNS) {
|
|
280
|
-
if (content.includes(pattern)) {
|
|
281
|
-
failFn(`${relPath}: ${msg} (ga.mdc)`)
|
|
282
|
-
foundForbidden = true
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (!foundForbidden) {
|
|
286
|
-
passFn(`${relPath}: не містить заборонених кроків setup-bun/cache/install`)
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* У кроках `run` заборонено shell-продовження через `\\` перед переносом; замість `run: |` з `\\` використовуй `run: >-`.
|
|
292
|
-
* @param {string} relPath шлях для повідомлень
|
|
293
|
-
* @param {string} content вміст YAML
|
|
294
|
-
* @param {(msg: string) => void} failFn реєструє порушення (exit 1)
|
|
295
|
-
* @param {(msg: string) => void} passFn реєструє успішну перевірку
|
|
296
|
-
* @returns {void}
|
|
297
|
-
*/
|
|
298
|
-
function verifyNoRunShellLineContinuationBackslash(relPath, content, failFn, passFn) {
|
|
299
|
-
const root = parseWorkflowYaml(content)
|
|
300
|
-
if (!root) {
|
|
301
|
-
return
|
|
302
|
-
}
|
|
303
|
-
const hits = findRunStepsWithShellLineContinuationBackslash(root)
|
|
304
|
-
if (hits.length === 0) {
|
|
305
|
-
passFn(String.raw`${relPath}: run без shell-продовження через \ (ga.mdc)`)
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
for (const h of hits) {
|
|
309
|
-
failFn(
|
|
310
|
-
String.raw`${relPath}: job ${h.jobId}, крок ${h.stepIndex + 1}: у run заборонено продовження рядків через зворотний сліш; довгі команди оформи як folded block (run: >-) без \ на кінцях рядків (ga.mdc)`
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
136
|
/**
|
|
316
137
|
* Перевіряє apply-workflow на наявність paths trigger.
|
|
317
138
|
* @param {string} wfDir директорія workflows
|
|
@@ -549,12 +370,13 @@ export async function check() {
|
|
|
549
370
|
const ymlWorkflows = files.filter(f => f.endsWith('.yml'))
|
|
550
371
|
await checkMegalinter(wfDir, ymlWorkflows, pass, fail)
|
|
551
372
|
|
|
373
|
+
// Універсальні структурні перевірки (concurrency, заборонені setup-bun/cache,
|
|
374
|
+
// shell line-continuation `\`, checkout перед локальним setup-bun-deps)
|
|
375
|
+
// перенесено в Rego (`npm/policy/ga/workflow_common/`); їх запускає
|
|
376
|
+
// `bun run lint-ga` через conftest. Тут лишилася лише git-залежна перевірка
|
|
377
|
+
// `on.push.paths` glob-ів (вимагає `git ls-files`).
|
|
552
378
|
for (const f of ymlWorkflows) {
|
|
553
379
|
const content = await readFile(join(wfDir, f), 'utf8')
|
|
554
|
-
verifyCheckoutBeforeLocalSetupBunDeps(`${wfDir}/${f}`, content, fail, pass)
|
|
555
|
-
verifyNoDirectBunOrCache(`${wfDir}/${f}`, content, fail, pass)
|
|
556
|
-
verifyNoRunShellLineContinuationBackslash(`${wfDir}/${f}`, content, fail, pass)
|
|
557
|
-
verifyConcurrencyBlock(`${wfDir}/${f}`, content, fail, pass)
|
|
558
380
|
const parsed = parseWorkflowYaml(content)
|
|
559
381
|
if (parsed) {
|
|
560
382
|
verifyWorkflowEventPathsGlobsExist(`${wfDir}/${f}`, parsed, pass, fail)
|
|
@@ -16,7 +16,6 @@ import { readFile } from 'node:fs/promises'
|
|
|
16
16
|
import { dirname, join } from 'node:path'
|
|
17
17
|
import { fileURLToPath } from 'node:url'
|
|
18
18
|
|
|
19
|
-
import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-workflow.mjs'
|
|
20
19
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
21
20
|
|
|
22
21
|
/** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
|
|
@@ -34,7 +33,6 @@ export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vsc
|
|
|
34
33
|
|
|
35
34
|
const WHITESPACE_RE = /\s+/gu
|
|
36
35
|
const NON_DIGITS_RE = /\D+/u
|
|
37
|
-
const OXLINT_FIX_RE = /bunx\s+oxlint[^\n]*--fix/u
|
|
38
36
|
|
|
39
37
|
/**
|
|
40
38
|
* Нормалізує рядок скрипта для порівняння (зайві пробіли).
|
|
@@ -247,41 +245,11 @@ async function checkEslintConfig(passFn, failFn) {
|
|
|
247
245
|
}
|
|
248
246
|
}
|
|
249
247
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
*/
|
|
256
|
-
function checkPackageJsonLintDeps(pkg, passFn, failFn) {
|
|
257
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
258
|
-
if (allDeps.prettier) {
|
|
259
|
-
failFn('package.json: видали залежність prettier (oxfmt замість prettier, js-lint.mdc)')
|
|
260
|
-
} else {
|
|
261
|
-
passFn('package.json не містить prettier')
|
|
262
|
-
}
|
|
263
|
-
if (allDeps['@nitra/prettier-config']) {
|
|
264
|
-
failFn('package.json: видали @nitra/prettier-config (js-lint.mdc)')
|
|
265
|
-
} else {
|
|
266
|
-
passFn('package.json не містить @nitra/prettier-config')
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const nitraEslint = pkg.devDependencies?.['@nitra/eslint-config']
|
|
270
|
-
if (nitraEslint) {
|
|
271
|
-
passFn('@nitra/eslint-config є в devDependencies')
|
|
272
|
-
if (nitraEslintConfigMeetsMinVersion(nitraEslint)) {
|
|
273
|
-
passFn(
|
|
274
|
-
'@nitra/eslint-config: мінімум 3.9.2 (no-restricted-syntax для ForInStatement з 3.8.0 + вбудований ignore "**/adr/**" з 3.9.2 + @e18e/eslint-plugin транзитивно, js-lint.mdc)'
|
|
275
|
-
)
|
|
276
|
-
} else {
|
|
277
|
-
failFn(
|
|
278
|
-
'@nitra/eslint-config: онови до мінімум "^3.9.2" — з 3.9.2 у getConfig вбудовано ignore для "**/adr/**" (ADR-документи не валідуються), плюс транзитивний @e18e/eslint-plugin для oxlint і заборона for...in з 3.8.0 (js-lint.mdc)'
|
|
279
|
-
)
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
failFn('@nitra/eslint-config відсутній в devDependencies — додай: bun add -d @nitra/eslint-config')
|
|
283
|
-
}
|
|
284
|
-
}
|
|
248
|
+
// Перевірки `prettier` / `@nitra/prettier-config` у залежностях (text.mdc) і
|
|
249
|
+
// `@nitra/eslint-config ≥ 3.9.2` тепер у Rego: відповідно
|
|
250
|
+
// `npm/policy/text/package_json/` і `npm/policy/js_lint/package_json/`. Тут
|
|
251
|
+
// лишилася лише workspace-ітерація для `type: "module"` і engines, бо js_lint
|
|
252
|
+
// Rego запускається лише на кореневому `package.json`.
|
|
285
253
|
|
|
286
254
|
/**
|
|
287
255
|
* Перевіряє, що package.json має `"type": "module"`.
|
|
@@ -359,36 +327,18 @@ function checkEnginesBun(label, pkg, passFn, failFn) {
|
|
|
359
327
|
}
|
|
360
328
|
|
|
361
329
|
/**
|
|
362
|
-
*
|
|
330
|
+
* Workspace-ітерація: для кожного workspace `package.json` перевіряємо
|
|
331
|
+
* `type: "module"` і `engines.{node,bun}`. Кореневий `package.json` ці поля
|
|
332
|
+
* валідує `npm/policy/js_lint/package_json/`; lint-js скрипт і `@nitra/eslint-config`
|
|
333
|
+
* — теж у Rego.
|
|
363
334
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
364
335
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
365
336
|
*/
|
|
366
337
|
async function checkPackageJsonJsLint(passFn, failFn) {
|
|
367
338
|
if (!existsSync('package.json')) return
|
|
368
339
|
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
369
|
-
|
|
370
|
-
checkPackageJsonTypeModule('package.json', pkg, passFn, failFn)
|
|
371
|
-
|
|
372
340
|
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : []
|
|
373
341
|
await checkWorkspacePackages(workspaces, passFn, failFn)
|
|
374
|
-
|
|
375
|
-
const lintJs = pkg.scripts?.['lint-js']
|
|
376
|
-
if (lintJs) {
|
|
377
|
-
passFn('package.json містить скрипт lint-js')
|
|
378
|
-
if (isCanonicalLintJs(String(lintJs))) {
|
|
379
|
-
passFn(`lint-js збігається з каноном: ${CANONICAL_LINT_JS}`)
|
|
380
|
-
} else {
|
|
381
|
-
failFn(
|
|
382
|
-
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(String(lintJs)))}`
|
|
383
|
-
)
|
|
384
|
-
}
|
|
385
|
-
} else {
|
|
386
|
-
failFn(`package.json не містить скрипт "lint-js" — додай: ${JSON.stringify(CANONICAL_LINT_JS)}`)
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
checkPackageJsonLintDeps(pkg, passFn, failFn)
|
|
390
|
-
checkEnginesNode('package.json', pkg, passFn, failFn)
|
|
391
|
-
checkEnginesBun('package.json', pkg, passFn, failFn)
|
|
392
342
|
}
|
|
393
343
|
|
|
394
344
|
/**
|
|
@@ -457,67 +407,16 @@ async function checkVscodeExtensions(passFn, failFn) {
|
|
|
457
407
|
}
|
|
458
408
|
|
|
459
409
|
/**
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*/
|
|
465
|
-
function checkLintJsWorkflowFallback(content, passFn, failFn) {
|
|
466
|
-
const checks = [
|
|
467
|
-
['actions/checkout@v6', 'lint-js.yml: потрібен крок actions/checkout@v6 (ga.mdc)'],
|
|
468
|
-
['persist-credentials: false', 'lint-js.yml: checkout з persist-credentials: false'],
|
|
469
|
-
['./.github/actions/setup-bun-deps', 'lint-js.yml: потрібен uses: ./.github/actions/setup-bun-deps'],
|
|
470
|
-
['bunx oxlint', 'lint-js.yml: у run має бути bunx oxlint'],
|
|
471
|
-
['bunx eslint .', 'lint-js.yml: у run має бути bunx eslint . (без --fix у CI)'],
|
|
472
|
-
['bunx jscpd .', 'lint-js.yml: у run має бути bunx jscpd .']
|
|
473
|
-
]
|
|
474
|
-
for (const [needle, errMsg] of checks) {
|
|
475
|
-
if (content.includes(needle)) {
|
|
476
|
-
passFn(`lint-js.yml містить: ${needle}`)
|
|
477
|
-
} else {
|
|
478
|
-
failFn(errMsg)
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
if (content.includes('bunx oxlint') && OXLINT_FIX_RE.test(content)) {
|
|
482
|
-
failFn('lint-js.yml: у CI не використовуй bunx oxlint --fix (лише bunx oxlint)')
|
|
483
|
-
}
|
|
484
|
-
if (content.includes('eslint --fix')) {
|
|
485
|
-
failFn('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Перевіряє вміст lint-js.yml через YAML або fallback.
|
|
491
|
-
* @param {string} content вміст файлу
|
|
492
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
493
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
494
|
-
*/
|
|
495
|
-
function checkLintJsYmlContent(content, passFn, failFn) {
|
|
496
|
-
const root = parseWorkflowYaml(content)
|
|
497
|
-
if (root) {
|
|
498
|
-
const v = verifyLintJsWorkflowStructure(root)
|
|
499
|
-
if (v.ok) {
|
|
500
|
-
passFn('lint-js.yml: кроки checkout, setup-bun-deps, oxlint/eslint/jscpd (YAML + кроки)')
|
|
501
|
-
} else {
|
|
502
|
-
for (const msg of v.failures) {
|
|
503
|
-
failFn(`lint-js.yml: ${msg}`)
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
} else {
|
|
507
|
-
checkLintJsWorkflowFallback(content, passFn, failFn)
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Перевіряє lint-js.yml і lint.yml workflow.
|
|
410
|
+
* FS-existence для `lint-js.yml` + cross-file перевірка, що `lint.yml` (якщо існує)
|
|
411
|
+
* не дублює лінт JS-кроки. Структуру `lint-js.yml` (`actions/checkout@v6`,
|
|
412
|
+
* `persist-credentials: false`, `setup-bun-deps`, `bunx oxlint/eslint/jscpd .`,
|
|
413
|
+
* заборона `--fix` у CI) валідує `npm/policy/js_lint/lint_js_yml/`.
|
|
513
414
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
514
415
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
515
416
|
*/
|
|
516
417
|
async function checkLintJsWorkflows(passFn, failFn) {
|
|
517
418
|
if (existsSync('.github/workflows/lint-js.yml')) {
|
|
518
|
-
|
|
519
|
-
passFn('lint-js.yml існує')
|
|
520
|
-
checkLintJsYmlContent(content, passFn, failFn)
|
|
419
|
+
passFn('.github/workflows/lint-js.yml є (структуру перевіряє bun run lint-conftest → js_lint.lint_js_yml)')
|
|
521
420
|
} else {
|
|
522
421
|
failFn('.github/workflows/lint-js.yml не існує — створи його (див. check-js-lint.mjs / js-lint.mdc)')
|
|
523
422
|
}
|
|
@@ -22,20 +22,11 @@ import { join } from 'node:path'
|
|
|
22
22
|
import { promisify } from 'node:util'
|
|
23
23
|
|
|
24
24
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
25
|
-
import {
|
|
26
|
-
hasIdTokenWritePermission,
|
|
27
|
-
hasNpmPublishStepWithPackage,
|
|
28
|
-
parseWorkflowYaml,
|
|
29
|
-
pushHasMainBranch,
|
|
30
|
-
pushPathsIncludeNpmGlob
|
|
31
|
-
} from './utils/gha-workflow.mjs'
|
|
32
25
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
33
26
|
import { walkDir } from './utils/walkDir.mjs'
|
|
34
27
|
|
|
35
28
|
const execFileAsync = promisify(execFile)
|
|
36
29
|
|
|
37
|
-
const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
|
|
38
|
-
|
|
39
30
|
/** Перший заголовок релізу у Keep a Changelog (`## [1.2.3]`). */
|
|
40
31
|
const CHANGELOG_FIRST_VERSION_RE = /^## \[([^\]]+)\]/m
|
|
41
32
|
|
|
@@ -115,39 +106,6 @@ function missingHkEmitTypesConfigFragments(hkText) {
|
|
|
115
106
|
return need.filter(s => !hkText.includes(s))
|
|
116
107
|
}
|
|
117
108
|
|
|
118
|
-
/**
|
|
119
|
-
* Перевіряє `npm/tsconfig.emit-types.json` на мінімальний набір опцій для `emitDeclarationOnly` у `types/`.
|
|
120
|
-
* @param {unknown} parsed результат `JSON.parse` конфігурації
|
|
121
|
-
* @returns {string[]} повідомлення про помилки (порожній — OK)
|
|
122
|
-
*/
|
|
123
|
-
function emitTypesConfigIssues(parsed) {
|
|
124
|
-
const issues = []
|
|
125
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
126
|
-
return ['некоректний JSON']
|
|
127
|
-
}
|
|
128
|
-
const co = /** @type {{ [k: string]: unknown }} */ (parsed).compilerOptions
|
|
129
|
-
if (!co || typeof co !== 'object') {
|
|
130
|
-
return ['відсутній compilerOptions']
|
|
131
|
-
}
|
|
132
|
-
const get = k => /** @type {{ [k: string]: unknown }} */ (co)[k]
|
|
133
|
-
if (get('allowJs') !== true) {
|
|
134
|
-
issues.push('compilerOptions.allowJs має бути true')
|
|
135
|
-
}
|
|
136
|
-
if (get('declaration') !== true) {
|
|
137
|
-
issues.push('compilerOptions.declaration має бути true')
|
|
138
|
-
}
|
|
139
|
-
if (get('emitDeclarationOnly') !== true) {
|
|
140
|
-
issues.push('compilerOptions.emitDeclarationOnly має бути true')
|
|
141
|
-
}
|
|
142
|
-
if (get('outDir') !== 'types') {
|
|
143
|
-
issues.push('compilerOptions.outDir має бути "types"')
|
|
144
|
-
}
|
|
145
|
-
if (get('skipLibCheck') !== true) {
|
|
146
|
-
issues.push('compilerOptions.skipLibCheck має бути true')
|
|
147
|
-
}
|
|
148
|
-
return issues
|
|
149
|
-
}
|
|
150
|
-
|
|
151
109
|
/**
|
|
152
110
|
* Шлях на дискі до файлу з поля `types` у `npm/package.json` (значення на кшталт `./types/bin/x.d.ts`).
|
|
153
111
|
* @param {string} typesField значення поля `types` з `package.json`
|
|
@@ -162,30 +120,9 @@ function npmTypesFileFromPackageField(typesField) {
|
|
|
162
120
|
}
|
|
163
121
|
|
|
164
122
|
/**
|
|
165
|
-
* Перевіряє
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
169
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
170
|
-
*/
|
|
171
|
-
function checkNpmTypesField(typesField, useSrcJsLayout, passFn, failFn) {
|
|
172
|
-
if (useSrcJsLayout) {
|
|
173
|
-
if (typesField === TYPES_INDEX) {
|
|
174
|
-
passFn(`npm/package.json: "types": "${TYPES_INDEX}" (layout npm/src + .js)`)
|
|
175
|
-
} else {
|
|
176
|
-
failFn(`npm/package.json: при наявності .js під npm/src очікується "types": "${TYPES_INDEX}"`)
|
|
177
|
-
}
|
|
178
|
-
} else if (typeof typesField === 'string' && TYPES_FILE_RE.test(typesField)) {
|
|
179
|
-
passFn(`npm/package.json: "types" вказує на файл під ./types/… (${typesField})`)
|
|
180
|
-
} else {
|
|
181
|
-
failFn(
|
|
182
|
-
'npm/package.json: без .js під npm/src поле types має бути рядком виду ./types/….d.ts або .d.mts (див. npm-module.mdc)'
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Перевіряє npm/package.json на типи та files.
|
|
123
|
+
* Перевіряє наявність на диску файлу зі значення `types` у `npm/package.json`
|
|
124
|
+
* (cross-file: JSON-поле + FS). Структуру самого поля валідує
|
|
125
|
+
* `npm/policy/npm_module/npm_package_json/`; тут — лише чи файл реально існує.
|
|
189
126
|
* @param {boolean} useSrcJsLayout чи використовується layout з npm/src
|
|
190
127
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
191
128
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
@@ -195,14 +132,6 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
|
|
|
195
132
|
const npmPkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
|
|
196
133
|
const typesField = npmPkg.types
|
|
197
134
|
|
|
198
|
-
checkNpmTypesField(typesField, useSrcJsLayout, passFn, failFn)
|
|
199
|
-
|
|
200
|
-
if (Array.isArray(npmPkg.files) && npmPkg.files.includes('types')) {
|
|
201
|
-
passFn('npm/package.json: files містить "types"')
|
|
202
|
-
} else {
|
|
203
|
-
failFn('npm/package.json: масив files має містити "types"')
|
|
204
|
-
}
|
|
205
|
-
|
|
206
135
|
const typesPath = useSrcJsLayout ? join('npm', 'types', 'index.d.ts') : npmTypesFileFromPackageField(typesField)
|
|
207
136
|
const missingTypesMsg = useSrcJsLayout
|
|
208
137
|
? `Відсутній ${join('npm', 'types', 'index.d.ts')} (згенеруй tsc з npm-module.mdc)`
|
|
@@ -215,31 +144,19 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
|
|
|
215
144
|
}
|
|
216
145
|
|
|
217
146
|
/**
|
|
218
|
-
*
|
|
147
|
+
* FS-existence для `npm/tsconfig.emit-types.json` (структуру `compilerOptions`
|
|
148
|
+
* валідує `npm/policy/npm_module/emit_types_config/`).
|
|
219
149
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
220
150
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
221
151
|
*/
|
|
222
|
-
|
|
152
|
+
function checkEmitTypesConfig(passFn, failFn) {
|
|
223
153
|
if (!existsSync(EMIT_TYPES_CONFIG)) {
|
|
224
154
|
failFn(
|
|
225
155
|
`Без .js під npm/src потрібен ${EMIT_TYPES_CONFIG} (див. npm-module.mdc: emit через tsconfig, без штучного src/index.js)`
|
|
226
156
|
)
|
|
227
157
|
return
|
|
228
158
|
}
|
|
229
|
-
passFn(`${EMIT_TYPES_CONFIG}
|
|
230
|
-
let raw
|
|
231
|
-
try {
|
|
232
|
-
raw = JSON.parse(await readFile(EMIT_TYPES_CONFIG, 'utf8'))
|
|
233
|
-
} catch {
|
|
234
|
-
failFn(`${EMIT_TYPES_CONFIG}: некоректний JSON`)
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
const issues = emitTypesConfigIssues(raw)
|
|
238
|
-
if (issues.length === 0) {
|
|
239
|
-
passFn(`${EMIT_TYPES_CONFIG}: compilerOptions придатні для emitDeclarationOnly → types/`)
|
|
240
|
-
} else {
|
|
241
|
-
failFn(`${EMIT_TYPES_CONFIG}: ${issues.join('; ')}`)
|
|
242
|
-
}
|
|
159
|
+
passFn(`${EMIT_TYPES_CONFIG} є (структуру перевіряє bun run lint-conftest → npm_module.emit_types_config)`)
|
|
243
160
|
}
|
|
244
161
|
|
|
245
162
|
/**
|
|
@@ -364,71 +281,25 @@ async function checkDirtyNpmRequiresVersionBump(passFn, failFn) {
|
|
|
364
281
|
}
|
|
365
282
|
|
|
366
283
|
/**
|
|
367
|
-
*
|
|
284
|
+
* FS-existence для `npm-publish.yml` workflow. Поля workflow (`on.push.paths`,
|
|
285
|
+
* `branches`, `id-token: write`, JS-DevTools/npm-publish step) валідує
|
|
286
|
+
* `npm/policy/npm_module/npm_publish_yml/`.
|
|
368
287
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
369
288
|
* @param {(msg: string) => void} failFn callback при виявленому порушенні
|
|
370
|
-
* @returns {Promise<void>}
|
|
371
289
|
*/
|
|
372
|
-
|
|
290
|
+
function checkPublishWorkflow(passFn, failFn) {
|
|
373
291
|
const publishWf = '.github/workflows/npm-publish.yml'
|
|
374
|
-
if (
|
|
292
|
+
if (existsSync(publishWf)) {
|
|
293
|
+
passFn(`${publishWf} є (структуру перевіряє bun run lint-conftest → npm_module.npm_publish_yml)`)
|
|
294
|
+
} else {
|
|
375
295
|
failFn(`Відсутній ${publishWf} (npm-module.mdc: npm publish)`)
|
|
376
|
-
return
|
|
377
|
-
}
|
|
378
|
-
passFn(`${publishWf} існує`)
|
|
379
|
-
const pub = await readFile(publishWf, 'utf8')
|
|
380
|
-
const root = parseWorkflowYaml(pub)
|
|
381
|
-
if (root) {
|
|
382
|
-
const checks = [
|
|
383
|
-
{
|
|
384
|
-
ok: pushPathsIncludeNpmGlob(root),
|
|
385
|
-
pass: `${publishWf}: on.push.paths містить npm/**`,
|
|
386
|
-
fail: `${publishWf}: у on.push.paths має бути npm/**`
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
ok: pushHasMainBranch(root),
|
|
390
|
-
pass: `${publishWf}: очікується branch main`,
|
|
391
|
-
fail: `${publishWf}: очікується branch main`
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
ok: hasIdTokenWritePermission(root),
|
|
395
|
-
pass: `${publishWf}: permissions містить id-token: write (OIDC)`,
|
|
396
|
-
fail: `${publishWf}: permissions має містити id-token: write (OIDC)`
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
ok: hasNpmPublishStepWithPackage(root),
|
|
400
|
-
pass: `${publishWf}: uses JS-DevTools/npm-publish та with.package npm/package.json`,
|
|
401
|
-
fail: `${publishWf}: очікується uses: JS-DevTools/npm-publish та with.package: npm/package.json`
|
|
402
|
-
}
|
|
403
|
-
]
|
|
404
|
-
for (const c of checks) {
|
|
405
|
-
if (c.ok) {
|
|
406
|
-
passFn(c.pass)
|
|
407
|
-
} else {
|
|
408
|
-
failFn(c.fail)
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
return
|
|
412
|
-
}
|
|
413
|
-
const need = [
|
|
414
|
-
{ sub: 'npm/**', msg: `${publishWf}: у on.push.paths має бути npm/**` },
|
|
415
|
-
{ sub: 'branches:', msg: `${publishWf}: очікується on.push.branches` },
|
|
416
|
-
{ sub: 'main', msg: `${publishWf}: очікується branch main` },
|
|
417
|
-
{ sub: 'id-token: write', msg: `${publishWf}: permissions має містити id-token: write (OIDC)` },
|
|
418
|
-
{ sub: 'JS-DevTools/npm-publish', msg: `${publishWf}: очікується uses: JS-DevTools/npm-publish` },
|
|
419
|
-
{ sub: 'package: npm/package.json', msg: `${publishWf}: with.package має бути npm/package.json` }
|
|
420
|
-
]
|
|
421
|
-
for (const { sub, msg } of need) {
|
|
422
|
-
if (pub.includes(sub)) {
|
|
423
|
-
passFn(`${publishWf} містить «${sub}»`)
|
|
424
|
-
} else {
|
|
425
|
-
failFn(msg)
|
|
426
|
-
}
|
|
427
296
|
}
|
|
428
297
|
}
|
|
429
298
|
|
|
430
299
|
/**
|
|
431
|
-
* Перевіряє базову структуру
|
|
300
|
+
* Перевіряє базову структуру монорепо: наявність каталогу `npm/` і
|
|
301
|
+
* `npm/package.json`. Поле `workspaces ∋ "npm"` у кореневому `package.json`
|
|
302
|
+
* валідує `npm/policy/npm_module/root_package_json/`.
|
|
432
303
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
433
304
|
* @param {(msg: string) => void} fail callback при помилці
|
|
434
305
|
*/
|
|
@@ -450,15 +321,6 @@ async function checkNpmModuleBasicStructure(pass, fail) {
|
|
|
450
321
|
fail('npm/ директорія не існує')
|
|
451
322
|
}
|
|
452
323
|
|
|
453
|
-
if (existsSync('package.json')) {
|
|
454
|
-
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
455
|
-
if (Array.isArray(pkg.workspaces) && pkg.workspaces.includes('npm')) {
|
|
456
|
-
pass('package.json workspaces містить "npm"')
|
|
457
|
-
} else {
|
|
458
|
-
fail('package.json workspaces має містити "npm"')
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
324
|
if (existsSync('npm/package.json')) {
|
|
463
325
|
pass('npm/package.json існує')
|
|
464
326
|
} else {
|