@nitra/cursor 1.8.204 → 1.8.207
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 +52 -1
- package/bin/auto-rules.md +2 -0
- package/mdc/rego.mdc +77 -0
- package/package.json +1 -1
- package/policy/abie/health_check_policy/health_check_policy.rego +73 -0
- package/policy/abie/http_route_base/http_route_base.rego +45 -0
- package/policy/adr/settings_json/settings_json.rego +31 -0
- package/policy/adr/settings_local_json/settings_local_json.rego +28 -0
- package/policy/bun/bunfig/bunfig.rego +33 -0
- package/policy/bun/package_json/package_json.rego +94 -0
- package/policy/capacitor/package_json/package_json.rego +45 -0
- package/policy/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -26
- package/policy/ga/clean_merged_branch/clean_merged_branch.rego +0 -25
- package/policy/ga/git_ai/git_ai.rego +83 -0
- package/policy/ga/lint_ga/lint_ga.rego +118 -0
- package/policy/ga/workflow_common/workflow_common.rego +161 -0
- package/policy/graphql/package_json/package_json.rego +35 -0
- package/policy/hasura/svc_hl/svc_hl.rego +27 -0
- package/policy/image_compress/package_json/package_json.rego +94 -0
- package/policy/js_bun_db/package_json/package_json.rego +28 -0
- package/policy/js_lint/lint_js_yml/lint_js_yml.rego +98 -0
- package/policy/js_lint/package_json/package_json.rego +137 -0
- package/policy/js_mssql/package_json/package_json.rego +57 -0
- package/policy/js_run/configmap/configmap.rego +45 -0
- package/policy/js_run/jsconfig/jsconfig.rego +66 -0
- package/policy/js_run/package_json/package_json.rego +31 -0
- package/policy/k8s/manifest/manifest.rego +130 -0
- package/policy/npm_module/emit_types_config/emit_types_config.rego +37 -0
- package/policy/npm_module/npm_package_json/npm_package_json.rego +55 -0
- package/policy/npm_module/npm_publish_yml/npm_publish_yml.rego +79 -0
- package/policy/npm_module/root_package_json/root_package_json.rego +28 -0
- package/policy/php/lint_php_yml/lint_php_yml.rego +32 -0
- package/policy/php/package_json/package_json.rego +19 -0
- package/policy/style_lint/lint_style_yml/lint_style_yml.rego +35 -0
- package/policy/style_lint/package_json/package_json.rego +49 -0
- package/policy/text/cspell/cspell.rego +91 -0
- package/policy/text/markdownlint/markdownlint.rego +21 -0
- package/policy/text/oxfmtrc/oxfmtrc.rego +90 -0
- package/policy/text/package_json/package_json.rego +88 -0
- package/policy/vue/package_json/package_json.rego +54 -0
- package/scripts/auto-rules.mjs +10 -0
- package/scripts/check-adr.mjs +7 -3
- package/scripts/check-bun.mjs +21 -117
- package/scripts/check-ga.mjs +0 -284
- package/scripts/check-graphql.mjs +6 -45
- package/scripts/check-hasura.mjs +4 -5
- package/scripts/check-image-avif.mjs +3 -3
- package/scripts/check-image-compress.mjs +25 -132
- package/scripts/check-js-bun-db.mjs +3 -50
- package/scripts/check-js-run.mjs +9 -12
- package/scripts/check-k8s.mjs +6 -5
- package/scripts/check-npm-module.mjs +17 -8
- package/scripts/check-php.mjs +16 -51
- package/scripts/check-style-lint.mjs +28 -52
- package/scripts/check-text.mjs +47 -219
- package/scripts/check-vue.mjs +3 -16
- package/scripts/lint-conftest.mjs +351 -0
- package/scripts/lint-ga.mjs +49 -2
- package/scripts/lint-rego.mjs +67 -21
- package/scripts/run-shellcheck-text.mjs +3 -6
- package/scripts/utils/depcheck-workflow.mjs +2 -6
package/scripts/check-bun.mjs
CHANGED
|
@@ -1,54 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Перевіряє відповідність репозиторію правилам Bun (bun.mdc).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* **Що тут лишилося** (FS / cross-file — не покривається conftest):
|
|
5
|
+
* - наявність `bun.lock`, `bunfig.toml`, `package.json` у корені (FS-existence);
|
|
6
|
+
* - заборонені lockfile та артефакти yarn/pnpm (`package-lock.json`, `yarn.lock`,
|
|
7
|
+
* `pnpm-lock.yaml`, `.yarnrc.yml`, директорія `.yarn/`);
|
|
8
|
+
* - якщо в `.n-cursor.json` у `rules` є `docker` або `k8s`, у кореневому
|
|
9
|
+
* `package.json` має бути відповідний скрипт `lint-docker` / `lint-k8s`
|
|
10
|
+
* (cross-file: два JSON-файли).
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Якщо в кореневому `package.json` є скрипти з префіксом `lint-`, перевіряє наявність агрегованого
|
|
15
|
-
* скрипта `lint`, у якому через `bun run <ім’я>` викликаються всі такі скрипти, і що рядок `lint`
|
|
16
|
-
* закінчується на `&& oxfmt .`.
|
|
12
|
+
* **Що покрила Rego** (`bun run lint-conftest`):
|
|
13
|
+
* - `npm/policy/bun/bunfig/` — `[install].linker == "hoisted"` у `bunfig.toml`;
|
|
14
|
+
* - `npm/policy/bun/package_json/` — відсутність `packageManager` / `dependencies`
|
|
15
|
+
* у кореневому `package.json`, у `devDependencies` лише `@nitra/*`, агрегований
|
|
16
|
+
* `lint`-скрипт покриває всі `lint-*` через `bun run` і завершується `&& oxfmt .`.
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync } from 'node:fs'
|
|
19
19
|
import { readFile } from 'node:fs/promises'
|
|
20
20
|
|
|
21
21
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
22
22
|
|
|
23
|
-
const OXFMT_END_RE = /&&[ \t]+oxfmt[ \t]+\.[ \t]*$/
|
|
24
|
-
/** Пробіли/таби без `\s` (уникаємо super-linear backtracking у sonarjs/slow-regex). */
|
|
25
|
-
const HOISTED_LINKER_RE = /^[ \t]*linker[ \t]*=[ \t]*"hoisted"[ \t]*$/m
|
|
26
|
-
const INSTALL_SECTION_RE = /^[ \t]*\[install\][ \t]*$/m
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Перевіряє `bunfig.toml` на секцію `[install]` з `linker = "hoisted"`.
|
|
30
|
-
* @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер
|
|
31
|
-
*/
|
|
32
|
-
async function checkBunfigHoisted(reporter) {
|
|
33
|
-
const { pass, fail } = reporter
|
|
34
|
-
if (!existsSync('bunfig.toml')) {
|
|
35
|
-
fail('Відсутній bunfig.toml — створи з [install] linker = "hoisted" (bun.mdc)')
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
const content = await readFile('bunfig.toml', 'utf8')
|
|
39
|
-
if (!INSTALL_SECTION_RE.test(content)) {
|
|
40
|
-
fail('bunfig.toml: відсутня секція [install] (bun.mdc)')
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
if (HOISTED_LINKER_RE.test(content)) {
|
|
44
|
-
pass('bunfig.toml: [install] linker = "hoisted"')
|
|
45
|
-
} else {
|
|
46
|
-
fail('bunfig.toml: у секції [install] має бути linker = "hoisted" (bun.mdc)')
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
23
|
/**
|
|
51
24
|
* Чи ім'я пакета дозволене в кореневих `devDependencies` за bun.mdc (лише **`@nitra/*`**).
|
|
25
|
+
*
|
|
26
|
+
* Залишилася як експорт для `check-text.mjs` і тестів — `bun.package_json` Rego
|
|
27
|
+
* робить ту саму перевірку для check-runner-а.
|
|
52
28
|
* @param {string} name ключ з поля `devDependencies`
|
|
53
29
|
* @returns {boolean} true, якщо префікс дозволений
|
|
54
30
|
*/
|
|
@@ -76,66 +52,6 @@ async function loadNCursorRules() {
|
|
|
76
52
|
}
|
|
77
53
|
}
|
|
78
54
|
|
|
79
|
-
/**
|
|
80
|
-
* @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер для збору результатів
|
|
81
|
-
* @param {Record<string, unknown>} pkg розібраний package.json
|
|
82
|
-
*/
|
|
83
|
-
function checkDevDependencies(reporter, pkg) {
|
|
84
|
-
const { pass, fail } = reporter
|
|
85
|
-
const dev = pkg.devDependencies
|
|
86
|
-
if (dev === undefined) {
|
|
87
|
-
pass('Кореневий package.json без devDependencies')
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
if (dev === null || typeof dev !== 'object' || Array.isArray(dev)) {
|
|
91
|
-
fail(
|
|
92
|
-
'Кореневий package.json: `devDependencies` має бути object з ключами пакетів і діапазонами версій (не null, не масив)'
|
|
93
|
-
)
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
const bad = Object.keys(/** @type {object} */ (dev)).filter(n => !isAllowedRootDevDependency(n))
|
|
97
|
-
if (bad.length > 0) {
|
|
98
|
-
fail(`Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: ${bad.join(', ')} (bun.mdc)`)
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
const n = Object.keys(/** @type {object} */ (dev)).length
|
|
102
|
-
pass(
|
|
103
|
-
n === 0
|
|
104
|
-
? 'Кореневі devDependencies порожні або відсутні (лише @nitra/*)'
|
|
105
|
-
: `Кореневі devDependencies: лише @nitra/* (${n} пак.)`
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер для збору результатів
|
|
111
|
-
* @param {Record<string, string>} scripts scripts з package.json
|
|
112
|
-
*/
|
|
113
|
-
function checkLintAggregate(reporter, scripts) {
|
|
114
|
-
const { pass, fail } = reporter
|
|
115
|
-
const lintPrefixed = Object.keys(scripts).filter(name => name.startsWith('lint-'))
|
|
116
|
-
if (lintPrefixed.length === 0) return
|
|
117
|
-
const aggregate = typeof scripts.lint === 'string' ? scripts.lint : ''
|
|
118
|
-
if (!aggregate.trim()) {
|
|
119
|
-
const scriptList = lintPrefixed.map(s => `\`${s}\``).join(', ')
|
|
120
|
-
fail(
|
|
121
|
-
`У package.json є скрипти ${scriptList}, але немає агрегованого \`lint\` — додай скрипт, який запускає їх через \`bun run\``
|
|
122
|
-
)
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
const missing = lintPrefixed.filter(name => !aggregate.includes(`bun run ${name}`))
|
|
126
|
-
if (missing.length > 0) {
|
|
127
|
-
const missingList = missing.map(s => '`' + s + '`').join(', ')
|
|
128
|
-
fail(`Скрипт \`lint\` має викликати всі lint-* через bun run; відсутньо: ${missingList}`)
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
pass('package.json: агрегований `lint` покриває всі `lint-*` скрипти')
|
|
132
|
-
if (OXFMT_END_RE.test(aggregate.trim())) {
|
|
133
|
-
pass('package.json: `lint` завершується `&& oxfmt .`')
|
|
134
|
-
} else {
|
|
135
|
-
fail('Скрипт `lint` має закінчуватися на `&& oxfmt .`')
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
55
|
/**
|
|
140
56
|
* @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер для збору результатів
|
|
141
57
|
* @param {Record<string, string>} scripts scripts з package.json
|
|
@@ -188,34 +104,22 @@ export async function check() {
|
|
|
188
104
|
fail('Відсутній bun.lock — запусти bun i')
|
|
189
105
|
}
|
|
190
106
|
|
|
191
|
-
|
|
107
|
+
if (!existsSync('bunfig.toml')) {
|
|
108
|
+
fail('Відсутній bunfig.toml — створи з [install] linker = "hoisted" (bun.mdc)')
|
|
109
|
+
} else {
|
|
110
|
+
pass('bunfig.toml є (структуру перевіряє bun run lint-conftest → bun.bunfig)')
|
|
111
|
+
}
|
|
192
112
|
|
|
193
113
|
const cursorRules = await loadNCursorRules()
|
|
194
114
|
|
|
195
115
|
if (!existsSync('package.json')) {
|
|
116
|
+
fail('Відсутній package.json у корені')
|
|
196
117
|
return reporter.getExitCode()
|
|
197
118
|
}
|
|
198
119
|
|
|
199
120
|
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
200
|
-
if (pkg.packageManager) {
|
|
201
|
-
fail(`package.json містить поле packageManager: "${pkg.packageManager}" — видали його`)
|
|
202
|
-
} else {
|
|
203
|
-
pass('package.json не містить packageManager')
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (pkg.dependencies === undefined) {
|
|
207
|
-
pass('Кореневий package.json без поля `dependencies`')
|
|
208
|
-
} else {
|
|
209
|
-
fail(
|
|
210
|
-
'Кореневий package.json не повинен містити поле `dependencies` — додай залежності в workspace-пакети (bun.mdc)'
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
checkDevDependencies(reporter, pkg)
|
|
215
|
-
|
|
216
121
|
const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
|
|
217
122
|
checkCursorRuleScripts(reporter, scripts, cursorRules)
|
|
218
|
-
checkLintAggregate(reporter, scripts)
|
|
219
123
|
|
|
220
124
|
return reporter.getExitCode()
|
|
221
125
|
}
|
package/scripts/check-ga.mjs
CHANGED
|
@@ -24,15 +24,11 @@ import { join } from 'node:path'
|
|
|
24
24
|
|
|
25
25
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
26
26
|
import {
|
|
27
|
-
anyRunStepIncludes,
|
|
28
27
|
eventPathsIncludeExact,
|
|
29
28
|
findForbiddenUsesOrRunPatterns,
|
|
30
29
|
findRunStepsWithShellLineContinuationBackslash,
|
|
31
30
|
hasAnyStepUsesContaining,
|
|
32
31
|
hasCheckoutBeforeLocalSetupBunDeps,
|
|
33
|
-
flattenWorkflowSteps,
|
|
34
|
-
getStepRun,
|
|
35
|
-
getStepUses,
|
|
36
32
|
parseWorkflowYaml
|
|
37
33
|
} from './utils/gha-workflow.mjs'
|
|
38
34
|
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
@@ -160,156 +156,6 @@ function getObjKey(obj, key) {
|
|
|
160
156
|
: undefined
|
|
161
157
|
}
|
|
162
158
|
|
|
163
|
-
/**
|
|
164
|
-
* Очікує, що значення є рядком рівно `expected`.
|
|
165
|
-
* @param {unknown} v значення
|
|
166
|
-
* @param {string} expected очікуваний рядок
|
|
167
|
-
* @returns {boolean} true, якщо збігається
|
|
168
|
-
*/
|
|
169
|
-
function isExactString(v, expected) {
|
|
170
|
-
return typeof v === 'string' && v === expected
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Перевіряє тригери `on.push` / `on.pull_request` у `lint-ga.yml`.
|
|
175
|
-
* @param {unknown} on корінь `on:` з YAML
|
|
176
|
-
* @param {(msg: string) => void} failFn fail
|
|
177
|
-
*/
|
|
178
|
-
function validateLintGaOnTriggers(on, failFn) {
|
|
179
|
-
const push = getObjKey(on, 'push')
|
|
180
|
-
const pr = getObjKey(on, 'pull_request')
|
|
181
|
-
const pushBranches = getObjKey(push, 'branches')
|
|
182
|
-
const pushPaths = getObjKey(push, 'paths')
|
|
183
|
-
const prBranches = getObjKey(pr, 'branches')
|
|
184
|
-
|
|
185
|
-
if (!Array.isArray(pushBranches) || !(pushBranches.includes('dev') && pushBranches.includes('main'))) {
|
|
186
|
-
failFn('lint-ga.yml: on.push.branches має містити dev і main (ga.mdc)')
|
|
187
|
-
}
|
|
188
|
-
if (!Array.isArray(prBranches) || !(prBranches.includes('dev') && prBranches.includes('main'))) {
|
|
189
|
-
failFn('lint-ga.yml: on.pull_request.branches має містити dev і main (ga.mdc)')
|
|
190
|
-
}
|
|
191
|
-
if (
|
|
192
|
-
!Array.isArray(pushPaths) ||
|
|
193
|
-
!(pushPaths.includes('.github/actions/**') && pushPaths.includes('.github/workflows/**'))
|
|
194
|
-
) {
|
|
195
|
-
failFn('lint-ga.yml: on.push.paths має містити .github/actions/** і .github/workflows/** (ga.mdc)')
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Перевіряє структуру workflow `lint-ga.yml` (ga.mdc).
|
|
201
|
-
* @param {Record<string, unknown> | null} root parsed YAML
|
|
202
|
-
* @param {(msg: string) => void} passFn pass
|
|
203
|
-
* @param {(msg: string) => void} failFn fail
|
|
204
|
-
*/
|
|
205
|
-
function validateLintGaWorkflowStructure(root, passFn, failFn) {
|
|
206
|
-
if (!root) {
|
|
207
|
-
failFn('lint-ga.yml: YAML не вдалося розібрати (ga.mdc)')
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (!isExactString(root.name, 'Lint GA')) {
|
|
212
|
-
failFn('lint-ga.yml: name має бути "Lint GA" (ga.mdc)')
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
validateLintGaOnTriggers(root.on, failFn)
|
|
216
|
-
|
|
217
|
-
validateConcurrencyOnRoot('lint-ga.yml', root, passFn, failFn)
|
|
218
|
-
|
|
219
|
-
const jobs = getObjKey(root, 'jobs')
|
|
220
|
-
const job = getObjKey(jobs, 'lint-ga')
|
|
221
|
-
if (!job) {
|
|
222
|
-
failFn('lint-ga.yml: jobs.lint-ga відсутній (ga.mdc)')
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (!isExactString(getObjKey(job, 'runs-on'), 'ubuntu-latest')) {
|
|
227
|
-
failFn('lint-ga.yml: runs-on має бути ubuntu-latest (ga.mdc)')
|
|
228
|
-
}
|
|
229
|
-
const perm = getObjKey(job, 'permissions')
|
|
230
|
-
if (getObjKey(perm, 'contents') !== 'read') {
|
|
231
|
-
failFn('lint-ga.yml: permissions мають бути contents: read (ga.mdc)')
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const steps = getObjKey(job, 'steps')
|
|
235
|
-
if (!Array.isArray(steps) || steps.length === 0) {
|
|
236
|
-
failFn('lint-ga.yml: jobs.lint-ga.steps відсутні (ga.mdc)')
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const flat = flattenWorkflowSteps(root)
|
|
241
|
-
const usesList = new Set(flat.map(s => getStepUses(s.step)))
|
|
242
|
-
const runBlob = flat.map(s => getStepRun(s.step)).join('\n')
|
|
243
|
-
|
|
244
|
-
if (!usesList.has('actions/checkout@v6')) {
|
|
245
|
-
failFn('lint-ga.yml: має бути uses: actions/checkout@v6 (ga.mdc)')
|
|
246
|
-
}
|
|
247
|
-
if (!usesList.has('./.github/actions/setup-bun-deps')) {
|
|
248
|
-
failFn('lint-ga.yml: має бути uses: ./.github/actions/setup-bun-deps (ga.mdc)')
|
|
249
|
-
}
|
|
250
|
-
if (!usesList.has('astral-sh/setup-uv@v8.0.0')) {
|
|
251
|
-
failFn('lint-ga.yml: має бути uses: astral-sh/setup-uv@v8.0.0 (ga.mdc)')
|
|
252
|
-
}
|
|
253
|
-
if (runBlob.includes('bun run lint-ga')) {
|
|
254
|
-
passFn('lint-ga.yml: структура jobs/steps OK')
|
|
255
|
-
} else {
|
|
256
|
-
failFn('lint-ga.yml: має бути крок run: bun run lint-ga (ga.mdc)')
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Перевіряє структуру workflow `git-ai.yml` (ga.mdc).
|
|
262
|
-
* @param {Record<string, unknown> | null} root parsed YAML
|
|
263
|
-
* @param {(msg: string) => void} passFn pass
|
|
264
|
-
* @param {(msg: string) => void} failFn fail
|
|
265
|
-
*/
|
|
266
|
-
function validateGitAiWorkflowStructure(root, passFn, failFn) {
|
|
267
|
-
if (!root) {
|
|
268
|
-
failFn('git-ai.yml: YAML не вдалося розібрати (ga.mdc)')
|
|
269
|
-
return
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (!isExactString(root.name, 'Git AI')) {
|
|
273
|
-
failFn('git-ai.yml: name має бути "Git AI" (ga.mdc)')
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const on = root.on
|
|
277
|
-
const pr = getObjKey(on, 'pull_request')
|
|
278
|
-
const types = getObjKey(pr, 'types')
|
|
279
|
-
if (!Array.isArray(types) || !types.includes('closed')) {
|
|
280
|
-
failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
validateConcurrencyOnRoot('git-ai.yml', root, passFn, failFn)
|
|
284
|
-
|
|
285
|
-
const jobs = getObjKey(root, 'jobs')
|
|
286
|
-
const job = getObjKey(jobs, 'git-ai')
|
|
287
|
-
if (!job) {
|
|
288
|
-
failFn('git-ai.yml: jobs.git-ai відсутній (ga.mdc)')
|
|
289
|
-
return
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (!String(getObjKey(job, 'if') ?? '').includes('github.event.pull_request.merged == true')) {
|
|
293
|
-
failFn('git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)')
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const perm = getObjKey(job, 'permissions')
|
|
297
|
-
if (getObjKey(perm, 'contents') !== 'write') {
|
|
298
|
-
failFn('git-ai.yml: permissions мають бути contents: write (ga.mdc)')
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const flat = flattenWorkflowSteps(root)
|
|
302
|
-
const runBlob = flat.map(s => getStepRun(s.step)).join('\n')
|
|
303
|
-
if (!runBlob.includes('curl -fsSL https://usegitai.com/install.sh | bash')) {
|
|
304
|
-
failFn('git-ai.yml: має встановлювати git-ai через curl | bash (ga.mdc)')
|
|
305
|
-
}
|
|
306
|
-
if (runBlob.includes('git-ai ci github run')) {
|
|
307
|
-
passFn('git-ai.yml: структура jobs/steps OK')
|
|
308
|
-
} else {
|
|
309
|
-
failFn('git-ai.yml: має виконувати git-ai ci github run (ga.mdc)')
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
159
|
/**
|
|
314
160
|
* Перевіряє блок `concurrency` на вже розпарсеному корені workflow (ga.mdc).
|
|
315
161
|
*
|
|
@@ -624,33 +470,6 @@ function checkShellcheckInstalled(passFn, failFn) {
|
|
|
624
470
|
)
|
|
625
471
|
}
|
|
626
472
|
|
|
627
|
-
/**
|
|
628
|
-
* Перевіряє lint-ga.yml workflow.
|
|
629
|
-
* @param {string} wfDir директорія workflows
|
|
630
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
631
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
632
|
-
*/
|
|
633
|
-
async function checkLintGaWorkflow(wfDir, passFn, failFn) {
|
|
634
|
-
const lintGaWf = join(wfDir, 'lint-ga.yml')
|
|
635
|
-
if (!existsSync(lintGaWf)) return
|
|
636
|
-
const lgContent = await readFile(lintGaWf, 'utf8')
|
|
637
|
-
const root = parseWorkflowYaml(lgContent)
|
|
638
|
-
const hasBunRun = root ? anyRunStepIncludes(root, 'bun run lint-ga') : lgContent.includes('bun run lint-ga')
|
|
639
|
-
const hasSetupUv = root
|
|
640
|
-
? hasAnyStepUsesContaining(root, ['astral-sh/setup-uv']) || lgContent.includes('astral-sh/setup-uv')
|
|
641
|
-
: lgContent.includes('astral-sh/setup-uv')
|
|
642
|
-
if (hasBunRun) {
|
|
643
|
-
passFn('lint-ga.yml викликає bun run lint-ga')
|
|
644
|
-
} else {
|
|
645
|
-
failFn('lint-ga.yml: крок має містити bun run lint-ga')
|
|
646
|
-
}
|
|
647
|
-
if (hasSetupUv) {
|
|
648
|
-
passFn('lint-ga.yml містить astral-sh/setup-uv')
|
|
649
|
-
} else {
|
|
650
|
-
failFn('lint-ga.yml: додай astral-sh/setup-uv для uvx zizmor (ga.mdc)')
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
473
|
/**
|
|
655
474
|
* Перевіряє розширення workflow-файлів і наявність обов'язкових workflow.
|
|
656
475
|
* @param {string} wfDir шлях до директорії workflows
|
|
@@ -684,105 +503,6 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
|
|
|
684
503
|
}
|
|
685
504
|
}
|
|
686
505
|
|
|
687
|
-
/**
|
|
688
|
-
* Перевіряє, чи on.pull_request.types у parsed YAML містить 'closed'.
|
|
689
|
-
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
690
|
-
* @returns {boolean} true, якщо тригер pull_request має тип closed
|
|
691
|
-
*/
|
|
692
|
-
function hasPullRequestClosedTrigger(root) {
|
|
693
|
-
const on = root.on
|
|
694
|
-
if (!on || typeof on !== 'object') return false
|
|
695
|
-
const pr = /** @type {Record<string, unknown>} */ (on)['pull_request']
|
|
696
|
-
if (!pr || typeof pr !== 'object') return false
|
|
697
|
-
const types = /** @type {Record<string, unknown>} */ (pr).types
|
|
698
|
-
return Array.isArray(types) && types.includes('closed')
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Перевіряє, чи будь-який job у parsed YAML має if-умову з 'merged'.
|
|
703
|
-
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
704
|
-
* @returns {boolean} true, якщо хоча б один job містить умову merged
|
|
705
|
-
*/
|
|
706
|
-
function hasJobMergedCondition(root) {
|
|
707
|
-
const { jobs } = root
|
|
708
|
-
if (!jobs || typeof jobs !== 'object') return false
|
|
709
|
-
return Object.values(jobs).some(job => {
|
|
710
|
-
if (!job || typeof job !== 'object') return false
|
|
711
|
-
const ifCond = String(/** @type {Record<string, unknown>} */ (job).if ?? '')
|
|
712
|
-
return ifCond.includes('merged')
|
|
713
|
-
})
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* Перевіряє parsed YAML git-ai.yml: тригер closed та умова merged.
|
|
718
|
-
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
719
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
720
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
721
|
-
*/
|
|
722
|
-
function validateGitAiParsedYaml(root, passFn, failFn) {
|
|
723
|
-
if (hasPullRequestClosedTrigger(root)) {
|
|
724
|
-
passFn('git-ai.yml: on.pull_request.types містить closed')
|
|
725
|
-
} else {
|
|
726
|
-
failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (hasJobMergedCondition(root)) {
|
|
730
|
-
passFn('git-ai.yml: job має умову merged')
|
|
731
|
-
} else {
|
|
732
|
-
failFn('git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)')
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* Перевіряє git-ai.yml: тригер pull_request з types: [closed], умова merged у job, виклик git-ai.
|
|
738
|
-
* @param {string} wfDir директорія workflows
|
|
739
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
740
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
741
|
-
*/
|
|
742
|
-
async function checkGitAiWorkflow(wfDir, passFn, failFn) {
|
|
743
|
-
const gitAiWf = join(wfDir, 'git-ai.yml')
|
|
744
|
-
if (!existsSync(gitAiWf)) return
|
|
745
|
-
const content = await readFile(gitAiWf, 'utf8')
|
|
746
|
-
const root = parseWorkflowYaml(content)
|
|
747
|
-
|
|
748
|
-
if (root) {
|
|
749
|
-
validateGitAiParsedYaml(root, passFn, failFn)
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const hasGitAiRun = root ? anyRunStepIncludes(root, 'git-ai ci github run') : content.includes('git-ai ci github run')
|
|
753
|
-
if (hasGitAiRun) {
|
|
754
|
-
passFn('git-ai.yml: крок виконує git-ai ci github run')
|
|
755
|
-
} else {
|
|
756
|
-
failFn('git-ai.yml: крок має містити git-ai ci github run (ga.mdc)')
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Перевіряє, що “канонічні” workflows відповідають ga.mdc (структура і значення).
|
|
762
|
-
*
|
|
763
|
-
* Структурні валідатори `clean-ga-workflows.yml` і `clean-merged-branch.yml` мігровано в Rego-полісі
|
|
764
|
-
* під `npm/policy/ga/clean_ga_workflows/` та `npm/policy/ga/clean_merged_branch/` (виконує conftest з
|
|
765
|
-
* `bun run lint-ga`). Тут лишаються `lint-ga.yml` і `git-ai.yml` — їх перенесення в наступних ітераціях.
|
|
766
|
-
* @param {string} wfDir директорія workflows
|
|
767
|
-
* @param {(msg: string) => void} passFn pass
|
|
768
|
-
* @param {(msg: string) => void} failFn fail
|
|
769
|
-
*/
|
|
770
|
-
async function checkCanonicalWorkflowsMatchRule(wfDir, passFn, failFn) {
|
|
771
|
-
const paths = {
|
|
772
|
-
lintGa: join(wfDir, 'lint-ga.yml'),
|
|
773
|
-
gitAi: join(wfDir, 'git-ai.yml')
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (existsSync(paths.lintGa)) {
|
|
777
|
-
const c = await readFile(paths.lintGa, 'utf8')
|
|
778
|
-
validateLintGaWorkflowStructure(parseWorkflowYaml(c), passFn, failFn)
|
|
779
|
-
}
|
|
780
|
-
if (existsSync(paths.gitAi)) {
|
|
781
|
-
const c = await readFile(paths.gitAi, 'utf8')
|
|
782
|
-
validateGitAiWorkflowStructure(parseWorkflowYaml(c), passFn, failFn)
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
506
|
/**
|
|
787
507
|
* Перевіряє відповідність проєкту правилам ga.mdc
|
|
788
508
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -841,12 +561,8 @@ export async function check() {
|
|
|
841
561
|
}
|
|
842
562
|
}
|
|
843
563
|
|
|
844
|
-
await checkCanonicalWorkflowsMatchRule(wfDir, pass, fail)
|
|
845
|
-
|
|
846
564
|
await checkZizmor(pass, fail)
|
|
847
565
|
await checkLintGaScript(pass, fail)
|
|
848
|
-
await checkLintGaWorkflow(wfDir, pass, fail)
|
|
849
|
-
await checkGitAiWorkflow(wfDir, pass, fail)
|
|
850
566
|
checkShellcheckInstalled(pass, fail)
|
|
851
567
|
|
|
852
568
|
return reporter.getExitCode()
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Перевіряє правило graphql.mdc: наявність **`.graphqlrc.yml
|
|
3
|
-
* **`graphql.vscode-graphql
|
|
4
|
-
* **`package.json`**, якщо у дереві є **`gql\`…\``**.
|
|
2
|
+
* Перевіряє правило graphql.mdc: наявність **`.graphqlrc.yml`** і рекомендації
|
|
3
|
+
* **`graphql.vscode-graphql`**, якщо у дереві є **`gql\`…\``**.
|
|
5
4
|
*
|
|
6
5
|
* Обхід репозиторію — **`walkDir`** від **`process.cwd()`** (пропуски як у інших check). Кандидати — **`.vue`** та **`.js`/`.ts`/`.jsx`/`.tsx`** тощо; пропуск **`.d.ts`**, **auto-imports.d.ts** тощо — **`shouldSkipFileForGqlScan`**.
|
|
7
6
|
*
|
|
8
7
|
* Виявлення **`gql`** — **oxc-parser** після витягування `<script>` з SFC (**`graphql-gql-scan.mjs`**). Якщо збігів немає — перевірка завершується успішно без вимог до конфігів.
|
|
8
|
+
*
|
|
9
|
+
* Перевірку `scripts.dump-schema == REQUIRED_DUMP_SCHEMA_SCRIPT` у `package.json`
|
|
10
|
+
* перенесено в Rego (`npm/policy/graphql/package_json/`); `bun run lint-conftest`
|
|
11
|
+
* запускає її окремо.
|
|
9
12
|
*/
|
|
10
13
|
import { existsSync } from 'node:fs'
|
|
11
14
|
import { readFile } from 'node:fs/promises'
|
|
@@ -25,9 +28,6 @@ export const GRAPHQL_RC_FILENAME = '.graphqlrc.yml'
|
|
|
25
28
|
|
|
26
29
|
/** Розширення VS Code з graphql.mdc. */
|
|
27
30
|
export const REQUIRED_GRAPHQL_VSCODE_EXTENSION = 'graphql.vscode-graphql'
|
|
28
|
-
/** Команда dump-schema з graphql.mdc. */
|
|
29
|
-
export const REQUIRED_DUMP_SCHEMA_SCRIPT =
|
|
30
|
-
"bunx graphqurl http://localhost:4040/v1/graphql -H 'X-Hasura-Admin-Secret: secret' --introspect > schema.graphql"
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Збирає абсолютні шляхи source-файлів, які підлягають скануванню на gql templates.
|
|
@@ -106,44 +106,6 @@ async function checkExtensionsRecommendation(pass, fail) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
110
|
-
* Перевіряє `package.json` і значення scripts.dump-schema.
|
|
111
|
-
* @param {(msg: string) => void} pass success-репортер
|
|
112
|
-
* @param {(msg: string) => void} fail fail-репортер
|
|
113
|
-
* @returns {Promise<void>}
|
|
114
|
-
*/
|
|
115
|
-
async function checkPackageDumpSchemaScript(pass, fail) {
|
|
116
|
-
if (!existsSync('package.json')) {
|
|
117
|
-
fail('Відсутній package.json у корені репозиторію')
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
let pkg
|
|
122
|
-
try {
|
|
123
|
-
pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
124
|
-
} catch {
|
|
125
|
-
fail('package.json не є валідним JSON')
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const scripts = pkg.scripts
|
|
130
|
-
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
|
|
131
|
-
fail('package.json: поле scripts має бути обʼєктом')
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!Object.hasOwn(scripts, 'dump-schema')) {
|
|
136
|
-
fail('package.json: відсутній scripts.dump-schema (graphql.mdc)')
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (scripts['dump-schema'] === REQUIRED_DUMP_SCHEMA_SCRIPT) {
|
|
141
|
-
pass('package.json: scripts.dump-schema відповідає graphql.mdc')
|
|
142
|
-
} else {
|
|
143
|
-
fail(`package.json: scripts.dump-schema має бути "${REQUIRED_DUMP_SCHEMA_SCRIPT}" (graphql.mdc)`)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
109
|
/**
|
|
148
110
|
* Перевіряє graphql.mdc: умовна вимога .graphqlrc.yml, graphql.vscode-graphql
|
|
149
111
|
* і scripts.dump-schema за наявності gql tagged templates.
|
|
@@ -176,7 +138,6 @@ export async function check() {
|
|
|
176
138
|
}
|
|
177
139
|
|
|
178
140
|
await checkExtensionsRecommendation(pass, fail)
|
|
179
|
-
await checkPackageDumpSchemaScript(pass, fail)
|
|
180
141
|
|
|
181
142
|
return reporter.getExitCode()
|
|
182
143
|
}
|
package/scripts/check-hasura.mjs
CHANGED
|
@@ -45,9 +45,7 @@ const ENV_FILE_RE = /\.env$/u
|
|
|
45
45
|
const HASURA_ENDPOINT_LINE_RE = /^[ \t]*(?:export[ \t]+)?HASURA_GRAPHQL_ENDPOINT[ \t]*=[ \t]*['"]?([^'"\r\n#]+)/mu
|
|
46
46
|
// Дозволяємо два DNS-суфікси кластера: `<name>.internal` (GKE/GCP) і `cluster.local`
|
|
47
47
|
// (стандартний k8s / Yandex Cloud). У YC namespace.yaml + cluster mode дають коротший суфікс.
|
|
48
|
-
const INTERNAL_HASURA_URL_RE =
|
|
49
|
-
/^http:\/\/([^./]+)\.([^./]+)\.svc\.((?:[^./:]+\.internal)|cluster\.local):(\d+)\/?$/u
|
|
50
|
-
const CLUSTER_LOCAL_SUFFIX = 'cluster.local'
|
|
48
|
+
const INTERNAL_HASURA_URL_RE = /^http:\/\/([^./]+)\.([^./]+)\.svc\.((?:[^./:]+\.internal)|cluster\.local):(\d+)\/?$/u
|
|
51
49
|
const INTERNAL_DNS_SUFFIX = '.internal'
|
|
52
50
|
|
|
53
51
|
/**
|
|
@@ -150,8 +148,9 @@ async function checkEnvFile(relPath, expected, reporter) {
|
|
|
150
148
|
const value = m[1].trim()
|
|
151
149
|
const parsed = parseInternalHasuraEndpoint(value)
|
|
152
150
|
if (!parsed.ok) {
|
|
153
|
-
|
|
154
|
-
const example =
|
|
151
|
+
|
|
152
|
+
const example =
|
|
153
|
+
"https://<service>.<namespace>.svc.<cluster>.internal:<port> або http://<service>.<namespace>.svc.cluster.local:<port>"
|
|
155
154
|
fail(
|
|
156
155
|
`${relPath}: HASURA_GRAPHQL_ENDPOINT="${value}" — потрібен внутрішній кластерний URL виду ${example} (hasura.mdc)`
|
|
157
156
|
)
|
|
@@ -68,6 +68,7 @@ const VUE_RASTER_STATIC_SRC_RE = /(?<![:\-_.])\bsrc\s*=\s*['"]([^'"\s]+\.(?:png|
|
|
|
68
68
|
* є сиротами і підлягають видаленню.
|
|
69
69
|
*/
|
|
70
70
|
const VUE_AVIF_REF_RE = /['"]([^'"\s]+\.(?:png|jpe?g|gif)\.avif)['"]/giu
|
|
71
|
+
const RASTER_IMAGE_EXT_RE = /\.(?:png|jpe?g|gif)$/iu
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
74
|
* Чи у `package.json` пакета вимкнено avif-перевірку Vue-імпортів.
|
|
@@ -113,8 +114,7 @@ function resolveImageCandidates(importPath, sourceAbsPath, packageRootAbs) {
|
|
|
113
114
|
/** @type {string[]} */
|
|
114
115
|
const candidates = []
|
|
115
116
|
if (packageRootAbs) {
|
|
116
|
-
candidates.push(join(packageRootAbs, 'public', importPath))
|
|
117
|
-
candidates.push(join(packageRootAbs, importPath))
|
|
117
|
+
candidates.push(join(packageRootAbs, 'public', importPath), join(packageRootAbs, importPath))
|
|
118
118
|
}
|
|
119
119
|
candidates.push(join(process.cwd(), importPath))
|
|
120
120
|
return candidates
|
|
@@ -291,7 +291,7 @@ async function hasAnyRasterImage(ignorePaths) {
|
|
|
291
291
|
process.cwd(),
|
|
292
292
|
absPath => {
|
|
293
293
|
if (found) return
|
|
294
|
-
if (
|
|
294
|
+
if (RASTER_IMAGE_EXT_RE.test(absPath)) found = true
|
|
295
295
|
},
|
|
296
296
|
ignorePaths
|
|
297
297
|
)
|