@7n/rules 1.4.1 → 1.5.0

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/n-rules.js +65 -17
  3. package/package.json +11 -1
  4. package/rules/abie/clean_merged_ignore_branches/concern.json +3 -0
  5. package/rules/docker/lint_docker_yml/concern.json +3 -0
  6. package/rules/js/lint_js_yml/concern.json +3 -0
  7. package/rules/k8s/lint_k8s_yml/concern.json +3 -0
  8. package/rules/npm-module/npm_publish_yml/concern.json +3 -0
  9. package/rules/php/lint_php_yml/concern.json +3 -0
  10. package/rules/python/lint_python_yml/concern.json +3 -0
  11. package/rules/rust/lint_rust_yml/concern.json +3 -0
  12. package/rules/rust/toolchain_cache/concern.json +10 -1
  13. package/rules/security/lint_security_yml/concern.json +3 -0
  14. package/rules/style/lint_style_yml/concern.json +3 -0
  15. package/rules/tauri/main.mdc +1 -1
  16. package/rules/tauri/release/release.mdc +15 -0
  17. package/rules/text/lint_text/concern.json +3 -0
  18. package/schemas/concern.json +86 -16
  19. package/schemas/n-rules.json +11 -1
  20. package/schemas/v8r-catalog.json +123 -27
  21. package/scripts/auto-rules.mjs +47 -6
  22. package/scripts/docs/auto-rules.md +3 -1
  23. package/scripts/docs/skills-cli.md +3 -2
  24. package/scripts/docs/upgrade-n-rules-and-install.md +3 -1
  25. package/scripts/lib/acp-runner.mjs +174 -0
  26. package/scripts/lib/concern-meta.mjs +8 -1
  27. package/scripts/lib/docs/acp-runner.md +58 -0
  28. package/scripts/lib/docs/concern-meta.md +1 -1
  29. package/scripts/lib/docs/read-n-rules-config-lite.md +3 -1
  30. package/scripts/lib/docs/resolve-plugins.md +17 -0
  31. package/scripts/lib/docs/run-conftest-batch.md +3 -1
  32. package/scripts/lib/lint-surface/docs/policy-lint-adapter.md +3 -1
  33. package/scripts/lib/lint-surface/docs/run-detectors.md +5 -1
  34. package/scripts/lib/lint-surface/policy-lint-adapter.mjs +2 -0
  35. package/scripts/lib/lint-surface/run-detectors.mjs +72 -5
  36. package/scripts/lib/read-n-rules-config-lite.mjs +5 -3
  37. package/scripts/lib/resolve-plugins.mjs +248 -0
  38. package/scripts/lib/run-conftest-batch.mjs +3 -1
  39. package/scripts/skills-cli.mjs +30 -52
  40. package/scripts/upgrade-n-rules-and-install.mjs +5 -4
  41. package/rules/ga/clean_ga_workflows/clean_ga_workflows.mdc +0 -20
  42. package/rules/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -112
  43. package/rules/ga/clean_ga_workflows/concern.json +0 -10
  44. package/rules/ga/clean_ga_workflows/docs/fix-clean_ga_workflows.md +0 -33
  45. package/rules/ga/clean_ga_workflows/docs/index.md +0 -9
  46. package/rules/ga/clean_ga_workflows/fix-clean_ga_workflows.mjs +0 -8
  47. package/rules/ga/clean_ga_workflows/template/clean-ga-workflows.yml.snippet.yml +0 -26
  48. package/rules/ga/clean_merged_branch/clean_merged_branch.mdc +0 -24
  49. package/rules/ga/clean_merged_branch/clean_merged_branch.rego +0 -163
  50. package/rules/ga/clean_merged_branch/concern.json +0 -10
  51. package/rules/ga/clean_merged_branch/docs/fix-clean_merged_branch.md +0 -27
  52. package/rules/ga/clean_merged_branch/docs/index.md +0 -9
  53. package/rules/ga/clean_merged_branch/fix-clean_merged_branch.mjs +0 -8
  54. package/rules/ga/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +0 -37
  55. package/rules/ga/docs/index.md +0 -11
  56. package/rules/ga/git_ai/concern.json +0 -11
  57. package/rules/ga/git_ai/docs/fix-git_ai.md +0 -30
  58. package/rules/ga/git_ai/docs/index.md +0 -9
  59. package/rules/ga/git_ai/fix-git_ai.mjs +0 -5
  60. package/rules/ga/git_ai/git_ai.mdc +0 -19
  61. package/rules/ga/git_ai/git_ai.rego +0 -76
  62. package/rules/ga/git_ai/template/git-ai.yml.snippet.yml +0 -30
  63. package/rules/ga/lint_ga/concern.json +0 -10
  64. package/rules/ga/lint_ga/docs/fix-lint_ga.md +0 -32
  65. package/rules/ga/lint_ga/docs/index.md +0 -9
  66. package/rules/ga/lint_ga/fix-lint_ga.mjs +0 -5
  67. package/rules/ga/lint_ga/lint_ga.mdc +0 -21
  68. package/rules/ga/lint_ga/lint_ga.rego +0 -132
  69. package/rules/ga/lint_ga/template/lint-ga.yml.snippet.yml +0 -41
  70. package/rules/ga/main.json +0 -1
  71. package/rules/ga/main.mdc +0 -17
  72. package/rules/ga/vscode_extensions/concern.json +0 -11
  73. package/rules/ga/vscode_extensions/docs/fix-vscode_extensions.md +0 -25
  74. package/rules/ga/vscode_extensions/docs/index.md +0 -11
  75. package/rules/ga/vscode_extensions/fix-vscode_extensions.mjs +0 -1
  76. package/rules/ga/vscode_extensions/template/extensions.json.snippet.json +0 -1
  77. package/rules/ga/vscode_extensions/vscode_extensions.mdc +0 -9
  78. package/rules/ga/vscode_extensions/vscode_extensions.rego +0 -18
  79. package/rules/ga/vscode_settings/concern.json +0 -11
  80. package/rules/ga/vscode_settings/docs/fix-vscode_settings.md +0 -26
  81. package/rules/ga/vscode_settings/docs/index.md +0 -9
  82. package/rules/ga/vscode_settings/fix-vscode_settings.mjs +0 -5
  83. package/rules/ga/vscode_settings/template/settings.json.snippet.json +0 -1
  84. package/rules/ga/vscode_settings/vscode_settings.mdc +0 -9
  85. package/rules/ga/vscode_settings/vscode_settings.rego +0 -22
  86. package/rules/ga/workflow_common/concern.json +0 -9
  87. package/rules/ga/workflow_common/template/uses-min-versions.snippet.json +0 -4
  88. package/rules/ga/workflow_common/workflow_common.mdc +0 -18
  89. package/rules/ga/workflow_common/workflow_common.rego +0 -288
  90. package/rules/ga/workflows/concern.json +0 -8
  91. package/rules/ga/workflows/docs/fix-workflows.md +0 -37
  92. package/rules/ga/workflows/docs/main.md +0 -38
  93. package/rules/ga/workflows/fix-workflows.mjs +0 -212
  94. package/rules/ga/workflows/main.mjs +0 -433
  95. package/rules/ga/workflows/workflows.mdc +0 -33
  96. package/rules/ga/zizmor_yml/concern.json +0 -11
  97. package/rules/ga/zizmor_yml/docs/fix-zizmor_yml.md +0 -28
  98. package/rules/ga/zizmor_yml/docs/index.md +0 -9
  99. package/rules/ga/zizmor_yml/fix-zizmor_yml.mjs +0 -3
  100. package/rules/ga/zizmor_yml/template/zizmor.yml.snippet.yml +0 -5
  101. package/rules/ga/zizmor_yml/zizmor_yml.mdc +0 -9
  102. package/rules/ga/zizmor_yml/zizmor_yml.rego +0 -27
