feat(tools): ruff/mypy pre-flight gate in create-pr tool #52

Merged
slaid098 merged 3 commits from feat/tools/create-pr-ruff-preflight into main 2026-08-11 17:43:45 +03:00
Owner

Что сделано

Добавлен deterministic ruff/mypy pre-flight gate в .opencode/tools/create-pr.ts — вызывается между defaultBranch lookup и runGh pr create. Gate проверяет ruff format --check, ruff check и mypy перед созданием PR, блокируя PR при violations и пропуская gate для non-Python репо / broken environment.

Реализация:

  • ruffMypyPreflight(cwd) функция через spawnSync("uv", ["run", "ruff"/"mypy", ...]) — bypass permission.bash (ADR-016, deterministic gate).
  • Guard на pyproject.toml через test -f — skip gate для non-Python репо.
  • Lint targets ["src/", "tests/", ".opencode/scripts/"] — filter существующих через test -d.
  • Hard fail (ruff/mypy violations) → ❌ <tool> failed (exit N):\n<output>\n\nFix locally with: <command>\nThen amend + force-push, re-run create-pr. — PR НЕ создаётся.
  • Soft fail (uv not found / timeout / ruff not in deps) → ⚠️ ... warning в console, PR создаётся (gate skip, НЕ блокировать).
  • ruffMypyPreflight возвращает { blocked: boolean, message: string | null } — blocked=true прерывает, blocked=false + message warns и продолжает.

tests/_ts_loader.mjs расширен env vars для stub: PYPROJECT_STUB (present/missing), LINT_DIRS_STUB (comma-separated), UV_RUFF_STUB (pass/fail/timeout/nopython/notfound) + per-tool overrides UV_RUFF_FORMAT_STUB, UV_RUFF_CHECK_STUB, UV_MYPY_STUB.

7 новых тестов в tests/test_create_pr_tool.py: ruff format fail, ruff check fail, mypy fail, all pass, no pyproject, uv not found, timeout.

ADR-097 docs/decisions/097-pr-52-create-pr-ruff-preflight.md — обоснование выбора create-pr.ts vs commit.ts vs pipeline-status.py новая фаза, цитирование ADR-016 (TS spawnSync bypass permissions).

Почему

run-pipeline уходил в loop "CI упал на ruff → fix_ci → push → CI снова падает". Агент делает commit + push + PR, CI ловит ruff/mypy ошибки, проверяемые локально. Куча токенов и round-trip Forgejo CI. Локальный deterministic gate перед созданием PR предотвращает этот loop — ловит final state ветки после всех коммитов.

.pre-commit-config.yaml — отдельный layer на commit, но pre-commit install нигде не гарантирован (worktree/Docker без hook). Gate в create-pr.ts — второй layer на PR creation (финальная проверка).

Watch out

Спека issue #49 содержала противоречие: "Задача" пункт 1 код-скелет показывал return "⚠️ uv not found..." (подразумевая return строки = блокировка), но "Контракты" и "Критерии приемки" явно говорили "soft fail ⚠️, PR создаётся (НЕ блокировать)". Реализация следует "Контрактам" (explicit contract > код-скелет): soft fail → console.warn, PR создаётся. Зафиксировано в ADR-097 раздел "Спека отклонение".

