feat(merge-pr): delete local PR branch after merge + wrong-repo guard #28

Closed
opened 2026-08-08 13:44:11 +03:00 by slaid098 · 0 comments
Owner

Контекст

Зачем: после мержа PR через merge_pr tool локальная ветка PR остаётся в worktree (Forgejo API удаляет только remote-ветку — delete_branch_after_merge: true), что засоряет git branch и ломает следующий git checkout master && git pull на повторных циклах. Вторая проблема — wrong-repo: если opencode открыт в repo-B, а PR #N принадлежит repo-A, merge_pr без repo аргумента пошлёт POST в repo-B → 404 → агент видит «⚠️ merge_pr failed ... HTTP 404» без информации, что PR в другом репо.

Контекст: .opencode/tools/merge-pr.ts (23 строки): вызывает runGh(["pr","merge",N,"--squash","--delete-branch"], args.repo, { cwd: context.worktree }); после мержа НЕ делает ничего локального. .opencode/tools/_shared.ts: resolveForgejoRepo (строки 30-41) — repo ?? git -C cwd config --get remote.origin.url (regex [:/]([^/]+)/([^/]+?)(?:\.git)?$), при status!=0 → null; callForgejoGh (строки 161-177): для "pr merge" → POST /repos/{full}/pulls/{n}/merge {Do:"squash", delete_branch_after_merge:true}, okStatus 200; callForgejo (строки 49-77): не-okStatus → {status:1, stderr:"Forgejo API ... → HTTP <code>: <body>"}. Паттерн GET-запросов уже есть: pipeline-status.py:157 — GET /repos/{repo}/pulls/{n}; _shared.ts:119 — GET /repos/{full}/labels. git в контейнере есть (Dockerfile apt-get install git), worktree смонтирован (docker-compose volume app_data/workspaces:/root/workspace). Паттерн spawnSync("git", [...], {cwd: context.worktree}) уже используется в commit.ts, create-pr.ts, memory-save.ts. Plugin tools НЕ проходят через permission.bash (доказано: commit.ts работает при deny на git commit). run-pipeline/SKILL.md Template F (строки 134-147): merge-pr({pr_number: M}); строки 144-145: «Если вернулась ⚠️ merge-pr failed ... → репорт пользователю, STOP (НЕ retry через raw bash...)». В .opencode/opencode.json (строки 337-365) agent tools: merge_pr: false для general/reviewer/memory-syncer (main agent наследует all true). Тесты: tests/test_merge_pr_tool.py (184 строки, 8 тестов, запуск uv run pytest, стабы через tests/_ts_loader.mjs: стаб fetch строки 357-370, стаб git remote строки 337-339). TS-тесты tests/test_merge_pr_tool.ts — документационные (bun недоступен).

Задача

  1. .opencode/tools/merge-pr.ts + .opencode/tools/_shared.ts:
    a. Pre-flight перед POST merge: GET /repos/{full}/pulls/{n} (получить head.ref, base.ref) и GET /repos/{full} (default_branch). Если PR не найден (404) → НЕ мержить, вернуть диагностику: «PR #N не найден в (определён из remote origin worktree). Похоже, ты работаешь не в том проекте — PR принадлежит другому репо. Открой opencode в правильном проекте и запусти пайплайн там». Без предложения «передай repo» как решения.
    b. После успешного мержа — локальная очистка ТОЛЬКО если репо PR == репо worktree (resolveForgejoRepo от context.worktree):
    • Удалить только локальную ветку смерженного PR (head.ref из pre-flight).
    • Если текущая ветка (git rev-parse --abbrev-ref HEAD) == head.ref → git checkout <default_branch> → git pull → git branch -D <head.ref>.
    • Если текущая ветка != head.ref (параллельная работа в этом же worktree) → git branch -D <head.ref> БЕЗ переключения веток.
    • Если локальной ветки нет → пропустить, не падать.
    • Чужие/старые ветки НЕ трогать.
    • Если git pull упал (локальные изменения/расхождение) → не падать: сообщить «мерж прошёл, но pull не выполнен: <причина>», ветку PR всё равно удалить.
      c. Если репо PR != репо worktree → мерж выполняется (если repo передан явно), локальная очистка ПРОПУСКАЕТСЯ с явным сообщением «локальная очистка пропущена: worktree ≠ репо PR ».
      d. Финальное сообщение тула: «PR #N merged (squash, remote branch deleted). Local <head.ref> deleted, switched to .» (или варианты без переключения/без удаления — адаптируй под фактически выполненные шаги).
  2. .opencode/skills/run-pipeline/SKILL.md Template F (строки 134-147): при ошибке «PR not found / 404» → STOP + «проверь, в том ли проекте ты работаешь (worktree ≠ репо PR)», НЕ retry с repo аргументом.
  3. .opencode/opencode.json: обновить description тула merge_pr (упомянуть: pre-flight проверка, удаление локальной ветки PR, wrong-repo guard). Описание тула задаётся в .opencode/tools/merge-pr.ts (description в tool({...})).
  4. tests/test_merge_pr_tool.py: добавить тесты: (а) 404 на GET pre-flight → диагностика wrong-repo, POST merge НЕ вызывается; (б) успешный мерж → удаление локальной ветки PR с checkout на default + pull; (в) текущая ветка != ветка PR → удаление без переключения; (г) репо PR != worktree → мерж без локальной очистки + сообщение; (д) локальной ветки нет → пропуск без падения. При необходимости расширить стабы в tests/_ts_loader.mjs (fetch для GET /pulls/{n} и GET /repos/{full}, git команды: rev-parse, branch, checkout, pull). Старые тесты должны остаться зелёными (существующие стабы уже покрывают git remote и rev-parse).

