fix(tools): pass head field in create-pr for Forgejo POST /pulls #18

Merged
slaid098 merged 1 commit from fix/create-pr/head-field into main 2026-08-07 13:34:18 +03:00
Owner

Что сделано

Исправлен баг #9: create-pr tool не передавал head field в Forgejo POST /pulls → HTTP 422 [Head]: Required.

  • .opencode/tools/create-pr.ts: добавлено автоопределение текущей git-ветки через git -C <cwd> rev-parse --abbrev-ref HEAD и передача --head <branch> в runGh(). Detached HEAD → понятная ошибка, без обращения к API.
  • .opencode/tools/_shared.ts: в callForgejoGh для pr create поле head теперь required — если --head отсутствует в argv, возвращается явная ошибка валидации (НЕ тихо опускается из POST body). base остаётся опциональным (Forgejo default).
  • tests/_ts_loader.mjs: добавлен stub-перехват для git rev-parse --abbrev-ref HEAD (возвращает feature/test-branch по умолчанию, переопределяется через GIT_HEAD_STUB env var для тестов detached HEAD / git failure).
  • tests/test_create_pr_tool.py: 4 новых теста — head field в POST body, explicit override, detached HEAD error, git failure error. Обновлены 2 существующих теста (теперь 2 git-вызова: rev-parse + remote lookup).
  • tests/test_create_pr_tool.ts: обновлён список зеркалируемых тест-кейсов.

Почему

Forgejo POST /pulls требует поле head (название ветки источника) — без него возвращает HTTP 422. gh CLI автоопределяет head из текущей ветки, но dispatch-путь в _shared.ts парсит --head из argv, а create-pr.ts его не передавал → head undefined → опускался из POST body (...(head ? {head} : {})) → Forgejo 422. Workaround (raw curl с ручным head field) использовался для PR #12, #13, #14, #15, #17 — теперь tool работает нативно.

Watch out

  • create-pr теперь делает 2 git-вызова вместо 1 (rev-parse для head + config remote для repo при опущенном repo). С explicit repo — только 1 (rev-parse для head).
  • _shared.ts теперь fail-loud на отсутствующий --head: если другой tool вызовет runGh(["pr","create",...]) без --head, получит явную ошибку вместо тихого 422 от Forgejo.
  • base остаётся опциональным — Forgejo использует default_branch репо (обычно main).
  • Pre-existing mypy error check-permissions.py:114 (no-any-return) не затронут — на main, вне scope.