ADR файл назван 097-pr-52-create-pr-ruff-preflight.md — PR номер предсказан (последний #51 → следующий #52). Если PR получит другой номер, ADR нужно переименовать.

Pending

— Переименование ADR файла если PR номер ≠ 52 (угадывал заранее).
— Gate для non-Python репо (frontend JS/TS, biome/eslint) — отдельный issue.
— Активация pre-commit install в setup/init flow — отдельный PR.
— Gate в commit.ts (per-commit) — отдельный вопрос (pre-commit hook layer).

Closes #49

Closes #49

## Что сделано Добавлен deterministic ruff/mypy pre-flight gate в `.opencode/tools/create-pr.ts` — вызывается между `defaultBranch` lookup и `runGh pr create`. Gate проверяет ruff format --check, ruff check и mypy перед созданием PR, блокируя PR при violations и пропуская gate для non-Python репо / broken environment. Реализация: - `ruffMypyPreflight(cwd)` функция через `spawnSync("uv", ["run", "ruff"/"mypy", ...])` — bypass `permission.bash` (ADR-016, deterministic gate). - Guard на `pyproject.toml` через `test -f` — skip gate для non-Python репо. - Lint targets `["src/", "tests/", ".opencode/scripts/"]` — filter существующих через `test -d`. - Hard fail (ruff/mypy violations) → `❌ <tool> failed (exit N):\n<output>\n\nFix locally with: <command>\nThen amend + force-push, re-run create-pr.` — PR НЕ создаётся. - Soft fail (uv not found / timeout / ruff not in deps) → `⚠️ ...` warning в console, PR создаётся (gate skip, НЕ блокировать). - `ruffMypyPreflight` возвращает `{ blocked: boolean, message: string | null }` — `blocked=true` прерывает, `blocked=false` + message warns и продолжает. `tests/_ts_loader.mjs` расширен env vars для stub: `PYPROJECT_STUB` (present/missing), `LINT_DIRS_STUB` (comma-separated), `UV_RUFF_STUB` (pass/fail/timeout/nopython/notfound) + per-tool overrides `UV_RUFF_FORMAT_STUB`, `UV_RUFF_CHECK_STUB`, `UV_MYPY_STUB`. 7 новых тестов в `tests/test_create_pr_tool.py`: ruff format fail, ruff check fail, mypy fail, all pass, no pyproject, uv not found, timeout. ADR-097 `docs/decisions/097-pr-52-create-pr-ruff-preflight.md` — обоснование выбора `create-pr.ts` vs `commit.ts` vs `pipeline-status.py` новая фаза, цитирование ADR-016 (TS spawnSync bypass permissions). ## Почему `run-pipeline` уходил в loop "CI упал на ruff → fix_ci → push → CI снова падает". Агент делает commit + push + PR, CI ловит ruff/mypy ошибки, проверяемые локально. Куча токенов и round-trip Forgejo CI. Локальный deterministic gate перед созданием PR предотвращает этот loop — ловит final state ветки после всех коммитов. `.pre-commit-config.yaml` — отдельный layer на commit, но `pre-commit install` нигде не гарантирован (worktree/Docker без hook). Gate в `create-pr.ts` — второй layer на PR creation (финальная проверка). ## Watch out Спека issue #49 содержала противоречие: "Задача" пункт 1 код-скелет показывал `return "⚠️ uv not found..."` (подразумевая return строки = блокировка), но "Контракты" и "Критерии приемки" явно говорили "soft fail ⚠️, PR создаётся (НЕ блокировать)". Реализация следует "Контрактам" (explicit contract > код-скелет): soft fail → `console.warn`, PR создаётся. Зафиксировано в ADR-097 раздел "Спека отклонение". ADR файл назван `097-pr-52-create-pr-ruff-preflight.md` — PR номер предсказан (последний #51 → следующий #52). Если PR получит другой номер, ADR нужно переименовать. ## Pending — Переименование ADR файла если PR номер ≠ 52 (угадывал заранее). — Gate для non-Python репо (frontend JS/TS, biome/eslint) — отдельный issue. — Активация `pre-commit install` в setup/init flow — отдельный PR. — Gate в `commit.ts` (per-commit) — отдельный вопрос (pre-commit hook layer). Closes #49 Closes #49
docs(adr): add ADR-097 create-pr ruff/mypy pre-flight gate
All checks were successful
CI (always) / bootstrap (pull_request) Successful in 7s
CI / bootstrap (pull_request) Successful in 10s
CI / lint (pull_request) Successful in 26s
CI / complexity (pull_request) Successful in 26s
CI / typecheck (pull_request) Successful in 27s
CI / test (3.13) (pull_request) Successful in 1m39s
896ccf3a16
Author
Owner

Code Review Summary

Pre-flight ruff/mypy gate в create-pr.ts — качественная реализация с thorough error handling (4 failure mode: ENOENT, SIGTERM timeout, NOT_FOUND_RE infrastructure, exit!=0 violations), 7 тестов покрывают все пути, ADR-097 детально обосновывает выбор create-pr.ts vs commit.ts vs pipeline-status.py. Cross-file контракт с run-pipeline/SKILL.md Template A сохранён (tool возвращает string, gate прозрачен для caller).

Positives

  • Константы извлечены (LINT_TARGETS, RUFF_TIMEOUT_MS, MYPY_TIMEOUT_MS, NOT_FOUND_RE) — нет magic numbers
  • filterExistingTargets helper — чистая фильтрация существующих директорий
  • Soft fail vs hard fail разделение корректно: infrastructure failure (uv not found, timeout, ruff not in deps) НЕ блокирует PR; violations блокируют. Соответствует issue #49 Контракты
  • 7 тестов с descriptive names, каждый имеет assertions, stubs через env vars (deterministic, no real subprocess)
  • ADR рассматривает 4 альтернативы, фиксирует отклонение от спеки (explicit contract > код-скелет)
  • Gate placement (после defaultBranch lookup, до runGh pr create) — правильная позиция для pre-PR check

Suggestions (info, not blocking)

  • create-pr.ts:35-86 [duplication] Паттерн spawnSync → error check → SIGTERM check → status check → NOT_FOUND_RE → hard fail повторяется 3x (ruffFormat, ruffCheck, mypy). Можно извлечь helper runGateTool(cmd, args, timeout, label, fixHint): {blocked, message} — сократит ~30 строк. На пороге абстракции (3 = минимум), текущий код читаем, не блокирующее.
  • create-pr.ts:41 [error message] ruffFormat.error ловит любой spawn error (ENOENT, EACCES, etc.), но message говорит "uv not found". Для EACCES message будет неточным. На практике ENOENT — доминирующий случай, minor.
  • docs/decisions/097-pr-52-create-pr-ruff-preflight.md [style] Нет trailing newline (\ No newline at end of file в diff). Некоторые pre-commit hooks flag это.
  • create-pr.ts:78 [scope] mypy проверяет только src/, не .opencode/scripts/ (хотя scripts в LINT_TARGETS для ruff). Design choice, зафиксирован в ADR — OK, но стоит явно отметить в ADR почему mypy scope уже ruff scope.

Verdict: APPROVE

## Code Review Summary Pre-flight ruff/mypy gate в `create-pr.ts` — качественная реализация с thorough error handling (4 failure mode: ENOENT, SIGTERM timeout, NOT_FOUND_RE infrastructure, exit!=0 violations), 7 тестов покрывают все пути, ADR-097 детально обосновывает выбор `create-pr.ts` vs `commit.ts` vs `pipeline-status.py`. Cross-file контракт с `run-pipeline/SKILL.md` Template A сохранён (tool возвращает string, gate прозрачен для caller). ### Positives - Константы извлечены (`LINT_TARGETS`, `RUFF_TIMEOUT_MS`, `MYPY_TIMEOUT_MS`, `NOT_FOUND_RE`) — нет magic numbers - `filterExistingTargets` helper — чистая фильтрация существующих директорий - Soft fail vs hard fail разделение корректно: infrastructure failure (uv not found, timeout, ruff not in deps) НЕ блокирует PR; violations блокируют. Соответствует issue #49 Контракты - 7 тестов с descriptive names, каждый имеет assertions, stubs через env vars (deterministic, no real subprocess) - ADR рассматривает 4 альтернативы, фиксирует отклонение от спеки (explicit contract > код-скелет) - Gate placement (после `defaultBranch` lookup, до `runGh pr create`) — правильная позиция для pre-PR check ### Suggestions (info, not blocking) - **create-pr.ts:35-86** [duplication] Паттерн `spawnSync → error check → SIGTERM check → status check → NOT_FOUND_RE → hard fail` повторяется 3x (ruffFormat, ruffCheck, mypy). Можно извлечь helper `runGateTool(cmd, args, timeout, label, fixHint): {blocked, message}` — сократит ~30 строк. На пороге абстракции (3 = минимум), текущий код читаем, не блокирующее. - **create-pr.ts:41** [error message] `ruffFormat.error` ловит любой spawn error (ENOENT, EACCES, etc.), но message говорит "uv not found". Для EACCES message будет неточным. На практике ENOENT — доминирующий случай, minor. - **docs/decisions/097-pr-52-create-pr-ruff-preflight.md** [style] Нет trailing newline (`\ No newline at end of file` в diff). Некоторые pre-commit hooks flag это. - **create-pr.ts:78** [scope] mypy проверяет только `src/`, не `.opencode/scripts/` (хотя scripts в `LINT_TARGETS` для ruff). Design choice, зафиксирован в ADR — OK, но стоит явно отметить в ADR почему mypy scope уже ruff scope. ### Verdict: APPROVE
slaid098 deleted branch feat/tools/create-pr-ruff-preflight 2026-08-11 17:43:45 +03:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/opencode-config!52
No description provided.