@nitra/cursor 1.13.11 → 1.13.12
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 +17 -0
- package/package.json +1 -1
- package/rules/ga/fix/workflows/check.mjs +12 -3
- package/rules/ga/ga.mdc +8 -137
- package/rules/ga/policy/clean_ga_workflows/clean_ga_workflows.rego +38 -49
- package/rules/ga/policy/clean_ga_workflows/template/clean-ga-workflows.yml.snippet.yml +26 -0
- package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +55 -57
- package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +37 -0
- package/rules/ga/policy/git_ai/git_ai.rego +28 -35
- package/rules/ga/policy/git_ai/template/git-ai.yml.snippet.yml +30 -0
- package/rules/ga/policy/lint_ga/lint_ga.rego +46 -55
- package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +35 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.13.12] - 2026-05-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `ga` rule template/ міграція доповнена (Phase 3.5 — 4 full-canon workflow концерни, пропущені в 1.13.9): `clean_ga_workflows`, `clean_merged_branch`, `lint_ga`, `git_ai`. Кожен має повний YAML канон у `template/<workflow>.yml.snippet.yml`, rego читає expected-значення з `data.template.snippet.<path>` (path лишається у rego, literals — у template). Для кожного — `*_test.rego` із canonical/wrong/drift тестами.
|
|
12
|
+
- Wiring у `ga/fix/workflows/check.mjs`: `runAllGaRego` тепер `await loadTemplate(concernDir)` і передає `templateData` у `runConftestBatch` для кожного workflow концерну.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `ga.mdc` — 4 inline YAML-блоки повних workflow канонів замінено на markdown-посилання до `template/<workflow>.yml.snippet.yml`. Файл скоротився суттєво — канон тепер живе як data, а не як прозовий приклад.
|
|
17
|
+
- `template/clean-merged-branch.yml.snippet.yml`: `dry_run: false` (явний bool) замість `dry_run: no` — `yaml` npm (YAML 1.2) лишає `no` рядком, а Go-yaml у conftest нормалізує до `false`; пишемо канонізовану форму під runtime conftest-парсингу.
|
|
18
|
+
- `docs/adr/template-dir-concern-inventory.md` — додано 4 нові full-canon ga.* концерни з ✓; оновлено summary (89 концернів, 43 з template — мігровано 13/43 = 30%).
|
|
19
|
+
|
|
20
|
+
### TODO
|
|
21
|
+
|
|
22
|
+
- `ga.workflow_common` — cross-workflow forbidden-patterns (concurrency, depcheck deny, shell line-continuation). Не fully full-canon — окремий випадок, мігрується пізніше.
|
|
23
|
+
|
|
7
24
|
## [1.13.11] - 2026-05-17
|
|
8
25
|
|
|
9
26
|
### Added
|
package/package.json
CHANGED
|
@@ -17,12 +17,17 @@
|
|
|
17
17
|
import { existsSync } from 'node:fs'
|
|
18
18
|
import { readdir, readFile } from 'node:fs/promises'
|
|
19
19
|
import { execFileSync } from 'node:child_process'
|
|
20
|
-
import { join } from 'node:path'
|
|
20
|
+
import { basename, dirname, join } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
21
22
|
|
|
22
23
|
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
23
24
|
import { eventPathsIncludeExact, parseWorkflowYaml } from '../../../../scripts/utils/gha-workflow.mjs'
|
|
24
25
|
import { resolveCmd } from '../../../../scripts/utils/resolve-cmd.mjs'
|
|
25
26
|
import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
|
|
27
|
+
import { loadTemplate } from '../../../../scripts/utils/template.mjs'
|
|
28
|
+
|
|
29
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
30
|
+
const GA_POLICY_DIR = join(HERE, '..', '..', 'policy')
|
|
26
31
|
|
|
27
32
|
/** Шаблони наявності MegaLinter у вмісті workflow */
|
|
28
33
|
const MEGALINTER_USE_PATTERNS = [/oxsecurity\/megalinter-action/i, /megalinter\/megalinter/i]
|
|
@@ -278,13 +283,17 @@ const GA_PER_WORKFLOW_REGO_TARGETS = [
|
|
|
278
283
|
* @param {(msg: string) => void} fail callback при помилці
|
|
279
284
|
* @returns {void}
|
|
280
285
|
*/
|
|
281
|
-
function runAllGaRego(wfDir, ymlWorkflows, pass, fail) {
|
|
286
|
+
async function runAllGaRego(wfDir, ymlWorkflows, pass, fail) {
|
|
282
287
|
for (const target of GA_PER_WORKFLOW_REGO_TARGETS) {
|
|
283
288
|
if (!existsSync(target.workflow)) continue
|
|
289
|
+
const concernDir = join(GA_POLICY_DIR, target.policyDirRel.split('/')[1])
|
|
290
|
+
const tpl = await loadTemplate(concernDir)
|
|
291
|
+
const templateData = tpl[basename(target.workflow)]
|
|
284
292
|
const violations = runConftestBatch({
|
|
285
293
|
policyDirRel: target.policyDirRel,
|
|
286
294
|
namespace: target.namespace,
|
|
287
|
-
files: [target.workflow]
|
|
295
|
+
files: [target.workflow],
|
|
296
|
+
templateData
|
|
288
297
|
})
|
|
289
298
|
for (const v of violations) fail(`${target.workflow}: ${v.message}`)
|
|
290
299
|
if (violations.length === 0) {
|
package/rules/ga/ga.mdc
CHANGED
|
@@ -17,152 +17,23 @@ concurrency:
|
|
|
17
17
|
|
|
18
18
|
Без винятків — у scheduled cleanup-воркфлоу, у `pull_request: types: [closed]`, у publish-воркфлоу теж. Це уникає паралельних запусків того самого workflow на тій самій ref і скасовує попередні в чергу нових.
|
|
19
19
|
|
|
20
|
-
Повинен бути файл
|
|
20
|
+
Повинен бути файл `.github/workflows/clean-ga-workflows.yml`:
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
name: Clean action for removing completed workflow runs
|
|
24
|
-
|
|
25
|
-
on:
|
|
26
|
-
schedule:
|
|
27
|
-
- cron: '0 1 16 * *'
|
|
28
|
-
|
|
29
|
-
# Allow workflow to be manually run from the GitHub UI
|
|
30
|
-
workflow_dispatch: {}
|
|
31
|
-
|
|
32
|
-
concurrency:
|
|
33
|
-
group: ${{ github.ref }}-${{ github.workflow }}
|
|
34
|
-
cancel-in-progress: true
|
|
35
|
-
|
|
36
|
-
jobs:
|
|
37
|
-
cleanup_old_workflows:
|
|
38
|
-
runs-on: ubuntu-latest
|
|
39
|
-
permissions:
|
|
40
|
-
actions: write
|
|
41
|
-
contents: read
|
|
42
|
-
steps:
|
|
43
|
-
- name: Delete workflow runs
|
|
44
|
-
uses: dmvict/clean-workflow-runs@v1
|
|
45
|
-
with:
|
|
46
|
-
token: ${{ github.token }}
|
|
47
|
-
save_period: 31
|
|
48
|
-
save_min_runs_number: 0
|
|
49
|
-
|
|
50
|
-
```
|
|
22
|
+
- Канон: [clean-ga-workflows.yml.snippet.yml](./policy/clean_ga_workflows/template/clean-ga-workflows.yml.snippet.yml)
|
|
51
23
|
|
|
52
|
-
Повинен бути файл
|
|
24
|
+
Повинен бути файл `.github/workflows/clean-merged-branch.yml`:
|
|
53
25
|
|
|
54
|
-
|
|
55
|
-
name: Clean abandoned branches
|
|
56
|
-
|
|
57
|
-
on:
|
|
58
|
-
# Run daily at midnight
|
|
59
|
-
schedule:
|
|
60
|
-
- cron: '0 1 15 * *'
|
|
61
|
-
|
|
62
|
-
# Allow workflow to be manually run from the GitHub UI
|
|
63
|
-
workflow_dispatch: {}
|
|
64
|
-
|
|
65
|
-
concurrency:
|
|
66
|
-
group: ${{ github.ref }}-${{ github.workflow }}
|
|
67
|
-
cancel-in-progress: true
|
|
68
|
-
|
|
69
|
-
jobs:
|
|
70
|
-
cleanup_old_branches:
|
|
71
|
-
runs-on: ubuntu-latest
|
|
72
|
-
permissions:
|
|
73
|
-
contents: write
|
|
74
|
-
steps:
|
|
75
|
-
- id: delete_stuff
|
|
76
|
-
name: Delete those pesky dead branches
|
|
77
|
-
uses: phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3
|
|
78
|
-
with:
|
|
79
|
-
github_token: ${{ github.token }}
|
|
80
|
-
last_commit_age_days: 90
|
|
81
|
-
ignore_branches: main,dev
|
|
82
|
-
dry_run: no
|
|
83
|
-
|
|
84
|
-
- name: Get output
|
|
85
|
-
env:
|
|
86
|
-
DELETED_BRANCHES: ${{ steps.delete_stuff.outputs.deleted_branches }}
|
|
87
|
-
run: |
|
|
88
|
-
echo "Deleted branches: ${DELETED_BRANCHES}"
|
|
89
|
-
```
|
|
26
|
+
- Канон: [clean-merged-branch.yml.snippet.yml](./policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml)
|
|
90
27
|
|
|
91
28
|
Інші гілки в `ignore_branches` — допустимо.
|
|
92
|
-
Повинен бути файл .github/workflows/lint-ga.yml, зі змістом:
|
|
93
29
|
|
|
94
|
-
|
|
95
|
-
name: Lint GA
|
|
96
|
-
|
|
97
|
-
on:
|
|
98
|
-
push:
|
|
99
|
-
branches:
|
|
100
|
-
- dev
|
|
101
|
-
- main
|
|
102
|
-
paths:
|
|
103
|
-
- '.github/actions/**'
|
|
104
|
-
- '.github/workflows/**'
|
|
105
|
-
pull_request:
|
|
106
|
-
branches:
|
|
107
|
-
- dev
|
|
108
|
-
- main
|
|
30
|
+
Повинен бути файл `.github/workflows/lint-ga.yml`:
|
|
109
31
|
|
|
110
|
-
|
|
111
|
-
group: ${{ github.ref }}-${{ github.workflow }}
|
|
112
|
-
cancel-in-progress: true
|
|
113
|
-
|
|
114
|
-
jobs:
|
|
115
|
-
lint-ga:
|
|
116
|
-
runs-on: ubuntu-latest
|
|
117
|
-
permissions:
|
|
118
|
-
contents: read
|
|
119
|
-
steps:
|
|
120
|
-
- uses: actions/checkout@v6
|
|
121
|
-
with:
|
|
122
|
-
persist-credentials: false
|
|
123
|
-
|
|
124
|
-
- uses: ./.github/actions/setup-bun-deps
|
|
125
|
-
|
|
126
|
-
- uses: astral-sh/setup-uv@v8.0.0
|
|
32
|
+
- Канон: [lint-ga.yml.snippet.yml](./policy/lint_ga/template/lint-ga.yml.snippet.yml)
|
|
127
33
|
|
|
128
|
-
|
|
129
|
-
run: bun run lint-ga
|
|
130
|
-
```
|
|
34
|
+
Повинен бути файл `.github/workflows/git-ai.yml`:
|
|
131
35
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
```yaml
|
|
135
|
-
name: Git AI
|
|
136
|
-
|
|
137
|
-
on:
|
|
138
|
-
pull_request:
|
|
139
|
-
types: [closed]
|
|
140
|
-
|
|
141
|
-
concurrency:
|
|
142
|
-
group: ${{ github.ref }}-${{ github.workflow }}
|
|
143
|
-
cancel-in-progress: true
|
|
144
|
-
|
|
145
|
-
jobs:
|
|
146
|
-
git-ai:
|
|
147
|
-
if: github.event.pull_request.merged == true
|
|
148
|
-
runs-on: ubuntu-latest
|
|
149
|
-
permissions:
|
|
150
|
-
contents: write
|
|
151
|
-
|
|
152
|
-
steps:
|
|
153
|
-
- name: Install git-ai
|
|
154
|
-
run: |
|
|
155
|
-
curl -fsSL https://usegitai.com/install.sh | bash
|
|
156
|
-
echo "$HOME/.git-ai/bin" >> $GITHUB_PATH
|
|
157
|
-
- name: Run git-ai
|
|
158
|
-
id: run-git-ai
|
|
159
|
-
env:
|
|
160
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
161
|
-
run: |
|
|
162
|
-
git config --global user.name "github-actions[bot]"
|
|
163
|
-
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
164
|
-
git-ai ci github run
|
|
165
|
-
```
|
|
36
|
+
- Канон: [git-ai.yml.snippet.yml](./policy/git_ai/template/git-ai.yml.snippet.yml)
|
|
166
37
|
|
|
167
38
|
**Локальний composite** (`uses: ./.github/actions/setup-bun-deps` або `./npm/github-actions/setup-bun-deps`): **спочатку** обов’язковий крок **`actions/checkout@v6`** (`persist-credentials: false`), інакше runner не знайде `action.yml`. Сам composite: **`actions/setup-node@v6`** (**Node 24**), **Bun**, **`actions/cache@v5`**, **`bun install --frozen-lockfile`**.
|
|
168
39
|
|
|
@@ -1,42 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Перевірка `.github/workflows/clean-ga-workflows.yml` (ga.mdc).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
8
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
9
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
10
|
-
#
|
|
11
|
-
# Усі `deny`-правила йдуть контигно (regal: messy-rule); helpers і константи —
|
|
12
|
-
# секціями вище та нижче.
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/clean-ga-workflows.yml.snippet.yml
|
|
5
|
+
# (повний YAML канон). Path-and-value перевірки — у цьому rego (per-concern
|
|
6
|
+
# field-by-field з .data.template.snippet.<path>); жодних inline literals.
|
|
13
7
|
package ga.clean_ga_workflows
|
|
14
8
|
|
|
15
9
|
import rego.v1
|
|
16
10
|
|
|
17
|
-
# ──
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# interpolation. Збираємо очікувані рядки з фрагментів через `concat`, як це
|
|
21
|
-
# зроблено в check-ga.mjs, щоб і Rego-парсер, і людина-читач не плуталися.
|
|
11
|
+
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
12
|
+
# GHA YAML quirk: ключ `on:` — YAML 1.1 boolean `true`, у conftest серіалізується
|
|
13
|
+
# як рядковий ключ "true". Template (через --data JSON) має ключ "on".
|
|
22
14
|
|
|
23
|
-
|
|
15
|
+
gha_on := input["true"]
|
|
24
16
|
|
|
25
|
-
|
|
17
|
+
step0 := input.jobs.cleanup_old_workflows.steps[0]
|
|
26
18
|
|
|
27
|
-
|
|
19
|
+
# Експектації з template.
|
|
20
|
+
expected_name := data.template.snippet.name
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
# GHA YAML quirk: ключ `on:` — YAML 1.1 boolean `true`, конфтест серіалізує його
|
|
32
|
-
# як рядковий ключ "true". Ані `input.on`, ані `input["on"]`, ані `input[true]`
|
|
33
|
-
# не працюють — лише `input["true"]`.
|
|
22
|
+
expected_cron := data.template.snippet.on.schedule[0].cron
|
|
34
23
|
|
|
35
|
-
|
|
24
|
+
expected_step0 := data.template.snippet.jobs.cleanup_old_workflows.steps[0]
|
|
36
25
|
|
|
37
|
-
|
|
26
|
+
expected_perms := data.template.snippet.jobs.cleanup_old_workflows.permissions
|
|
38
27
|
|
|
39
|
-
|
|
28
|
+
expected_runs_on := data.template.snippet.jobs.cleanup_old_workflows["runs-on"]
|
|
29
|
+
|
|
30
|
+
# ── deny rules ─────────────────────────────────────────────────────────────
|
|
40
31
|
|
|
41
32
|
deny contains msg if {
|
|
42
33
|
input.name != expected_name
|
|
@@ -49,7 +40,7 @@ deny contains msg if {
|
|
|
49
40
|
}
|
|
50
41
|
|
|
51
42
|
deny contains msg if {
|
|
52
|
-
not
|
|
43
|
+
not is_object(object.get(gha_on, "workflow_dispatch", null))
|
|
53
44
|
msg := "clean-ga-workflows.yml: має бути workflow_dispatch: {} (ga.mdc)"
|
|
54
45
|
}
|
|
55
46
|
|
|
@@ -60,32 +51,34 @@ deny contains msg if {
|
|
|
60
51
|
|
|
61
52
|
deny contains msg if {
|
|
62
53
|
job := input.jobs.cleanup_old_workflows
|
|
63
|
-
job["runs-on"] !=
|
|
64
|
-
msg := "clean-ga-workflows.yml: runs-on має бути
|
|
54
|
+
job["runs-on"] != expected_runs_on
|
|
55
|
+
msg := sprintf("clean-ga-workflows.yml: runs-on має бути %s (ga.mdc)", [expected_runs_on])
|
|
65
56
|
}
|
|
66
57
|
|
|
67
58
|
deny contains msg if {
|
|
68
59
|
perms := input.jobs.cleanup_old_workflows.permissions
|
|
69
|
-
not
|
|
60
|
+
not perms_match(perms, expected_perms)
|
|
70
61
|
msg := "clean-ga-workflows.yml: permissions мають бути actions: write, contents: read (ga.mdc)"
|
|
71
62
|
}
|
|
72
63
|
|
|
73
64
|
deny contains msg if {
|
|
74
|
-
step0.name !=
|
|
75
|
-
msg := "clean-ga-workflows.yml: перший крок має мати name:
|
|
65
|
+
step0.name != expected_step0.name
|
|
66
|
+
msg := sprintf("clean-ga-workflows.yml: перший крок має мати name: %s (ga.mdc)", [expected_step0.name])
|
|
76
67
|
}
|
|
77
68
|
|
|
78
69
|
deny contains msg if {
|
|
79
|
-
step0.uses !=
|
|
80
|
-
msg := "clean-ga-workflows.yml: перший крок має uses:
|
|
70
|
+
step0.uses != expected_step0.uses
|
|
71
|
+
msg := sprintf("clean-ga-workflows.yml: перший крок має uses: %s (ga.mdc)", [expected_step0.uses])
|
|
81
72
|
}
|
|
82
73
|
|
|
83
|
-
# Триплет полів `with`: token (gh-токен), save_period=31, save_min_runs_number=0.
|
|
84
|
-
# В JS-перевірці помилка спільна для всіх трьох — лишаємо такий самий формат, щоб
|
|
85
|
-
# повідомлення збігалися.
|
|
86
74
|
deny contains msg if {
|
|
87
75
|
not step0_with_canonical
|
|
88
|
-
msg := "clean-ga-workflows.yml: with має містити token/save_period/save_min_runs_number як у ga.mdc"
|
|
76
|
+
msg := "clean-ga-workflows.yml: with має містити token/save_period/save_min_runs_number як у template (ga.mdc)"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
deny contains msg if {
|
|
80
|
+
step0.with.save_period != expected_step0.with.save_period
|
|
81
|
+
msg := sprintf("clean-ga-workflows.yml: with.save_period має бути %d (ga.mdc)", [expected_step0.with.save_period])
|
|
89
82
|
}
|
|
90
83
|
|
|
91
84
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -94,17 +87,13 @@ has_expected_cron if {
|
|
|
94
87
|
gha_on.schedule[_].cron == expected_cron
|
|
95
88
|
}
|
|
96
89
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
actions_write_contents_read(perms) if {
|
|
102
|
-
perms.actions == "write"
|
|
103
|
-
perms.contents == "read"
|
|
90
|
+
perms_match(actual, expected) if {
|
|
91
|
+
actual.actions == expected.actions
|
|
92
|
+
actual.contents == expected.contents
|
|
104
93
|
}
|
|
105
94
|
|
|
106
95
|
step0_with_canonical if {
|
|
107
|
-
step0.with.token ==
|
|
108
|
-
step0.with.save_period ==
|
|
109
|
-
step0.with.save_min_runs_number ==
|
|
96
|
+
step0.with.token == expected_step0.with.token
|
|
97
|
+
step0.with.save_period == expected_step0.with.save_period
|
|
98
|
+
step0.with.save_min_runs_number == expected_step0.with.save_min_runs_number
|
|
110
99
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Clean action for removing completed workflow runs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 1 16 * *'
|
|
6
|
+
|
|
7
|
+
# Allow workflow to be manually run from the GitHub UI
|
|
8
|
+
workflow_dispatch: {}
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
cleanup_old_workflows:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
actions: write
|
|
19
|
+
contents: read
|
|
20
|
+
steps:
|
|
21
|
+
- name: Delete workflow runs
|
|
22
|
+
uses: dmvict/clean-workflow-runs@v1
|
|
23
|
+
with:
|
|
24
|
+
token: ${{ github.token }}
|
|
25
|
+
save_period: 31
|
|
26
|
+
save_min_runs_number: 0
|
|
@@ -1,44 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Перевірка `.github/workflows/clean-merged-branch.yml` (ga.mdc).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# -p npm/policy/ga --namespace ga.clean_merged_branch
|
|
6
|
-
#
|
|
7
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
8
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
9
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/clean-merged-branch.yml.snippet.yml.
|
|
10
5
|
package ga.clean_merged_branch
|
|
11
6
|
|
|
12
7
|
import rego.v1
|
|
13
8
|
|
|
14
|
-
# ──
|
|
15
|
-
#
|
|
16
|
-
# Шаблонні токени GitHub Actions (`${{ … }}`) збираємо з фрагментів через
|
|
17
|
-
# `concat`, бо `{{` у Rego починає string interpolation.
|
|
18
|
-
|
|
19
|
-
expected_github_token := concat("", ["$", "{{ github.token }}"])
|
|
9
|
+
# ── Аліаси ─────────────────────────────────────────────────────────────────
|
|
20
10
|
|
|
21
|
-
|
|
11
|
+
gha_on := input["true"]
|
|
22
12
|
|
|
23
|
-
|
|
13
|
+
steps := input.jobs.cleanup_old_branches.steps
|
|
24
14
|
|
|
25
|
-
|
|
15
|
+
step0 := steps[0]
|
|
26
16
|
|
|
27
|
-
|
|
17
|
+
step1 := steps[1]
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
# YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
|
|
19
|
+
expected_name := data.template.snippet.name
|
|
32
20
|
|
|
33
|
-
|
|
21
|
+
expected_cron := data.template.snippet.on.schedule[0].cron
|
|
34
22
|
|
|
35
|
-
|
|
23
|
+
expected_step0 := data.template.snippet.jobs.cleanup_old_branches.steps[0]
|
|
36
24
|
|
|
37
|
-
|
|
25
|
+
expected_step1 := data.template.snippet.jobs.cleanup_old_branches.steps[1]
|
|
38
26
|
|
|
39
|
-
|
|
27
|
+
expected_perms := data.template.snippet.jobs.cleanup_old_branches.permissions
|
|
40
28
|
|
|
41
|
-
# ── deny rules
|
|
29
|
+
# ── deny rules ─────────────────────────────────────────────────────────────
|
|
42
30
|
|
|
43
31
|
deny contains msg if {
|
|
44
32
|
input.name != expected_name
|
|
@@ -51,7 +39,7 @@ deny contains msg if {
|
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
deny contains msg if {
|
|
54
|
-
not
|
|
42
|
+
not is_object(object.get(gha_on, "workflow_dispatch", null))
|
|
55
43
|
msg := "clean-merged-branch.yml: має бути workflow_dispatch: {} (ga.mdc)"
|
|
56
44
|
}
|
|
57
45
|
|
|
@@ -61,65 +49,73 @@ deny contains msg if {
|
|
|
61
49
|
}
|
|
62
50
|
|
|
63
51
|
deny contains msg if {
|
|
64
|
-
input.jobs.cleanup_old_branches.permissions.contents !=
|
|
65
|
-
msg := "clean-merged-branch.yml: permissions
|
|
52
|
+
input.jobs.cleanup_old_branches.permissions.contents != expected_perms.contents
|
|
53
|
+
msg := sprintf("clean-merged-branch.yml: permissions.contents має бути %s (ga.mdc)", [expected_perms.contents])
|
|
66
54
|
}
|
|
67
55
|
|
|
68
56
|
deny contains msg if {
|
|
69
57
|
count(steps) < 2
|
|
70
|
-
msg := "clean-merged-branch.yml: steps має містити 2 кроки як у ga.mdc"
|
|
58
|
+
msg := "clean-merged-branch.yml: steps має містити 2 кроки як у template (ga.mdc)"
|
|
71
59
|
}
|
|
72
60
|
|
|
73
61
|
# ── Step 0 (delete_stuff) ──────────────────────────────────────────────────
|
|
74
62
|
|
|
75
63
|
deny contains msg if {
|
|
76
|
-
step0.id !=
|
|
77
|
-
msg := "clean-merged-branch.yml: перший крок має id:
|
|
64
|
+
step0.id != expected_step0.id
|
|
65
|
+
msg := sprintf("clean-merged-branch.yml: перший крок має id: %s (ga.mdc)", [expected_step0.id])
|
|
78
66
|
}
|
|
79
67
|
|
|
80
68
|
deny contains msg if {
|
|
81
|
-
step0.uses !=
|
|
82
|
-
msg := "clean-merged-branch.yml: перший крок має uses
|
|
69
|
+
step0.uses != expected_step0.uses
|
|
70
|
+
msg := sprintf("clean-merged-branch.yml: перший крок має uses: %s (ga.mdc)", [expected_step0.uses])
|
|
83
71
|
}
|
|
84
72
|
|
|
85
73
|
deny contains msg if {
|
|
86
|
-
step0.with.github_token !=
|
|
87
|
-
msg := sprintf(
|
|
74
|
+
step0.with.github_token != expected_step0.with.github_token
|
|
75
|
+
msg := sprintf(
|
|
76
|
+
"clean-merged-branch.yml: with.github_token має бути %s (ga.mdc)",
|
|
77
|
+
[expected_step0.with.github_token],
|
|
78
|
+
)
|
|
88
79
|
}
|
|
89
80
|
|
|
90
81
|
deny contains msg if {
|
|
91
|
-
step0.with.last_commit_age_days !=
|
|
92
|
-
msg :=
|
|
82
|
+
step0.with.last_commit_age_days != expected_step0.with.last_commit_age_days
|
|
83
|
+
msg := sprintf(
|
|
84
|
+
"clean-merged-branch.yml: with.last_commit_age_days має бути %d (ga.mdc)",
|
|
85
|
+
[expected_step0.with.last_commit_age_days],
|
|
86
|
+
)
|
|
93
87
|
}
|
|
94
88
|
|
|
95
89
|
deny contains msg if {
|
|
96
|
-
not
|
|
97
|
-
msg :=
|
|
90
|
+
not ignore_branches_subset
|
|
91
|
+
msg := sprintf(
|
|
92
|
+
"clean-merged-branch.yml: with.ignore_branches має містити %s (ga.mdc)",
|
|
93
|
+
[expected_step0.with.ignore_branches],
|
|
94
|
+
)
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
# `dry_run: no`
|
|
101
|
-
#
|
|
102
|
-
# (Якщо комусь схочеться явного `"no"` — треба буде брати in quotes у YAML.)
|
|
97
|
+
# YAML 1.1 quirk: `dry_run: no` парситься як boolean false у Go-yaml (conftest).
|
|
98
|
+
# Template (від `yaml` npm) теж нормалізовано до false у фікстурі.
|
|
103
99
|
deny contains msg if {
|
|
104
|
-
step0.with.dry_run !=
|
|
100
|
+
step0.with.dry_run != expected_step0.with.dry_run # noqa: rules-style-no-equality-with-false
|
|
105
101
|
msg := "clean-merged-branch.yml: with.dry_run має бути no (ga.mdc)"
|
|
106
102
|
}
|
|
107
103
|
|
|
108
104
|
# ── Step 1 (Get output) ────────────────────────────────────────────────────
|
|
109
105
|
|
|
110
106
|
deny contains msg if {
|
|
111
|
-
step1.name !=
|
|
112
|
-
msg := "clean-merged-branch.yml: другий крок має name:
|
|
107
|
+
step1.name != expected_step1.name
|
|
108
|
+
msg := sprintf("clean-merged-branch.yml: другий крок має name: %s (ga.mdc)", [expected_step1.name])
|
|
113
109
|
}
|
|
114
110
|
|
|
115
111
|
deny contains msg if {
|
|
116
|
-
step1.env.DELETED_BRANCHES !=
|
|
117
|
-
msg := "clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у ga.mdc"
|
|
112
|
+
step1.env.DELETED_BRANCHES != expected_step1.env.DELETED_BRANCHES
|
|
113
|
+
msg := "clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у template (ga.mdc)"
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
deny contains msg if {
|
|
121
117
|
not echo_deleted_branches
|
|
122
|
-
msg := "clean-merged-branch.yml: run має echo Deleted branches як у ga.mdc"
|
|
118
|
+
msg := "clean-merged-branch.yml: run має echo Deleted branches як у template (ga.mdc)"
|
|
123
119
|
}
|
|
124
120
|
|
|
125
121
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -128,15 +124,17 @@ has_expected_cron if {
|
|
|
128
124
|
gha_on.schedule[_].cron == expected_cron
|
|
129
125
|
}
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
# Кожна гілка з template-літералу (через кому) має бути присутня у actual.
|
|
128
|
+
ignore_branches_subset if {
|
|
129
|
+
required_branches := split(expected_step0.with.ignore_branches, ",")
|
|
130
|
+
actual := step0.with.ignore_branches
|
|
131
|
+
every b in required_branches {
|
|
132
|
+
contains(actual, trim_space(b))
|
|
133
|
+
}
|
|
138
134
|
}
|
|
139
135
|
|
|
140
136
|
echo_deleted_branches if {
|
|
141
|
-
|
|
137
|
+
# Звіряємо substring "echo "Deleted branches: …${DELETED_BRANCHES}…"" — формується з template run.
|
|
138
|
+
contains(step1.run, "Deleted branches:")
|
|
139
|
+
contains(step1.run, "${DELETED_BRANCHES}")
|
|
142
140
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Clean abandoned branches
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
# Run daily at midnight
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: '0 1 15 * *'
|
|
7
|
+
|
|
8
|
+
# Allow workflow to be manually run from the GitHub UI
|
|
9
|
+
workflow_dispatch: {}
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
cleanup_old_branches:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
permissions:
|
|
19
|
+
contents: write
|
|
20
|
+
steps:
|
|
21
|
+
- id: delete_stuff
|
|
22
|
+
name: Delete those pesky dead branches
|
|
23
|
+
uses: phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3
|
|
24
|
+
with:
|
|
25
|
+
github_token: ${{ github.token }}
|
|
26
|
+
last_commit_age_days: 90
|
|
27
|
+
ignore_branches: main,dev
|
|
28
|
+
# `no` у workflow → Go-yaml парсить як bool false; тут пишемо явно false,
|
|
29
|
+
# бо template читає `yaml` (YAML 1.2), де `no` лишається рядком — а нам
|
|
30
|
+
# потрібна семантична відповідність runtime-парсингу conftest.
|
|
31
|
+
dry_run: false
|
|
32
|
+
|
|
33
|
+
- name: Get output
|
|
34
|
+
env:
|
|
35
|
+
DELETED_BRANCHES: ${{ steps.delete_stuff.outputs.deleted_branches }}
|
|
36
|
+
run: |
|
|
37
|
+
echo "Deleted branches: ${DELETED_BRANCHES}"
|
|
@@ -1,42 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Перевірка `.github/workflows/git-ai.yml` (ga.mdc).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
8
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
9
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/git-ai.yml.snippet.yml.
|
|
5
|
+
# Substring-перевірки (`if:`, `run:` блоки) — на основі ключових фраз з template
|
|
6
|
+
# steps, бо повний multi-line `run:` дуже крихкий для exact-match.
|
|
10
7
|
package ga.git_ai
|
|
11
8
|
|
|
12
9
|
import rego.v1
|
|
13
10
|
|
|
14
|
-
# ──
|
|
15
|
-
|
|
16
|
-
expected_name := "Git AI"
|
|
17
|
-
|
|
18
|
-
expected_if_substring := "github.event.pull_request.merged == true"
|
|
19
|
-
|
|
20
|
-
expected_install_substring := "curl -fsSL https://usegitai.com/install.sh | bash"
|
|
21
|
-
|
|
22
|
-
expected_run_substring := "git-ai ci github run"
|
|
23
|
-
|
|
24
|
-
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
25
|
-
#
|
|
26
|
-
# YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
|
|
11
|
+
# ── Аліаси ─────────────────────────────────────────────────────────────────
|
|
27
12
|
|
|
28
13
|
gha_on := input["true"]
|
|
29
14
|
|
|
30
|
-
# Job-id містить дефіс — звертаємося через `[…]`. Імʼя `job` (без префіксу пакету)
|
|
31
|
-
# — щоб уникнути regal-правила `rule-name-repeats-package`.
|
|
32
15
|
job := input.jobs["git-ai"]
|
|
33
16
|
|
|
34
|
-
# Усі `run:` зі steps цього job-а, склеєні в один blob — для substring-перевірки.
|
|
35
17
|
job_run_blob := concat("\n", [run |
|
|
36
18
|
run := job.steps[_].run
|
|
37
19
|
])
|
|
38
20
|
|
|
39
|
-
|
|
21
|
+
expected_name := data.template.snippet.name
|
|
22
|
+
|
|
23
|
+
expected_types := {t | some t in data.template.snippet.on.pull_request.types}
|
|
24
|
+
|
|
25
|
+
expected_if := data.template.snippet.jobs["git-ai"].if
|
|
26
|
+
|
|
27
|
+
expected_perms := data.template.snippet.jobs["git-ai"].permissions
|
|
28
|
+
|
|
29
|
+
# Substring-маркери з template `run:` блоків — ключові команди, наявність яких
|
|
30
|
+
# гарантує що workflow робить очікувані дії. Конкретний multi-line — не порівнюємо.
|
|
31
|
+
install_substring := "https://usegitai.com/install.sh"
|
|
32
|
+
|
|
33
|
+
run_substring := "git-ai ci github run"
|
|
34
|
+
|
|
35
|
+
# ── deny rules ─────────────────────────────────────────────────────────────
|
|
40
36
|
|
|
41
37
|
deny contains msg if {
|
|
42
38
|
input.name != expected_name
|
|
@@ -54,30 +50,27 @@ deny contains msg if {
|
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
deny contains msg if {
|
|
57
|
-
not contains(job_if_str,
|
|
58
|
-
msg := "git-ai.yml: job має містити if:
|
|
53
|
+
not contains(job_if_str, expected_if)
|
|
54
|
+
msg := sprintf("git-ai.yml: job має містити if: %s (ga.mdc)", [expected_if])
|
|
59
55
|
}
|
|
60
56
|
|
|
61
57
|
deny contains msg if {
|
|
62
|
-
job.permissions.contents !=
|
|
63
|
-
msg := "git-ai.yml: permissions
|
|
58
|
+
job.permissions.contents != expected_perms.contents
|
|
59
|
+
msg := sprintf("git-ai.yml: permissions.contents має бути %s (ga.mdc)", [expected_perms.contents])
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
deny contains msg if {
|
|
67
|
-
not contains(job_run_blob,
|
|
63
|
+
not contains(job_run_blob, install_substring)
|
|
68
64
|
msg := "git-ai.yml: має встановлювати git-ai через curl | bash (ga.mdc)"
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
deny contains msg if {
|
|
72
|
-
not contains(job_run_blob,
|
|
73
|
-
msg := "git-ai.yml: має виконувати
|
|
68
|
+
not contains(job_run_blob, run_substring)
|
|
69
|
+
msg := sprintf("git-ai.yml: має виконувати %s (ga.mdc)", [run_substring])
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
77
73
|
|
|
78
|
-
# `if` поле job-а може бути відсутнім — тоді `sprintf` дає невизначене значення
|
|
79
|
-
# і спрацьовує `default`, повертаючи порожній рядок; `contains(…)` нижче дасть
|
|
80
|
-
# false і відповідне `deny`-правило спрацює зі зрозумілим повідомленням.
|
|
81
74
|
default job_if_str := ""
|
|
82
75
|
|
|
83
76
|
job_if_str := sprintf("%v", [job.if])
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Git AI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [closed]
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
git-ai:
|
|
13
|
+
if: github.event.pull_request.merged == true
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Install git-ai
|
|
20
|
+
run: |
|
|
21
|
+
curl -fsSL https://usegitai.com/install.sh | bash
|
|
22
|
+
echo "$HOME/.git-ai/bin" >> $GITHUB_PATH
|
|
23
|
+
- name: Run git-ai
|
|
24
|
+
id: run-git-ai
|
|
25
|
+
env:
|
|
26
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
27
|
+
run: |
|
|
28
|
+
git config --global user.name "github-actions[bot]"
|
|
29
|
+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
30
|
+
git-ai ci github run
|
|
@@ -1,44 +1,50 @@
|
|
|
1
|
-
#
|
|
2
|
-
# `npm/scripts/check-ga.mjs` (ga.mdc).
|
|
1
|
+
# Перевірка `.github/workflows/lint-ga.yml` (ga.mdc).
|
|
3
2
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# -p npm/policy/ga --namespace ga.lint_ga
|
|
7
|
-
#
|
|
8
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
9
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
10
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/lint-ga.yml.snippet.yml.
|
|
11
5
|
package ga.lint_ga
|
|
12
6
|
|
|
13
7
|
import rego.v1
|
|
14
8
|
|
|
15
|
-
# ──
|
|
16
|
-
|
|
17
|
-
expected_name := "Lint GA"
|
|
18
|
-
|
|
19
|
-
expected_branches := {"dev", "main"}
|
|
20
|
-
|
|
21
|
-
expected_push_paths := {".github/actions/**", ".github/workflows/**"}
|
|
22
|
-
|
|
23
|
-
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
24
|
-
#
|
|
25
|
-
# YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
|
|
9
|
+
# ── Аліаси ─────────────────────────────────────────────────────────────────
|
|
26
10
|
|
|
27
11
|
gha_on := input["true"]
|
|
28
12
|
|
|
29
|
-
# Job-id містить дефіс — звертаємося через `[…]`. Імʼя `job` (без префіксу пакету)
|
|
30
|
-
# — щоб уникнути regal-правила `rule-name-repeats-package`.
|
|
31
13
|
job := input.jobs["lint-ga"]
|
|
32
14
|
|
|
33
|
-
# Усі `uses:` зі steps цього job-а — для перевірки членства.
|
|
34
15
|
job_uses_set contains job.steps[_].uses
|
|
35
16
|
|
|
36
|
-
# Усі `run:` зі steps цього job-а, склеєні в один blob — для substring-перевірки.
|
|
37
17
|
job_run_blob := concat("\n", [run |
|
|
38
18
|
run := job.steps[_].run
|
|
39
19
|
])
|
|
40
20
|
|
|
41
|
-
|
|
21
|
+
expected_name := data.template.snippet.name
|
|
22
|
+
|
|
23
|
+
expected_push_branches := {b | some b in data.template.snippet.on.push.branches}
|
|
24
|
+
|
|
25
|
+
expected_pr_branches := {b | some b in data.template.snippet.on.pull_request.branches}
|
|
26
|
+
|
|
27
|
+
expected_push_paths := {p | some p in data.template.snippet.on.push.paths}
|
|
28
|
+
|
|
29
|
+
expected_runs_on := data.template.snippet.jobs["lint-ga"]["runs-on"]
|
|
30
|
+
|
|
31
|
+
expected_perms := data.template.snippet.jobs["lint-ga"].permissions
|
|
32
|
+
|
|
33
|
+
# Required `uses:` зі template — фільтруємо тільки кроки що мають `uses`.
|
|
34
|
+
expected_uses_set contains u if {
|
|
35
|
+
some step in data.template.snippet.jobs["lint-ga"].steps
|
|
36
|
+
u := object.get(step, "uses", "")
|
|
37
|
+
u != ""
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Required `run:` substrings — collected from steps with `run`.
|
|
41
|
+
expected_run_blob := concat("\n", [r |
|
|
42
|
+
some step in data.template.snippet.jobs["lint-ga"].steps
|
|
43
|
+
r := object.get(step, "run", "")
|
|
44
|
+
r != ""
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
# ── deny rules ─────────────────────────────────────────────────────────────
|
|
42
48
|
|
|
43
49
|
deny contains msg if {
|
|
44
50
|
input.name != expected_name
|
|
@@ -46,17 +52,17 @@ deny contains msg if {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
deny contains msg if {
|
|
49
|
-
not
|
|
55
|
+
not branches_superset_of(gha_on.push.branches, expected_push_branches)
|
|
50
56
|
msg := "lint-ga.yml: on.push.branches має містити dev і main (ga.mdc)"
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
deny contains msg if {
|
|
54
|
-
not
|
|
60
|
+
not branches_superset_of(gha_on.pull_request.branches, expected_pr_branches)
|
|
55
61
|
msg := "lint-ga.yml: on.pull_request.branches має містити dev і main (ga.mdc)"
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
deny contains msg if {
|
|
59
|
-
not
|
|
65
|
+
not paths_superset_of(gha_on.push.paths, expected_push_paths)
|
|
60
66
|
msg := "lint-ga.yml: on.push.paths має містити .github/actions/** і .github/workflows/** (ga.mdc)"
|
|
61
67
|
}
|
|
62
68
|
|
|
@@ -66,13 +72,13 @@ deny contains msg if {
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
deny contains msg if {
|
|
69
|
-
job["runs-on"] !=
|
|
70
|
-
msg := "lint-ga.yml: runs-on має бути
|
|
75
|
+
job["runs-on"] != expected_runs_on
|
|
76
|
+
msg := sprintf("lint-ga.yml: runs-on має бути %s (ga.mdc)", [expected_runs_on])
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
deny contains msg if {
|
|
74
|
-
job.permissions.contents !=
|
|
75
|
-
msg := "lint-ga.yml: permissions
|
|
80
|
+
job.permissions.contents != expected_perms.contents
|
|
81
|
+
msg := sprintf("lint-ga.yml: permissions.contents має бути %s (ga.mdc)", [expected_perms.contents])
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
deny contains msg if {
|
|
@@ -81,38 +87,23 @@ deny contains msg if {
|
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
deny contains msg if {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
deny contains msg if {
|
|
89
|
-
not "./.github/actions/setup-bun-deps" in job_uses_set
|
|
90
|
-
msg := "lint-ga.yml: має бути uses: ./.github/actions/setup-bun-deps (ga.mdc)"
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
deny contains msg if {
|
|
94
|
-
not "astral-sh/setup-uv@v8.0.0" in job_uses_set
|
|
95
|
-
msg := "lint-ga.yml: має бути uses: astral-sh/setup-uv@v8.0.0 (ga.mdc)"
|
|
90
|
+
some required_use in expected_uses_set
|
|
91
|
+
not required_use in job_uses_set
|
|
92
|
+
msg := sprintf("lint-ga.yml: має бути uses: %s (ga.mdc)", [required_use])
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
deny contains msg if {
|
|
96
|
+
expected_run_blob != ""
|
|
99
97
|
not contains(job_run_blob, "bun run lint-ga")
|
|
100
98
|
msg := "lint-ga.yml: має бути крок run: bun run lint-ga (ga.mdc)"
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
expected_branches & {b | some b in branches} == expected_branches
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
pr_branches_have_dev_and_main if {
|
|
111
|
-
branches := gha_on.pull_request.branches
|
|
112
|
-
expected_branches & {b | some b in branches} == expected_branches
|
|
103
|
+
branches_superset_of(actual, expected) if {
|
|
104
|
+
expected & {b | some b in actual} == expected
|
|
113
105
|
}
|
|
114
106
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expected_push_paths & {p | some p in paths} == expected_push_paths
|
|
107
|
+
paths_superset_of(actual, expected) if {
|
|
108
|
+
expected & {p | some p in actual} == expected
|
|
118
109
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Lint GA
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- dev
|
|
7
|
+
- main
|
|
8
|
+
paths:
|
|
9
|
+
- '.github/actions/**'
|
|
10
|
+
- '.github/workflows/**'
|
|
11
|
+
pull_request:
|
|
12
|
+
branches:
|
|
13
|
+
- dev
|
|
14
|
+
- main
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
lint-ga:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v6
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
|
|
30
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
31
|
+
|
|
32
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
33
|
+
|
|
34
|
+
- name: Lint GA
|
|
35
|
+
run: bun run lint-ga
|