Files
wiki/tasks/orchestrator/reports/dev-2026-06-04-deploy-gate-path-fix.md
2026-06-04 13:40:01 +03:00

126 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` (стр. 1324):
```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/<repo>/<safe-branch>/docs/work-items/<WI>/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/<repo>` (НЕ worktree),
`git -C <clone> fetch origin main` (timeout 30s) + `git show origin/main:docs/work-items/<WI>/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/<repo>/<safe-branch>/docs/work-items/<WI>/14-deploy-log.md` (worktree фичи; файла нет).
- **Деплоер писал:** `docs/work-items/<WI>/14-deploy-log.md` → мержил в `origin/main` отдельным PR.
- **ПОСЛЕ (читает):** worktree (как раньше) → если нет, fallback `git show origin/main:docs/work-items/<WI>/14-deploy-log.md` на shared-клоне `settings.repos_dir/<repo>` → если нет нигде, «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.