fix(tools): cleanup local pr-N branches in merge-pr tool #50

Closed
opened 2026-08-11 17:18:23 +03:00 by slaid098 · 0 comments
Owner

Контекст

Зачем: fix_ci фаза run-pipeline создаёт локальные ветки pr-M (через git fetch origin pull/M/head:pr-M && git checkout pr-M), но merge-pr.ts после merge чистит только оригинальную feature-ветку (по headRef), а pr-M не трогает → локальные ветки pr-N копятся бесконечно. В репо уже 9 мусорных веток: pr-19, pr-25, pr-280, pr-30, pr-32, pr-39, pr-39-review, pr-45, pr-46.

Контекст: run-pipeline/SKILL.md:97 (Template D, fix_ci) инструктирует subagent'а: git fetch origin pull/M/head:pr-M && git checkout pr-M — git-native способ checkout PR без gh pr checkout (后者 запрещён/API restrictions в Forgejo-режиме). Это создаёт локальную ветку pr-M.

merge-pr.ts после merge делает локальную очистку: git branch -D headRef (где headRef — оригинальное имя feature-ветки PR, например fix/skills/curl-max-time). Но pr-M НЕ совпадает с headRef — merge-pr.ts её не трогает. Каждый fix_ci-цикл оставляет локальную pr-M навсегда.

Это баг утечки (не фича). Намеренный механизм — pr-M для fix_ci; утечка — отсутствие очистки после merge.

Задача

  1. В .opencode/tools/merge-pr.ts в блоке локальной очистки (после gh pr merge --delete-branch и существующего git branch -D headRef) добавить удаление pr-${pr_number}:
    • const prBranch = "pr-" + args.pr_number (или через pr-${pr_number} template literal).
    • const listRes = spawnSync("git", ["-C", cwd, "branch", "--list", prBranch], { encoding: "utf-8", timeout: 5000 }) — guard: если listRes.stdout.trim() пустой → ветки нет, skip. Если есть → git branch -D prBranch.
    • Обработка spawnSync error/timeout — аналогично существующим git-вызовам в merge-pr.ts (soft fail с ⚠️, НЕ блокировать merge — ветка почистится手动 или при следующем clone).
  2. Добавить тест test_merge_pr_cleans_pr_branch в tests/test_merge_pr_tool.py — stub spawnSync для: git branch --list pr-45 → stdout pr-45\n (ветка существует) → следующий git branch -D pr-45 → exit 0. Expect: merge success + pr-45 удалена (stub verified called).
  3. Добавить тест test_merge_pr_no_pr_branch — stub git branch --list pr-46 → stdout empty (ветки нет) → git branch -D НЕ вызывается. Expect: merge success, skip cleanup.
  4. Создать ADR NNN-pr-<this_pr>-merge-pr-cleanup-pr-branches.md (номер определи через ls docs/decisions/) — обосновать: утечка pr-N веток, fix в merge-pr.ts (auto-cleanup при merge) vs run-pipeline/SKILL.md Template D (manual cleanup после fix_ci). Выбрать merge-pr.ts — deterministic, не требует правки SKILL.md протокола.

Контракты

  • merge-pr({ pr_number: M }) — без изменений в signature/args.
  • После успешного merge (existing behavior) + branch cleanup:
    • Удалить локальную pr-${pr_number} если существует (git branch --list non-empty).
    • Если pr-${pr_number} не существует → skip (no error).
    • Если git branch -D fail → soft fail ⚠️ failed to delete local pr-M branch: <stderr> (НЕ блокировать merge — основной merge уже прошёл).
  • headRef cleanup (existing behavior) — без изменений.