Контракты

  • Pre-flight: GET /repos/{full}/pulls/{n} → head.ref, base.ref; GET /repos/{full} → default_branch
  • 404 на pre-flight PR → мерж НЕ выполняется, возвращается wrong-repo диагностика
  • Локальная очистка: удаляется ТОЛЬКО ветка смерженного PR (head.ref)
  • Финальное сообщение тула — единый формат «PR #N merged (squash, remote branch deleted). Local <head.ref> deleted, switched to .» с вариантами
  • Проверка: uv run pytest tests/test_merge_pr_tool.py — все старые + новые тесты зелёные

Инварианты

  • Локальные git-операции — только через spawnSync внутри тула (НЕ через bash агента; permissions не блокируют plugin tools)
  • Default branch — из GET /repos/{full} (default_branch), НЕ хардкод main
  • Удаляется ТОЛЬКО ветка смерженного PR. Никаких git clean, никаких чужих веток
  • Wrong-repo = ловушка (STOP + направление в правильный проект), НЕ поддержка работы из чужого worktree
  • Мерж по-прежнему только через POST /pulls/{n}/merge {Do:"squash", delete_branch_after_merge:true}

Граничные случаи

  • PR не найден (404) → wrong-repo диагностика, без мержа и без локальных git-операций
  • git pull упал → сообщить «мерж прошёл, но pull не выполнен: <причина>», ветку PR всё равно удалить
  • Локальной ветки head.ref нет → пропуск удаления, не падать
  • Текущая ветка == head.ref → checkout default + pull + branch -D
  • Текущая ветка != head.ref → только branch -D, без переключения
  • repo PR != worktree → мерж выполняется, локальная очистка пропущена с явным сообщением
  • resolveForgejoRepo вернул null (нет remote) → ошибка как сейчас («origin remote not parseable»)

Влияние на связанные компоненты

  • .opencode/skills/run-pipeline/SKILL.md Template F — парная правка (STOP при 404, без retry с repo)
  • .opencode/tools/_shared.ts — новые хелперы (pre-flight, resolveForgejoRepo для сравнения репо) — остальные 3 тула (create-issue, create-pr, post-review) используют runGh, их поведение не меняется
  • tests/test_merge_pr_tool.py + tests/_ts_loader.mjs — расширение стабов
  • pipeline-status.py — НЕ меняется (строка 157 GET /pulls/{n} уже существует — паттерн)
  • opencode.json agent tools (merge_pr: false для subagents) — НЕ меняется

Вне scope

  • ❌ Поддержка работы пайплайна из чужого worktree (--repo в pipeline-status.py и др.) — осознанно, отдельная работа
  • ❌ Чистка неотслеживаемых/временных файлов (git clean — deny)
  • ❌ Удаление чужих/старых локальных веток
  • ❌ Изменение pipeline-status.py
  • ❌ Изменение create-issue/create-pr/post-review тулов

Критерии приемки

  • merge_pr из чужого worktree → явная диагностика «ты не в том проекте», мерж НЕ выполняется
  • После мержа локальная ветка PR удалена, при необходимости выполнен checkout на default + pull
  • Параллельная работа (текущая ветка != ветка PR) не ломается: ветка PR удалена, переключения нет
  • Репо PR != worktree → мерж без локальной очистки + явное сообщение
  • Template F обновлён (STOP при 404, без retry с repo)
  • description merge_pr в .opencode/tools/merge-pr.ts обновлён (pre-flight, удаление локальной ветки, wrong-repo guard)
  • uv run pytest tests/test_merge_pr_tool.py проходит (все старые + новые тесты)
