@7n/rules 1.4.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-template/hooks/.gitignore.snippet +9 -0
- package/CHANGELOG.md +18 -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/sync-claude-config.md +1 -1
- 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/sync-claude-config.mjs +2 -2
- 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 +0,0 @@
|
|
|
1
|
-
{ "[github-actions-workflow]": { "editor.defaultFormatter": "oxc.oxc-vscode" } }
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
## `.vscode/settings.json` — налаштування форматування GitHub Actions workflow
|
|
2
|
-
|
|
3
|
-
Rego-пакет: `ga.vscode_settings`
|
|
4
|
-
|
|
5
|
-
**Цільовий файл:** `.vscode/settings.json`
|
|
6
|
-
|
|
7
|
-
Перевіряє 2-рівневу структуру: для кожного `<block-key>` з template кожен `<leaf-key>` у відповідному блоці input має точно відповідати очікуваному значенню. Ключі можуть містити дужки та крапки (VS Code-конвенція, наприклад `[github-actions-workflow]`).
|
|
8
|
-
|
|
9
|
-
Канон: [settings.json.snippet.json](./template/settings.json.snippet.json)
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# Перевірка `.vscode/settings.json` для GitHub Actions workflow (ga.mdc).
|
|
2
|
-
#
|
|
3
|
-
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
-
# Структура --data сформована з template/settings.json.snippet.json.
|
|
5
|
-
# Snippet — 2-рівнева мапа: <language-block-key>.<setting-key> = <expected>
|
|
6
|
-
# (VS Code-конвенція: ключі типу `[github-actions-workflow]` і `editor.defaultFormatter`
|
|
7
|
-
# — це літеральні string-keys із дужками/крапкою, не вкладені обʼєкти).
|
|
8
|
-
#
|
|
9
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
10
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
11
|
-
package ga.vscode_settings
|
|
12
|
-
|
|
13
|
-
import rego.v1
|
|
14
|
-
|
|
15
|
-
deny contains msg if {
|
|
16
|
-
some block_key, expected_inner in data.template.snippet
|
|
17
|
-
inner := object.get(input, block_key, {})
|
|
18
|
-
some leaf_key, expected_value in expected_inner
|
|
19
|
-
actual := object.get(inner, leaf_key, null)
|
|
20
|
-
actual != expected_value
|
|
21
|
-
msg := sprintf(".vscode/settings.json: %s.%s має бути %q (ga.mdc)", [block_key, leaf_key, expected_value])
|
|
22
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
## Універсальні Rego-перевірки для всіх workflow-файлів
|
|
2
|
-
|
|
3
|
-
Rego-пакет: `ga.workflow_common`
|
|
4
|
-
|
|
5
|
-
**Цільові файли:** `.github/workflows/*.yml` (кожен файл окремо)
|
|
6
|
-
|
|
7
|
-
### Що перевіряється
|
|
8
|
-
|
|
9
|
-
- **concurrency** — обов'язкова наявність блоку; `group` = `${{ github.ref }}-${{ github.workflow }}`; `cancel-in-progress: true`
|
|
10
|
-
- **Заборонені кроки** — `oven-sh/setup-bun`, `actions/cache`, `bun install` у будь-якому `uses:`/`run:` (мають бути інкапсульовані у composite `setup-bun-deps`)
|
|
11
|
-
- **Порядок кроків** — якщо є `setup-bun-deps`, перед ним обов'язковий `actions/checkout@`
|
|
12
|
-
- **Shell-продовження** — `\` перед переносом рядка у `run:` заборонено; треба `run: >-`
|
|
13
|
-
- **Заборонені команди** — `depcheck` у `run:` (мігровано на `knip`)
|
|
14
|
-
- **Мінімальні версії** — `uses:` з marketplace-actions перевіряються за канонічним JSON; SHA-пін (`owner/action@<40-hex SHA>`) задовольняє вимогу мінімальної версії — НЕ замінюй наявний SHA-пін тегом
|
|
15
|
-
|
|
16
|
-
Канон мінімальних версій: [uses-min-versions.snippet.json](./template/uses-min-versions.snippet.json)
|
|
17
|
-
|
|
18
|
-
Детальна документація поведінки — у [ga-workflow_common](../../js/workflow_common.mdc).
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
# Універсальні перевірки для будь-якого `.github/workflows/*.yml` (ga.mdc).
|
|
2
|
-
#
|
|
3
|
-
# Порт `verifyNoDirectBunOrCache`, `verifyNoRunShellLineContinuationBackslash`,
|
|
4
|
-
# `verifyCheckoutBeforeLocalSetupBunDeps` та `validateConcurrencyOnRoot` з
|
|
5
|
-
# `npm/scripts/rules/ga/fix.mjs`. На відміну від `lint_ga`/`clean_ga_workflows`/
|
|
6
|
-
# `clean_merged_branch`/`git_ai`, цей пакет не привʼязаний до конкретного
|
|
7
|
-
# workflow — `conftest test` запускається на кожному файлі окремо з
|
|
8
|
-
# `--namespace ga.workflow_common`, і `input` — це окремий розпарсений YAML.
|
|
9
|
-
#
|
|
10
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
11
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
12
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
13
|
-
package ga.workflow_common
|
|
14
|
-
|
|
15
|
-
import rego.v1
|
|
16
|
-
|
|
17
|
-
# ── Очікувані значення ─────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
|
|
20
|
-
|
|
21
|
-
# Локальні composite setup-bun-deps (ga.mdc) — два варіанти шляху:
|
|
22
|
-
# `.github/actions/...` (cursor-репо) і `npm/github-actions/...` (npm-пакет dev-локально).
|
|
23
|
-
local_setup_bun_markers := {
|
|
24
|
-
"./.github/actions/setup-bun-deps",
|
|
25
|
-
"./npm/github-actions/setup-bun-deps",
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
# Заборонені підрядки в кроках `uses` та `run` (ga.mdc): дублюючі setup/cache/install,
|
|
29
|
-
# які мають бути всередині composite-action `setup-bun-deps`.
|
|
30
|
-
forbidden_step_substrings := {
|
|
31
|
-
"oven-sh/setup-bun": "використовуй .github/actions/setup-bun-deps замість oven-sh/setup-bun",
|
|
32
|
-
"actions/cache": "використовуй .github/actions/setup-bun-deps замість actions/cache",
|
|
33
|
-
"bun install": "використовуй .github/actions/setup-bun-deps замість bun install",
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
# Заборонені бінарки у `run:` кроках (ga.mdc). `depcheck` мігровано на `knip`
|
|
37
|
-
# у `js.mdc` — окремий крок у workflow не потрібен. Регексп ловить виклики
|
|
38
|
-
# через `npx`, `bunx`, `npm exec`, або як standalone-команду на початку рядка.
|
|
39
|
-
forbidden_run_command_patterns := {"depcheck": `(?:^|[\s;&|])(?:npx|bunx|npm exec|pnpm exec)?[ \t]*depcheck\b`}
|
|
40
|
-
|
|
41
|
-
# Шаблони довгих повідомлень — через `concat`, щоб дотримуватися regal style/line-length.
|
|
42
|
-
|
|
43
|
-
concurrency_missing_template := concat(" ", [
|
|
44
|
-
"відсутня секція concurrency —",
|
|
45
|
-
"додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
|
|
46
|
-
])
|
|
47
|
-
|
|
48
|
-
shell_continuation_template := concat(" ", [
|
|
49
|
-
"jobs.%s.steps[%d]: у run заборонено продовження рядків через зворотний сліш;",
|
|
50
|
-
"оформи як folded block (run: >-) (ga.mdc)",
|
|
51
|
-
])
|
|
52
|
-
|
|
53
|
-
setup_bun_no_checkout_template := concat(" ", [
|
|
54
|
-
"jobs.%s: перед локальним setup-bun-deps потрібен крок actions/checkout@v6 —",
|
|
55
|
-
"інакше runner не знайде action.yml (ga.mdc)",
|
|
56
|
-
])
|
|
57
|
-
|
|
58
|
-
min_uses_version_template := concat(" ", [
|
|
59
|
-
"jobs.%s.steps[%d]: %s має бути >= v%s (зараз %q) —",
|
|
60
|
-
"онови ref у uses: (ga.mdc)",
|
|
61
|
-
])
|
|
62
|
-
|
|
63
|
-
forbidden_run_command_template := concat(" ", [
|
|
64
|
-
"jobs.%s.steps[%d]: `%s` заборонено у workflow —",
|
|
65
|
-
"мігровано на knip (js.mdc, ga.mdc)",
|
|
66
|
-
])
|
|
67
|
-
|
|
68
|
-
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
# Усі jobs (з гарантією, що це обʼєкт) — щоб не падати на нетипових YAML.
|
|
71
|
-
jobs := input.jobs
|
|
72
|
-
|
|
73
|
-
# Плоский список усіх кроків з усіх jobs з метаданими — для перевірок, де job-id
|
|
74
|
-
# і позиція кроку нам потрібні в повідомленні (shell line continuation).
|
|
75
|
-
all_flat_steps contains entry if {
|
|
76
|
-
some job_id, step_index
|
|
77
|
-
step := jobs[job_id].steps[step_index]
|
|
78
|
-
entry := {"job_id": job_id, "step_index": step_index, "step": step}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
# ── deny: заборонені setup-bun/cache/install у будь-якому кроці ────────────
|
|
82
|
-
|
|
83
|
-
deny contains msg if {
|
|
84
|
-
some entry in all_flat_steps
|
|
85
|
-
some pattern, hint in forbidden_step_substrings
|
|
86
|
-
step_uses_or_run_blob(entry.step) != ""
|
|
87
|
-
contains(step_uses_or_run_blob(entry.step), pattern)
|
|
88
|
-
msg := sprintf("jobs.%s.steps[%d]: %s (ga.mdc)", [entry.job_id, entry.step_index, hint])
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# ── deny: depcheck у будь-якому `run:` ────────────────────────────────────
|
|
92
|
-
#
|
|
93
|
-
# `depcheck` мігровано на `knip` (js.mdc); `knip` вже запускається у lint-js
|
|
94
|
-
# CI як частина `bunx knip` у скрипті, тож окремий depcheck-крок зайвий і має
|
|
95
|
-
# бути видалений з workflow-файлів.
|
|
96
|
-
|
|
97
|
-
deny contains msg if {
|
|
98
|
-
some entry in all_flat_steps
|
|
99
|
-
some name, pattern in forbidden_run_command_patterns
|
|
100
|
-
regex.match(pattern, step_run_text(entry.step))
|
|
101
|
-
msg := sprintf(forbidden_run_command_template, [entry.job_id, entry.step_index, name])
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
# ── deny: shell-продовження `\` перед переносом рядка у `run:` ─────────────
|
|
105
|
-
#
|
|
106
|
-
# `\` + `\n` — bash line-continuation; у workflow замінюй на folded block `>-`
|
|
107
|
-
# без зворотних слішів (ga.mdc).
|
|
108
|
-
|
|
109
|
-
deny contains msg if {
|
|
110
|
-
some entry in all_flat_steps
|
|
111
|
-
run_text := step_run_text(entry.step)
|
|
112
|
-
regex.match(`\\\r?\n`, run_text)
|
|
113
|
-
msg := sprintf(shell_continuation_template, [entry.job_id, entry.step_index])
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
# ── deny: setup-bun-deps без попереднього checkout у тому ж job ────────────
|
|
117
|
-
#
|
|
118
|
-
# Без `actions/checkout` локальний composite-action недоступний — runner не
|
|
119
|
-
# знайде `action.yml` у дереві. Перевіряємо порядок індексів кроків у кожному
|
|
120
|
-
# job: setup-bun-deps має бути після принаймні одного `actions/checkout@`.
|
|
121
|
-
|
|
122
|
-
deny contains msg if {
|
|
123
|
-
some job_id, job in jobs
|
|
124
|
-
first_setup := first_local_setup_bun_index(job)
|
|
125
|
-
first_setup >= 0
|
|
126
|
-
not has_checkout_before(job, first_setup)
|
|
127
|
-
msg := sprintf(setup_bun_no_checkout_template, [job_id])
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
# ── deny: actions/checkout без persist-credentials: false ─────────────────
|
|
131
|
-
#
|
|
132
|
-
# `persist-credentials: false` запобігає кешуванню git-токена після checkout,
|
|
133
|
-
# що є обов'язковою вимогою безпеки (ga.mdc).
|
|
134
|
-
|
|
135
|
-
deny contains msg if {
|
|
136
|
-
some _job_id, job in jobs
|
|
137
|
-
some step in object.get(job, "steps", [])
|
|
138
|
-
uses := object.get(step, "uses", "")
|
|
139
|
-
startswith(uses, "actions/checkout@")
|
|
140
|
-
creds := object.get(object.get(step, "with", {}), "persist-credentials", true)
|
|
141
|
-
creds != false
|
|
142
|
-
msg := sprintf("jobs.%s: actions/checkout@v6 потребує `with: persist-credentials: false` (ga.mdc)", [_job_id])
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
# ── deny: concurrency блок ─────────────────────────────────────────────────
|
|
146
|
-
#
|
|
147
|
-
# Дублює окремі per-workflow перевірки для clean-ga-workflows / clean-merged-branch /
|
|
148
|
-
# lint-ga / git-ai, але вкриває й решту workflow-файлів (apply-k8s, lint-js, …),
|
|
149
|
-
# для яких поки немає виділеної polysi.
|
|
150
|
-
|
|
151
|
-
deny contains msg if {
|
|
152
|
-
# `object.get(…, default)` повертає `false` коли ключа немає — інакше `not is_object(…)`
|
|
153
|
-
# над відсутнім полем дає `undefined`, не `true`, і правило мовчки не спрацьовує.
|
|
154
|
-
not is_object(object.get(input, "concurrency", false))
|
|
155
|
-
msg := sprintf(concurrency_missing_template, [expected_concurrency_group])
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
deny contains msg if {
|
|
159
|
-
is_object(object.get(input, "concurrency", false))
|
|
160
|
-
input.concurrency.group != expected_concurrency_group
|
|
161
|
-
msg := sprintf("concurrency.group має бути %s (ga.mdc)", [expected_concurrency_group])
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
deny contains msg if {
|
|
165
|
-
is_object(object.get(input, "concurrency", false))
|
|
166
|
-
input.concurrency["cancel-in-progress"] != true
|
|
167
|
-
msg := "concurrency.cancel-in-progress має бути true (ga.mdc)"
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
# ── deny: мінімальні версії marketplace actions у `uses:` ─────────────────
|
|
171
|
-
#
|
|
172
|
-
# Канон — `template/uses-min-versions.snippet.json` (через --data).
|
|
173
|
-
# Перевіряє semver-подібні теги `vX.Y.Z` / `vN`; SHA-pin (40 hex) пропускаємо.
|
|
174
|
-
|
|
175
|
-
deny contains msg if {
|
|
176
|
-
some entry in all_flat_steps
|
|
177
|
-
uses := object.get(entry.step, "uses", "")
|
|
178
|
-
uses != ""
|
|
179
|
-
some action_slug, min_ver in data.template.snippet
|
|
180
|
-
action_uses_matches(uses, action_slug)
|
|
181
|
-
ref := action_uses_ref(uses)
|
|
182
|
-
not action_ref_meets_min(ref, min_ver)
|
|
183
|
-
msg := sprintf(min_uses_version_template, [
|
|
184
|
-
entry.job_id,
|
|
185
|
-
entry.step_index,
|
|
186
|
-
action_slug,
|
|
187
|
-
min_ver,
|
|
188
|
-
ref,
|
|
189
|
-
])
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
# ── helpers ────────────────────────────────────────────────────────────────
|
|
193
|
-
|
|
194
|
-
# Об'єднаний рядок `uses` + `run` для одного кроку — для substring-пошуку
|
|
195
|
-
# заборонених патернів. `run` може бути рядком або масивом рядків (YAML).
|
|
196
|
-
step_uses_or_run_blob(step) := blob if {
|
|
197
|
-
uses := object.get(step, "uses", "")
|
|
198
|
-
run_text := step_run_text(step)
|
|
199
|
-
blob := concat("\n", [uses, run_text])
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
# Текст `run:` як один рядок: підтримує рядкові та масивові форми (YAML).
|
|
203
|
-
step_run_text(step) := step.run if is_string(step.run)
|
|
204
|
-
|
|
205
|
-
else := concat("\n", [s | some s in step.run]) if is_array(step.run)
|
|
206
|
-
|
|
207
|
-
else := ""
|
|
208
|
-
|
|
209
|
-
# Індекс першого кроку з локальним setup-bun-deps; -1 якщо такого немає.
|
|
210
|
-
first_local_setup_bun_index(job) := min(indices) if {
|
|
211
|
-
indices := [i |
|
|
212
|
-
some i, step in job.steps
|
|
213
|
-
uses := object.get(step, "uses", "")
|
|
214
|
-
some marker in local_setup_bun_markers
|
|
215
|
-
contains(uses, marker)
|
|
216
|
-
]
|
|
217
|
-
count(indices) > 0
|
|
218
|
-
} else := -1
|
|
219
|
-
|
|
220
|
-
# Чи є в `job.steps[0..before]` крок `actions/checkout@…`.
|
|
221
|
-
has_checkout_before(job, before) if {
|
|
222
|
-
some i, step in job.steps
|
|
223
|
-
i < before
|
|
224
|
-
uses := object.get(step, "uses", "")
|
|
225
|
-
contains(uses, "actions/checkout@")
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
# `uses:` починається з `owner/repo@` для заданого slug.
|
|
229
|
-
action_uses_matches(uses, slug) if {
|
|
230
|
-
startswith(uses, concat("", [slug, "@"]))
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
# Ref після останнього `@` у `uses:` (owner/repo@ref).
|
|
234
|
-
action_uses_ref(uses) := ref if {
|
|
235
|
-
parts := split(uses, "@")
|
|
236
|
-
count(parts) >= 2
|
|
237
|
-
ref := parts[count(parts) - 1]
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
# SHA-pin — semver-політика не застосовується.
|
|
241
|
-
action_ref_is_sha_pin(ref) if {
|
|
242
|
-
regex.match(`^[0-9a-fA-F]{40}$`, ref)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
# Semver ref >= min (обидва як X.Y.Z після optional `v`).
|
|
246
|
-
action_ref_meets_min(ref, _) if {
|
|
247
|
-
action_ref_is_sha_pin(ref)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
action_ref_meets_min(ref, min_ver) if {
|
|
251
|
-
not action_ref_is_sha_pin(ref)
|
|
252
|
-
version_triple_gte(version_triple(ref), version_triple(min_ver))
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
version_triple(raw) := [major, minor, patch] if {
|
|
256
|
-
stripped := trim_prefix(trim_prefix(raw, "v"), "V")
|
|
257
|
-
parts := split_to_numbers(stripped)
|
|
258
|
-
major := version_part(parts, 0)
|
|
259
|
-
minor := version_part(parts, 1)
|
|
260
|
-
patch := version_part(parts, 2)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
version_part(parts, idx) := parts[idx] if {
|
|
264
|
-
count(parts) > idx
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
else := 0
|
|
268
|
-
|
|
269
|
-
version_triple_gte(a, b) if {
|
|
270
|
-
a[0] > b[0]
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
version_triple_gte(a, b) if {
|
|
274
|
-
a[0] == b[0]
|
|
275
|
-
a[1] > b[1]
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
version_triple_gte(a, b) if {
|
|
279
|
-
a[0] == b[0]
|
|
280
|
-
a[1] == b[1]
|
|
281
|
-
a[2] >= b[2]
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
split_to_numbers(spec) := nums if {
|
|
285
|
-
tokens := regex.split(`\D+`, spec)
|
|
286
|
-
non_empty := [t | some t in tokens; t != ""]
|
|
287
|
-
nums := [n | some t in non_empty; n := to_number(t)]
|
|
288
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: fix-workflows.mjs
|
|
4
|
-
resource: npm/rules/ga/workflows/fix-workflows.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 5f7fc736
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Огляд
|
|
10
|
-
|
|
11
|
-
T0-autofix для `ga/workflows`: детерміновані текстові правки GitHub Actions workflow-файлів без
|
|
12
|
-
участі LLM, керовані structured fix-hint детектора (поле `data.kind` у violation). Виконується у
|
|
13
|
-
fix-фазі перед LLM-ладдером; правки незворотні (поза rollback), а коректність підтверджує
|
|
14
|
-
повторний прогін детектора.
|
|
15
|
-
|
|
16
|
-
## Поведінка
|
|
17
|
-
|
|
18
|
-
- **checkout-persist-credentials** — у кожен крок `actions/checkout`, де бракує
|
|
19
|
-
`persist-credentials: false`, дописує цей ключ: створює блок `with:` (відступ за колонкою
|
|
20
|
-
`uses:`) або додає ключ у наявний `with:`. Кілька checkout-кроків у файлі обробляються всі.
|
|
21
|
-
- **unmatched-paths-glob** — прибирає з `on.<event>.paths` (push / pull_request) list-елементи,
|
|
22
|
-
що відповідають застарілим glob-ам із fix-hint; видалення обмежене блоком `paths:`, тож
|
|
23
|
-
однойменні значення деінде не зачіпаються.
|
|
24
|
-
|
|
25
|
-
Правки текстові — коментарі, форматування й порожні рядки зберігаються, diff мінімальний.
|
|
26
|
-
|
|
27
|
-
## Публічний API
|
|
28
|
-
|
|
29
|
-
- `patterns` — масив T0-патернів (`id` / `test` / `apply`), що його споживає центральний fix-pipeline.
|
|
30
|
-
- `addPersistCredentials`, `removePathsGlobs` — чисті трансформери вмісту файлу: повертають новий
|
|
31
|
-
текст або `null`, якщо змін немає.
|
|
32
|
-
|
|
33
|
-
## Гарантії поведінки
|
|
34
|
-
|
|
35
|
-
- Жодного запису без фактичної зміни вмісту; перед записом реєструється pre-image через `recordWrite`.
|
|
36
|
-
- Регулярні вирази лінійні (без ReDoS); парсинг list-елементів — рядковими операціями.
|
|
37
|
-
- Не-checkout кроки та значення поза блоком `paths:` лишаються недоторканими.
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: main.mjs
|
|
4
|
-
resource: npm/rules/ga/workflows/main.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: ac064a5f
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Огляд
|
|
10
|
-
|
|
11
|
-
Please provide the code you want me to document.
|
|
12
|
-
|
|
13
|
-
## Поведінка
|
|
14
|
-
|
|
15
|
-
Поведінка:
|
|
16
|
-
checkShellcheckInstalled перевіряє локальну наявність інструменту `shellcheck` у системному PATH, щоб забезпечити узгодженість логіки CI та локального запуску.
|
|
17
|
-
main виконує комплексну перевірку відповідності репозиторію правилам `ga.mdc`, включаючи валідацію структури GitHub Actions workflow, аналіз посилань на файли за допомогою `git ls-files` та пошук залишків MegaLinter. При цьому він ігнорує шляхи, що стосуються `.github` та `.git`.
|
|
18
|
-
lint запускає стандартний лінтинг, який включає перевірку `actionlint` та `zizmor` для забезпечення якості YAML-файлів та робочого середовища, а також ініціює повну перевірку `main`.
|
|
19
|
-
runLintGaCli запускає процес лінтингу `ga` через командний інтерфейс.
|
|
20
|
-
|
|
21
|
-
## Публічний API
|
|
22
|
-
|
|
23
|
-
Підтверджую. Я дію як технічний письменник.
|
|
24
|
-
|
|
25
|
-
Ось переписаний список у вигляді стислих маркерів відповідно до ваших вимог:
|
|
26
|
-
|
|
27
|
-
- checkShellcheckInstalled — Визначає, чи є інструмент `shellcheck` у системному шляху, щоб уникнути ложного позитивного результату локального линтування, коли CI-оркестр вимагає його присутності.
|
|
28
|
-
- main — Валідує відповідність усієї проєктної конфігурації вимогам, визначеним у `ga.mdc`.
|
|
29
|
-
- Plan B-патерн — Виконує первинну валідацію структури CI-workflow за допомогою Rego-полісі у `npm/policy/ga/`; додатково проводить крос-файлові перевірки та аналіз конфігурацій (наприклад, для VS Code), ініційовавши цей процес за допомогою `bun run lint-ga`.
|
|
30
|
-
- lint — Виконує повноцінний аналіз конфігурацій CI/CD з використанням `actionlint` та `zizmor`.
|
|
31
|
-
- runLintGaCli — Запускає повний набір перевірок, що включають Rego-валідацію, аналіз `actionlint` та зовнішні інструменти для забезпечення консистентності конфігурацій проєкту.
|
|
32
|
-
|
|
33
|
-
## Гарантії поведінки
|
|
34
|
-
|
|
35
|
-
- Read-only: не виконує операцій запису (ФС/БД).
|
|
36
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
37
|
-
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
38
|
-
- Свідомо пропускає шляхи: `.github`, `.git`.
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/** @see ./docs/fix-workflows.md */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* T0-autofix для `ga/workflows` — детерміновані правки, керовані structured `data`
|
|
5
|
-
* детектора (#3 fix-hints), БЕЗ LLM. Покриває дві механічні родини порушень:
|
|
6
|
-
* - `checkout-persist-credentials` — дописати `with: persist-credentials: false`
|
|
7
|
-
* у кожен `actions/checkout` крок, де його бракує;
|
|
8
|
-
* - `unmatched-paths-glob` — прибрати застарілий glob із `on.<event>.paths`.
|
|
9
|
-
*
|
|
10
|
-
* Трансформації **текстові** (зберігають коментарі/формат/blank-lines, мінімальний diff);
|
|
11
|
-
* семантичну коректність гарантує canonical re-detect runner-а (T0 permanent, поза rollback).
|
|
12
|
-
* Чисті трансформери експортуються для unit-тестів.
|
|
13
|
-
*/
|
|
14
|
-
import { applyToFiles } from '../../../scripts/utils/apply-to-files.mjs'
|
|
15
|
-
|
|
16
|
-
const CHECKOUT_USES_RE = /uses:\s*actions\/checkout@/u
|
|
17
|
-
const WITH_LINE_RE = /^\s*with:\s*$/u
|
|
18
|
-
const PERSIST_KEY_RE = /^\s*persist-credentials\s*:/u
|
|
19
|
-
const QUOTE_EDGE_RE = /^['"]|['"]$/gu
|
|
20
|
-
const PATHS_KEY_RE = /^\s*paths:\s*$/u
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Індекс першого непорожнього рядка з `from` (включно).
|
|
24
|
-
* @param {string[]} lines усі рядки
|
|
25
|
-
* @param {number} from стартовий індекс
|
|
26
|
-
* @returns {number} індекс непорожнього рядка (або `lines.length`)
|
|
27
|
-
*/
|
|
28
|
-
function nextNonEmpty(lines, from) {
|
|
29
|
-
let j = from
|
|
30
|
-
while (j < lines.length && lines[j].trim() === '') j++
|
|
31
|
-
return j
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Чи блок `with:` (на рядку `withLine`, колонка ключа `col`) уже містить
|
|
36
|
-
* `persist-credentials`. Скан до dedent.
|
|
37
|
-
* @param {string[]} lines усі рядки
|
|
38
|
-
* @param {number} withLine індекс рядка `with:`
|
|
39
|
-
* @param {number} col колонка ключа `with:`
|
|
40
|
-
* @returns {boolean} true, якщо ключ уже присутній
|
|
41
|
-
*/
|
|
42
|
-
function withBlockHasPersist(lines, withLine, col) {
|
|
43
|
-
for (let k = withLine + 1; k < lines.length; k++) {
|
|
44
|
-
if (lines[k].trim() === '') continue
|
|
45
|
-
const c = lines[k].length - lines[k].trimStart().length
|
|
46
|
-
if (c <= col) return false // dedent → блок завершився
|
|
47
|
-
if (PERSIST_KEY_RE.test(lines[k])) return true
|
|
48
|
-
}
|
|
49
|
-
return false
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Insert-план для одного `actions/checkout` кроку на рядку `i` (колонка `uses:` = `col`).
|
|
54
|
-
* @param {string[]} lines усі рядки
|
|
55
|
-
* @param {number} i індекс рядка `uses:`
|
|
56
|
-
* @param {number} col колонка `uses:`
|
|
57
|
-
* @returns {{ at: number, text: string[] } | null} вставка або null, якщо ключ уже є
|
|
58
|
-
*/
|
|
59
|
-
function persistInsertFor(lines, i, col) {
|
|
60
|
-
const ind = ' '.repeat(col)
|
|
61
|
-
const j = nextNonEmpty(lines, i + 1)
|
|
62
|
-
const hasWithBlock = j < lines.length && WITH_LINE_RE.test(lines[j]) && lines[j].indexOf('with:') === col
|
|
63
|
-
if (hasWithBlock) {
|
|
64
|
-
return withBlockHasPersist(lines, j, col) ? null : { at: j + 1, text: [`${ind} persist-credentials: false`] }
|
|
65
|
-
}
|
|
66
|
-
return { at: i + 1, text: [`${ind}with:`, `${ind} persist-credentials: false`] }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Дописує `with: persist-credentials: false` у кожен `actions/checkout` крок,
|
|
71
|
-
* де його бракує. Дві форми: крок без `with:` (створити блок) і з наявним `with:`
|
|
72
|
-
* (вставити ключ). Відступ `with:` = колонка `uses:`; ключа = +2.
|
|
73
|
-
* @param {string} content вміст workflow-файла
|
|
74
|
-
* @returns {string|null} новий вміст або null, якщо нічого не змінилось
|
|
75
|
-
*/
|
|
76
|
-
export function addPersistCredentials(content) {
|
|
77
|
-
const lines = content.split('\n')
|
|
78
|
-
/** @type {Array<{ at: number, text: string[] }>} */
|
|
79
|
-
const inserts = []
|
|
80
|
-
for (let i = 0; i < lines.length; i++) {
|
|
81
|
-
const col = lines[i].indexOf('uses:')
|
|
82
|
-
if (col === -1 || !CHECKOUT_USES_RE.test(lines[i])) continue
|
|
83
|
-
const ins = persistInsertFor(lines, i, col)
|
|
84
|
-
if (ins) inserts.push(ins)
|
|
85
|
-
}
|
|
86
|
-
if (inserts.length === 0) return null
|
|
87
|
-
// Згори вниз — індекси не зсуваються під час splice.
|
|
88
|
-
inserts.sort((a, b) => b.at - a.at)
|
|
89
|
-
for (const ins of inserts) lines.splice(ins.at, 0, ...ins.text)
|
|
90
|
-
return lines.join('\n')
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Прибирає list-елементи із заданими значеннями всередині блоків `paths:`
|
|
95
|
-
* (`on.push.paths` / `on.pull_request.paths`). Scoped до paths-блоку, щоб не
|
|
96
|
-
* зачепити однойменні значення деінде.
|
|
97
|
-
* @param {string} content вміст workflow-файла
|
|
98
|
-
* @param {Set<string>} globs значення glob-ів на видалення (без лапок)
|
|
99
|
-
* @returns {string|null} новий вміст або null, якщо нічого не змінилось
|
|
100
|
-
*/
|
|
101
|
-
export function removePathsGlobs(content, globs) {
|
|
102
|
-
const lines = content.split('\n')
|
|
103
|
-
/** @type {string[]} */
|
|
104
|
-
const out = []
|
|
105
|
-
let pathsCol = -1 // колонка ключа `paths:` поточного блоку; -1 = поза блоком
|
|
106
|
-
let changed = false
|
|
107
|
-
for (const line of lines) {
|
|
108
|
-
if (PATHS_KEY_RE.test(line)) {
|
|
109
|
-
pathsCol = line.indexOf('paths:')
|
|
110
|
-
out.push(line)
|
|
111
|
-
continue
|
|
112
|
-
}
|
|
113
|
-
if (pathsCol >= 0 && line.trim() !== '') {
|
|
114
|
-
const col = line.length - line.trimStart().length
|
|
115
|
-
if (col > pathsCol) {
|
|
116
|
-
// Лінійний парс list-елемента `- <value>` (без regex → без ReDoS).
|
|
117
|
-
const trimmed = line.trimStart()
|
|
118
|
-
if (trimmed.startsWith('- ')) {
|
|
119
|
-
const val = trimmed.slice(2).trim().replace(QUOTE_EDGE_RE, '')
|
|
120
|
-
if (globs.has(val)) {
|
|
121
|
-
changed = true
|
|
122
|
-
continue // викидаємо рядок
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
out.push(line)
|
|
126
|
-
continue
|
|
127
|
-
}
|
|
128
|
-
pathsCol = -1 // dedent → блок `paths:` завершився
|
|
129
|
-
}
|
|
130
|
-
out.push(line)
|
|
131
|
-
}
|
|
132
|
-
return changed ? out.join('\n') : null
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const WRAPPED_NCURSOR_RE = /\b(?:bunx|npx)\s+n-(?:cursor|rules)/u
|
|
136
|
-
const RUN_INLINE_NCURSOR_MATCH = /^(\s*(?:-\s*)?run:\s*)n-(?:cursor|rules)(\s.*)$/u
|
|
137
|
-
const BARE_LINE_NCURSOR_MATCH = /^(\s+)n-(?:cursor|rules)(\s.*)$/u
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Префіксує bare `n-rules …` (чи legacy `n-rules …`) у `run`-кроках через `bunx n-rules` (у CI
|
|
141
|
-
* бінарник не на PATH). Пропускає рядки, де вже є `bunx`/`npx`. Покриває inline `run:` і рядок у run-блоці.
|
|
142
|
-
* @param {string} content вміст workflow-файла
|
|
143
|
-
* @returns {string|null} новий вміст або null, якщо нічого не змінилось
|
|
144
|
-
*/
|
|
145
|
-
export function prefixBunxNCursor(content) {
|
|
146
|
-
let changed = false
|
|
147
|
-
const out = content.split('\n').map(line => {
|
|
148
|
-
if (WRAPPED_NCURSOR_RE.test(line)) return line
|
|
149
|
-
const inline = RUN_INLINE_NCURSOR_MATCH.exec(line)
|
|
150
|
-
if (inline) {
|
|
151
|
-
changed = true
|
|
152
|
-
return `${inline[1]}bunx n-rules${inline[2]}`
|
|
153
|
-
}
|
|
154
|
-
const bare = BARE_LINE_NCURSOR_MATCH.exec(line)
|
|
155
|
-
if (bare) {
|
|
156
|
-
changed = true
|
|
157
|
-
return `${bare[1]}bunx n-rules${bare[2]}`
|
|
158
|
-
}
|
|
159
|
-
return line
|
|
160
|
-
})
|
|
161
|
-
return changed ? out.join('\n') : null
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** @type {import('../../../scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
165
|
-
export const patterns = [
|
|
166
|
-
{
|
|
167
|
-
id: 'ga-workflows-checkout-persist-credentials',
|
|
168
|
-
test: violations => violations.some(v => v.data?.kind === 'checkout-persist-credentials' && v.file),
|
|
169
|
-
apply: (violations, ctx) => {
|
|
170
|
-
const targets = violations.filter(v => v.data?.kind === 'checkout-persist-credentials' && v.file)
|
|
171
|
-
const touchedFiles = applyToFiles(targets, ctx, () => addPersistCredentials)
|
|
172
|
-
return touchedFiles.length > 0
|
|
173
|
-
? { touchedFiles, message: `persist-credentials: false → ${touchedFiles.length} workflow(s)` }
|
|
174
|
-
: { touchedFiles: [] }
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
id: 'ga-workflows-unmatched-paths-glob',
|
|
179
|
-
test: violations => violations.some(v => v.data?.kind === 'unmatched-paths-glob' && v.file),
|
|
180
|
-
apply: (violations, ctx) => {
|
|
181
|
-
const targets = violations.filter(v => v.data?.kind === 'unmatched-paths-glob' && v.file)
|
|
182
|
-
// Глоби на видалення згруповані per-file.
|
|
183
|
-
/** @type {Map<string, Set<string>>} */
|
|
184
|
-
const byFile = new Map()
|
|
185
|
-
for (const v of targets) {
|
|
186
|
-
const glob = typeof v.data?.glob === 'string' ? v.data.glob : null
|
|
187
|
-
if (!glob) continue
|
|
188
|
-
if (!byFile.has(v.file)) byFile.set(v.file, new Set())
|
|
189
|
-
byFile.get(v.file).add(glob)
|
|
190
|
-
}
|
|
191
|
-
const touchedFiles = applyToFiles(
|
|
192
|
-
targets,
|
|
193
|
-
ctx,
|
|
194
|
-
rel => content => removePathsGlobs(content, byFile.get(rel) ?? new Set())
|
|
195
|
-
)
|
|
196
|
-
return touchedFiles.length > 0
|
|
197
|
-
? { touchedFiles, message: `прибрано не-матчені paths-glob у ${touchedFiles.length} файл(ах)` }
|
|
198
|
-
: { touchedFiles: [] }
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
id: 'ga-workflows-bare-n-rules',
|
|
203
|
-
test: violations => violations.some(v => v.data?.kind === 'bare-n-rules' && v.file),
|
|
204
|
-
apply: (violations, ctx) => {
|
|
205
|
-
const targets = violations.filter(v => v.data?.kind === 'bare-n-rules' && v.file)
|
|
206
|
-
const touchedFiles = applyToFiles(targets, ctx, () => prefixBunxNCursor)
|
|
207
|
-
return touchedFiles.length > 0
|
|
208
|
-
? { touchedFiles, message: `bunx-префікс n-rules → ${touchedFiles.length} workflow(s)` }
|
|
209
|
-
: { touchedFiles: [] }
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
]
|