@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.
- package/CHANGELOG.md +12 -0
- package/bin/n-rules.js +65 -17
- package/package.json +11 -1
- package/rules/abie/clean_merged_ignore_branches/concern.json +3 -0
- package/rules/docker/lint_docker_yml/concern.json +3 -0
- package/rules/js/lint_js_yml/concern.json +3 -0
- package/rules/k8s/lint_k8s_yml/concern.json +3 -0
- package/rules/npm-module/npm_publish_yml/concern.json +3 -0
- package/rules/php/lint_php_yml/concern.json +3 -0
- package/rules/python/lint_python_yml/concern.json +3 -0
- package/rules/rust/lint_rust_yml/concern.json +3 -0
- package/rules/rust/toolchain_cache/concern.json +10 -1
- package/rules/security/lint_security_yml/concern.json +3 -0
- package/rules/style/lint_style_yml/concern.json +3 -0
- package/rules/tauri/main.mdc +1 -1
- package/rules/tauri/release/release.mdc +15 -0
- package/rules/text/lint_text/concern.json +3 -0
- package/schemas/concern.json +86 -16
- package/schemas/n-rules.json +11 -1
- package/schemas/v8r-catalog.json +123 -27
- package/scripts/auto-rules.mjs +47 -6
- package/scripts/docs/auto-rules.md +3 -1
- package/scripts/docs/skills-cli.md +3 -2
- package/scripts/docs/upgrade-n-rules-and-install.md +3 -1
- package/scripts/lib/acp-runner.mjs +174 -0
- package/scripts/lib/concern-meta.mjs +8 -1
- package/scripts/lib/docs/acp-runner.md +58 -0
- package/scripts/lib/docs/concern-meta.md +1 -1
- package/scripts/lib/docs/read-n-rules-config-lite.md +3 -1
- package/scripts/lib/docs/resolve-plugins.md +17 -0
- package/scripts/lib/docs/run-conftest-batch.md +3 -1
- package/scripts/lib/lint-surface/docs/policy-lint-adapter.md +3 -1
- package/scripts/lib/lint-surface/docs/run-detectors.md +5 -1
- package/scripts/lib/lint-surface/policy-lint-adapter.mjs +2 -0
- package/scripts/lib/lint-surface/run-detectors.mjs +72 -5
- package/scripts/lib/read-n-rules-config-lite.mjs +5 -3
- package/scripts/lib/resolve-plugins.mjs +248 -0
- package/scripts/lib/run-conftest-batch.mjs +3 -1
- package/scripts/skills-cli.mjs +30 -52
- package/scripts/upgrade-n-rules-and-install.mjs +5 -4
- package/rules/ga/clean_ga_workflows/clean_ga_workflows.mdc +0 -20
- package/rules/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -112
- package/rules/ga/clean_ga_workflows/concern.json +0 -10
- package/rules/ga/clean_ga_workflows/docs/fix-clean_ga_workflows.md +0 -33
- package/rules/ga/clean_ga_workflows/docs/index.md +0 -9
- package/rules/ga/clean_ga_workflows/fix-clean_ga_workflows.mjs +0 -8
- package/rules/ga/clean_ga_workflows/template/clean-ga-workflows.yml.snippet.yml +0 -26
- package/rules/ga/clean_merged_branch/clean_merged_branch.mdc +0 -24
- package/rules/ga/clean_merged_branch/clean_merged_branch.rego +0 -163
- package/rules/ga/clean_merged_branch/concern.json +0 -10
- package/rules/ga/clean_merged_branch/docs/fix-clean_merged_branch.md +0 -27
- package/rules/ga/clean_merged_branch/docs/index.md +0 -9
- package/rules/ga/clean_merged_branch/fix-clean_merged_branch.mjs +0 -8
- package/rules/ga/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +0 -37
- package/rules/ga/docs/index.md +0 -11
- package/rules/ga/git_ai/concern.json +0 -11
- package/rules/ga/git_ai/docs/fix-git_ai.md +0 -30
- package/rules/ga/git_ai/docs/index.md +0 -9
- package/rules/ga/git_ai/fix-git_ai.mjs +0 -5
- package/rules/ga/git_ai/git_ai.mdc +0 -19
- package/rules/ga/git_ai/git_ai.rego +0 -76
- package/rules/ga/git_ai/template/git-ai.yml.snippet.yml +0 -30
- package/rules/ga/lint_ga/concern.json +0 -10
- package/rules/ga/lint_ga/docs/fix-lint_ga.md +0 -32
- package/rules/ga/lint_ga/docs/index.md +0 -9
- package/rules/ga/lint_ga/fix-lint_ga.mjs +0 -5
- package/rules/ga/lint_ga/lint_ga.mdc +0 -21
- package/rules/ga/lint_ga/lint_ga.rego +0 -132
- package/rules/ga/lint_ga/template/lint-ga.yml.snippet.yml +0 -41
- package/rules/ga/main.json +0 -1
- package/rules/ga/main.mdc +0 -17
- package/rules/ga/vscode_extensions/concern.json +0 -11
- package/rules/ga/vscode_extensions/docs/fix-vscode_extensions.md +0 -25
- package/rules/ga/vscode_extensions/docs/index.md +0 -11
- package/rules/ga/vscode_extensions/fix-vscode_extensions.mjs +0 -1
- package/rules/ga/vscode_extensions/template/extensions.json.snippet.json +0 -1
- package/rules/ga/vscode_extensions/vscode_extensions.mdc +0 -9
- package/rules/ga/vscode_extensions/vscode_extensions.rego +0 -18
- package/rules/ga/vscode_settings/concern.json +0 -11
- package/rules/ga/vscode_settings/docs/fix-vscode_settings.md +0 -26
- package/rules/ga/vscode_settings/docs/index.md +0 -9
- package/rules/ga/vscode_settings/fix-vscode_settings.mjs +0 -5
- package/rules/ga/vscode_settings/template/settings.json.snippet.json +0 -1
- package/rules/ga/vscode_settings/vscode_settings.mdc +0 -9
- package/rules/ga/vscode_settings/vscode_settings.rego +0 -22
- package/rules/ga/workflow_common/concern.json +0 -9
- package/rules/ga/workflow_common/template/uses-min-versions.snippet.json +0 -4
- package/rules/ga/workflow_common/workflow_common.mdc +0 -18
- package/rules/ga/workflow_common/workflow_common.rego +0 -288
- package/rules/ga/workflows/concern.json +0 -8
- package/rules/ga/workflows/docs/fix-workflows.md +0 -37
- package/rules/ga/workflows/docs/main.md +0 -38
- package/rules/ga/workflows/fix-workflows.mjs +0 -212
- package/rules/ga/workflows/main.mjs +0 -433
- package/rules/ga/workflows/workflows.mdc +0 -33
- package/rules/ga/zizmor_yml/concern.json +0 -11
- package/rules/ga/zizmor_yml/docs/fix-zizmor_yml.md +0 -28
- package/rules/ga/zizmor_yml/docs/index.md +0 -9
- package/rules/ga/zizmor_yml/fix-zizmor_yml.mjs +0 -3
- package/rules/ga/zizmor_yml/template/zizmor.yml.snippet.yml +0 -5
- package/rules/ga/zizmor_yml/zizmor_yml.mdc +0 -9
- 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
|
-
## `.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
|
-
}
|