@@ -1,433 +0,0 @@
1
- /** @see ./docs/workflows.md */
2
- import { existsSync } from 'node:fs'
3
- import { readdir, readFile } from 'node:fs/promises'
4
- import { execFileSync } from 'node:child_process'
5
- import { basename, dirname, join, relative } from 'node:path'
6
- import { fileURLToPath } from 'node:url'
7
-
8
- import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
9
- import { eventPathsIncludeExact, parseWorkflowYaml } from '../../../scripts/lib/gha-workflow.mjs'
10
- import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
11
- import { runConftestBatch } from '../../../scripts/lib/run-conftest-batch.mjs'
12
- import { loadTemplate } from '../../../scripts/lib/template.mjs'
13
- import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
14
- import { runLintStep } from '../../../scripts/lib/run-lint-step.mjs'
15
-
16
- const HERE = dirname(fileURLToPath(import.meta.url))
17
- const GA_POLICY_DIR = join(HERE, '..')
18
-
19
- /** Шаблони наявності MegaLinter у вмісті workflow */
20
- const MEGALINTER_USE_PATTERNS = [/oxsecurity\/megalinter-action/i, /megalinter\/megalinter/i]
21
-
22
- /** Типові конфіги MegaLinter у корені репо */
23
- const MEGALINTER_CONFIG_NAMES = ['.mega-linter.yml', '.megalinter.yaml', '.mega-linter.yaml']
24
-
25
- /** Обовʼязкові workflow-файли (ga.mdc). */
26
- const REQUIRED_WORKFLOWS = ['clean-ga-workflows.yml', 'clean-merged-branch.yml', 'lint-ga.yml', 'git-ai.yml']
27
-
28
- /** Патерн rego-violation про відсутній `persist-credentials`. */
29
- const CHECKOUT_PERSIST_RE = /persist-credentials/u
30
-
31
- /**
32
- * Structured fix-hint (#3) для rego-violation про `actions/checkout` без
33
- * `persist-credentials: false` — щоб T0 (`fix-workflows.mjs`) автофіксив детерміновано,
34
- * не парсячи message. Повертає `{ reason, file, data }` або undefined.
35
- * @param {string} file posix-relative шлях workflow-файла від cwd
36
- * @param {unknown} message текст rego-violation
37
- * @returns {{ reason: string, file: string, data: { kind: string } } | undefined} fix-hint або undefined
38
- */
39
- function checkoutPersistHint(file, message) {
40
- return CHECKOUT_PERSIST_RE.test(String(message ?? ''))
41
- ? { reason: 'checkout-persist-credentials', file, data: { kind: 'checkout-persist-credentials' } }
42
- : undefined
43
- }
44
-
45
- /**
46
- * Повертає true, якщо glob у GitHub Actions `on.*.paths` матчитсья хоча б на один tracked файл у репозиторії.
47
- *
48
- * Використовує `git ls-files` з pathspec-магiєю `:(glob)`, щоб не реалізовувати glob engine вручну
49
- * і не сканувати файлову систему рекурсивно.
50
- * @param {string} globPattern glob з workflow (наприклад "files/**" або "image-migration-new/**")
51
- * @param {string} cwd робочий каталог для `git`
52
- * @returns {boolean} true, якщо є хоча б один збіг
53
- */
54
- function gitHasAnyTrackedFileMatchingGlob(globPattern, cwd) {
55
- const p = String(globPattern ?? '').trim()
56
- if (!p) return false
57
- if (p.startsWith('!')) return true
58
- try {
59
- const out = execFileSync('git', ['ls-files', '-z', '--', `:(glob)${p}`], { encoding: 'utf8', cwd })
60
- return out.length > 0
61
- } catch {
62
- return false
63
- }
64
- }
65
-
66
- /**
67
- * Чи варто перевіряти glob з `on.*.paths` на наявність збігів у репозиторії.
68
- *
69
- * У багатьох workflow (особливо лінтерах) `paths` часто містить “широкі” шаблони по розширеннях
70
- * (наприклад `*.vue`, `*.php`), які можуть бути відсутні в конкретному репозиторії й це ок.
71
- * Запит цієї перевірки — ловити посилання на неіснуючі директорії/шляхи (типово `some-dir/**`).
72
- * @param {string} p glob з workflow
73
- * @returns {boolean} true, якщо треба валідувати наявність файлів
74
- */
75
- function shouldValidateWorkflowPathsGlob(p) {
76
- // Негативні патерни — лише виключають, їх існування не перевіряємо.
77
- if (p.startsWith('!')) return false
78
-
79
- // “Розширення-фільтри” (або brace-варіанти) пропускаємо: вони можуть бути заготовками.
80
- return !p.includes('*.')
81
- }
82
-
83
- /**
84
- * Перевіряє один glob з `on.<event>.paths` на наявність збігів у репо.
85
- * @param {string} relPath шлях workflow для повідомлень
86
- * @param {string} eventName назва події (push / pull_request)
87
- * @param {unknown} raw сирий елемент масиву paths
88
- * @param {(msg: string) => void} passFn pass
89
- * @param {(msg: string) => void} failFn fail
90
- * @param {string} cwd робочий каталог для `git`
91
- */
92
- function verifyOnePathsGlob(relPath, eventName, raw, passFn, failFn, cwd) {
93
- const p = String(raw ?? '').trim()
94
- if (!p) return
95
- if (!shouldValidateWorkflowPathsGlob(p)) {
96
- passFn(`${relPath}: on.${eventName}.paths glob пропущено для перевірки існування: ${JSON.stringify(p)}`)
97
- return
98
- }
99
- if (gitHasAnyTrackedFileMatchingGlob(p, cwd)) {
100
- passFn(`${relPath}: on.${eventName}.paths glob матчитсья: ${JSON.stringify(p)}`)
101
- } else {
102
- failFn(`${relPath}: on.${eventName}.paths glob не матчитсья ні на один файл: ${JSON.stringify(p)}`, {
103
- reason: 'unmatched-paths-glob',
104
- file: relPath,
105
- data: { kind: 'unmatched-paths-glob', event: eventName, glob: p }
106
- })
107
- }
108
- }
109
-
110
- /**
111
- * Валідує `on.push.paths` / `on.pull_request.paths`: кожен позитивний glob має мати збіги в репозиторії.
112
- * @param {string} relPath шлях workflow для повідомлень
113
- * @param {Record<string, unknown>} root parsed YAML workflow
114
- * @param {(msg: string) => void} passFn pass
115
- * @param {(msg: string) => void} failFn fail
116
- * @param {string} cwd робочий каталог для `git`
117
- */
118
- function verifyWorkflowEventPathsGlobsExist(relPath, root, passFn, failFn, cwd) {
119
- const on = getObjKey(root, 'on')
120
- if (!on || typeof on !== 'object') return
121
-
122
- /** @type {Array<[eventName: string, paths: unknown]>} */
123
- const candidates = [
124
- ['push', getObjKey(getObjKey(on, 'push'), 'paths')],
125
- ['pull_request', getObjKey(getObjKey(on, 'pull_request'), 'paths')]
126
- ]
127
-
128
- for (const [eventName, paths] of candidates) {
129
- if (!Array.isArray(paths)) continue
130
- for (const raw of paths) {
131
- verifyOnePathsGlob(relPath, eventName, raw, passFn, failFn, cwd)
132
- }
133
- }
134
- }
135
-
136
- const RUN_INLINE_NCURSOR_RE = /^\s*(?:-\s*)?run:\s*n-(?:cursor|rules)\s/u
137
- const BARE_LINE_NCURSOR_RE = /^\s+n-(?:cursor|rules)\s/u
138
- const WRAPPED_NCURSOR_RE = /\b(?:bunx|npx)\s+n-(?:cursor|rules)/u
139
-
140
- /**
141
- * Прапорить кроки, що інвокають **bare `n-rules`** (чи legacy `n-rules`, не `bunx`/`npx`): у CI-раннері
142
- * `n-rules` не на PATH (`node_modules/.bin` не додається), тож `run: n-rules …` падає з
143
- * exit 127. Канон — `bunx n-rules …` (як у npm-publish.yml). Structured fix-hint для T0.
144
- * @param {string} relPath posix-relative шлях workflow
145
- * @param {string} content сирий вміст workflow
146
- * @param {(msg: string, opts?: object) => void} failFn реєстрація порушення
147
- */
148
- function verifyNoBareNCursor(relPath, content, failFn) {
149
- for (const [i, line] of content.split('\n').entries()) {
150
- if (WRAPPED_NCURSOR_RE.test(line)) continue
151
- if (!RUN_INLINE_NCURSOR_RE.test(line) && !BARE_LINE_NCURSOR_RE.test(line)) continue
152
- failFn(
153
- `${relPath}: \`n-rules …\` (рядок ${i + 1}) має бути \`bunx n-rules …\` — n-rules не на PATH у CI (ga.mdc)`,
154
- {
155
- reason: 'bare-n-rules',
156
- file: relPath,
157
- data: { kind: 'bare-n-rules' }
158
- }
159
- )
160
- }
161
- }
162
-
163
- /**
164
- * Безпечний доступ до вкладеного поля (лише для обʼєктів).
165
- * @param {unknown} obj значення-кандидат на обʼєкт
166
- * @param {string} key ключ
167
- * @returns {unknown} значення поля або undefined
168
- */
169
- function getObjKey(obj, key) {
170
- return obj && typeof obj === 'object' && !Array.isArray(obj)
171
- ? /** @type {Record<string, unknown>} */ (obj)[key]
172
- : undefined
173
- }
174
-
175
- /**
176
- * Перевіряє apply-workflow на наявність paths trigger.
177
- * @param {string} wfDir абсолютна директорія workflows
178
- * @param {string[]} files список файлів у директорії
179
- * @param {string} filename параметр filename
180
- * @param {string} expectedPath параметр expectedPath
181
- * @param {(msg: string) => void} passFn callback при успішній перевірці
182
- * @param {(msg: string) => void} failFn callback при помилці
183
- */
184
- async function checkApplyWorkflow(wfDir, files, filename, expectedPath, passFn, failFn) {
185
- if (!files.includes(filename)) return
186
- const content = await readFile(join(wfDir, filename), 'utf8')
187
- const root = parseWorkflowYaml(content)
188
- const ok = root ? eventPathsIncludeExact(root, 'push', expectedPath) : content.includes(expectedPath)
189
- if (ok) {
190
- passFn(`${filename} має правильний paths trigger`)
191
- } else {
192
- failFn(`${filename} не містить paths: ${expectedPath}`)
193
- }
194
- }
195
-
196
- /**
197
- * Перевіряє відсутність MegaLinter у workflows та конфіг-файлах.
198
- * @param {string} wfDir абсолютна директорія workflows
199
- * @param {string[]} ymlWorkflows параметр ymlWorkflows
200
- * @param {string} wfDirRel відносний шлях директорії workflows для повідомлень
201
- * @param {string} cwd корінь репозиторію
202
- * @param {(msg: string) => void} passFn callback при успішній перевірці
203
- * @param {(msg: string) => void} failFn callback при помилці
204
- */
205
- async function checkMegalinter(wfDir, ymlWorkflows, wfDirRel, cwd, passFn, failFn) {
206
- let found = false
207
- for (const f of ymlWorkflows) {
208
- const content = await readFile(join(wfDir, f), 'utf8')
209
- if (MEGALINTER_USE_PATTERNS.some(re => re.test(content))) {
210
- found = true
211
- failFn(`MegaLinter у workflow ${wfDirRel}/${f} — видали інтеграцію (ga.mdc: MegaLinter)`)
212
- }
213
- }
214
- for (const name of MEGALINTER_CONFIG_NAMES) {
215
- if (!existsSync(join(cwd, name))) {
216
- continue
217
- }
218
-
219
- found = true
220
- failFn(`Файл ${name} — видали конфіг MegaLinter (ga.mdc: MegaLinter)`)
221
- }
222
- if (!found) passFn('Залишків MegaLinter не виявлено')
223
- }
224
-
225
- /**
226
- * Перевіряє наявність локального `shellcheck` у PATH. `actionlint` (`bunx github-actionlint`)
227
- * запускає shell-перевірки в кроках `run:` workflow тільки коли `shellcheck` доступний; інакше
228
- * мовчки пропускає SC-правила, через що локальний `bun lint-ga` зелений, а CI на ubuntu-latest
229
- * (де shellcheck передвстановлений) падає. Тому відсутність бінарника локально — `fail`.
230
- *
231
- * Кросплатформно: `resolveCmd` використовує `which`/`where` і однаково знаходить `shellcheck`
232
- * та `shellcheck.exe` на Windows.
233
- * @param {(msg: string) => void} passFn callback при успішній перевірці
234
- * @param {(msg: string) => void} failFn callback при помилці
235
- */
236
- export function checkShellcheckInstalled(passFn, failFn) {
237
- if (resolveCmd('shellcheck')) {
238
- passFn('shellcheck встановлений локально, actionlint виконуватиме SC-правила, як у CI')
239
- return
240
- }
241
- failFn(
242
- [
243
- 'shellcheck не знайдено в PATH — actionlint без нього мовчки пропускає shell-перевірки в run: блоках,',
244
- 'тому локальний `bun lint-ga` буде зелений, а CI на ubuntu-latest (де shellcheck передвстановлений) падатиме.',
245
- 'Встанови: macOS — `brew install shellcheck`; Debian/Ubuntu — `sudo apt-get install -y shellcheck`;',
246
- 'Arch — `sudo pacman -S shellcheck` (ga.mdc)'
247
- ].join(' ')
248
- )
249
- }
250
-
251
- /**
252
- * Перевіряє розширення workflow-файлів і наявність обов'язкових workflow.
253
- * @param {string} wfDirRel відносний шлях директорії workflows для повідомлень
254
- * @param {string[]} files список файлів у wfDir
255
- * @param {(msg: string) => void} pass callback при успішній перевірці
256
- * @param {(msg: string) => void} fail callback при помилці
257
- */
258
- function checkGaWorkflowFiles(wfDirRel, files, pass, fail) {
259
- const yamlFiles = files.filter(f => f.endsWith('.yaml'))
260
- if (yamlFiles.length > 0) {
261
- for (const f of yamlFiles) {
262
- fail(`Workflow з розширенням .yaml: ${wfDirRel}/${f} — перейменуй на .yml`)
263
- }
264
- } else {
265
- pass('Всі workflows мають розширення .yml')
266
- }
267
-
268
- const notYmlFiles = files.filter(f => !f.endsWith('.yml'))
269
- if (notYmlFiles.length > 0) {
270
- for (const f of notYmlFiles) {
271
- fail(`Workflow має бути з розширенням .yml: ${wfDirRel}/${f} (ga.mdc)`)
272
- }
273
- }
274
-
275
- for (const f of REQUIRED_WORKFLOWS) {
276
- if (files.includes(f)) {
277
- pass(`${f} існує`)
278
- } else {
279
- fail(`Відсутній ${wfDirRel}/${f}`)
280
- }
281
- }
282
- }
283
-
284
- /**
285
- * Per-workflow Rego-полісі: namespace ↔ конкретний workflow-файл. Кожен пакет
286
- * у `npm/policy/ga/<name>/` містить правила специфічні для ОДНОГО workflow,
287
- * тому conftest викликаємо з `--namespace` окремо на кожен файл (інакше правила
288
- * чужого workflow застосуються до неправильного файла).
289
- * @type {Array<{ workflow: string, namespace: string, policyDirRel: string }>}
290
- */
291
- const GA_PER_WORKFLOW_REGO_TARGETS = [
292
- {
293
- workflow: '.github/workflows/clean-ga-workflows.yml',
294
- namespace: 'ga.clean_ga_workflows',
295
- policyDirRel: 'ga/clean_ga_workflows'
296
- },
297
- {
298
- workflow: '.github/workflows/clean-merged-branch.yml',
299
- namespace: 'ga.clean_merged_branch',
300
- policyDirRel: 'ga/clean_merged_branch'
301
- },
302
- {
303
- workflow: '.github/workflows/lint-ga.yml',
304
- namespace: 'ga.lint_ga',
305
- policyDirRel: 'ga/lint_ga'
306
- },
307
- {
308
- workflow: '.github/workflows/git-ai.yml',
309
- namespace: 'ga.git_ai',
310
- policyDirRel: 'ga/git_ai'
311
- }
312
- ]
313
-
314
- /**
315
- * Plan B (rego-authoritative): на початку перевірки правила ga прогнати усі
316
- * Rego-полісі з `npm/policy/ga/`. Спочатку — per-workflow (4 окремі спавни,
317
- * бо кожен namespace застосовний лише до свого файла), потім один батч-спавн
318
- * `ga.workflow_common` на всі `.github/workflows/*.yml`. Hard-fail без
319
- * `conftest` у PATH — узгоджено з Plan B (див. `runConftestBatch`).
320
- * @param {string} wfDir абсолютний шлях до `.github/workflows`
321
- * @param {string[]} ymlWorkflows відносні (від `wfDir`) імена файлів `*.yml`
322
- * @param {string} cwd корінь репозиторію
323
- * @param {(msg: string) => void} pass callback при успішній перевірці
324
- * @param {(msg: string) => void} fail callback при помилці
325
- * @returns {void}
326
- */
327
- async function runAllGaRego(wfDir, ymlWorkflows, cwd, pass, fail) {
328
- for (const target of GA_PER_WORKFLOW_REGO_TARGETS) {
329
- const targetAbs = join(cwd, target.workflow)
330
- if (!existsSync(targetAbs)) continue
331
- const concernDir = join(GA_POLICY_DIR, target.policyDirRel.split('/', 2)[1])
332
- const tpl = await loadTemplate(concernDir)
333
- const templateData = tpl[basename(target.workflow)]
334
- const violations = runConftestBatch({
335
- policyDirRel: target.policyDirRel,
336
- namespace: target.namespace,
337
- files: [targetAbs],
338
- templateData
339
- })
340
- for (const v of violations)
341
- fail(`${target.workflow}: ${v.message}`, checkoutPersistHint(target.workflow, v.message))
342
- if (violations.length === 0) {
343
- pass(`${target.workflow}: відповідає ${target.namespace} (rego)`)
344
- }
345
- }
346
-
347
- if (ymlWorkflows.length === 0) return
348
- const wfFiles = ymlWorkflows.map(f => join(wfDir, f))
349
- const workflowCommonDir = join(GA_POLICY_DIR, 'workflow_common')
350
- const workflowCommonTpl = await loadTemplate(workflowCommonDir)
351
- const usesMinVersionsSnippet = workflowCommonTpl['uses-min-versions']?.snippet
352
- const violations = runConftestBatch({
353
- policyDirRel: 'ga/workflow_common',
354
- namespace: 'ga.workflow_common',
355
- files: wfFiles,
356
- templateData: usesMinVersionsSnippet ? { snippet: usesMinVersionsSnippet } : undefined
357
- })
358
- for (const v of violations) {
359
- fail(`${v.filename}: ${v.message}`, checkoutPersistHint(relative(cwd, v.filename), v.message))
360
- }
361
- if (violations.length === 0) {
362
- pass(`${wfFiles.length} workflow(s) відповідають ga.workflow_common (rego)`)
363
- }
364
- }
365
-
366
- /**
367
- * Перевіряє відповідність проєкту правилам ga.mdc.
368
- *
369
- * Plan B-патерн: пер-документна валідація workflow-структури делегована
370
- * Rego-полісі у `npm/policy/ga/`; виклик через `runAllGaRego` (батч-conftest)
371
- * — це перший крок `check()`. Далі — JS-частина (cross-file перевірки на
372
- * наявність файлів, `git ls-files`-залежні `on.push.paths` glob, vscode/zizmor
373
- * config, megalinter залишки тощо). `bun run lint-ga` додатково запускає
374
- * `actionlint` + `zizmor` зовнішніми тулчейнами і **викликає цю ж `check()`** —
375
- * тобто rego-частина живе тут, не в `lint-ga.mjs`.
376
- * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx detector-контекст (cwd, ruleId, concernId)
377
- * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} порушення concern-а
378
- */
379
- export async function lint(ctx) {
380
- const reporter = createViolationReporter(ctx)
381
- const { pass, fail } = reporter
382
- const cwd = ctx.cwd
383
-
384
- // Зовнішні тули (read-only): actionlint + zizmor. 127 = тул відсутній → skip.
385
- ensureTool('shellcheck')
386
- ensureTool('conftest')
387
- const stepOpts = { verbose: ctx.verbose === true }
388
- const actionlintCode = runLintStep('actionlint', 'bunx', ['github-actionlint'], stepOpts)
389
- if (actionlintCode !== 0 && actionlintCode !== 127) fail('actionlint знайшов порушення (ga.mdc)', 'actionlint')
390
- if (resolveCmd('uv')) {
391
- const zizmorCode = runLintStep('zizmor', 'uvx', ['zizmor', '--offline', '--collect=workflows', '.'], stepOpts)
392
- if (zizmorCode !== 0 && zizmorCode !== 127) fail('zizmor знайшов ризики у workflow (ga.mdc)', 'zizmor')
393
- }
394
-
395
- const wfDirRel = '.github/workflows'
396
- const wfDir = join(cwd, wfDirRel)
397
- if (!existsSync(wfDir)) {
398
- fail(`Директорія ${wfDirRel} не існує`)
399
- return reporter.result()
400
- }
401
-
402
- const files = await readdir(wfDir)
403
- const ymlWorkflows = files.filter(f => f.endsWith('.yml'))
404
-
405
- // Rego-крок (per-workflow + workflow_common) — оркестрація ga policy sub-concern-ів.
406
- await runAllGaRego(wfDir, ymlWorkflows, cwd, pass, fail)
407
-
408
- const setupBunDepsActionRel = '.github/actions/setup-bun-deps/action.yml'
409
- if (!existsSync(join(cwd, setupBunDepsActionRel))) {
410
- fail(
411
- `Відсутній ${setupBunDepsActionRel} — запустіть npx @7n/rules або скопіюйте з пакету (ga.mdc: composite setup-bun-deps)`
412
- )
413
- }
414
-
415
- checkGaWorkflowFiles(wfDirRel, files, pass, fail)
416
-
417
- await checkApplyWorkflow(wfDir, files, 'apply-k8s.yml', '**/k8s/**/*.yaml', pass, fail)
418
- await checkApplyWorkflow(wfDir, files, 'apply-nats-consumer.yml', '**/consumer.yaml', pass, fail)
419
-
420
- await checkMegalinter(wfDir, ymlWorkflows, wfDirRel, cwd, pass, fail)
421
-
422
- // git-залежна перевірка `on.push.paths` glob-ів (вимагає `git ls-files`) — лишається в JS.
423
- for (const f of ymlWorkflows) {
424
- const content = await readFile(join(wfDir, f), 'utf8')
425
- const parsed = parseWorkflowYaml(content)
426
- if (parsed) verifyWorkflowEventPathsGlobsExist(`${wfDirRel}/${f}`, parsed, pass, fail, cwd)
427
- verifyNoBareNCursor(`${wfDirRel}/${f}`, content, fail)
428
- }
429
-
430
- checkShellcheckInstalled(pass, fail)
431
-
432
- return reporter.result()
433
- }
@@ -1,33 +0,0 @@
1
- ## Структура workflow-файлів та обов'язкові workflows
2
-
3
- У `.github/workflows/` лише **`.yml`** (не `.yaml`). Обов'язкові файли:
4
-
5
- - **`clean-ga-workflows.yml`**
6
- - **`clean-merged-branch.yml`**
7
- - **`lint-ga.yml`**
8
- - **`git-ai.yml`**
9
-
10
- Якщо є **`apply-k8s.yml`** — тригер `on.push.paths` має містити `**/k8s/**/*.yaml`.
11
- Якщо є **`apply-nats-consumer.yml`** — тригер `on.push.paths` має містити `**/consumer.yaml`.
12
-
13
- ## Перевірка glob-патернів `on.push.paths`
14
-
15
- Кожен позитивний glob у `on.push.paths` / `on.pull_request.paths` (крім `!`-негацій та `*`-extension-фільтрів) має матчитися хоча б на один tracked файл у репозиторії (`git ls-files`). Шаблони на кшталт `*.vue` чи `{*.ts,*.js}` пропускаються — вони можуть бути заготовками для майбутніх файлів. Конкретні директорії (`some-dir/**`) — перевіряються.
16
-
17
- ## Заборона MegaLinter
18
-
19
- **MegaLinter** не використовувати. Треба видалити:
20
-
21
- - workflow-файли з `oxsecurity/megalinter-action` або `megalinter/megalinter`
22
- - конфіги: `.mega-linter.yml`, `.megalinter.yaml`, `.mega-linter.yaml`
23
- - будь-які залежності та згадки в CI / pre-commit / документації
24
-
25
- ## Shellcheck у PATH
26
-
27
- `actionlint` (через `bunx github-actionlint`) запускає shell-перевірки в кроках `run:` лише коли `shellcheck` доступний у PATH — інакше мовчки пропускає SC-правила. Локальний `bun lint-ga` лишається зеленим, а CI на `ubuntu-latest` (де shellcheck передвстановлений) падає.
28
-
29
- Встанови `shellcheck`:
30
-
31
- - macOS: `brew install shellcheck`
32
- - Debian/Ubuntu: `sudo apt-get install -y shellcheck`
33
- - Arch: `sudo pacman -S shellcheck`
@@ -1,11 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
- "fixability": "config",
4
- "policy": {
5
- "files": {
6
- "single": ".github/zizmor.yml",
7
- "required": true
8
- },
9
- "missingMessage": ".github/zizmor.yml не існує — потрібен для zizmor (ga.mdc)"
10
- }
11
- }
@@ -1,28 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: fix-zizmor_yml.mjs
4
- resource: npm/rules/ga/zizmor_yml/fix-zizmor_yml.mjs
5
- docgen:
6
- crc: e1cfb71f
7
- model: openai-codex/gpt-5.4-mini
8
- tier: cloud-min
9
- score: 100
10
- issues: judge:inaccurate:0.98
11
- judgeModel: openai-codex/gpt-5.4-mini
12
- ---
13
-
14
- ## Огляд
15
-
16
- `patterns` описує, як привести цільові файли репозиторію до потрібного вигляду з урахуванням свідомо пропущених шляхів `.github` і `.git`. Функція працює read-only і не пише у ФС чи БД.
17
-
18
- ## Поведінка
19
-
20
- 1. `patterns` задає набір правил для автоматичного виправлення конфігурації `zizmor` у репозиторії.
21
- 2. Воно орієнтується на файл `.github/zizmor.yml` і застосовується лише до цього цільового місця.
22
- 3. Під час роботи свідомо не зачіпає шляхи `.github` і `.git` поза визначеним цільовим файлом, щоб не втручатися в службові та інфраструктурні області.
23
- 4. Працює у режимі read-only: саме значення `patterns` лише описує поведінку, без запису в файлову систему чи базу даних.
24
-
25
- ## Гарантії поведінки
26
-
27
- - Read-only: не виконує операцій запису (ФС/БД).
28
- - Свідомо пропускає шляхи: `.github`, `.git`.
@@ -1,9 +0,0 @@
1
- ---
2
- type: Directory Index
3
- title: npm/rules/ga/zizmor_yml
4
- resource: npm/rules/ga/zizmor_yml/
5
- ---
6
-
7
- | Файл | Тип |
8
- | --------------------------------------- | --------- |
9
- | [fix-zizmor_yml.mjs](fix-zizmor_yml.md) | JS Module |
@@ -1,3 +0,0 @@
1
- import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
-
3
- export const patterns = [createTemplateFixPattern({ id: 'ga-zizmor_yml-template', targetPath: '.github/zizmor.yml' })]
@@ -1,5 +0,0 @@
1
- rules:
2
- unpinned-uses:
3
- config:
4
- policies:
5
- '*': ref-pin
@@ -1,9 +0,0 @@
1
- ## `.github/zizmor.yml` — конфігурація zizmor для unpinned-uses
2
-
3
- Rego-пакет: `ga.zizmor_yml`
4
-
5
- **Цільовий файл:** `.github/zizmor.yml`
6
-
7
- Перевіряє `rules.unpinned-uses.config.policies["*"]` — значення має точно відповідати template (`ref-pin`). Всі вкладені об'єкти читаються через `object.get` із дефолтами, тож відсутні ключі коректно детектуються як порушення.
8
-
9
- Канон: [zizmor.yml.snippet.yml](./template/zizmor.yml.snippet.yml)
@@ -1,27 +0,0 @@
1
- # Перевірка `.github/zizmor.yml` для GitHub Actions (ga.mdc).
2
- #
3
- # Канон надходить через --data: { "template": { "snippet": ... } }
4
- # Структура --data сформована з template/zizmor.yml.snippet.yml.
5
- # Канонічний шлях — `rules.unpinned-uses.config.policies."*"`; expected value
6
- # (наприклад `"ref-pin"`) приходить із template, path лишається тут.
7
- #
8
- # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
9
- # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
10
- package ga.zizmor_yml
11
-
12
- import rego.v1
13
-
14
- deny contains msg if {
15
- expected := data.template.snippet.rules["unpinned-uses"].config.policies["*"]
16
- policies := object.get(
17
- object.get(
18
- object.get(object.get(input, "rules", {}), "unpinned-uses", {}),
19
- "config",
20
- {},
21
- ),
22
- "policies",
23
- {},
24
- )
25
- object.get(policies, "*", null) != expected
26
- msg := sprintf(".github/zizmor.yml: rules.unpinned-uses.config.policies[%q] має бути %q (ga.mdc)", ["*", expected])
27
- }