# Dev Report: orchestrator — ложный FAILED деплоя (рассинхрон путей 14-deploy-log.md) Дата: 2026-06-04 Статус: DONE ## Задача QG `check_deploy_status` ложно заворачивал успешные деплои в FAILED и откатывал `deploy → development`. Причина: deployer пишет `14-deploy-log.md` (deploy_status: SUCCESS) и мержит артефакты в **main** отдельным PR, а гейт читает лог из **worktree ветки фичи** через `_repo_path(repo, branch)` — там лога нет → «Deploy log not found» → FAILED. Починить ТОЛЬКО поиск лога деплоя. НЕ трогать merge-gate (`gitea.py`, `current_stage=="deploy"`). ## ШАГ 0 — подтверждение по коду (verbatim) ### Где гейт читал лог (ДО фикса) — `src/qg/checks.py` `_repo_path` (стр. 13–24): ```python def _repo_path(repo: str, branch: str | None = None) -> str: if branch: wt = get_worktree_path(repo, branch) if os.path.isdir(wt): return wt return os.path.join(settings.repos_dir, repo) ``` `check_deploy_status` (стр. ~284, ДО): ```python repo_path = _repo_path(repo, branch) log_path = os.path.join(repo_path, f"docs/work-items/{work_item_id}/14-deploy-log.md") if not os.path.isfile(log_path): return False, "Deploy log not found (14-deploy-log.md)" ``` `get_worktree_path` (`src/git_worktree.py:33`): ```python def get_worktree_path(repo: str, branch: str) -> str: return os.path.join(settings.worktrees_dir, repo, _safe(branch)) ``` → при наличии ветки путь = `/repos/_wt///docs/work-items//14-deploy-log.md` (worktree ФИЧИ). Отсутствие файла → текст «Deploy log not found (14-deploy-log.md)» → FAILED. ### Где деплоер пишет лог (verbatim подтверждение) - `src/usage.py:328`: `"deployer": ("Deploy log", "14-deploy-log.md")` — артефакт деплоера. - Промпт деплоера лежит в целевом репо (`.openclaw/agents/deployer.md` в enduro-trails), не в orchestrator. Лог пишется и **мержится в main отдельным PR** (для ET-013 — PR #27, commit `4e925cc`). - **Живое подтверждение в проде:** в shared-клоне `/home/slin/repos/enduro-trails`: ``` $ git show origin/main:docs/work-items/ET-013/14-deploy-log.md --- deploy_status: SUCCESS version: v0.0.5 work_item: ET-013 pr: 26 merge_commit: be7a052 ``` При этом в worktree ветки фичи файла нет → отсюда ложный FAILED. **Рассинхрон подтверждён:** пишется/мержится в `origin/main`, читается из worktree фичи. ## Выбранный вариант: РЕКОМЕНДУЕМЫЙ (robust, origin/main fallback) Выбран вариант с чтением из `origin/main`, а не «деплоер пишет в worktree», потому что: 1. Меняет только логику чтения в `check_deploy_status` — ноль изменений в пайплайне деплоера/мержа артефактов (меньше риска, ничего нового не ломает). 2. Переживает текущее поведение «деплоер мержит лог в main отдельным PR» как есть. 3. Не зависит от состояния worktree (может быть зачищен/переаллоцирован к моменту гейта). 4. Альтернатива (писать в worktree ДО гейта) потребовала бы менять промпт деплоера в целевом репо + порядок merge артефактов — больше поверхности изменений. ### Логика (ПОСЛЕ фикса) — порядок поиска: worktree → origin/main → not found ```python repo_path = _repo_path(repo, branch) log_path = os.path.join(repo_path, f"docs/work-items/{work_item_id}/14-deploy-log.md") if os.path.isfile(log_path): ...read & _parse_deploy_status(content) # регресс сохранён main_content = _deploy_log_from_main(repo, work_item_id) # git fetch origin main + git show if main_content is not None: return _parse_deploy_status(main_content) # ET-013 fix: SUCCESS в main → PASS return False, "Deploy log not found (14-deploy-log.md)" ``` `_deploy_log_from_main`: использует shared-клон `settings.repos_dir/` (НЕ worktree), `git -C fetch origin main` (timeout 30s) + `git show origin/main:docs/work-items//14-deploy-log.md` (timeout 15s). Любая ошибка git (нет клона / нет `.git` / сеть / нет файла в main) → возвращает `None` (деградация к «not found»), **никогда не бросает исключение**. Парсинг frontmatter вынесен в `_parse_deploy_status` (читает только `deploy_status:`). ## Изменённые файлы - `src/qg/checks.py` — `import subprocess`; новые `_parse_deploy_status`, `_deploy_log_from_main`; `check_deploy_status` с origin/main-fallback. - `tests/test_qg.py` — 5 новых тестов в `TestCheckDeployStatus`. ## Тесты - worktree `SUCCESS` → PASS (регресс не сломан) — `test_success_verdict_passes` (+short-circuit тест). - нет в worktree, `SUCCESS` в origin/main → PASS (**ET-013 fix**) — `test_origin_main_success_passes_when_absent_in_worktree`. - `FAILED` в main → FAILED — `test_origin_main_failed_fails`. - нет нигде → not found — `test_absent_everywhere_fails`. - fetch падает (TimeoutExpired) → деградация без исключения — `test_fetch_failure_degrades_no_exception`. - worktree-лог короткозамыкает origin/main lookup — `test_worktree_log_short_circuits_main_lookup`. ### Вывод pytest (на проде, в контейнере orchestrator против `/repos/orchestrator`) - `tests/test_qg.py`: **33 passed** (было 28 + 5 новых). - Полный suite (детерминированно, `-p no:randomly`): **277 passed, 9 failed**. - 9 failed = те же off-limits HMAC/401 в `test_webhooks.py` (НЕ чинить). - Baseline на чистом дереве: **272 passed, 9 failed**. Дельта = +5 (мои тесты), 0 новых падений. - Списки FAILED у baseline и у фикса **идентичны** (сравнил отсортированно). - Прим.: один прогон без `no:randomly` дал «10 failed/276 passed» — это flaky-ordering в 401-webhook-тестах, не связано с фиксом (детерминированный прогон стабильно 9+277). ## Результат - Ветка `fix/deploy-gate-log-path` от актуального origin/main. - Commit: `4e4cc6c`. - **PR #23** в Gitea: https://git.mva154.duckdns.org/admin/orchestrator/pulls/23 (НЕ смержен, на ревью). - Push в main НЕ делал. merge-gate `gitea.py` НЕ тронут. Off-limits (PLANE_STATES, set_issue_done, launcher:475, HMAC, queue, usage_comment, cost_usd, трекер, миграции) — не тронуты. - Живая валидация ET-013: `_deploy_log_from_main("enduro-trails","ET-013")` фетчит и читает `deploy_status: SUCCESS` из origin/main → теперь PASS (раньше был ложный FAILED). ## Путь до/после (verbatim) - **ДО (читал):** `_repo_path(repo, branch)` → `/repos/_wt///docs/work-items//14-deploy-log.md` (worktree фичи; файла нет). - **Деплоер писал:** `docs/work-items//14-deploy-log.md` → мержил в `origin/main` отдельным PR. - **ПОСЛЕ (читает):** worktree (как раньше) → если нет, fallback `git show origin/main:docs/work-items//14-deploy-log.md` на shared-клоне `settings.repos_dir/` → если нет нигде, «not found». ## Проблемы и решения - Тесты не в образе контейнера (в `/app` запечён только `src/`). Прогонял в контейнере против смонтированного host-репо: `docker exec orchestrator sh -c 'cd /repos/orchestrator && python -m pytest ...'`. - Кажущийся +1 fail в одном прогоне — flaky-ordering 401-webhook тестов; подтверждено детерминированным `-p no:randomly` (стабильно 9 failed) и идентичными списками FAILED.