## Контекст Зачем: после мержа PR через merge_pr tool локальная ветка PR остаётся в worktree (Forgejo API удаляет только remote-ветку — delete_branch_after_merge: true), что засоряет `git branch` и ломает следующий `git checkout master && git pull` на повторных циклах. Вторая проблема — wrong-repo: если opencode открыт в repo-B, а PR #N принадлежит repo-A, merge_pr без repo аргумента пошлёт POST в repo-B → 404 → агент видит «⚠️ merge_pr failed ... HTTP 404» без информации, что PR в другом репо. Контекст: `.opencode/tools/merge-pr.ts` (23 строки): вызывает `runGh(["pr","merge",N,"--squash","--delete-branch"], args.repo, { cwd: context.worktree })`; после мержа НЕ делает ничего локального. `.opencode/tools/_shared.ts`: `resolveForgejoRepo` (строки 30-41) — `repo ?? git -C cwd config --get remote.origin.url` (regex `[:/]([^/]+)/([^/]+?)(?:\.git)?$`), при status!=0 → null; `callForgejoGh` (строки 161-177): для "pr merge" → POST `/repos/{full}/pulls/{n}/merge` `{Do:"squash", delete_branch_after_merge:true}`, okStatus 200; `callForgejo` (строки 49-77): не-okStatus → `{status:1, stderr:"Forgejo API ... → HTTP <code>: <body>"}`. Паттерн GET-запросов уже есть: `pipeline-status.py:157` — GET `/repos/{repo}/pulls/{n}`; `_shared.ts:119` — GET `/repos/{full}/labels`. git в контейнере есть (Dockerfile apt-get install git), worktree смонтирован (docker-compose volume app_data/workspaces:/root/workspace). Паттерн `spawnSync("git", [...], {cwd: context.worktree})` уже используется в commit.ts, create-pr.ts, memory-save.ts. Plugin tools НЕ проходят через permission.bash (доказано: commit.ts работает при deny на git commit). `run-pipeline/SKILL.md` Template F (строки 134-147): `merge-pr({pr_number: M})`; строки 144-145: «Если вернулась ⚠️ merge-pr failed ... → репорт пользователю, STOP (НЕ retry через raw bash...)». В `.opencode/opencode.json` (строки 337-365) agent tools: `merge_pr: false` для general/reviewer/memory-syncer (main agent наследует all true). Тесты: `tests/test_merge_pr_tool.py` (184 строки, 8 тестов, запуск `uv run pytest`, стабы через `tests/_ts_loader.mjs`: стаб fetch строки 357-370, стаб git remote строки 337-339). TS-тесты `tests/test_merge_pr_tool.ts` — документационные (bun недоступен). ## Задача 1. `.opencode/tools/merge-pr.ts` + `.opencode/tools/_shared.ts`: a. Pre-flight перед POST merge: GET `/repos/{full}/pulls/{n}` (получить `head.ref`, `base.ref`) и GET `/repos/{full}` (`default_branch`). Если PR не найден (404) → НЕ мержить, вернуть диагностику: «PR #N не найден в <repo> (определён из remote origin worktree). Похоже, ты работаешь не в том проекте — PR принадлежит другому репо. Открой opencode в правильном проекте и запусти пайплайн там». Без предложения «передай repo» как решения. b. После успешного мержа — локальная очистка ТОЛЬКО если репо PR == репо worktree (resolveForgejoRepo от context.worktree): - Удалить только локальную ветку смерженного PR (head.ref из pre-flight). - Если текущая ветка (`git rev-parse --abbrev-ref HEAD`) == head.ref → `git checkout <default_branch>` → `git pull` → `git branch -D <head.ref>`. - Если текущая ветка != head.ref (параллельная работа в этом же worktree) → `git branch -D <head.ref>` БЕЗ переключения веток. - Если локальной ветки нет → пропустить, не падать. - Чужие/старые ветки НЕ трогать. - Если `git pull` упал (локальные изменения/расхождение) → не падать: сообщить «мерж прошёл, но pull не выполнен: <причина>», ветку PR всё равно удалить. c. Если репо PR != репо worktree → мерж выполняется (если repo передан явно), локальная очистка ПРОПУСКАЕТСЯ с явным сообщением «локальная очистка пропущена: worktree <repo-B> ≠ репо PR <repo-A>». d. Финальное сообщение тула: «PR #N merged (squash, remote branch deleted). Local <head.ref> deleted, switched to <default>.» (или варианты без переключения/без удаления — адаптируй под фактически выполненные шаги). 2. `.opencode/skills/run-pipeline/SKILL.md` Template F (строки 134-147): при ошибке «PR not found / 404» → STOP + «проверь, в том ли проекте ты работаешь (worktree ≠ репо PR)», НЕ retry с repo аргументом. 3. `.opencode/opencode.json`: обновить description тула merge_pr (упомянуть: pre-flight проверка, удаление локальной ветки PR, wrong-repo guard). Описание тула задаётся в `.opencode/tools/merge-pr.ts` (description в `tool({...})`). 4. `tests/test_merge_pr_tool.py`: добавить тесты: (а) 404 на GET pre-flight → диагностика wrong-repo, POST merge НЕ вызывается; (б) успешный мерж → удаление локальной ветки PR с checkout на default + pull; (в) текущая ветка != ветка PR → удаление без переключения; (г) репо PR != worktree → мерж без локальной очистки + сообщение; (д) локальной ветки нет → пропуск без падения. При необходимости расширить стабы в `tests/_ts_loader.mjs` (fetch для GET `/pulls/{n}` и GET `/repos/{full}`, git команды: rev-parse, branch, checkout, pull). Старые тесты должны остаться зелёными (существующие стабы уже покрывают git remote и rev-parse). ## Контракты - Pre-flight: GET /repos/{full}/pulls/{n} → head.ref, base.ref; GET /repos/{full} → default_branch - 404 на pre-flight PR → мерж НЕ выполняется, возвращается wrong-repo диагностика - Локальная очистка: удаляется ТОЛЬКО ветка смерженного PR (head.ref) - Финальное сообщение тула — единый формат «PR #N merged (squash, remote branch deleted). Local <head.ref> deleted, switched to <default>.» с вариантами - Проверка: `uv run pytest tests/test_merge_pr_tool.py` — все старые + новые тесты зелёные ## Инварианты - Локальные git-операции — только через spawnSync внутри тула (НЕ через bash агента; permissions не блокируют plugin tools) - Default branch — из GET /repos/{full} (`default_branch`), НЕ хардкод main - Удаляется ТОЛЬКО ветка смерженного PR. Никаких git clean, никаких чужих веток - Wrong-repo = ловушка (STOP + направление в правильный проект), НЕ поддержка работы из чужого worktree - Мерж по-прежнему только через POST /pulls/{n}/merge {Do:"squash", delete_branch_after_merge:true} ## Граничные случаи - PR не найден (404) → wrong-repo диагностика, без мержа и без локальных git-операций - git pull упал → сообщить «мерж прошёл, но pull не выполнен: <причина>», ветку PR всё равно удалить - Локальной ветки head.ref нет → пропуск удаления, не падать - Текущая ветка == head.ref → checkout default + pull + branch -D - Текущая ветка != head.ref → только branch -D, без переключения - repo PR != worktree → мерж выполняется, локальная очистка пропущена с явным сообщением - resolveForgejoRepo вернул null (нет remote) → ошибка как сейчас («origin remote not parseable») ## Влияние на связанные компоненты - `.opencode/skills/run-pipeline/SKILL.md` Template F — парная правка (STOP при 404, без retry с repo) - `.opencode/tools/_shared.ts` — новые хелперы (pre-flight, resolveForgejoRepo для сравнения репо) — остальные 3 тула (create-issue, create-pr, post-review) используют runGh, их поведение не меняется - `tests/test_merge_pr_tool.py` + `tests/_ts_loader.mjs` — расширение стабов - `pipeline-status.py` — НЕ меняется (строка 157 GET /pulls/{n} уже существует — паттерн) - opencode.json agent tools (merge_pr: false для subagents) — НЕ меняется ## Вне scope - ❌ Поддержка работы пайплайна из чужого worktree (--repo в pipeline-status.py и др.) — осознанно, отдельная работа - ❌ Чистка неотслеживаемых/временных файлов (git clean — deny) - ❌ Удаление чужих/старых локальных веток - ❌ Изменение pipeline-status.py - ❌ Изменение create-issue/create-pr/post-review тулов ## Критерии приемки - [ ] merge_pr из чужого worktree → явная диагностика «ты не в том проекте», мерж НЕ выполняется - [ ] После мержа локальная ветка PR удалена, при необходимости выполнен checkout на default + pull - [ ] Параллельная работа (текущая ветка != ветка PR) не ломается: ветка PR удалена, переключения нет - [ ] Репо PR != worktree → мерж без локальной очистки + явное сообщение - [ ] Template F обновлён (STOP при 404, без retry с repo) - [ ] description merge_pr в .opencode/tools/merge-pr.ts обновлён (pre-flight, удаление локальной ветки, wrong-repo guard) - [ ] `uv run pytest tests/test_merge_pr_tool.py` проходит (все старые + новые тесты)
Sign in to join this conversation.
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#28
No description provided.