From 849d5ee15b2fd0913256fa8ab2e9c2a4bc8cd808 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 19 Apr 2026 21:56:12 +0000 Subject: [PATCH 1/2] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/netkeep80/pjson/issues/17 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..253eeaf --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-04-19T21:56:12.856Z for PR creation at branch issue-17-263cc7dd79fc for issue https://github.com/netkeep80/pjson/issues/17 \ No newline at end of file From a95b8e550e964a220d388717562db8314851fa26 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 19 Apr 2026 22:04:58 +0000 Subject: [PATCH 2/2] Add repo-guard PR workflow --- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .github/workflows/repo-guard.yml | 36 ++++ .github/workflows/requirements.yml | 11 ++ .gitkeep | 1 - README.md | 24 ++- repo-policy.json | 3 +- scripts/validate-repo-guard-workflow.js | 251 ++++++++++++++++++++++++ 7 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/repo-guard.yml delete mode 100644 .gitkeep create mode 100644 scripts/validate-repo-guard-workflow.js diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 72addbb..3677b9c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,8 +14,10 @@ budgets: max_new_files: 6 max_new_docs: 1 max_net_added_lines: 800 -# Add requirement IDs such as FR-001 when this PR affects, implements, -# or verifies them. +# Put requirement IDs such as FR-001 in anchors.affects when the diff changes +# behavior, docs, tests, scripts, CI, or policy tied to those requirements. +# Use anchors.implements for new implementation work and anchors.verifies for +# new tests/checks that verify a requirement. anchors: affects: [] implements: [] diff --git a/.github/workflows/repo-guard.yml b/.github/workflows/repo-guard.yml new file mode 100644 index 0000000..aa56474 --- /dev/null +++ b/.github/workflows/repo-guard.yml @@ -0,0 +1,36 @@ +name: repo-guard policy check + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, edited] + branches: ["**"] + +permissions: + contents: read + issues: read + pull-requests: read + +jobs: + policy-check: + name: repo-guard requirements policy + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && !github.event.pull_request.draft + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run repo-guard + id: repo_guard + uses: netkeep80/repo-guard@88fdc275cbc9bd835cc20c638d83d832027182c7 + with: + mode: check-pr + enforcement: blocking + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish repo-guard summary + if: always() + run: | + echo "repo-guard result: ${{ steps.repo_guard.outputs.result }}" >> "$GITHUB_STEP_SUMMARY" + echo "${{ steps.repo_guard.outputs.summary }}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/requirements.yml b/.github/workflows/requirements.yml index 20f4bfc..edc439e 100644 --- a/.github/workflows/requirements.yml +++ b/.github/workflows/requirements.yml @@ -6,13 +6,21 @@ on: - 'requirements/**' - 'scripts/validate-requirements.js' - 'scripts/validate-docs-headings.js' + - 'scripts/validate-repo-guard-workflow.js' - 'docs/**' + - 'repo-policy.json' + - '.github/workflows/**' + - '.github/PULL_REQUEST_TEMPLATE.md' pull_request: paths: - 'requirements/**' - 'scripts/validate-requirements.js' - 'scripts/validate-docs-headings.js' + - 'scripts/validate-repo-guard-workflow.js' - 'docs/**' + - 'repo-policy.json' + - '.github/workflows/**' + - '.github/PULL_REQUEST_TEMPLATE.md' jobs: validate: @@ -31,3 +39,6 @@ jobs: - name: Валидация трассировки заголовков документации run: node scripts/validate-docs-headings.js + + - name: Валидация repo-guard workflow + run: node scripts/validate-repo-guard-workflow.js diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 253eeaf..0000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-04-19T21:56:12.856Z for PR creation at branch issue-17-263cc7dd79fc for issue https://github.com/netkeep80/pjson/issues/17 \ No newline at end of file diff --git a/README.md b/README.md index 65d17e2..eb0918e 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,30 @@ node scripts/validate-requirements.js node /path/to/repo-guard/src/repo-guard.mjs --repo-root . ``` +### PR workflow repo-guard + +PR-gate находится в [`.github/workflows/repo-guard.yml`](.github/workflows/repo-guard.yml). +Workflow запускает `repo-guard` в `check-pr` режиме для событий +`pull_request`, использует `fetch-depth: 0` для корректного diff +`base...head` и передаёт `GH_TOKEN` для чтения PR body или связанной issue. +Начальный режим enforcement — `blocking`, потому что `repo-policy.json` уже +содержит requirements-aware rules, diff budgets и file-level guardrails для +governance, requirements, docs, scripts и CI изменений. +Существующий workflow валидации требований остаётся параллельным контролем. + Для PR подготовлен пример change contract в -[`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md). +[`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md). В PR body +нужно сохранять блок `repo-guard-yaml` и обновлять его вместе с diff: + +- `anchors.affects` — требования, на которые влияет изменение поведения, + документации, тестов, скриптов, CI или policy; +- `anchors.implements` — требования, для которых PR добавляет реализацию; +- `anchors.verifies` — требования, для которых PR добавляет тест или другой + исполняемый check. + +Заявленные в `anchors.affects` требования должны сопровождаться evidence-файлом +из списков `must_touch_any` в `repo-policy.json`; иначе repo-guard покажет +диагностику в GitHub Actions summary и заблокирует PR. ## Формат комментариев в исходном коде diff --git a/repo-policy.json b/repo-policy.json index 4e3bfde..ed3a544 100644 --- a/repo-policy.json +++ b/repo-policy.json @@ -31,7 +31,8 @@ "requirements/README.md", "requirements/schemas/", "scripts/validate-requirements.js", - "scripts/validate-docs-headings.js" + "scripts/validate-docs-headings.js", + "scripts/validate-repo-guard-workflow.js" ], "operational_paths": [ ".claude/**", diff --git a/scripts/validate-repo-guard-workflow.js b/scripts/validate-repo-guard-workflow.js new file mode 100644 index 0000000..37b91aa --- /dev/null +++ b/scripts/validate-repo-guard-workflow.js @@ -0,0 +1,251 @@ +#!/usr/bin/env node + +/** + * @req FR-006 — Валидация трассировки требований в CI + * + * Проверяет, что repo-guard подключён как PR-gate с requirements-aware policy + * и что шаблон PR/документация описывают контракт изменения. + * + * Использование: node scripts/validate-repo-guard-workflow.js + * Код возврата: 0 если все проверки пройдены, 1 если есть ошибки. + */ + +const fs = require('fs'); +const path = require('path'); + +const PROJECT_ROOT = path.join(__dirname, '..'); +const EXPECTED_ACTION = 'netkeep80/repo-guard@88fdc275cbc9bd835cc20c638d83d832027182c7'; +const EXPECTED_ENFORCEMENT = 'blocking'; + +let errors = 0; + +function error(msg) { + console.error(`ОШИБКА: ${msg}`); + errors++; +} + +function info(msg) { + console.log(`ИНФО: ${msg}`); +} + +function readText(relPath) { + const fullPath = path.join(PROJECT_ROOT, relPath); + if (!fs.existsSync(fullPath)) { + error(`${relPath}: файл не найден`); + return null; + } + return fs.readFileSync(fullPath, 'utf8'); +} + +function readJson(relPath) { + const text = readText(relPath); + if (text === null) return null; + + try { + return JSON.parse(text); + } catch (e) { + error(`${relPath}: невалидный JSON - ${e.message}`); + return null; + } +} + +function requireContains(label, text, needle, message) { + if (!text.includes(needle)) { + error(`${label}: ${message}`); + } +} + +function requireNotContains(label, text, needle, message) { + if (text.includes(needle)) { + error(`${label}: ${message}`); + } +} + +function requireArrayIncludes(label, values, expected) { + if (!Array.isArray(values) || !values.includes(expected)) { + error(`${label}: отсутствует '${expected}'`); + } +} + +info('Проверка repo-guard workflow...'); + +const workflow = readText('.github/workflows/repo-guard.yml'); +if (workflow !== null) { + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + 'pull_request:', + 'workflow должен запускаться в pull_request контексте' + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + 'types: [opened, synchronize, reopened, ready_for_review, edited]', + 'workflow должен перезапускаться при изменении PR body/контракта' + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + 'contents: read', + 'workflow должен задавать минимальные permissions.contents' + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + 'issues: read', + 'workflow должен разрешать чтение linked issue для fallback-контракта' + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + 'pull-requests: read', + 'workflow должен разрешать чтение PR body' + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + 'fetch-depth: 0', + 'repo-guard нужен полный git history для diff base...head' + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + `uses: ${EXPECTED_ACTION}`, + `workflow должен использовать pinned Action ${EXPECTED_ACTION}` + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + 'mode: check-pr', + 'workflow должен запускать repo-guard в режиме check-pr' + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + `enforcement: ${EXPECTED_ENFORCEMENT}`, + `workflow должен использовать режим ${EXPECTED_ENFORCEMENT}` + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + 'GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}', + 'workflow должен передавать GH_TOKEN для чтения PR/issue контекста' + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + 'if: always()', + 'workflow должен публиковать summary даже при нарушениях policy' + ); + requireContains( + '.github/workflows/repo-guard.yml', + workflow, + '$GITHUB_STEP_SUMMARY', + 'workflow должен выводить readable diagnostics в job summary' + ); + requireNotContains( + '.github/workflows/repo-guard.yml', + workflow, + 'continue-on-error', + 'workflow не должен маскировать runtime/configuration failures' + ); + requireNotContains( + '.github/workflows/repo-guard.yml', + workflow, + 'git clone https://github.com/netkeep80/repo-guard.git', + 'workflow должен использовать reusable Action, а не ручной clone' + ); + requireNotContains( + '.github/workflows/repo-guard.yml', + workflow, + 'node /tmp/repo-guard/src/repo-guard.mjs', + 'workflow не должен запускать временно клонированный CLI напрямую' + ); +} + +info('Проверка repo-policy.json...'); + +const policy = readJson('repo-policy.json'); +if (policy !== null) { + const policyMode = policy.enforcement && policy.enforcement.mode; + if (policyMode !== EXPECTED_ENFORCEMENT) { + error(`repo-policy.json: enforcement.mode должен быть '${EXPECTED_ENFORCEMENT}', получено '${policyMode}'`); + } + + const governancePaths = policy.paths && policy.paths.governance_paths; + requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, 'repo-policy.json'); + requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, '.github/PULL_REQUEST_TEMPLATE.md'); + requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, '.github/workflows/'); + requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, 'scripts/validate-repo-guard-workflow.js'); + + const traceRules = Array.isArray(policy.trace_rules) ? policy.trace_rules : []; + const ruleIds = traceRules.map(rule => rule.id); + requireArrayIncludes('repo-policy.json trace_rules', ruleIds, 'code-req-refs-must-resolve'); + requireArrayIncludes('repo-policy.json trace_rules', ruleIds, 'doc-req-refs-must-resolve'); + requireArrayIncludes('repo-policy.json trace_rules', ruleIds, 'declared-affected-anchors-need-evidence'); + + const declaredAnchorsRule = traceRules.find(rule => rule.id === 'declared-affected-anchors-need-evidence'); + if (!declaredAnchorsRule || declaredAnchorsRule.contract_field !== 'anchors.affects') { + error("repo-policy.json: правило declared-affected-anchors-need-evidence должно читать contract_field 'anchors.affects'"); + } +} + +info('Проверка PR template и README...'); + +const prTemplate = readText('.github/PULL_REQUEST_TEMPLATE.md'); +if (prTemplate !== null) { + requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, '```repo-guard-yaml', 'шаблон должен содержать repo-guard YAML block'); + requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'anchors:', 'шаблон должен содержать секцию anchors'); + requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'affects:', 'шаблон должен позволять объявлять affected anchors'); + requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'implements:', 'шаблон должен позволять объявлять implemented anchors'); + requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'verifies:', 'шаблон должен позволять объявлять verified anchors'); +} + +const readme = readText('README.md'); +if (readme !== null) { + requireContains('README.md', readme, '.github/workflows/repo-guard.yml', 'документация должна упоминать PR workflow repo-guard'); + requireContains('README.md', readme, 'anchors.affects', 'документация должна объяснять affected anchors в PR body'); + requireContains('README.md', readme, 'anchors.implements', 'документация должна объяснять implemented anchors в PR body'); + requireContains('README.md', readme, 'anchors.verifies', 'документация должна объяснять verified anchors в PR body'); +} + +info('Проверка включения validator в CI...'); + +const requirementsWorkflow = readText('.github/workflows/requirements.yml'); +if (requirementsWorkflow !== null) { + requireContains( + '.github/workflows/requirements.yml', + requirementsWorkflow, + 'node scripts/validate-repo-guard-workflow.js', + 'CI должен запускать validate-repo-guard-workflow.js' + ); + requireContains( + '.github/workflows/requirements.yml', + requirementsWorkflow, + '.github/workflows/**', + 'CI должен перезапускаться при изменении workflows' + ); + requireContains( + '.github/workflows/requirements.yml', + requirementsWorkflow, + '.github/PULL_REQUEST_TEMPLATE.md', + 'CI должен перезапускаться при изменении PR template' + ); + requireContains( + '.github/workflows/requirements.yml', + requirementsWorkflow, + 'repo-policy.json', + 'CI должен перезапускаться при изменении repo-policy.json' + ); +} + +console.log(''); +console.log('=== Итоги валидации repo-guard workflow ==='); +console.log(`Ошибок: ${errors}`); + +if (errors > 0) { + console.log('\nВалидация НЕ ПРОЙДЕНА'); + process.exit(1); +} + +console.log('\nВалидация ПРОЙДЕНА');