8.7 KiB
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):
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, ДО):
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):
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, commit4e925cc). - Живое подтверждение в проде: в shared-клоне
/home/slin/repos/enduro-trails:При этом в worktree ветки фичи файла нет → отсюда ложный FAILED.$ 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
Рассинхрон подтверждён: пишется/мержится в origin/main, читается из worktree фичи.
Выбранный вариант: РЕКОМЕНДУЕМЫЙ (robust, origin/main fallback)
Выбран вариант с чтением из origin/main, а не «деплоер пишет в worktree», потому что:
- Меняет только логику чтения в
check_deploy_status— ноль изменений в пайплайне деплоера/мержа артефактов (меньше риска, ничего нового не ломает). - Переживает текущее поведение «деплоер мержит лог в main отдельным PR» как есть.
- Не зависит от состояния worktree (может быть зачищен/переаллоцирован к моменту гейта).
- Альтернатива (писать в worktree ДО гейта) потребовала бы менять промпт деплоера в целевом репо + порядок merge артефактов — больше поверхности изменений.
Логика (ПОСЛЕ фикса) — порядок поиска: worktree → origin/main → not found
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).
- 9 failed = те же off-limits HMAC/401 в
Результат
- Ветка
fix/deploy-gate-log-pathот актуального origin/main. - Commit:
4e4cc6c. - PR #23 в Gitea: admin/orchestrator#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.