@7n/rules-ci-github 1.9.8 → 2.0.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/CHANGELOG.md +20 -0
- package/package.json +11 -4
- package/rules/ci_artifact/consume/collect-artifacts.mjs +24 -0
- package/rules/ci_artifact/consume/concern.json +5 -0
- package/rules/ci_artifact/consume/consume.mdc +26 -0
- package/rules/ci_artifact/consume/docs/collect-artifacts.md +22 -0
- package/rules/ci_artifact/consume/docs/fix-consume.md +29 -0
- package/rules/ci_artifact/consume/docs/index.md +11 -0
- package/rules/ci_artifact/consume/docs/main.md +39 -0
- package/rules/ci_artifact/consume/fix-consume.mjs +56 -0
- package/rules/ci_artifact/consume/main.mjs +84 -0
- package/rules/ci_artifact/main.json +1 -0
- package/rules/ci_artifact/main.mdc +16 -0
- package/rules/text/lint_text/template/lint-text.yml.snippet.yml +0 -2
- package/slots/ci-artifact-consumer.mjs +290 -0
- package/slots/docs/ci-artifact-consumer.md +30 -0
- package/slots/docs/index.md +9 -0
- package/rules/php/lint_php_yml/concern.json +0 -11
- package/rules/php/lint_php_yml/docs/fix-lint_php_yml.md +0 -37
- package/rules/php/lint_php_yml/docs/index.md +0 -9
- package/rules/php/lint_php_yml/fix-lint_php_yml.mjs +0 -15
- package/rules/php/lint_php_yml/lint_php_yml.mdc +0 -23
- package/rules/php/lint_php_yml/lint_php_yml.rego +0 -34
- package/rules/php/lint_php_yml/template/lint-php.yml.snippet.yml +0 -47
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.1] - 2026-07-27
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- peerDependency @7n/rules піднято до >=1.52.0 — перша core-версія з universal slot bus (plugin API v2)
|
|
8
|
+
|
|
9
|
+
## [2.0.0] - 2026-07-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Generic ci.artifact@1 consumer: deep-subset merge, GitHub Actions workflow artifacts (required-file/patch-existing), T0-фікс
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Маніфест плагіна переведено на universal slot bus (`requiresPluginApi: 2`, `slots.provides` з `rules.directory@1` замість `contributes.rules`) — spec 2026-07-27-universal-plugin-slots-lang-php-extraction, Фаза 2.
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- PHP-specific mixin-концерн `rules/php/lint_php_yml/**` видалено (Фаза 4 spec universal-plugin-slots-lang-php-extraction) — GitHub `lint-php.yml` тепер обслуговує generic `ci.artifact@1` consumer через contribution `@7n/rules-lang-php`; статичний `lint-text.yml` template більше не містить безумовний `**/*.php` glob — тепер `patch-existing` contribution того самого плагіна
|
|
22
|
+
|
|
3
23
|
## [1.9.8] - 2026-07-26
|
|
4
24
|
|
|
5
25
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules-ci-github",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Плагін @7n/rules: канон GitHub Actions (.github/workflows) — обов'язкові workflow, zizmor, VS Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"type": "module",
|
|
23
23
|
"files": [
|
|
24
24
|
"rules",
|
|
25
|
+
"slots",
|
|
25
26
|
"CHANGELOG.md",
|
|
26
27
|
"!**/*.test.mjs",
|
|
27
28
|
"!**/*_test.rego",
|
|
@@ -29,15 +30,21 @@
|
|
|
29
30
|
"!**/tests/**"
|
|
30
31
|
],
|
|
31
32
|
"n-rules": {
|
|
33
|
+
"requiresPluginApi": 2,
|
|
32
34
|
"capabilities": [
|
|
33
35
|
"ci:github"
|
|
34
36
|
],
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
+
"slots": {
|
|
38
|
+
"provides": [
|
|
39
|
+
{ "slot": "rules.directory", "version": 1, "id": "github-rules", "resource": "./rules" }
|
|
40
|
+
],
|
|
41
|
+
"consumes": [
|
|
42
|
+
{ "slot": "ci.artifact", "versions": [1], "handler": "./slots/ci-artifact-consumer.mjs" }
|
|
43
|
+
]
|
|
37
44
|
}
|
|
38
45
|
},
|
|
39
46
|
"peerDependencies": {
|
|
40
|
-
"@7n/rules": ">=1.
|
|
47
|
+
"@7n/rules": ">=1.52.0"
|
|
41
48
|
},
|
|
42
49
|
"publishConfig": {
|
|
43
50
|
"access": "public"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Тонка обгортка над `collectCiArtifactContributions`
|
|
3
|
+
* (`@7n/rules/scripts/lib/ci-artifact-collect.mjs`) з capability цього consumer-а (spec
|
|
4
|
+
* `2026-07-27-universal-plugin-slots-lang-php-extraction`, §7.2, Фаза 3). Спільна для
|
|
5
|
+
* detector-а (`main.mjs`) і T0-фіксу (`fix-consume.mjs`) — щоб обидва бачили ОДИН і той самий
|
|
6
|
+
* набір contributions в ОДНОМУ й тому ж graph-порядку (детермінований порядок застосування при
|
|
7
|
+
* двох contributors в один target file, spec §10 Фаза 3 п.4).
|
|
8
|
+
*/
|
|
9
|
+
import { collectCiArtifactContributions } from '@7n/rules/scripts/lib/ci-artifact-collect.mjs'
|
|
10
|
+
|
|
11
|
+
/** Capability, для якої релевантні contributions цього consumer-а. */
|
|
12
|
+
const TARGET_CAPABILITY = 'ci:github'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {import('@7n/rules/scripts/lib/slot-contracts-ci.mjs').CiArtifactCandidate} ArtifactCandidate
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} cwd абсолютний корінь consumer-репо
|
|
20
|
+
* @returns {ReturnType<typeof collectCiArtifactContributions>} результат {@link collectCiArtifactContributions} для `ci:github`
|
|
21
|
+
*/
|
|
22
|
+
export function collectArtifacts(cwd) {
|
|
23
|
+
return collectCiArtifactContributions(cwd, TARGET_CAPABILITY)
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## Generic `ci.artifact@1` consumer (GitHub Actions)
|
|
2
|
+
|
|
3
|
+
Правило матеріалізує CI-артефакти, які постачають language-плагіни (напр. майбутній
|
|
4
|
+
`@7n/rules-lang-php`) через universal typed slot `ci.artifact@1` (mixin
|
|
5
|
+
`@7n/rules-ci-github`) — без жодного PHP чи іншого мовного literal у цьому пакеті: уся
|
|
6
|
+
domain-семантика приходить з payload-у contribution-а (`targetCapability`, `artifactId`,
|
|
7
|
+
`targetPath`, `template`, `mergeStrategy`, `mode`, `fix`).
|
|
8
|
+
|
|
9
|
+
### Поведінка
|
|
10
|
+
|
|
11
|
+
| mode | ціль існує | ціль відсутня |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `required-file` | deep-subset diff проти `template` | violation, T0 створює файл з `template` |
|
|
14
|
+
| `patch-existing` | deep-subset diff проти `template` | без violation (файл належить іншому концерну) |
|
|
15
|
+
|
|
16
|
+
`mergeStrategy: "deep-subset"`: обʼєкти мерджаться рекурсивно по ключах canonical-фрагмента
|
|
17
|
+
(природно покриває GitHub `jobs` — вже keyed object); scalar-масиви (напр. `on.push.paths`) —
|
|
18
|
+
ordered set-union, наявні consumer-specific записи не видаляються; масиви обʼєктів (напр.
|
|
19
|
+
`steps`) — identity-based: canonical-елемент шукається в actual за першим наявним полем
|
|
20
|
+
`id` → `uses` → `name`, мерджаться лише його поля.
|
|
21
|
+
|
|
22
|
+
T0-фікс (`fixability: "config"`, без LLM ladder) застосовується лише коли
|
|
23
|
+
`descriptor.fix === true`; ідемпотентний — повторний прогін після фіксу дає 0 violations.
|
|
24
|
+
|
|
25
|
+
Дві contributions з ОДНАКОВИМ `artifactId` від РІЗНИХ плагінів — domain collision (spec §9.10):
|
|
26
|
+
жодна з них не матеріалізується, замість цього — один violation з provenance обох сторін.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: collect-artifacts.mjs
|
|
4
|
+
resource: plugins/ci-github/rules/ci_artifact/consume/collect-artifacts.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: c6f96721
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Тонка обгортка над `collectCiArtifactContributions` (`@7n/rules/scripts/lib/ci-artifact-collect.mjs`) з capability цього consumer-а (spec `2026-07-27-universal-plugin-slots-lang-php-extraction`, §7.2). Спільна для detector-а (`main.mjs`) і T0-фіксу (`fix-consume.mjs`) — щоб обидва бачили один і той самий набір contributions в одному й тому ж graph-порядку.
|
|
10
|
+
|
|
11
|
+
## Поведінка
|
|
12
|
+
|
|
13
|
+
`collectArtifacts(cwd)` викликає `collectCiArtifactContributions(cwd, 'ci:github')` — лише фіксує `targetCapability` цього consumer-а, уся інша логіка (collect + collision-детекція) живе в спільному `@7n/rules` helper-і.
|
|
14
|
+
|
|
15
|
+
## Публічний API
|
|
16
|
+
|
|
17
|
+
`collectArtifacts` — повертає `{ relevant, collisions, errors }` для `ci:github`.
|
|
18
|
+
|
|
19
|
+
## Гарантії поведінки
|
|
20
|
+
|
|
21
|
+
* **Один consumer — одна capability**: `TARGET_CAPABILITY = 'ci:github'` — фіксоване значення, не параметризується ззовні.
|
|
22
|
+
* **Той самий контракт, що й Azure-consumer**: обидва просто передають різну capability у спільний `collectCiArtifactContributions`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: fix-consume.mjs
|
|
4
|
+
resource: plugins/ci-github/rules/ci_artifact/consume/fix-consume.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 28c6361c
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 80
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
`patterns` — T0-правило для `ci.artifact@1`, що детерміновано приводить кожен цільовий файл до канонічного `template` без LLM. Воно застосовує всі `descriptor.fix === true` contributions у тому самому graph-порядку, що й detector у `collectArtifacts`: якщо дві contributions впливають на один target file, кожен наступний виклик `applyDeepSubsetFix` читає файл уже після merge попередньої contribution, тож наступна contribution бачить застосовану попередню.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
1. `patterns` визначає T0-правило для `ci.artifact@1`, яке виправляє лише ті порушення, де артефакт позначено як придатний до автоматичного виправлення.
|
|
20
|
+
2. Правило спрацьовує тільки для дефіциту або розбіжності артефакта, якщо для цього порушення явно дозволено fix.
|
|
21
|
+
3. Після спрацювання воно збирає релевантні артефактні внески й відбирає лише ті цілі, які мають увімкнений fix.
|
|
22
|
+
4. Для кожного такого цільового файла воно відновлює канонічний стан на основі узгодженого шаблону.
|
|
23
|
+
5. Якщо для внеску шаблон не вдається отримати, правило пропускає цей випадок і не маскує проблему: її далі має показати detector.
|
|
24
|
+
6. Коли один і той самий файл має кілька придатних внесків, правило застосовує їх у тому ж порядку, що й detector, щоб кожен наступний внесок бачив уже попередні зміни.
|
|
25
|
+
7. Результатом є список файлів, які були фактично змінені під час виправлення.
|
|
26
|
+
|
|
27
|
+
## Гарантії поведінки
|
|
28
|
+
|
|
29
|
+
- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: plugins/ci-github/rules/ci_artifact/consume
|
|
4
|
+
resource: plugins/ci-github/rules/ci_artifact/consume/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| --------------------------------------------- | --------- |
|
|
9
|
+
| [collect-artifacts.mjs](collect-artifacts.md) | JS Module |
|
|
10
|
+
| [fix-consume.mjs](fix-consume.md) | JS Module |
|
|
11
|
+
| [main.mjs](main.md) | JS Module |
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: plugins/ci-github/rules/ci_artifact/consume/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 89ed27be
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge-refine:kept-original,judge:inaccurate:0.95
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Публічною точкою входу є `lint`, яка перевіряє відповідність знайденого внеску канонічному стану цільового файла й повертає результат без змін у репозиторії. Вона окремо фіксує, коли відсутній обов’язковий файл, і коли відсутність не є критичною.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
1. Збирає поточні правила для `ci:github` у межах робочого каталогу й використовує `package.json` як джерело пакування та публікації пов’язаних артефактів.
|
|
21
|
+
2. Перевіряє кожну релевантну contribution на відповідність канонічному стану цільового файлу та фіксує діагностику для всього набору знайдених артефактів.
|
|
22
|
+
3. Якщо для contribution не вдається отримати канонічний шаблон, позначає це як помилку шаблону й прив’язує її до цільового файлу.
|
|
23
|
+
4. Якщо цільовий файл відсутній, розрізняє два бізнес-випадки: для обов’язкового файла створення вважається порушенням, для інших сценаріїв відсутність мовчки пропускається.
|
|
24
|
+
5. Якщо файл існує, порівнює його фактичний вміст із канонічним станом і фіксує всі розбіжності як окремі порушення.
|
|
25
|
+
6. Повертає підсумок перевірки через `lint` без зміни файлів у репозиторії.
|
|
26
|
+
|
|
27
|
+
## Публічний API
|
|
28
|
+
|
|
29
|
+
- lint — Detector generic-consumer-а слоту `ci.artifact@1` для `ci:github` (spec §7.2, Фаза 3):
|
|
30
|
+
матеріалізує КОЖНУ активну contribution проти поточного стану consumer-репо — без жодного
|
|
31
|
+
PHP/lang-specific literal тут, уся domain-семантика приходить із payload-у contribution-а.
|
|
32
|
+
|
|
33
|
+
## Сценарії використання
|
|
34
|
+
|
|
35
|
+
- `plugins/ci-github/rules/ci_artifact/consume/tests/consume.test.mjs` (ci-github ci.artifact consumer) — required-file: файл відсутній → 1 violation, T0 створює canonical файл; deep-subset: відсутній canonical крок → violation, T0 idempotent-фікс; set-union scalar-масивів: consumer-specific шлях у on.push.paths не видаляється; step identity за id/uses/name: зайві поля кроку не викликають дублювання; patch-existing: target відсутній → 0 violations (файл належить іншому концерну); ще 3
|
|
36
|
+
|
|
37
|
+
## Гарантії поведінки
|
|
38
|
+
|
|
39
|
+
- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T0-фікс generic-consumer-а слоту `ci.artifact@1` (`mergeStrategy: "deep-subset"`, spec §7.2):
|
|
3
|
+
* приводить кожен цільовий файл до канонічного `template` — детерміновано, без LLM.
|
|
4
|
+
*
|
|
5
|
+
* Застосовує ВСІ `descriptor.fix === true` contributions у ТОМУ САМОМУ graph-порядку, що
|
|
6
|
+
* detector (`collectArtifacts`, spec §10 Фаза 3 п.4 — deterministic order при двох contributors
|
|
7
|
+
* в один target file): кожен наступний виклик `applyDeepSubsetFix` читає файл ПІСЛЯ merge-у
|
|
8
|
+
* попередньої contribution, тож contribution Б (напр. `patch-existing`) бачить уже застосовану
|
|
9
|
+
* contribution А (напр. `required-file`).
|
|
10
|
+
*/
|
|
11
|
+
import { applyDeepSubsetFix, loadCanonicalTemplate } from '../../../slots/ci-artifact-consumer.mjs'
|
|
12
|
+
import { collectArtifacts } from './collect-artifacts.mjs'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Чи `data` — violation цього concern-а, для якої дозволений deterministic fix.
|
|
16
|
+
* @param {Record<string, unknown> | undefined} data `violation.data`
|
|
17
|
+
* @returns {boolean} true — fix-eligible
|
|
18
|
+
*/
|
|
19
|
+
function isFixableViolationData(data) {
|
|
20
|
+
return (data?.kind === 'artifact-missing' || data?.kind === 'artifact-mismatch') && data?.fix === true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @type {import('@7n/rules/scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
24
|
+
export const patterns = [
|
|
25
|
+
{
|
|
26
|
+
id: 'ci-github-ci-artifact-consume',
|
|
27
|
+
test: violations => violations.some(v => isFixableViolationData(v.data)),
|
|
28
|
+
apply: async (violations, ctx) => {
|
|
29
|
+
const targeted = violations.filter(v => isFixableViolationData(v.data))
|
|
30
|
+
if (targeted.length === 0) return { touchedFiles: [] }
|
|
31
|
+
|
|
32
|
+
const { relevant } = await collectArtifacts(ctx.cwd)
|
|
33
|
+
const touched = new Set()
|
|
34
|
+
for (const { contribution, descriptor } of relevant) {
|
|
35
|
+
if (!descriptor.fix) continue
|
|
36
|
+
const isTargeted = targeted.some(
|
|
37
|
+
v => v.data?.artifactId === descriptor.artifactId && v.file === descriptor.targetPath
|
|
38
|
+
)
|
|
39
|
+
if (!isTargeted) continue
|
|
40
|
+
|
|
41
|
+
const templateResult = await loadCanonicalTemplate(contribution, descriptor)
|
|
42
|
+
if (!templateResult.ok) continue // template-error — детектор уже репортить, T0 нічого не пише
|
|
43
|
+
|
|
44
|
+
const result = await applyDeepSubsetFix({
|
|
45
|
+
cwd: ctx.cwd,
|
|
46
|
+
targetPath: descriptor.targetPath,
|
|
47
|
+
canonical: templateResult.canonical,
|
|
48
|
+
templateText: templateResult.templateText,
|
|
49
|
+
recordWrite: ctx.recordWrite
|
|
50
|
+
})
|
|
51
|
+
for (const f of result.touchedFiles) touched.add(f)
|
|
52
|
+
}
|
|
53
|
+
return { touchedFiles: [...touched] }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** @see ./docs/main.md */
|
|
2
|
+
import { reportCiArtifactCollectionDiagnostics } from '@7n/rules/scripts/lib/ci-artifact-collect.mjs'
|
|
3
|
+
import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
|
|
4
|
+
|
|
5
|
+
// `slots/` — публікується разом з пакетом (spec §7.2); `package.json#files` цього плагіна
|
|
6
|
+
// доповнить оркестратор інтеграції (Фаза 3 явно НЕ чіпає plugin manifests, spec-задача).
|
|
7
|
+
import { diagnoseArtifact, loadCanonicalTemplate } from '../../../slots/ci-artifact-consumer.mjs'
|
|
8
|
+
import { collectArtifacts } from './collect-artifacts.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Діагностує ОДИН artifact (mode `required-file` → missing-check; інакше deep-subset diff проти
|
|
12
|
+
* поточного стану `targetPath`). Винесено з {@link lint} — та сама причина.
|
|
13
|
+
* @param {ReturnType<typeof createViolationReporter>} reporter репортер порушень
|
|
14
|
+
* @param {{ cwd: string }} ctx контекст лінту (лише `cwd` тут потрібен)
|
|
15
|
+
* @param {import('./collect-artifacts.mjs').ArtifactCandidate} candidate одна валідна (без колізій) contribution
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
async function reportArtifact(reporter, ctx, { contribution, descriptor }) {
|
|
19
|
+
const templateResult = await loadCanonicalTemplate(contribution, descriptor)
|
|
20
|
+
if (!templateResult.ok) {
|
|
21
|
+
reporter.fail(`ci.artifact "${descriptor.artifactId}" (${contribution.pluginName}): ${templateResult.reason}`, {
|
|
22
|
+
reason: 'template-error',
|
|
23
|
+
file: descriptor.targetPath
|
|
24
|
+
})
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const diag = await diagnoseArtifact({
|
|
29
|
+
cwd: ctx.cwd,
|
|
30
|
+
targetPath: descriptor.targetPath,
|
|
31
|
+
canonical: templateResult.canonical
|
|
32
|
+
})
|
|
33
|
+
if (diag.missing) {
|
|
34
|
+
// patch-existing: target-файл належить ІНШОМУ, окремому концерну (spec §7.1) — його
|
|
35
|
+
// відсутність не порушення ЦЬОГО concern-а, generic consumer мовчки пропускає.
|
|
36
|
+
if (descriptor.mode !== 'required-file') return
|
|
37
|
+
reporter.fail(
|
|
38
|
+
`${descriptor.targetPath} відсутній — T0 створить canonical файл (contribution "${descriptor.artifactId}" від ${contribution.pluginName})`,
|
|
39
|
+
{
|
|
40
|
+
reason: 'artifact-missing',
|
|
41
|
+
file: descriptor.targetPath,
|
|
42
|
+
data: {
|
|
43
|
+
kind: 'artifact-missing',
|
|
44
|
+
artifactId: descriptor.artifactId,
|
|
45
|
+
contributorPlugin: contribution.pluginName,
|
|
46
|
+
contributionId: contribution.id,
|
|
47
|
+
fix: descriptor.fix
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
for (const v of diag.violations) {
|
|
54
|
+
reporter.fail(
|
|
55
|
+
`${descriptor.targetPath}: ${v} (contribution "${descriptor.artifactId}" від ${contribution.pluginName})`,
|
|
56
|
+
{
|
|
57
|
+
reason: 'artifact-mismatch',
|
|
58
|
+
file: descriptor.targetPath,
|
|
59
|
+
data: {
|
|
60
|
+
kind: 'artifact-mismatch',
|
|
61
|
+
artifactId: descriptor.artifactId,
|
|
62
|
+
contributorPlugin: contribution.pluginName,
|
|
63
|
+
contributionId: contribution.id,
|
|
64
|
+
fix: descriptor.fix
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Detector generic-consumer-а слоту `ci.artifact@1` для `ci:github` (spec §7.2, Фаза 3):
|
|
73
|
+
* матеріалізує КОЖНУ активну contribution проти поточного стану consumer-репо — без жодного
|
|
74
|
+
* PHP/lang-specific literal тут, уся domain-семантика приходить із payload-у contribution-а.
|
|
75
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
|
|
76
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат перевірки
|
|
77
|
+
*/
|
|
78
|
+
export async function lint(ctx) {
|
|
79
|
+
const reporter = createViolationReporter(ctx)
|
|
80
|
+
const collected = await collectArtifacts(ctx.cwd)
|
|
81
|
+
reportCiArtifactCollectionDiagnostics(reporter, collected)
|
|
82
|
+
for (const candidate of collected.relevant) await reportArtifact(reporter, ctx, candidate)
|
|
83
|
+
return reporter.result()
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "auto": "завжди" }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generic-споживач ci.artifact@1 — CI-артефакти, які декларують активні мовні плагіни (required-file/patch-existing), матеріалізуються й перевіряються CI-плагіном провайдера
|
|
3
|
+
version: '1.0'
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Правило `ci_artifact` — generic-споживач slot-у `ci.artifact@1` (spec
|
|
8
|
+
`2026-07-27-universal-plugin-slots-lang-php-extraction`). Мовні плагіни (напр.
|
|
9
|
+
`@7n/rules-lang-php`) декларують CI-артефакти декларативно — descriptor + template; це правило
|
|
10
|
+
матеріалізує їх для свого CI-провайдера: перевіряє наявність/канонічність цільових файлів
|
|
11
|
+
(`required-file`), домерджує provider-specific патчі до чинних generic-артефактів
|
|
12
|
+
(`patch-existing`) і, де дозволено descriptor-ом (`fix: true`), виконує детермінований T0-фікс.
|
|
13
|
+
|
|
14
|
+
Правило активне завжди при встановленому CI-плагіні: без активних contributions воно — тихий
|
|
15
|
+
no-op (zero-touch), тому мовний плагін не потребує CI-плагіна і навпаки. Колізія `artifactId`
|
|
16
|
+
між двома плагінами — помилка з provenance обох сторін, без тихого перезапису.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic consumer слоту `ci.artifact@1` для `@7n/rules-ci-github` (spec
|
|
3
|
+
* `2026-07-27-universal-plugin-slots-lang-php-extraction`, §7.2, Фаза 3).
|
|
4
|
+
*
|
|
5
|
+
* `mergeStrategy: "deep-subset"` — GitHub Actions workflow YAML — рекурсивний structural
|
|
6
|
+
* merge: об'єкти мерджаться по ключах (природно покриває "jobs за key" — `jobs` вже keyed
|
|
7
|
+
* object у GH workflow YAML, окремої identity-логіки не треба); scalar-масиви (напр.
|
|
8
|
+
* `on.push.paths`) — ordered set-union без видалення consumer-specific записів; масиви
|
|
9
|
+
* обʼєктів (напр. `steps`) — identity-based: кожен canonical елемент шукається в actual-масиві
|
|
10
|
+
* за першим наявним полем з `id` → `uses` → `name` (spec §7.2), і лише ЙОГО поля мерджаться —
|
|
11
|
+
* решта полів actual-елемента (і сусідні елементи) не чіпаються.
|
|
12
|
+
*
|
|
13
|
+
* Default export сумісний з `loadSlotConsumer` (`plugin-slots.mjs`, spec §3.3): стабільний
|
|
14
|
+
* `id` + `validate(payload)`. Generic rule (`plugins/ci-github/rules/ci_artifact/consume/`)
|
|
15
|
+
* імпортує цей модуль напряму (той самий package) для `diagnose`/`computeFix` — surface-specific
|
|
16
|
+
* методи, не частина universal broker-контракту.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { dirname, join } from 'node:path'
|
|
20
|
+
|
|
21
|
+
import { resolveArtifactTemplatePath, validateCiArtifactPayload } from '@7n/rules/plugin-api'
|
|
22
|
+
|
|
23
|
+
/** Пріоритет полів-ідентичності для елементів масиву обʼєктів (`steps`), spec §7.2. */
|
|
24
|
+
const ARRAY_IDENTITY_KEYS = ['id', 'uses', 'name']
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Поле-ідентичність canonical-елемента масиву (перше наявне з {@link ARRAY_IDENTITY_KEYS}).
|
|
28
|
+
* @param {unknown} el елемент canonical-масиву
|
|
29
|
+
* @returns {string | null} назва поля або `null`, якщо жодного немає (fallback — позиційний індекс)
|
|
30
|
+
*/
|
|
31
|
+
function identityKeyOf(el) {
|
|
32
|
+
if (el === null || typeof el !== 'object' || Array.isArray(el)) return null
|
|
33
|
+
for (const k of ARRAY_IDENTITY_KEYS) {
|
|
34
|
+
if (typeof el[k] === 'string') return k
|
|
35
|
+
}
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Індекс елемента `actual`-масиву з тією самою identity, що й `canonEl` — `null` identity
|
|
41
|
+
* (жодного з id/uses/name) шукає точний structural збіг (JSON-рівність) як fallback.
|
|
42
|
+
* @param {unknown[]} actualArray наявний масив
|
|
43
|
+
* @param {unknown} canonEl canonical-елемент
|
|
44
|
+
* @returns {number} індекс або `-1`
|
|
45
|
+
*/
|
|
46
|
+
function findIdentityIndex(actualArray, canonEl) {
|
|
47
|
+
const idKey = identityKeyOf(canonEl)
|
|
48
|
+
if (idKey !== null) {
|
|
49
|
+
return actualArray.findIndex(a => a !== null && typeof a === 'object' && a[idKey] === canonEl[idKey])
|
|
50
|
+
}
|
|
51
|
+
return actualArray.findIndex(a => JSON.stringify(a) === JSON.stringify(canonEl))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Людинозрозумілий опис елемента масиву для тексту порушення.
|
|
56
|
+
* @param {unknown} el елемент canonical-масиву
|
|
57
|
+
* @returns {string} опис
|
|
58
|
+
*/
|
|
59
|
+
function describeElement(el) {
|
|
60
|
+
const idKey = identityKeyOf(el)
|
|
61
|
+
if (idKey !== null) return `${idKey}: ${JSON.stringify(el[idKey])}`
|
|
62
|
+
return JSON.stringify(el)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Чи `canon`-масив — масив plain-обʼєктів (identity-branch) на відміну від scalar-масиву
|
|
67
|
+
* (set-union branch). Порожній масив трактуємо як scalar (немає що ідентифікувати).
|
|
68
|
+
* @param {unknown[]} canon canonical-масив
|
|
69
|
+
* @returns {boolean} true — identity-branch
|
|
70
|
+
*/
|
|
71
|
+
function isObjectArray(canon) {
|
|
72
|
+
return canon.length > 0 && canon.every(e => e !== null && typeof e === 'object' && !Array.isArray(e))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Diff-гілка масиву обʼєктів (`steps`): identity-match кожного canonical-елемента + рекурсія в
|
|
77
|
+
* знайдений елемент. Винесена окремо — інакше {@link diffDeepSubset} перевищує поріг когнітивної
|
|
78
|
+
* складності лінтера.
|
|
79
|
+
* @param {unknown[]} actual фактичний масив
|
|
80
|
+
* @param {unknown[]} canon canonical-масив обʼєктів
|
|
81
|
+
* @param {string[]} path поточний шлях (для повідомлень)
|
|
82
|
+
* @param {string} here відформатований `path` (кеш — уникає повторного `join`)
|
|
83
|
+
* @param {string[]} out акумулятор повідомлень (мутується)
|
|
84
|
+
* @returns {void}
|
|
85
|
+
*/
|
|
86
|
+
function diffObjectArrayBranch(actual, canon, path, here, out) {
|
|
87
|
+
for (const c of canon) {
|
|
88
|
+
const idx = findIdentityIndex(actual, c)
|
|
89
|
+
if (idx === -1) {
|
|
90
|
+
out.push(`${here}: відсутній елемент (${describeElement(c)})`)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
diffDeepSubset(actual[idx], c, [...path, `[${describeElement(c)}]`], out)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Diff-гілка scalar-масиву (напр. `on.push.paths`): ordered set-subset — кожен canonical
|
|
99
|
+
* елемент має бути присутній десь в `actual` (порядок і зайві consumer-specific записи не
|
|
100
|
+
* порушення).
|
|
101
|
+
* @param {unknown[]} actual фактичний масив
|
|
102
|
+
* @param {unknown[]} canon canonical-масив скалярів
|
|
103
|
+
* @param {string} here відформатований шлях (для повідомлень)
|
|
104
|
+
* @param {string[]} out акумулятор повідомлень (мутується)
|
|
105
|
+
* @returns {void}
|
|
106
|
+
*/
|
|
107
|
+
function diffScalarArrayBranch(actual, canon, here, out) {
|
|
108
|
+
for (const needle of canon) {
|
|
109
|
+
const present = actual.some(a => JSON.stringify(a) === JSON.stringify(needle))
|
|
110
|
+
if (!present) out.push(`${here}: має містити ${JSON.stringify(needle)}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Рекурсивний deep-subset diff (`mergeStrategy: "deep-subset"`, spec §7.2): кожен шлях
|
|
116
|
+
* canonical-дерева має бути присутній в `actual` з тим самим значенням (об'єкти — рекурсія по
|
|
117
|
+
* ключах canon; scalar-масиви — set-subset; масиви обʼєктів — identity-match + рекурсія в
|
|
118
|
+
* знайдений елемент). Не мутує вхід.
|
|
119
|
+
* @param {unknown} actual фактичне значення з документа
|
|
120
|
+
* @param {unknown} canon canonical-фрагмент
|
|
121
|
+
* @param {string[]} path поточний шлях (для повідомлень)
|
|
122
|
+
* @param {string[]} out акумулятор повідомлень (мутується)
|
|
123
|
+
* @returns {void}
|
|
124
|
+
*/
|
|
125
|
+
function diffDeepSubset(actual, canon, path, out) {
|
|
126
|
+
if (canon === null || canon === undefined) return
|
|
127
|
+
const here = path.join('.') || '(root)'
|
|
128
|
+
if (Array.isArray(canon)) {
|
|
129
|
+
if (!Array.isArray(actual)) {
|
|
130
|
+
out.push(`${here}: має бути масивом`)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (isObjectArray(canon)) diffObjectArrayBranch(actual, canon, path, here, out)
|
|
134
|
+
else diffScalarArrayBranch(actual, canon, here, out)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (typeof canon === 'object') {
|
|
138
|
+
if (actual === null || typeof actual !== 'object' || Array.isArray(actual)) {
|
|
139
|
+
out.push(`${here}: має бути обʼєктом`)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
for (const [k, v] of Object.entries(canon)) diffDeepSubset(actual[k], v, [...path, k], out)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
if (actual !== canon) out.push(`${here}: має бути ${JSON.stringify(canon)} (отримано ${JSON.stringify(actual)})`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Merge-гілка масиву обʼєктів (`steps`) у YAML `Document`: identity-match + рекурсія у поля
|
|
150
|
+
* знайденого елемента, або додавання нового елемента, якщо identity не знайдено. Винесена
|
|
151
|
+
* окремо — інакше {@link mergeYamlDocIdentity} перевищує поріг когнітивної складності лінтера.
|
|
152
|
+
* @param {import('yaml').Document} doc YAML-документ (мутується)
|
|
153
|
+
* @param {unknown[]} canon canonical-масив обʼєктів
|
|
154
|
+
* @param {Array<string|number>} path шлях масиву в документі
|
|
155
|
+
* @returns {void}
|
|
156
|
+
*/
|
|
157
|
+
function mergeObjectArrayBranch(doc, canon, path) {
|
|
158
|
+
for (const c of canon) {
|
|
159
|
+
const existingJs = doc.getIn(path)?.toJS(doc) ?? []
|
|
160
|
+
const idx = findIdentityIndex(existingJs, c)
|
|
161
|
+
if (idx === -1) {
|
|
162
|
+
doc.addIn(path, c)
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
for (const [k, v] of Object.entries(c)) mergeYamlDocIdentity(doc, v, [...path, idx, k])
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Merge-гілка scalar-масиву у YAML `Document`: ordered set-union — додає лише елементи canon,
|
|
171
|
+
* яких ще немає в `actual` (structural JSON-рівність), нічого не видаляє.
|
|
172
|
+
* @param {import('yaml').Document} doc YAML-документ (мутується)
|
|
173
|
+
* @param {unknown[]} canon canonical-масив скалярів
|
|
174
|
+
* @param {Array<string|number>} path шлях масиву в документі
|
|
175
|
+
* @returns {void}
|
|
176
|
+
*/
|
|
177
|
+
function mergeScalarArrayBranch(doc, canon, path) {
|
|
178
|
+
const existingJs = doc.getIn(path)?.toJS(doc) ?? []
|
|
179
|
+
for (const needle of canon) {
|
|
180
|
+
const present = existingJs.some(a => JSON.stringify(a) === JSON.stringify(needle))
|
|
181
|
+
if (!present) doc.addIn(path, needle)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Deep-merge `canon` у YAML `Document` за `path` (мутує `doc`) — identity-aware варіант
|
|
187
|
+
* `template-deep-merge.mjs#mergeYamlDoc`: масиви обʼєктів мерджаться за identity (лише поля
|
|
188
|
+
* знайденого елемента), а не «додати ще одну копію, якщо не structurally contained».
|
|
189
|
+
* @param {import('yaml').Document} doc YAML-документ (мутується)
|
|
190
|
+
* @param {unknown} canon canonical-фрагмент на цьому шляху
|
|
191
|
+
* @param {Array<string|number>} path шлях у документі
|
|
192
|
+
* @returns {void}
|
|
193
|
+
*/
|
|
194
|
+
function mergeYamlDocIdentity(doc, canon, path) {
|
|
195
|
+
if (Array.isArray(canon)) {
|
|
196
|
+
if (!doc.hasIn(path)) doc.setIn(path, doc.createNode([]))
|
|
197
|
+
if (isObjectArray(canon)) mergeObjectArrayBranch(doc, canon, path)
|
|
198
|
+
else mergeScalarArrayBranch(doc, canon, path)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
if (canon !== null && typeof canon === 'object') {
|
|
202
|
+
for (const [k, v] of Object.entries(canon)) mergeYamlDocIdentity(doc, v, [...path, k])
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
doc.setIn(path, canon)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Читає й парсить canonical template (YAML) contribution-у за безпечним шляхом. Не кидає —
|
|
210
|
+
* повертає diagnostic-сумісний результат, узгоджено з фейл-safe стилем `plugin-slots.mjs`.
|
|
211
|
+
* @param {{ packageRoot: string, resourcePath: string | null }} contribution provenance
|
|
212
|
+
* @param {import('@7n/rules/plugin-api').CiArtifactDescriptor} descriptor валідований payload
|
|
213
|
+
* @returns {{ ok: true, canonical: unknown, templateText: string } | { ok: false, reason: string }} результат
|
|
214
|
+
*/
|
|
215
|
+
export async function loadCanonicalTemplate(contribution, descriptor) {
|
|
216
|
+
const resolved = resolveArtifactTemplatePath(contribution, descriptor)
|
|
217
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason }
|
|
218
|
+
if (!resolved.exists) return { ok: false, reason: `template не знайдено (${resolved.absPath})` }
|
|
219
|
+
const { parse } = await import('yaml')
|
|
220
|
+
const templateText = readFileSync(resolved.absPath, 'utf8')
|
|
221
|
+
return { ok: true, canonical: parse(templateText), templateText }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Діагностує один `ci.artifact@1` artifact (mergeStrategy `deep-subset`) проти поточного стану
|
|
226
|
+
* `targetPath` у consumer-репо.
|
|
227
|
+
* @param {{ cwd: string, targetPath: string, canonical: unknown }} args вхід
|
|
228
|
+
* @returns {{ missing: boolean, violations: string[] }} `missing` — файл відсутній (mode-специфічна обробка — рішення викликача); `violations` — mismatch-и, коли файл існує
|
|
229
|
+
*/
|
|
230
|
+
export async function diagnoseArtifact({ cwd, targetPath, canonical }) {
|
|
231
|
+
const absTarget = join(cwd, targetPath)
|
|
232
|
+
if (!existsSync(absTarget)) return { missing: true, violations: [] }
|
|
233
|
+
const { parse } = await import('yaml')
|
|
234
|
+
let actual
|
|
235
|
+
try {
|
|
236
|
+
actual = parse(readFileSync(absTarget, 'utf8'))
|
|
237
|
+
} catch {
|
|
238
|
+
return { missing: false, violations: [`${targetPath}: не парситься як YAML`] }
|
|
239
|
+
}
|
|
240
|
+
const out = []
|
|
241
|
+
diffDeepSubset(actual, canonical, [], out)
|
|
242
|
+
return { missing: false, violations: out }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Застосовує T0-фікс одного `ci.artifact@1` artifact-у до `targetPath` (deep-subset,
|
|
247
|
+
* idempotent): файл відсутній → копіюється `templateText` як є; файл існує → identity-aware
|
|
248
|
+
* deep-merge через YAML `Document` (зберігає коментарі/форматування наявного файлу, як
|
|
249
|
+
* `template-deep-merge.mjs`).
|
|
250
|
+
* @param {{ cwd: string, targetPath: string, canonical: unknown, templateText: string, recordWrite?: (absPath: string) => void }} args вхід
|
|
251
|
+
* @returns {Promise<{ touchedFiles: string[] }>} torn-файли (T0Result-сумісно)
|
|
252
|
+
*/
|
|
253
|
+
export async function applyDeepSubsetFix({ cwd, targetPath, canonical, templateText, recordWrite }) {
|
|
254
|
+
const absTarget = join(cwd, targetPath)
|
|
255
|
+
if (!existsSync(absTarget)) {
|
|
256
|
+
recordWrite?.(absTarget)
|
|
257
|
+
const { mkdirSync } = await import('node:fs')
|
|
258
|
+
mkdirSync(dirname(absTarget), { recursive: true })
|
|
259
|
+
writeFileSync(absTarget, templateText, 'utf8')
|
|
260
|
+
return { touchedFiles: [absTarget] }
|
|
261
|
+
}
|
|
262
|
+
const prevText = readFileSync(absTarget, 'utf8')
|
|
263
|
+
const before = []
|
|
264
|
+
const { parse, parseDocument } = await import('yaml')
|
|
265
|
+
diffDeepSubset(parse(prevText), canonical, [], before)
|
|
266
|
+
if (before.length === 0) return { touchedFiles: [] } // вже канонічний — idempotent no-op
|
|
267
|
+
|
|
268
|
+
const doc = parseDocument(prevText)
|
|
269
|
+
if (doc.errors.length > 0) return { touchedFiles: [] } // невалідний YAML — не чіпаємо детермінованим фіксом
|
|
270
|
+
if (canonical !== null && typeof canonical === 'object' && !Array.isArray(canonical)) {
|
|
271
|
+
for (const [k, v] of Object.entries(canonical)) mergeYamlDocIdentity(doc, v, [k])
|
|
272
|
+
}
|
|
273
|
+
const nextText = doc.toString()
|
|
274
|
+
if (nextText === prevText) return { touchedFiles: [] }
|
|
275
|
+
recordWrite?.(absTarget)
|
|
276
|
+
writeFileSync(absTarget, nextText, 'utf8')
|
|
277
|
+
return { touchedFiles: [absTarget] }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** `loadSlotConsumer`-сумісний default export (spec §3.3): стабільний `id` + `validate(payload)`. */
|
|
281
|
+
export default {
|
|
282
|
+
id: 'ci-github-artifact',
|
|
283
|
+
/**
|
|
284
|
+
* @param {unknown} payload сирий (розпарсений) `ci.artifact@1` payload
|
|
285
|
+
* @returns {boolean} true — валідний payload
|
|
286
|
+
*/
|
|
287
|
+
validate(payload) {
|
|
288
|
+
return validateCiArtifactPayload(payload).ok
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: ci-artifact-consumer.mjs
|
|
4
|
+
resource: plugins/ci-github/slots/ci-artifact-consumer.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: fae878c9
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Generic consumer слоту `ci.artifact@1` для `@7n/rules-ci-github` (spec `2026-07-27-universal-plugin-slots-lang-php-extraction`, §7.2). Матеріалізує GitHub Actions workflow-артефакти будь-якого language-плагіна — без жодного PHP чи іншого мовного literal у цьому пакеті.
|
|
10
|
+
|
|
11
|
+
## Поведінка
|
|
12
|
+
|
|
13
|
+
`mergeStrategy: "deep-subset"` — рекурсивний structural merge: об'єкти мерджаться по ключах (природно покриває `jobs` — вже keyed object у GH workflow YAML); scalar-масиви (напр. `on.push.paths`) — ordered set-union без видалення consumer-specific записів; масиви обʼєктів (напр. `steps`) — identity-based: canonical-елемент шукається в actual-масиві за першим наявним полем `id` → `uses` → `name`, і лише його поля мерджаться — решта не чіпається.
|
|
14
|
+
|
|
15
|
+
1. **`loadCanonicalTemplate(contribution, descriptor)`** резолвить безпечний шлях `template` (`resolveArtifactTemplatePath`), читає й парсить його як YAML.
|
|
16
|
+
2. **`diagnoseArtifact({ cwd, targetPath, canonical })`** порівнює поточний стан `targetPath` з canonical-фрагментом (`diffDeepSubset`). Файл відсутній → `{ missing: true, violations: [] }` (mode-специфічна обробка — рішення викликача); файл є → список mismatch-повідомлень.
|
|
17
|
+
3. **`applyDeepSubsetFix({ cwd, targetPath, canonical, templateText, recordWrite })`** — T0-фікс: файл відсутній → копіюється `templateText` як є; файл існує → identity-aware deep-merge через YAML `Document` (зберігає коментарі/форматування наявного файлу). Idempotent: якщо diff порожній — жодного запису.
|
|
18
|
+
|
|
19
|
+
## Публічний API
|
|
20
|
+
|
|
21
|
+
`loadCanonicalTemplate` — читає й парсить canonical template одного artifact-у.
|
|
22
|
+
`diagnoseArtifact` — діагностує стан `targetPath` проти canonical-фрагмента.
|
|
23
|
+
`applyDeepSubsetFix` — застосовує T0-фікс (створення або identity-aware merge).
|
|
24
|
+
Default export — `loadSlotConsumer`-сумісний `{ id: 'ci-github-artifact', validate(payload) }`.
|
|
25
|
+
|
|
26
|
+
## Гарантії поведінки
|
|
27
|
+
|
|
28
|
+
* **Identity, не позиційний match**: елементи масивів обʼєктів ідентифікуються за `id`/`uses`/`name`, тому зайві consumer-specific поля на тому самому кроці не спричиняють дублювання при фіксі.
|
|
29
|
+
* **Ідемпотентність**: `applyDeepSubsetFix` спершу перевіряє diff і не пише файл, якщо канон уже задоволений.
|
|
30
|
+
* **Не володіє discovery**: не імпортує `plugin-slots.mjs` напряму — отримує `contribution`/`descriptor` вже перевіреними від викликача (generic rule).
|
|
@@ -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/workflows/lint-php.yml",
|
|
7
|
-
"required": true
|
|
8
|
-
},
|
|
9
|
-
"missingMessage": ".github/workflows/lint-php.yml не існує — створи за каноном правила (mixin @7n/rules-ci-github)"
|
|
10
|
-
}
|
|
11
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: fix-lint_php_yml.mjs
|
|
4
|
-
resource: plugins/ci-github/rules/php/lint_php_yml/fix-lint_php_yml.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 0699f2b1
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
tier: local-min
|
|
9
|
-
score: 100
|
|
10
|
-
issues: judge:error
|
|
11
|
-
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## Огляд
|
|
15
|
-
|
|
16
|
-
Приводить конфігурації CI-workflow для PHP-домену у консюмер-репозиторії до канонічного шаблону концерну `lint_php_yml`. Гарантує відповідність YAML-файлів лінту встановленому стандарту, додаючи відсутні ключі та застосовуючи канонічні значення, зберігаючи при цьому вихідні коментарі та ключі, що не входять до шаблону. Це забезпечує єдність збірки та лінтування в системі.
|
|
17
|
-
|
|
18
|
-
## Поведінка
|
|
19
|
-
|
|
20
|
-
1. Перевіряє файли, що знаходяться у каталозі `.github/workflows/lint-php.yml`.
|
|
21
|
-
2. Використовує `patterns` для застосування канонічного шаблону концерну `lint_php_yml` до цільового файлу.
|
|
22
|
-
3. При застосуванні шаблону відсутні ключі додаються, а канонічні значення мають пріоритет.
|
|
23
|
-
4. Коментарі та ключі, які знаходяться поза шаблоном, залишаються без змін.
|
|
24
|
-
5. Якщо файл цільового шляху відсутній, він створюється на основі наданого snippet-а.
|
|
25
|
-
6. Ігнорує шляхи `.github` та `.git` під час перевірки.
|
|
26
|
-
|
|
27
|
-
## Публічний API
|
|
28
|
-
|
|
29
|
-
- patterns — Один детермінований патерн: deep-merge канонічного snippet-а концерну в
|
|
30
|
-
`.github/workflows/lint-php.yml` консюмер-репо — відсутні ключі додаються,
|
|
31
|
-
канонічні значення мають пріоритет, коментарі й ключі поза шаблоном не
|
|
32
|
-
чіпаються; якщо файлу немає — створюється зі snippet-а.
|
|
33
|
-
|
|
34
|
-
## Гарантії поведінки
|
|
35
|
-
|
|
36
|
-
- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
|
|
37
|
-
- Свідомо пропускає шляхи: `.github`, `.git`.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Directory Index
|
|
3
|
-
title: plugins/ci-github/rules/php/lint_php_yml
|
|
4
|
-
resource: plugins/ci-github/rules/php/lint_php_yml/
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
| Файл | Тип |
|
|
8
|
-
| ------------------------------------------- | --------- |
|
|
9
|
-
| [fix-lint_php_yml.mjs](fix-lint_php_yml.md) | JS Module |
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* T0-фікс концерну lint_php_yml: приводить CI-workflow лінту php-домену
|
|
3
|
-
* у консюмер-репо до канонічного шаблону концерну.
|
|
4
|
-
*/
|
|
5
|
-
import { createTemplateFixPattern } from '@7n/rules/scripts/lib/fix/template-deep-merge.mjs'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Один детермінований патерн: deep-merge канонічного snippet-а концерну в
|
|
9
|
-
* `.github/workflows/lint-php.yml` консюмер-репо — відсутні ключі додаються,
|
|
10
|
-
* канонічні значення мають пріоритет, коментарі й ключі поза шаблоном не
|
|
11
|
-
* чіпаються; якщо файлу немає — створюється зі snippet-а.
|
|
12
|
-
*/
|
|
13
|
-
export const patterns = [
|
|
14
|
-
createTemplateFixPattern({ id: 'php-lint_php_yml-template', targetPath: '.github/workflows/lint-php.yml' })
|
|
15
|
-
]
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
## Структура `.github/workflows/lint-php.yml`
|
|
2
|
-
|
|
3
|
-
Rego-пакет: `php.lint_php_yml`
|
|
4
|
-
|
|
5
|
-
Цільовий файл: `.github/workflows/lint-php.yml` (парситься як YAML).
|
|
6
|
-
|
|
7
|
-
Перевіряє, що у workflow присутні всі канонічні `run:`-кроки з template-сніпету — крок `n-rules lint php --no-fix` має бути в одному зі steps jobs.
|
|
8
|
-
|
|
9
|
-
Канонічний template-сніпет: [lint-php.yml.snippet.yml](./template/lint-php.yml.snippet.yml)
|
|
10
|
-
|
|
11
|
-
**✓ Правильно** — job `php` містить крок:
|
|
12
|
-
|
|
13
|
-
```yaml
|
|
14
|
-
- name: Lint PHP
|
|
15
|
-
run: n-rules lint php --no-fix
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
**✗ Неправильно** — крок відсутній або містить інший run-рядок:
|
|
19
|
-
|
|
20
|
-
```yaml
|
|
21
|
-
- name: Lint PHP
|
|
22
|
-
run: echo something
|
|
23
|
-
```
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Перевірка `lint-php.yml` (php.mdc).
|
|
2
|
-
#
|
|
3
|
-
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
-
# Структура --data сформована з template/lint-php.yml.snippet.yml.
|
|
5
|
-
# Маркер `run:` збирається з template's php-job steps.
|
|
6
|
-
# Універсальні workflow-перевірки — у `ga.workflow_common`.
|
|
7
|
-
package php.lint_php_yml
|
|
8
|
-
|
|
9
|
-
import rego.v1
|
|
10
|
-
|
|
11
|
-
# Очікуваний `run:` маркер — конкатенація всіх run-блоків з template.
|
|
12
|
-
expected_run_blob := concat("\n", [r |
|
|
13
|
-
some step in data.template.snippet.jobs.php.steps
|
|
14
|
-
r := object.get(step, "run", "")
|
|
15
|
-
r != ""
|
|
16
|
-
])
|
|
17
|
-
|
|
18
|
-
all_run_text := concat("\n", [run_text |
|
|
19
|
-
some job in object.get(input, "jobs", {})
|
|
20
|
-
some step in object.get(job, "steps", [])
|
|
21
|
-
run_text := step_run_to_text(step)
|
|
22
|
-
])
|
|
23
|
-
|
|
24
|
-
deny contains msg if {
|
|
25
|
-
expected_run_blob != ""
|
|
26
|
-
not contains(all_run_text, expected_run_blob)
|
|
27
|
-
msg := sprintf("lint-php.yml: жоден крок run не містить %q (php.mdc)", [expected_run_blob])
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
step_run_to_text(step) := step.run if is_string(step.run)
|
|
31
|
-
|
|
32
|
-
else := concat("\n", [s | some s in step.run]) if is_array(step.run)
|
|
33
|
-
|
|
34
|
-
else := ""
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
name: Lint PHP
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- dev
|
|
7
|
-
- main
|
|
8
|
-
paths:
|
|
9
|
-
- '**/*.php'
|
|
10
|
-
- 'composer.json'
|
|
11
|
-
- 'composer.lock'
|
|
12
|
-
- 'phpstan.neon'
|
|
13
|
-
- 'phpstan.neon.dist'
|
|
14
|
-
- 'psalm.xml'
|
|
15
|
-
- '.github/workflows/lint-php.yml'
|
|
16
|
-
|
|
17
|
-
pull_request:
|
|
18
|
-
branches:
|
|
19
|
-
- dev
|
|
20
|
-
- main
|
|
21
|
-
|
|
22
|
-
concurrency:
|
|
23
|
-
group: ${{ github.ref }}-${{ github.workflow }}
|
|
24
|
-
cancel-in-progress: true
|
|
25
|
-
|
|
26
|
-
jobs:
|
|
27
|
-
php:
|
|
28
|
-
runs-on: ubuntu-latest
|
|
29
|
-
permissions:
|
|
30
|
-
contents: read
|
|
31
|
-
steps:
|
|
32
|
-
- uses: actions/checkout@v6
|
|
33
|
-
with:
|
|
34
|
-
persist-credentials: false
|
|
35
|
-
|
|
36
|
-
- uses: ./.github/actions/setup-bun-deps
|
|
37
|
-
|
|
38
|
-
- name: Install PHP
|
|
39
|
-
uses: shivammathur/setup-php@v2
|
|
40
|
-
with:
|
|
41
|
-
php-version: '8.5'
|
|
42
|
-
|
|
43
|
-
- name: Install Composer dependencies
|
|
44
|
-
run: composer install --no-interaction --no-progress --prefer-dist
|
|
45
|
-
|
|
46
|
-
- name: Lint PHP
|
|
47
|
-
run: bunx n-rules lint php --no-fix
|