Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/repo-guard.yml
Original file line number Diff line number Diff line change
@@ -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"
11 changes: 11 additions & 0 deletions .github/workflows/requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

## Формат комментариев в исходном коде

Expand Down
3 changes: 2 additions & 1 deletion repo-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/**",
Expand Down
251 changes: 251 additions & 0 deletions scripts/validate-repo-guard-workflow.js
Original file line number Diff line number Diff line change
@@ -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Валидация ПРОЙДЕНА');
Loading