Инварианты

  • Cleanup ТОЛЬКО pr-${pr_number} (НЕ pr-N для других PR, НЕ все pr-* ветки — только текущего PR).
  • git branch --list pr-M guard обязателен — git branch -D на несуществующей ветке упадёт с error.
  • Soft fail при git branch -D error — merge уже прошёл, cleanup — best-effort.
  • НЕ чистить remote pr-M ветки — их нет на remote (pr-M чисто локальные, созданные git fetch ... :pr-M).

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

  • PR merged без fix_ci фазы → pr-M локальной ветки нет → git branch --list empty → skip (no error).
  • PR merged после fix_ci → pr-M локальная ветка есть → git branch -D pr-M → cleanup success.
  • PR merged, но pr-M уже удалена вручную → git branch --list empty → skip.
  • git branch -D fail (ветка checked out сейчас — невозможный case, мы уже сделали git checkout defaultBranch в existing cleanup) → soft fail ⚠️, merge success.
  • pr-39-review (не-стандартное имя с суффиксом) — НЕ матчится pr-${pr_number} (это pr-39-review, не pr-39). Existing pr-39-review — ручная мусорная ветка, не удаляется автоматически (только pr-39 если PR #39 был fix_ci). Cleanup существующих 9 веток — отдельный bash (гигиена, не PR scope).

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

  • run-pipeline/SKILL.md Template D (fix_ci, line 97) — без изменений (создание pr-M через git fetch ... :pr-M остаётся; cleanup переезжает в merge-pr.ts).
  • run-pipeline/SKILL.md Template F (merge, calls merge-pr) — без изменений (agent вызывает merge-pr({pr_number: M}), tool сам делает cleanup).
  • commit.ts / create-pr.ts — без изменений.
  • tests/test_merge_pr_tool.py — 2 новых теста (см. Задача).
  • tests/_ts_loader.mjs — возможно обновить stub если spawnSync("git", ["branch", ...]) не покрывается существующим GIT stub (проверить, скорее всего уже работает через generic git stub).

Вне scope

  • ❌ Cleanup существующих 9 мусорных веток (pr-19, pr-25, pr-280, pr-30, pr-32, pr-39, pr-39-review, pr-45, pr-46) — локальный bash git branch -D ..., не PR scope (гигиена).
  • ❌ Правка run-pipeline/SKILL.md Template D — создание pr-M остаётся (нужно для fix_ci). Cleanup переезжает в merge-pr tool.
  • ❌ Mix runs баг — отдельный issue.
  • ❌ Ruff gate в create-pr — отдельный issue.
  • ❌ Auto-cleanup pr-* старее N дней — over-engineering, не нужно.

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

  • После merge PR с fix_ci (локальная pr-M существует) → merge-pr удаляет pr-M автоматически
  • После merge PR без fix_ci (локальной pr-M нет) → merge-pr skip cleanup (no error)
  • git branch -D fail → soft fail ⚠️, merge success (не блокируется)
  • headRef cleanup (existing) — без регрессии
  • 2 новых теста в test_merge_pr_tool.py проходят
  • Существующие tests/test_merge_pr_tool.py не ломаются
  • ruff format --check . && ruff check . && mypy src/ — green (на самом merge-pr.ts)
  • ADR создан (обоснование: fix в merge-pr.ts vs SKILL.md Template D)
## Контекст Зачем: fix_ci фаза run-pipeline создаёт локальные ветки `pr-M` (через `git fetch origin pull/M/head:pr-M && git checkout pr-M`), но `merge-pr.ts` после merge чистит только оригинальную feature-ветку (по `headRef`), а `pr-M` не трогает → локальные ветки `pr-N` копятся бесконечно. В репо уже 9 мусорных веток: `pr-19, pr-25, pr-280, pr-30, pr-32, pr-39, pr-39-review, pr-45, pr-46`. Контекст: `run-pipeline/SKILL.md:97` (Template D, fix_ci) инструктирует subagent'а: `git fetch origin pull/M/head:pr-M && git checkout pr-M` — git-native способ checkout PR без `gh pr checkout` (后者 запрещён/API restrictions в Forgejo-режиме). Это создаёт локальную ветку `pr-M`. `merge-pr.ts` после merge делает локальную очистку: `git branch -D headRef` (где `headRef` — оригинальное имя feature-ветки PR, например `fix/skills/curl-max-time`). Но `pr-M` НЕ совпадает с `headRef` — `merge-pr.ts` её не трогает. Каждый fix_ci-цикл оставляет локальную `pr-M` навсегда. Это баг утечки (не фича). Намеренный механизм — `pr-M` для fix_ci; утечка — отсутствие очистки после merge. ## Задача 1. В `.opencode/tools/merge-pr.ts` в блоке локальной очистки (после `gh pr merge --delete-branch` и существующего `git branch -D headRef`) добавить удаление `pr-${pr_number}`: - `const prBranch = "pr-" + args.pr_number` (или через `pr-${pr_number}` template literal). - `const listRes = spawnSync("git", ["-C", cwd, "branch", "--list", prBranch], { encoding: "utf-8", timeout: 5000 })` — guard: если `listRes.stdout.trim()` пустой → ветки нет, skip. Если есть → `git branch -D prBranch`. - Обработка `spawnSync` error/timeout — аналогично существующим git-вызовам в `merge-pr.ts` (soft fail с `⚠️`, НЕ блокировать merge — ветка почистится手动 или при следующем clone). 2. Добавить тест `test_merge_pr_cleans_pr_branch` в `tests/test_merge_pr_tool.py` — stub `spawnSync` для: `git branch --list pr-45` → stdout `pr-45\n` (ветка существует) → следующий `git branch -D pr-45` → exit 0. Expect: merge success + `pr-45` удалена (stub verified called). 3. Добавить тест `test_merge_pr_no_pr_branch` — stub `git branch --list pr-46` → stdout empty (ветки нет) → `git branch -D` НЕ вызывается. Expect: merge success, skip cleanup. 4. Создать ADR `NNN-pr-<this_pr>-merge-pr-cleanup-pr-branches.md` (номер определи через `ls docs/decisions/`) — обосновать: утечка `pr-N` веток, fix в `merge-pr.ts` (auto-cleanup при merge) vs `run-pipeline/SKILL.md` Template D (manual cleanup после fix_ci). Выбрать `merge-pr.ts` — deterministic, не требует правки SKILL.md протокола. ## Контракты - `merge-pr({ pr_number: M })` — без изменений в signature/args. - После успешного merge (existing behavior) + branch cleanup: - Удалить локальную `pr-${pr_number}` если существует (`git branch --list` non-empty). - Если `pr-${pr_number}` не существует → skip (no error). - Если `git branch -D` fail → soft fail `⚠️ failed to delete local pr-M branch: <stderr>` (НЕ блокировать merge — основной merge уже прошёл). - `headRef` cleanup (existing behavior) — без изменений. ## Инварианты - Cleanup ТОЛЬКО `pr-${pr_number}` (НЕ `pr-N` для других PR, НЕ все `pr-*` ветки — только текущего PR). - `git branch --list pr-M` guard обязателен — `git branch -D` на несуществующей ветке упадёт с error. - Soft fail при `git branch -D` error — merge уже прошёл, cleanup — best-effort. - НЕ чистить remote `pr-M` ветки — их нет на remote (`pr-M` чисто локальные, созданные `git fetch ... :pr-M`). ## Граничные случаи - PR merged без fix_ci фазы → `pr-M` локальной ветки нет → `git branch --list` empty → skip (no error). - PR merged после fix_ci → `pr-M` локальная ветка есть → `git branch -D pr-M` → cleanup success. - PR merged, но `pr-M` уже удалена вручную → `git branch --list` empty → skip. - `git branch -D` fail (ветка checked out сейчас — невозможный case, мы уже сделали `git checkout defaultBranch` в existing cleanup) → soft fail `⚠️`, merge success. - `pr-39-review` (не-стандартное имя с суффиксом) — НЕ матчится `pr-${pr_number}` (это `pr-39-review`, не `pr-39`). Existing `pr-39-review` — ручная мусорная ветка, не удаляется автоматически (только `pr-39` если PR #39 был fix_ci). Cleanup существующих 9 веток — отдельный bash (гигиена, не PR scope). ## Влияние на связанные компоненты - `run-pipeline/SKILL.md` Template D (fix_ci, line 97) — без изменений (создание `pr-M` через `git fetch ... :pr-M` остаётся; cleanup переезжает в `merge-pr.ts`). - `run-pipeline/SKILL.md` Template F (merge, calls `merge-pr`) — без изменений (agent вызывает `merge-pr({pr_number: M})`, tool сам делает cleanup). - `commit.ts` / `create-pr.ts` — без изменений. - `tests/test_merge_pr_tool.py` — 2 новых теста (см. Задача). - `tests/_ts_loader.mjs` — возможно обновить stub если `spawnSync("git", ["branch", ...])` не покрывается существующим GIT stub (проверить, скорее всего уже работает через generic git stub). ## Вне scope - ❌ Cleanup существующих 9 мусорных веток (`pr-19, pr-25, pr-280, pr-30, pr-32, pr-39, pr-39-review, pr-45, pr-46`) — локальный bash `git branch -D ...`, не PR scope (гигиена). - ❌ Правка `run-pipeline/SKILL.md` Template D — создание `pr-M` остаётся (нужно для fix_ci). Cleanup переезжает в merge-pr tool. - ❌ Mix runs баг — отдельный issue. - ❌ Ruff gate в create-pr — отдельный issue. - ❌ Auto-cleanup `pr-*` старее N дней — over-engineering, не нужно. ## Критерии приемки - [ ] После merge PR с fix_ci (локальная `pr-M` существует) → `merge-pr` удаляет `pr-M` автоматически - [ ] После merge PR без fix_ci (локальной `pr-M` нет) → `merge-pr` skip cleanup (no error) - [ ] `git branch -D` fail → soft fail `⚠️`, merge success (не блокируется) - [ ] `headRef` cleanup (existing) — без регрессии - [ ] 2 новых теста в `test_merge_pr_tool.py` проходят - [ ] Существующие `tests/test_merge_pr_tool.py` не ломаются - [ ] `ruff format --check . && ruff check . && mypy src/` — green (на самом `merge-pr.ts`) - [ ] ADR создан (обоснование: fix в merge-pr.ts vs SKILL.md Template D)
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#50
No description provided.