Pending

  • ADR для этого бага не создавался (bugfix, не архитектурное решение).
  • memory-syncer обновит memory-файл opencode-config-004.md после merge (запись о PR#18 + закрытие бага #9).

Closes #9

## Что сделано Исправлен баг #9: `create-pr` tool не передавал `head` field в Forgejo POST /pulls → HTTP 422 `[Head]: Required`. - `.opencode/tools/create-pr.ts`: добавлено автоопределение текущей git-ветки через `git -C <cwd> rev-parse --abbrev-ref HEAD` и передача `--head <branch>` в `runGh()`. Detached HEAD → понятная ошибка, без обращения к API. - `.opencode/tools/_shared.ts`: в `callForgejoGh` для `pr create` поле `head` теперь required — если `--head` отсутствует в argv, возвращается явная ошибка валидации (НЕ тихо опускается из POST body). `base` остаётся опциональным (Forgejo default). - `tests/_ts_loader.mjs`: добавлен stub-перехват для `git rev-parse --abbrev-ref HEAD` (возвращает `feature/test-branch` по умолчанию, переопределяется через `GIT_HEAD_STUB` env var для тестов detached HEAD / git failure). - `tests/test_create_pr_tool.py`: 4 новых теста — head field в POST body, explicit override, detached HEAD error, git failure error. Обновлены 2 существующих теста (теперь 2 git-вызова: rev-parse + remote lookup). - `tests/test_create_pr_tool.ts`: обновлён список зеркалируемых тест-кейсов. ## Почему Forgejo POST /pulls требует поле `head` (название ветки источника) — без него возвращает HTTP 422. `gh` CLI автоопределяет head из текущей ветки, но dispatch-путь в `_shared.ts` парсит `--head` из argv, а `create-pr.ts` его не передавал → `head` undefined → опускался из POST body (`...(head ? {head} : {})`) → Forgejo 422. Workaround (raw curl с ручным `head` field) использовался для PR #12, #13, #14, #15, #17 — теперь tool работает нативно. ## Watch out - `create-pr` теперь делает 2 git-вызова вместо 1 (rev-parse для head + config remote для repo при опущенном `repo`). С explicit `repo` — только 1 (rev-parse для head). - `_shared.ts` теперь fail-loud на отсутствующий `--head`: если другой tool вызовет `runGh(["pr","create",...])` без `--head`, получит явную ошибку вместо тихого 422 от Forgejo. - `base` остаётся опциональным — Forgejo использует default_branch репо (обычно `main`). - Pre-existing mypy error `check-permissions.py:114` (no-any-return) не затронут — на main, вне scope. ## Pending - ADR для этого бага не создавался (bugfix, не архитектурное решение). - `memory-syncer` обновит memory-файл `opencode-config-004.md` после merge (запись о PR#18 + закрытие бага #9). Closes #9
fix(tools): pass head field in create-pr for Forgejo POST /pulls
All checks were successful
CI (always) / bootstrap (pull_request) Successful in 3s
CI / bootstrap (pull_request) Successful in 5s
CI / typecheck (pull_request) Successful in 43s
CI / complexity (pull_request) Successful in 41s
CI / lint (pull_request) Successful in 41s
CI / test (3.13) (pull_request) Successful in 1m52s
a624fa8f39
Author
Owner

Code Review Summary

Исправление бага #9 корректное: create-pr теперь автоопределяет текущую ветку через git rev-parse --abbrev-ref HEAD и передаёт --head в Forgejo dispatch path, а _shared.ts fail-loud требует head для pr create (вместо тихого пропуска → HTTP 422). 4 новых теста + 2 обновлённых, все 18 pass; 72 теста в других tool-тестах (commit, create-issue, post-review, merge-pr, create-readme, draw-image) тоже pass — stub в _ts_loader.mjs не ломает shared loader.

Positives

  • Fail-loud guard (_shared.ts:190-198): head required для pr create — явная ошибка вместо malformed POST. Правильный подход (better than silent 422).
  • Detached HEAD + git failure guards (create-pr.ts:66): branchRes.status !== 0 || !head || head === "HEAD" — покрывает все 3 случая (git error, empty output, detached). Ошибка до API-вызова, без сетевого round-trip.
  • spawnSync с array args (create-pr.ts:62): args передаются как массив, не shell-строка → нет injection risk для branch names с пробелами/спецсимволами.
  • Stub изоляция (_ts_loader.mjs:345): условие args.includes("rev-parse") && args.includes("--abbrev-ref") — перехватывает только --abbrev-ref, не ломает --show-toplevel (используется в pipeline-status/spec-status, но те тесты stub subprocess.run напрямую, не через loader).
  • Cross-file paired update: writer (create-pr.ts передаёт --head) и reader (_shared.ts парсит --head) обновлены в одном PR — окно сломанного main закрыто.
  • Тесты покрывают контракты: head field в POST body, detached HEAD, git failure, explicit repo (1 git call вместо 0), cwd -C для обоих git-вызовов. Docstrings объясняют "почему" (issue #9, Forgejo 422), не "что".
  • base остаётся опциональным (_shared.ts:202): ...(base ? { base } : {}) — Forgejo default_branch используется, если --base не передан.

Suggestions (info, not blocking)

  • test_create_pr_tool.py:220 [naming] test_head_explicit_override — имя предполагает, что override существует, но docstring и assertions подтверждают обратное ("no user-facing head override — the tool is self-contained"). Тест проверяет, что auto-detected branch отправляется. Имя вводит в заблуждение; лучше test_head_auto_detected_no_override или test_head_always_auto_detected.
  • test_create_pr_tool.py:245,279 [DRY] test_detached_head_error и test_git_failure_no_head дублируют boilerplate subprocess.run(...) вместо _run_exec, т.к. нужно переопределить GIT_HEAD_STUB env var. Можно расширить _run_exec опциональным env параметром (_run_exec(args, responses, env=None) → merge в os.environ). Не критично — 2 теста, ~15 строк дубликата.

Verdict: APPROVE

## Code Review Summary Исправление бага #9 корректное: `create-pr` теперь автоопределяет текущую ветку через `git rev-parse --abbrev-ref HEAD` и передаёт `--head` в Forgejo dispatch path, а `_shared.ts` fail-loud требует `head` для `pr create` (вместо тихого пропуска → HTTP 422). 4 новых теста + 2 обновлённых, все 18 pass; 72 теста в других tool-тестах (commit, create-issue, post-review, merge-pr, create-readme, draw-image) тоже pass — stub в `_ts_loader.mjs` не ломает shared loader. ### Positives - **Fail-loud guard** (`_shared.ts:190-198`): `head` required для `pr create` — явная ошибка вместо malformed POST. Правильный подход (better than silent 422). - **Detached HEAD + git failure guards** (`create-pr.ts:66`): `branchRes.status !== 0 || !head || head === "HEAD"` — покрывает все 3 случая (git error, empty output, detached). Ошибка до API-вызова, без сетевого round-trip. - **`spawnSync` с array args** (`create-pr.ts:62`): args передаются как массив, не shell-строка → нет injection risk для branch names с пробелами/спецсимволами. - **Stub изоляция** (`_ts_loader.mjs:345`): условие `args.includes("rev-parse") && args.includes("--abbrev-ref")` — перехватывает только `--abbrev-ref`, не ломает `--show-toplevel` (используется в pipeline-status/spec-status, но те тесты stub `subprocess.run` напрямую, не через loader). - **Cross-file paired update**: writer (`create-pr.ts` передаёт `--head`) и reader (`_shared.ts` парсит `--head`) обновлены в одном PR — окно сломанного main закрыто. - **Тесты покрывают контракты**: head field в POST body, detached HEAD, git failure, explicit repo (1 git call вместо 0), cwd `-C` для обоих git-вызовов. Docstrings объясняют "почему" (issue #9, Forgejo 422), не "что". - **`base` остаётся опциональным** (`_shared.ts:202`): `...(base ? { base } : {})` — Forgejo default_branch используется, если `--base` не передан. ### Suggestions (info, not blocking) - **test_create_pr_tool.py:220** [naming] `test_head_explicit_override` — имя предполагает, что override существует, но docstring и assertions подтверждают обратное ("no user-facing head override — the tool is self-contained"). Тест проверяет, что auto-detected branch отправляется. Имя вводит в заблуждение; лучше `test_head_auto_detected_no_override` или `test_head_always_auto_detected`. - **test_create_pr_tool.py:245,279** [DRY] `test_detached_head_error` и `test_git_failure_no_head` дублируют boilerplate `subprocess.run(...)` вместо `_run_exec`, т.к. нужно переопределить `GIT_HEAD_STUB` env var. Можно расширить `_run_exec` опциональным `env` параметром (`_run_exec(args, responses, env=None)` → merge в `os.environ`). Не критично — 2 теста, ~15 строк дубликата. ### Verdict: APPROVE
slaid098 deleted branch fix/create-pr/head-field 2026-08-07 13:34:19 +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!18
No description provided.