fix(tools): add spawnSync timeout to local git calls #47

Merged
slaid098 merged 3 commits from fix/tools/synctimeout-git-calls into main 2026-08-11 17:09:11 +03:00
Owner

Что сделано

Добавил timeout опцию к spawnSync("git", ...) во всех TS-тулах, где её не было (кроме resolveForgejoRepo в _shared.ts — уже пофикшено в PR#41). При timeout возвращается читаемая ошибка вместо пустого stdout.

  • merge-pr.ts — git() helper (строки 5-18): добавлен timeout: 10000 (10с). Покрывает все 6 call sites (branch --list, rev-parse, checkout, pull, branch -D ×2). При timeout возвращает { status: null, stderr: "[git] <cmd> timed out after 10000ms" } — merge-pr graceful-fail с сообщением.
  • create-pr.ts (строка 62) — spawnSync("git rev-parse --abbrev-ref HEAD"): добавлен timeout: 5000 (5с, быстрая операция). При timeout возвращает ❌ [git] <cmd> timed out after 5000ms.
  • commit.ts — 3 spawnSync вызова:
    • git log --oneline -5 (recentCommits): timeout: 10000
    • git diff --cached --name-only: timeout: 10000
    • git commit -m: timeout: 30000 (30с — pre-commit hooks: ruff, mypy могут быть медленными на больших репо)
    • Общий helper gitTimeoutErr(cmd, ms) → [git] <cmd> timed out after Nms. Для commit добавлен hint про .git/index.lock.

Почему

PR#41 (issue #40) добавил timeout только к resolveForgejoRepo в _shared.ts. Остальные spawnSync("git", ...) в TS-тулах hang'али при медленном git (git pull на медленном remote, git checkout на FUSE/NFS, git commit с большим diff или медленными pre-commit hooks). Агент не получал ошибки — просто зависал. Теперь fail-fast с читаемой ошибкой, агент может retry.

Контракт: spawnSync(cmd, args, { timeout: N }) — Node/Bun поддерживает. При timeout процесс убивается SIGTERM → result.signal === "SIGTERM", result.status === null.

Watch out

  • git pull в merge-pr.ts:78 — 10с может быть tight на очень медленном remote (Forgejo за Nginx, медленный response). Если tight — retry на уровне выше (merge-pr уже возвращает читаемую ошибку, агент повторяет).
  • git commit с pre-commit hooks на очень большом репо может занять >30с — тогда commit fail с сообщением про .git/index.lock. Агент удаляет lock и retry.
  • При timeout git может оставить .git/index.lock — упомянуто в ошибке для commit.ts. Для merge-pr.ts/create-pr.ts lock менее вероятен (только read-операции), hint не добавлен.
  • Backward compatible: только добавляется timeout option + проверка signal === "SIGTERM". Существующие вызовы с быстрым git работают как раньше.
  • Untracked файлы (cover-*.svg в .opencode/draw-image/templates/, node_modules/) вне scope — не коммитил.

Pending

  • Тестов для merge-pr.ts/create-pr.ts/commit.ts нет (только _shared.test.ts для _shared.ts). Тесты для spawnSync timeout требуют mock'а child_process.spawnSync — вне scope этого fix.
  • Python subprocess.run git calls (pipeline-status.py, spec-status.py, project-status.py) — issue Волны 1 (Python oracles), вне scope (см. "Вне scope" в issue #44).
  • curl в skills — issue Волны 2, вне scope.

Closes #44

## Что сделано Добавил `timeout` опцию к `spawnSync("git", ...)` во всех TS-тулах, где её не было (кроме `resolveForgejoRepo` в `_shared.ts` — уже пофикшено в PR#41). При timeout возвращается читаемая ошибка вместо пустого stdout. - **`merge-pr.ts`** — `git()` helper (строки 5-18): добавлен `timeout: 10000` (10с). Покрывает все 6 call sites (branch --list, rev-parse, checkout, pull, branch -D ×2). При timeout возвращает `{ status: null, stderr: "[git] <cmd> timed out after 10000ms" }` — merge-pr graceful-fail с сообщением. - **`create-pr.ts`** (строка 62) — `spawnSync("git rev-parse --abbrev-ref HEAD")`: добавлен `timeout: 5000` (5с, быстрая операция). При timeout возвращает `❌ [git] <cmd> timed out after 5000ms`. - **`commit.ts`** — 3 `spawnSync` вызова: - `git log --oneline -5` (`recentCommits`): `timeout: 10000` - `git diff --cached --name-only`: `timeout: 10000` - `git commit -m`: `timeout: 30000` (30с — pre-commit hooks: ruff, mypy могут быть медленными на больших репо) - Общий helper `gitTimeoutErr(cmd, ms)` → `[git] <cmd> timed out after Nms`. Для commit добавлен hint про `.git/index.lock`. ## Почему PR#41 (issue #40) добавил timeout только к `resolveForgejoRepo` в `_shared.ts`. Остальные `spawnSync("git", ...)` в TS-тулах hang'али при медленном git (`git pull` на медленном remote, `git checkout` на FUSE/NFS, `git commit` с большим diff или медленными pre-commit hooks). Агент не получал ошибки — просто зависал. Теперь fail-fast с читаемой ошибкой, агент может retry. Контракт: `spawnSync(cmd, args, { timeout: N })` — Node/Bun поддерживает. При timeout процесс убивается SIGTERM → `result.signal === "SIGTERM"`, `result.status === null`. ## Watch out - `git pull` в `merge-pr.ts:78` — 10с может быть tight на очень медленном remote (Forgejo за Nginx, медленный response). Если tight — retry на уровне выше (merge-pr уже возвращает читаемую ошибку, агент повторяет). - `git commit` с pre-commit hooks на очень большом репо может занять >30с — тогда commit fail с сообщением про `.git/index.lock`. Агент удаляет lock и retry. - При timeout git может оставить `.git/index.lock` — упомянуто в ошибке для `commit.ts`. Для `merge-pr.ts`/`create-pr.ts` lock менее вероятен (только read-операции), hint не добавлен. - Backward compatible: только добавляется `timeout` option + проверка `signal === "SIGTERM"`. Существующие вызовы с быстрым git работают как раньше. - Untracked файлы (`cover-*.svg` в `.opencode/draw-image/templates/`, `node_modules/`) вне scope — не коммитил. ## Pending - Тестов для `merge-pr.ts`/`create-pr.ts`/`commit.ts` нет (только `_shared.test.ts` для `_shared.ts`). Тесты для spawnSync timeout требуют mock'а `child_process.spawnSync` — вне scope этого fix. - Python `subprocess.run` git calls (`pipeline-status.py`, `spec-status.py`, `project-status.py`) — issue Волны 1 (Python oracles), вне scope (см. "Вне scope" в issue #44). - `curl` в skills — issue Волны 2, вне scope. Closes #44
fix(tools): add spawnSync timeout to commit git calls
All checks were successful
CI (always) / bootstrap (pull_request) Successful in 4s
CI / bootstrap (pull_request) Successful in 7s
CI / lint (pull_request) Successful in 24s
CI / complexity (pull_request) Successful in 24s
CI / typecheck (pull_request) Successful in 25s
CI / test (3.13) (pull_request) Successful in 1m34s
30f49f2442
Author
Owner

Code Review Summary

PR добавляет spawnSync timeout к локальным git-вызовам в 3 TS-тулах (merge-pr.ts, create-pr.ts, commit.ts) — волна 3 в рамках hardening против зависаний git. Изменения минимальны, backward-compatible, не ломают существующие контракты.

Что правильно

  • merge-pr.ts:5-18 — git() helper централизованно добавляет timeout: 10000 ко всем 6 call sites через одну точку. При timeout (signal === "SIGTERM" && status === null) возвращает объект с status: null и читаемым stderr — callers'ы уже проверяют status === 0 / status !== 0, так что null корректно попадает в error path. Константа GIT_TIMEOUT_MS вынесена, не magic number.
  • commit.ts:15-21 — timeout'ы вынесены в именованные константы по операции (GIT_LOG_TIMEOUT_MS, GIT_DIFF_TIMEOUT_MS, GIT_COMMIT_TIMEOUT_MS). git commit имеет больший timeout (30000ms) для pre-commit hooks — корректное разделение. gitTimeoutErr() helper устраняет дублирование форматата ошибки.
  • commit.ts:74-76 — hint про .git/index.lock при SIGTERM от git commit — практически полезно, т.к. git при kill может оставить lock и следующий commit упадёт.
  • create-pr.ts:62-68 — timeout 5000ms для git rev-parse (быстрая операция, меньший timeout оправдан) + читаемая ошибка с полным cmd.
  • Backward compat: добавлен только timeout option, существующие вызовы и return-shapes сохранены. Cross-file impact: tools вызываются opencode runtime, oracle-скрипты не парсят их вывод — paired updates не требуются.

Замечания (info, не блокирующие)

  • create-pr.ts:64 [style] Magic number 5000 inline, в отличие от commit.ts и merge-pr.ts где timeout'ы вынесены в константы. Для консистентности можно вынести в const GIT_REV_PARSE_TIMEOUT_MS = 5000 и использовать в сообщении об ошибке (сейчас 5000ms захардкожено дважды: в option и в строке ошибки).
  • commit.ts:29-31 [ux] recentCommits() при timeout возвращает [git] git log --oneline -5 timed out after 10000ms, которая потом вставляется в Recent commits:\n${recentCommits(...)}. Пользователь увидит "Recent commits:\n[git] git log --oneline -5 timed out after 10000ms" — semantically странно (это не список коммитов). Можно вернуть "(git log timed out)" чтобы визуально отличалось от реальных коммитов. Не блокирующее — edge case (git log зависает крайне редко).
  • tests [coverage] Нет тестов для нового timeout-поведения git() helper в merge-pr и spawnSync-call'ов в commit. В _shared.test.ts:232 есть прецедент теста timeout для resolveForgejoRepo — можно аналогично покрыть, но spawnSync-timeout сложно мокать без реального hang, поэтому info-level.

Verdict: APPROVE

## Code Review Summary PR добавляет `spawnSync` timeout к локальным git-вызовам в 3 TS-тулах (`merge-pr.ts`, `create-pr.ts`, `commit.ts`) — волна 3 в рамках hardening против зависаний git. Изменения минимальны, backward-compatible, не ломают существующие контракты. ### Что правильно - **merge-pr.ts:5-18** — `git()` helper централизованно добавляет `timeout: 10000` ко всем 6 call sites через одну точку. При timeout (`signal === "SIGTERM" && status === null`) возвращает объект с `status: null` и читаемым `stderr` — callers'ы уже проверяют `status === 0` / `status !== 0`, так что `null` корректно попадает в error path. Константа `GIT_TIMEOUT_MS` вынесена, не magic number. - **commit.ts:15-21** — timeout'ы вынесены в именованные константы по операции (`GIT_LOG_TIMEOUT_MS`, `GIT_DIFF_TIMEOUT_MS`, `GIT_COMMIT_TIMEOUT_MS`). `git commit` имеет больший timeout (30000ms) для pre-commit hooks — корректное разделение. `gitTimeoutErr()` helper устраняет дублирование форматата ошибки. - **commit.ts:74-76** — hint про `.git/index.lock` при SIGTERM от `git commit` — практически полезно, т.к. git при kill может оставить lock и следующий commit упадёт. - **create-pr.ts:62-68** — timeout 5000ms для `git rev-parse` (быстрая операция, меньший timeout оправдан) + читаемая ошибка с полным cmd. - Backward compat: добавлен только `timeout` option, существующие вызовы и return-shapes сохранены. Cross-file impact: tools вызываются opencode runtime, oracle-скрипты не парсят их вывод — paired updates не требуются. ### Замечания (info, не блокирующие) - **create-pr.ts:64** [style] Magic number `5000` inline, в отличие от `commit.ts` и `merge-pr.ts` где timeout'ы вынесены в константы. Для консистентности можно вынести в `const GIT_REV_PARSE_TIMEOUT_MS = 5000` и использовать в сообщении об ошибке (сейчас `5000ms` захардкожено дважды: в option и в строке ошибки). - **commit.ts:29-31** [ux] `recentCommits()` при timeout возвращает `[git] git log --oneline -5 timed out after 10000ms`, которая потом вставляется в `Recent commits:\n${recentCommits(...)}`. Пользователь увидит "Recent commits:\n[git] git log --oneline -5 timed out after 10000ms" — semantically странно (это не список коммитов). Можно вернуть `"(git log timed out)"` чтобы визуально отличалось от реальных коммитов. Не блокирующее — edge case (git log зависает крайне редко). - **tests** [coverage] Нет тестов для нового timeout-поведения `git()` helper в merge-pr и spawnSync-call'ов в commit. В `_shared.test.ts:232` есть прецедент теста timeout для `resolveForgejoRepo` — можно аналогично покрыть, но spawnSync-timeout сложно мокать без реального hang, поэтому info-level. ### Verdict: APPROVE
slaid098 deleted branch fix/tools/synctimeout-git-calls 2026-08-11 17:09:11 +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!47
No description provided.