architect(ET): auto-commit from architect run_id=702

This commit is contained in:
2026-06-15 14:20:36 +03:00
committed by deployer
parent 860de5b0a5
commit 96b653d11c
6 changed files with 459 additions and 0 deletions

View File

@@ -742,6 +742,40 @@ sentinel-файлы (`<repos_dir>/.deploy-state-<repo>/<wi>/`), без мигр
Подробнее: [adr-0007](adr/adr-0007-executable-self-deploy.md), детально —
`docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`.
#### Гигиена shared deploy-базы: устойчивый self-deploy `git pull` (ORCH-112 — design)
**Инвариант (нормативно):** shared main checkout `<host_repos_dir>/<repo>`
(= `settings.deploy_host_repo_path` = `/home/slin/repos/orchestrator`) — это
**deploy/worktree-management база, НЕ редактируемый workspace.** Рабочие изменения туда
**не пишутся** конвейером/агентами: агенты работают в worktree'ах `/repos/_wt/<repo>/<branch>`
(`git_worktree.ensure_worktree`), `docker build` берёт контекст worktree
(`image_freshness._host_worktree_path`), fallback'и гейтов на main clone — только чтение
(`git show origin/main:...`). Локальные правки в deploy-базе по определению существовать не должны.
**Проблема (инцидент ORCH-111 от грязи ORCH-104):** хук `scripts/orchestrator-deploy-hook.sh`
делал **голый** `git pull origin main` в `$REPO` без гигиены рабочего дерева → любая локальная правка
tracked-файла (`src/config.py`) блокировала merge → деплой падал → ручное вмешательство (групповой риск
self-hosting). **Решение** — **resilient-pull, встроенный в хук** (`--deploy`): перед `git pull` хук
при обнаружении грязи приводит базу к чистому актуальному `origin/main`
(`git fetch` + `git reset --hard origin/main` + **скоупленный** `git clean -fd`). Гейт — env
`CHECKOUT_HYGIENE`, инжектится `self_deploy.build_deploy_command` когда новый чистый never-raise leaf
`src/checkout_hygiene.py`-`applies(repo)` истинен (kill-switch `checkout_hygiene_enabled` + скоуп
`checkout_hygiene_repos`, пусто → self-hosting only).
- **Сохранность (NFR-2, жёсткий контракт):** `git clean` — **только `-fd`, НИКОГДА `-x`** (иначе
удалит gitignored `.env`/прод-секреты, `data/*.db`/БД, `build/`); явные `-e '.deploy-prev-image-*'`
и `-e 'deploy-hook.log'` (untracked-но-НЕ-ignored — иначе сломается rollback `do_rollback`).
Sibling `<repos_dir>/.deploy-state-*` / `.merge-lease-*.json` (под родителем `$REPO`) и
`.git/worktrees/*` (внутри `.git/`) — вне области `git clean` в `$REPO`.
- **Сходимость после failed/cancelled** — этим же deploy-time self-heal (база сходится на следующем
деплое); `cancel_task` (ORCH-090) **не расширяется**, фоновый janitor **не вводится**.
- **Наблюдаемость** — хук пишет sentinel `hygiene` в deploy-state каталог; Phase-C finalizer читает и
шлёт Telegram-алерт (кликабельный номер, best-effort, never-raise).
- **Чистая база / kill-switch off** — голый fast-forward `git pull` байт-в-байт (happy-path без
регресса); `--build-staging` (build из worktree, без pull) не затронут. `STAGE_TRANSITIONS` / реестр
`QG_CHECKS` / `check_*` / machine-verdict / схема БД / exit-code-контракт хука (0/1/2) — не тронуты.
Детально — `docs/work-items/ORCH-112/06-adr/ADR-001-deploy-base-checkout-hygiene.md`, сквозной
[adr-0044](adr/adr-0044-deploy-base-checkout-hygiene.md).
#### Выделенный статус-триггер прод-деплоя «Confirm Deploy» (ORCH-059 — реализовано)
Перегрузка: один Plane-статус `Approved` служил И человеческим гейтом BRD на
`analysis` (`check_analysis_approved`), И триггером Фазы B прод-деплоя на `deploy`

View File

@@ -0,0 +1,67 @@
---
work_item: ORCH-112
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# adr-0044: Гигиена shared deploy-базы — устойчивый self-deploy `git pull`
Сквозное (cross-cutting) решение. Детальный per-work-item ADR —
`docs/work-items/ORCH-112/06-adr/ADR-001-deploy-base-checkout-hygiene.md`.
## Статус
Proposed (ORCH-112)
## Контекст (сквозной)
Глобальный путь прод-деплоя self-hosting (`deploy`-стадия, ORCH-036) исполняет хост-хук
`scripts/orchestrator-deploy-hook.sh`, чей шаг «2. Pull latest code» — **голый** `git pull origin main`
в shared main clone (`settings.deploy_host_repo_path`). Любая грязь рабочего дерева (модифицированный
tracked-файл и/или untracked-остатки failed/cancelled/брошенной задачи) **блокирует** merge → деплой
встаёт → ручное вмешательство. На self-hosting (один прод-инстанс на все проекты с общей БД/очередью)
это **групповой риск**: залипший self-deploy орка останавливает обслуживание всех проектов
(инцидент ORCH-111, грязь от ORCH-104).
## Решение (сквозное)
Вводится **resilient-pull, встроенный в прод-deploy-хук** (`--deploy`), + новый чистый never-raise
leaf-компонент `src/checkout_hygiene.py`:
- **Хук** перед `git pull origin main` приводит грязную deploy-базу к чистому актуальному `origin/main`
(`git fetch` + `git reset --hard origin/main` + **скоупленный** `git clean -fd`), **строго сохраняя**
rollback/лог-артефакты. Гейт — env `CHECKOUT_HYGIENE`, инжектится `self_deploy.build_deploy_command`.
- **Leaf** `checkout_hygiene` решает условность (`applies(repo)`: kill-switch `checkout_hygiene_enabled`
+ скоуп `checkout_hygiene_repos`, пусто → self-hosting only), строит env-префикс, читает sentinel
отчёта, шлёт Telegram-алерт. Образец `serial_gate`/`cancel`/`self_deploy`.
- **Сходимость** базы после failed/cancelled (FR-2) — этим же deploy-time self-heal; `cancel_task`
(ORCH-090) **не расширяется**, фоновый janitor **не вводится**.
- **Наблюдаемость** — хук пишет sentinel `hygiene`, Phase-C finalizer читает и шлёт Telegram-алерт
(best-effort, never-raise).
- **Инвариант** «main checkout — deploy/worktree-management база, НЕ workspace» документируется
(INFRA.md + architecture/README.md); de-facto энфорс — сам resilient-pull.
## Кросс-каттинг-инварианты (обязательны к соблюдению будущими задачами)
- **INV-HYGIENE-1 (никогда `-x`):** hygiene-`git clean` — только `git clean -fd`. `-x` удалил бы
gitignored `.env` (прод-секреты) / `data/*.db` (БД прода) / `build/`. Анти-регресс — статический тест.
- **INV-HYGIENE-2 (явные excludes):** `.deploy-prev-image-*` (rollback, `deploy_prod_prev_image_file`)
и `deploy-hook.log` — untracked-но-НЕ-ignored → обязательны `-e`-исключения; их удаление сломало бы
rollback.
- **INV-HYGIENE-3 (скоуп = `$REPO`):** гигиена оперирует только рабочим деревом deploy-базы;
sibling `<repos_dir>/.deploy-state-*` / `.merge-lease-*.json` и `.git/worktrees/*` — вне области.
- **Self-hosting safety (NFR-1):** никогда не трогать `main` на remote, не force-push, не рестартить
прод вне штатного гейта, не сносить worktree/ветки других активных задач.
- **Нулевая регрессия (NFR-5):** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` /
machine-verdict ключи / схема БД / exit-code-контракт хука (0/1/2, ORCH-036) — байт-в-байт. Это
устойчивость deploy-пути, **не** Quality Gate и **не** стадия.
## Связи
- Дополняет: adr-0007 (executable self-deploy, ORCH-036), adr-0008 (image-freshness, ORCH-058).
- Не нарушает: adr-0026 (STOP/cancel, ORCH-090) — каскад cancel не трогается.
## Откат
`ORCH_CHECKOUT_HYGIENE_ENABLED=false` → прод-деплой байт-в-байт до ORCH-112 (голый `git pull origin main`).
</content>

View File

@@ -21,6 +21,14 @@
/repos/<project> ← общий каталог репозиториев (host: /home/slin/repos)
```
> **Инвариант deploy-базы (ORCH-112, нормативно).** Shared main checkout
> `<host_repos_dir>/<repo>` (= `/home/slin/repos/orchestrator` == `/repos/orchestrator` в контейнере
> через bind-mount == `settings.deploy_host_repo_path`) — это **deploy/worktree-management база, НЕ
> редактируемый workspace.** Рабочие изменения туда **не пишутся** конвейером/агентами: агенты —
> worktree `/repos/_wt/<repo>/<branch>` (`git_worktree`), `docker build` — worktree-контекст,
> fallback'и гейтов — read-only `git show origin/main`. Self-deploy `git pull` устойчив к грязной
> базе (resilient-pull, см. self-hosting-страховки ниже).
## Контейнеры
| Контейнер | Роль | Порт | env_file | БД (хост) | Старт |
@@ -220,6 +228,7 @@ watchdog'а: **watchdog сигналит, pruner убирает**.
**Страховки:**
- Стадия `deploy-staging` (порт 8501) — обязательный гейт перед прод-деплоем орка. Прод-деплой недостижим, пока staging-гейт не зелёный (см. `STAGING.md`, ORCH-35). Гейт условный: реален только для self-hosting (repo=orchestrator), для остальных проектов — no-op.
- **Свежесть staging-образа (ORCH-058):** на ребре `deploy-staging → deploy` (ПОСЛЕ merge-gate, ДО Phase A) QG-под-чек `check_staging_image_fresh` пересобирает staging-образ из валидированного коммита и пересоздаёт 8501 (Strategy A), а хук перед build-once retag fail-closed сверяет OCI-лейбл `revision` с `EXPECTED_REVISION` (Strategy B). Гарантирует: в прод промоутится РОВНО провалидированный артефакт (инцидент LESSONS_ORCH-036 п.4 — тихий промоут устаревшего образа). Сборки/recreate — ТОЛЬКО staging (8501); FAIL → откат на `development`. Условный: реален только для self-hosting.
- **Гигиена shared deploy-базы (ORCH-112):** self-deploy `git pull origin main` устойчив к грязному рабочему дереву deploy-базы (модифицированные tracked + untracked-остатки failed/cancelled/брошенных задач). Хук `--deploy` перед pull приводит базу к чистому `origin/main` (resilient-pull: `git fetch` + `git reset --hard origin/main` + `git clean -fd`), **строго сохраняя** rollback-снимки `.deploy-prev-image-*`, `deploy-hook.log`, gitignored `.env`/`data/`/`*.db` (НИКОГДА `-x`!), sibling `.deploy-state-*`/`.merge-lease-*.json`, `.git/worktrees/*`. Гейт — kill-switch `ORCH_CHECKOUT_HYGIENE_ENABLED` (дефолт `True`; off → голый pull 1:1); скоуп `ORCH_CHECKOUT_HYGIENE_REPOS` (пусто → self-hosting only). Грязь базы детектируется → лог + Telegram-алерт (Phase-C finalizer). Решает инцидент ORCH-111 (грязь ORCH-104 заблокировала `git pull`). Детально — `docs/work-items/ORCH-112/06-adr/ADR-001`, сквозной adr-0044.
**Правила для агентов при задачах ORCH:**
1. НЕ перезапускать / не ронять прод-контейнер `orchestrator` в рамках задачи.

View File

@@ -0,0 +1,246 @@
---
work_item: ORCH-112
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# ADR-001: Гигиена shared deploy-базы — устойчивый self-deploy `git pull` через resilient-pull в хуке
Work Item: **ORCH-112** — failed/cancelled task artifacts must be cleaned from shared checkout
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0044-deploy-base-checkout-hygiene.md`** (решение
кросс-каттинговое: новый leaf-компонент на глобальном пути прод-деплоя + изменение прод-deploy-хука).
## Статус
Proposed
## Контекст
Self-deploy задачи **ORCH-111** упал на шаге `git pull origin main` хост-хука с
`error: Your local changes to the following files would be overwritten by merge: src/config.py`.
Деплой прерван → потребовал ручного вмешательства (на self-hosting это групповой риск — встаёт
деплой всех проектов). Грязь оставлена ранее **неуспешной/отменённой/перезапущенной** задачей
ORCH-104 в общем (shared) checkout.
Факты, сверенные с кодом:
1. **Точка отказа — голый pull.** `scripts/orchestrator-deploy-hook.sh:223-226` делает `cd "$REPO"`
(= `settings.deploy_host_repo_path` = main clone, строка 73) и **голый** `git pull origin main`
**без гигиены рабочего дерева**. Любая локальная правка tracked-файла блокирует merge. Во всём
`src/`+`scripts/` нет ни одного `git reset --hard` / `git clean` / `git stash` для приведения базы
к чистому состоянию.
2. **Инвариант «main clone ≠ workspace» соблюдён, но не задокументирован и не энфорснут.** Агенты
работают в worktree'ах `/repos/_wt/<repo>/<branch>` (`git_worktree.ensure_worktree`); `docker build`
берёт контекст **worktree** (`image_freshness._host_worktree_path`); fallback'и гейтов на main clone —
**только чтение** (`git show origin/main:...`). Значит грязь — почти наверняка ручной/брошенный WIP.
Но **нет механизма**, детектирующего/чистящего грязную базу, и **нет задокументированного
инварианта**.
3. **`cancel_task` чистит worktree + remote-ветку, но не shared checkout.** `stage_engine.cancel_task`
(строки 2333-2342): `remove_worktree(repo, branch)` + `gitea.delete_remote_branch(repo, branch)`
нулевое покрытие случая «грязная deploy-база».
4. **NFR-2 (сохранность) — жёсткое ограничение.** Сверено `.gitignore` + `src/config.py`:
- `.env`, `data/`, `*.db`, `.venv/`, `__pycache__/`, `build/`, `.env.staging`, `.env.watchdog`,
`deploy/bundled/repos/`**gitignored** → переживают `git clean -fd` (без `-x`) автоматически.
**Прод-секреты `.env` и БД `data/*.db` пропадут только при ошибочном `-x`.**
- `.deploy-prev-image-prod` (`deploy_prod_prev_image_file`) / `.deploy-prev-image-staging`,
а также `deploy-hook.log` (fallback-локация лога, хук строки 60-65) — **untracked, НЕ gitignored**
→ голый `git clean -fd` их **удалит** → сломает rollback (`do_rollback`, хук строки 107-139).
- Sibling-состояния `<repos_dir>/.deploy-state-<repo>/...` (`self_deploy._state_dir`) и
`<repos_dir>/.merge-lease-<repo>.json` (`merge_gate`, строка 315) лежат под **родителем** `$REPO`
(`settings.repos_dir`, не `$REPO`) → `git clean` в `$REPO` их физически не достаёт.
- `.git/worktrees/*` (админ-записи worktree) — внутри `.git/`, `git clean` его **никогда** не трогает.
«Как есть» не годится: deploy-база трактуется как «всегда чистая», что не гарантировано; устойчивость
должна быть на стороне системы — база обязана **самовосстанавливаться** в чистый `origin/main` перед pull.
## Решение
### Сводка
Сделать self-deploy `git pull` **устойчивым к грязной shared deploy-базе** через **resilient-pull,
встроенный в хук** (`--deploy`): перед `git pull origin main` добавляется детерминированный
hygiene-блок, который при обнаружении грязного дерева приводит базу к чистому актуальному `origin/main`
(`git fetch` + `git reset --hard origin/main` + **скоупленный** `git clean -fd`), **строго сохраняя**
rollback/лог-артефакты (NFR-2). Блок гейтится env-переменной `CHECKOUT_HYGIENE`, которую инжектит
`self_deploy.build_deploy_command` **только** когда новый leaf `src/checkout_hygiene.py`-`applies(repo)`
истинен (kill-switch + self-hosting-скоуп). Сходимость базы после failed/cancelled (FR-2) достигается
этим же deploy-time self-heal — **без** расширения `cancel_task` и **без** фонового «дворника».
Наблюдаемость — sentinel-файл от хука, который читает Phase-C finalizer и шлёт Telegram-алерт.
**Инвариант (NFR-5):** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` /
machine-verdict ключи (`deploy_status:`/`staging_status:`/`security_status:`) / схема БД /
exit-code-контракт хука (0/1/2, ORCH-036) — **байт-в-байт не тронуты**. Это устойчивость deploy-пути
и политика гигиены, **не** Quality Gate и **не** стадия.
### D1 — Механизм: resilient-pull в хуке (а не janitor / не container-side clean) — FR-1, AC-1/AC-2
Падающий `git pull` исполняется **на хосте** внутри detached-хука (`self_deploy.build_deploy_command`
→ ssh + setsid). Поэтому гигиена обязана выполняться **в самом хуке, на хосте, непосредственно перед
pull** — это исключает TOCTOU-гонку, которая возникла бы при чистке shared-mount из контейнера
параллельно бегущему detached-хуку.
Новый блок «2a. Resilient pull» в `scripts/orchestrator-deploy-hook.sh` **между** шагом «1. Capture
PREV_IMG» и шагом «2. Pull latest code», под `if [[ "${CHECKOUT_HYGIENE:-0}" == "1" ]]`:
```bash
# 2a. ORCH-112: resilient pull — converge the shared deploy-base to a clean
# origin/main BEFORE the pull, so a dirty working tree (manual/abandoned WIP)
# never blocks the deploy. Gated by CHECKOUT_HYGIENE (Python kill-switch +
# self-hosting scope). NEVER `-x` (would delete .env/data/*.db); EXCLUDES the
# rollback/log artefacts (NFR-2). Best-effort: any failure is logged and the
# bare `git pull` below still runs (never-break).
if [[ "${CHECKOUT_HYGIENE:-0}" == "1" ]]; then
dirty="$(git status --porcelain 2>/dev/null || true)"
if [[ -n "$dirty" ]]; then
log "HYGIENE: dirty deploy-base detected, converging to origin/main:"
log "$dirty"
git fetch origin main >> "$LOG" 2>&1 || log "HYGIENE: fetch failed (continuing)"
git reset --hard origin/main >> "$LOG" 2>&1 || log "HYGIENE: reset failed (continuing)"
git clean -fd \
-e '.deploy-prev-image-*' \
-e 'deploy-hook.log' \
>> "$LOG" 2>&1 || log "HYGIENE: clean failed (continuing)"
if [[ -n "${HYGIENE_REPORT:-}" ]]; then
{ printf 'dirty=1\n'; printf '%s\n' "$dirty"; } > "$HYGIENE_REPORT" 2>/dev/null || true
fi
else
log "HYGIENE: deploy-base already clean (no-op)"
fi
fi
```
- `git status --porcelain` детектирует грязь; **чистая база → блок no-op** (порожний вывод) → дальше
обычный `git pull` (AC-4, happy-path байт-в-байт).
- `git fetch origin main` обновляет `origin/main`-ref, чтобы `reset --hard origin/main` сходился к
**актуальному** источнику истины; tracked-правки (`src/config.py`) дискардятся (AC-1).
- `git clean -fd` снимает untracked-остатки (AC-2). **`-x` запрещён** (см. D2). Последующий
`git pull origin main` (строка 226, **не изменяется**) становится no-op fast-forward.
- Шаг 1 хука пишет `.deploy-prev-image-prod` **до** hygiene → exclude `-e '.deploy-prev-image-*'`
сохраняет rollback-снимок **этого** деплоя (AC-3).
### D2 — Сохранность deploy-состояния: NFR-2 как жёсткий контракт — AC-3
**INV-HYGIENE-1 (никогда `-x`).** Hygiene-`git clean` исполняется **только** как `git clean -fd`
(без `-x`). `-x` удалил бы gitignored `.env` (прод-секреты), `data/*.db` (БД прода), `build/` — что
катастрофично. Анти-регресс: статический тест ассертит, что hygiene-блок хука **не содержит** токена
`-x` (TC-05/TC-09).
**INV-HYGIENE-2 (явные excludes).** `.deploy-prev-image-*` и `deploy-hook.log` untracked-но-НЕ-ignored
→ обязательны `-e '.deploy-prev-image-*'` и `-e 'deploy-hook.log'`. TC-03 доказывает их неудаление и
работоспособность rollback после гигиены.
**INV-HYGIENE-3 (скоуп = `$REPO`).** Hygiene оперирует **только** рабочим деревом `$REPO`. Sibling
`.deploy-state-*` / `.merge-lease-*.json` (под `<repos_dir>`, родитель `$REPO`) и `.git/worktrees/*`
(внутри `.git/`) **вне** области `git clean` в `$REPO` — подтверждено топологией; гигиена их не достаёт.
### D3 — Гейтинг: leaf `src/checkout_hygiene.py` + инжекция env в `build_deploy_command` — FR-5, AC-6
Новый чистый never-raise leaf `src/checkout_hygiene.py` (по образцу `serial_gate`/`cancel`/`self_deploy`;
импортирует только `config`, лениво — `qg.checks.is_self_hosting_repo`):
- `applies(repo) -> bool``checkout_hygiene_enabled=False``False` (kill-switch, голый pull 1:1 до
ORCH-112); `checkout_hygiene_repos` (CSV) непуст → только перечисленные репо; **пусто →
self-hosting only** (`is_self_hosting_repo`, как `self_deploy_repos`). Локальный, без сети, **первым**.
- `hook_env(repo, work_item_id) -> str` — возвращает префикс `CHECKOUT_HYGIENE=1 HYGIENE_REPORT=<path>`
(через `shlex.quote`) **только** при `applies==True`; иначе `""`. `HYGIENE_REPORT` =
`os.path.join(self_deploy.host_state_dir(repo, work_item_id), "hygiene")` — в том же deploy-state
каталоге, что и `result` (shared mount, читается контейнером).
- `read_report(repo, work_item_id) -> dict | None` — читает sentinel `hygiene` через
`self_deploy.container_state_dir`; never-raise.
- `alert_dirty(...)` — best-effort `send_telegram` (кликабельный номер), never-raise (D5).
- `snapshot() -> dict` — read-only блок для `GET /queue` (флаг/скоуп; опционально).
`self_deploy.build_deploy_command` (строки 253-261) добавляет `checkout_hygiene.hook_env(repo, wi)` в
`env_assignments` (одна строка-присваивание в префиксе detached-команды; точно как ORCH-058
`EXPECTED_REVISION`). Когда `applies==False` → префикс пуст → хук видит `CHECKOUT_HYGIENE` неустановленным
→ блок 2a — no-op → **голый `git pull` (1:1 до ORCH-112)**.
Флаги в `src/config.py` (дефолт = боевое):
- `checkout_hygiene_enabled: bool = True` (env `ORCH_CHECKOUT_HYGIENE_ENABLED`);
- `checkout_hygiene_repos: str = ""` (env `ORCH_CHECKOUT_HYGIENE_REPOS`; пусто → self-hosting only).
### D4 — Сходимость после failed/cancelled: deploy-time self-heal достаточен; `cancel_task` НЕ расширяется — FR-2, AC-7
Нормальный конвейер **не пишет** в main clone (факт 2 контекста) → грязь — ручной/брошенный WIP.
Resilient-pull (D1) приводит базу к чистому `origin/main` **на следующем же self-deploy** → база
**сходится** независимо от источника остатков → FR-2/AC-7 удовлетворены deploy-time self-heal.
`stage_engine.cancel_task` (ORCH-090) **намеренно не расширяется** на shared-базу:
- избегаем касания критично-оконного каскада ORCH-090 (`cancel.in_critical_window`, adr-0026);
- избегаем container-side мутации deploy-базы (гонка с возможным параллельным деплоем);
- расширение было бы избыточным — self-heal в D1 уже гарантирует сходимость.
**Отвергнут активный «дворник» (janitor)** (фоновый актор, приводящий базу к чистоте по таймеру/событию):
новый background-поток + дополнительная гонка с detached-деплоем и держателем merge-lease, без выигрыша
сверх deploy-time self-heal.
### D5 — Наблюдаемость: sentinel хука → Telegram-алерт finalizer'а — FR-4, AC-8
Detached-хук (bash) не шлёт Telegram сам. Он пишет sentinel `hygiene` (`HYGIENE_REPORT`) в deploy-state
каталог. Phase-C finalizer (`stage_engine`, уже читает `result` через `self_deploy.read_result`) после
маппинга exit-code вызывает `checkout_hygiene.read_report` + `alert_dirty` (best-effort):
структурный лог + Telegram (`send_telegram`, кликабельный `ORCH-112` через `plane_issue_link`,
`disable_web_page_preview`). Сбой алерта **не валит** деплой (never-raise, AC-8). Опционально — блок
`checkout_hygiene` в `GET /queue`.
### D6 — Инвариант deploy-базы: документирование + de-facto энфорс — FR-3, AC-10
Инвариант «`<host_repos_dir>/<repo>` (main checkout) — deploy/worktree-management база, **НЕ**
редактируемый workspace; рабочие изменения туда не пишутся конвейером/агентами» документируется в
`docs/operations/INFRA.md` (топология + self-hosting) и `docs/architecture/README.md` (раздел ORCH-36).
**De-facto энфорс** — сам resilient-pull (D1): любая локальная правка дискардится при следующем деплое.
**Отвергнут** тяжёлый рантайм-guard на горячих путях (FR-3 «где осуществимо, без изменения горячих
путей») — он не нужен: self-heal уже обеспечивает сходимость, а guard добавил бы риск/латентность.
### D7 — Скоуп пути: только `--deploy` pull, не `--build-staging`
Hygiene врезается **только** в режим `--deploy` (где есть падающий `git pull`). Режим `--build-staging`
(хук строки 166-204) собирает образ из `BUILD_CONTEXT` = **worktree** и **не делает** `git pull`
hygiene там не нужна и не добавляется.
## Альтернативы
- **`git stash` вместо `reset --hard`** — отвергнуто: накапливает stash-записи в deploy-базе, не
«сходится к чистому origin/main», прячет источник грязи; сложнее и менее детерминирован.
- **Container-side Python clean перед ssh-dispatch** — отвергнуто (D1): TOCTOU-гонка с detached-хуком,
дублирование пути deploy-базы.
- **Активный фоновый janitor** — отвергнуто (D4): новый актор + гонки, нулевой выигрыш над self-heal.
- **Расширить `cancel_task` на shared-базу** — отвергнуто (D4): касание критично-оконного каскада
ORCH-090 + container-side мутация; избыточно.
- **Тяжёлый рантайм-guard инварианта** — отвергнуто (D6): не нужен при self-heal, риск на горячем пути.
- **`git clean -xfd`** — **категорически** отвергнуто (D2/INV-HYGIENE-1): удалит `.env`/`data/*.db`.
## Последствия
- **+** Self-deploy `git pull` устойчив к грязной shared-базе → нет ручного разруливания (BR-1, AC-1/2).
- **+** База **сходится** к чистому `origin/main` после failed/cancelled/брошенных задач (BR-2, AC-7).
- **+** Happy-path и kill-switch-off — байт-в-байт (BR-5, AC-4/AC-6); конвейер/БД/exit-коды не тронуты.
- **+** Наблюдаемость: оператор видит факт автоочистки (BR-4, AC-8); инвариант задокументирован (AC-10).
- **** `reset --hard` **дискардит** локальные правки deploy-базы безвозвратно. Митигейшн: это **ровно**
инвариант (база = `origin/main`, источник истины — remote; deploy лишь fast-forward'ит); скоуп —
self-hosting; наблюдаемость показывает, что именно сброшено.
- **** Новый leaf + хук-блок + флаги = площадь сопровождения. Митигейшн: leaf минимальный, образец
существующих leaf'ов; never-raise; полный набор анти-регресс-тестов (`04-test-plan.yaml`).
- **Откат:** `ORCH_CHECKOUT_HYGIENE_ENABLED=false` → деплой байт-в-байт до ORCH-112 (голый
`git pull origin main`); полный откат — revert leaf + хук-блок + флагов.
## Ссылки
- BRD: `docs/work-items/ORCH-112/01-brd.md`
- TRZ: `docs/work-items/ORCH-112/02-trz.md`
- Acceptance: `docs/work-items/ORCH-112/03-acceptance-criteria.md`
- Test-plan: `docs/work-items/ORCH-112/04-test-plan.yaml`
- Infra-requirements: `docs/work-items/ORCH-112/07-infra-requirements.md`
- Tech-risks: `docs/work-items/ORCH-112/10-tech-risks.md`
- Сквозной ADR: `docs/architecture/adr/adr-0044-deploy-base-checkout-hygiene.md`
- Сверено по коду: `scripts/orchestrator-deploy-hook.sh:73,107-139,223-226`, `src/self_deploy.py:119-135,
220-278`, `src/merge_gate.py:315`, `src/config.py:285-291`, `.gitignore`, `src/stage_engine.py:2333-2342`
- Трассировка (CLAUDE.md §9 / ORCH-078): ORCH-036 (self-deploy, adr-0007), ORCH-058 (image-freshness,
adr-0008), ORCH-090 (cancel, adr-0026) — инварианты не нарушаются.
</content>
</invoke>

View File

@@ -0,0 +1,60 @@
---
work_item: ORCH-112
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# 07 — Инфра-требования: ORCH-112 — гигиена shared deploy-базы
Work Item: **ORCH-112** · Repo: **orchestrator** · Стадия: architecture
> When-applicable. Затрагивается **жизненный цикл shared deploy-базы** (`<host_repos_dir>/<repo>`),
> а не топология контейнеров/портов/томов. Контейнеры/сеть/тома — `N/A`.
## I-1. Топология / окружения
**Без изменений в составе.** Контейнеры (`orchestrator` 8500 / `orchestrator-staging` 8501), сеть
(`network_mode: host`), порты, тома — прежние.
Затрагивается **deploy-база** `<host_repos_dir>/<repo>` (= `/home/slin/repos/orchestrator` ==
`/repos/orchestrator` в контейнере через bind-mount == `settings.deploy_host_repo_path`). **Нормативно
закрепляется инвариант:** deploy-база — **deploy/worktree-management база, НЕ редактируемый workspace**.
Рабочие изменения туда не пишутся конвейером/агентами (агенты — worktree `/repos/_wt/<repo>/<branch>`,
build — worktree-контекст, fallback'и гейтов — read-only `git show origin/main`). Документируется в
`docs/operations/INFRA.md` (топология + self-hosting) и `docs/architecture/README.md` (раздел ORCH-36).
**Контракт сохранности рабочего дерева deploy-базы (NFR-2, жёсткий):** автоочистка hygiene
(`git clean -fd`, **без** `-x`) **обязана сохранять**:
| Артефакт | Расположение | Почему сохраняется |
|----------|--------------|--------------------|
| `.deploy-prev-image-prod` / `.deploy-prev-image-staging` | `$REPO/` (untracked, НЕ ignored) | rollback-снимок → `-e '.deploy-prev-image-*'` |
| `deploy-hook.log` | `$REPO/` (fallback-лог) | аудит → `-e 'deploy-hook.log'` |
| `.env`, `data/`, `*.db`, `build/` | `$REPO/` (gitignored) | прод-секреты/БД → переживают `git clean` **без** `-x` |
| `.deploy-state-<repo>/*`, `.merge-lease-<repo>.json` | `<repos_dir>/` (sibling, родитель `$REPO`) | вне области `git clean` в `$REPO` |
| `.git/worktrees/*` | `$REPO/.git/` | `git clean` никогда не трогает `.git/` |
## I-2. Переменные окружения / секреты
Две новые env-переменные (`src/config.py`, дефолт = боевое; **обновить `.env.example`**):
| Ключ | Env | Дефолт | Назначение |
|------|-----|--------|-----------|
| `checkout_hygiene_enabled` | `ORCH_CHECKOUT_HYGIENE_ENABLED` | `True` | kill-switch resilient-pull; `False` → голый `git pull` (1:1 до ORCH-112) |
| `checkout_hygiene_repos` | `ORCH_CHECKOUT_HYGIENE_REPOS` | `""` | CSV-скоуп; пусто → self-hosting only (`orchestrator`) |
Внутренние env, инжектируемые в detached-хук `self_deploy.build_deploy_command` (не операторские):
`CHECKOUT_HYGIENE=1`, `HYGIENE_REPORT=<host_state_dir>/hygiene`. Новых секретов нет.
## I-3. Деплой / рестарт
**Рестарт прод-контейнера задачей ORCH-112 — НЕ требуется и ЗАПРЕЩЁН** (self-hosting инвариант,
CLAUDE.md). Изменение активируется штатно: новый промпт/хук/код `cat`-ается/деплоится в обычном
self-deploy-цикле через **staging-гейт (8501)** сначала, затем `Confirm Deploy` (ORCH-059). Хук-блок
hygiene исполняется **только** в `--deploy` режиме (где есть `git pull`); `--build-staging` собирает из
worktree и не пуллит → не затронут. Exit-code-контракт хука (0/1/2, ORCH-036) — байт-в-байт.
## I-4. CI/CD
**Без изменений** `.gitea/workflows/`. Новый тест-модуль `tests/test_deploy_checkout_hygiene.py`
(шелл-симуляция хука во временном git-репо, без сети/прода/ssh — образец
`tests/test_deploy_hook_rollback_sim.py`) исполняется обычным `pytest tests/ -q`. Полный регресс обязан
оставаться зелёным (NFR-5).
</content>

View File

@@ -0,0 +1,43 @@
---
work_item: ORCH-112
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-112 — гигиена shared deploy-базы
Work Item: **ORCH-112** · Repo: **orchestrator** · Стадия: architecture
> Информационный (гейтом не парсится). Риски реализации resilient-pull и их митигейшн.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | **`git clean -x`** ошибочно добавлен → удалит gitignored `.env` (прод-секреты) / `data/*.db` (БД прода) / `build/` — катастрофа на прод-deploy-базе | Низ. | **Выс.** | INV-HYGIENE-1 (ADR D2): только `git clean -fd`, **никогда** `-x`. Статический анти-регресс-тест ассертит отсутствие `-x` в hygiene-блоке хука (TC-05/TC-09); явная рамка в ADR + INFRA |
| TR-2 | Неверный/отсутствующий `-e` exclude → `git clean -fd` удалит `.deploy-prev-image-*` → сломан rollback (`do_rollback`) | Сред. | **Выс.** | INV-HYGIENE-2: обязательные `-e '.deploy-prev-image-*'` + `-e 'deploy-hook.log'`; TC-03 доказывает неудаление и работоспособность rollback; шаг 1 хука пишет prev-image **до** hygiene |
| TR-3 | Сбой шага hygiene (fetch/reset/clean) маскирует/ухудшает исход деплоя | Низ. | Сред. | never-break (ADR D1): каждая git-операция `\|\| log "...continuing"`; деплой продолжается к голому `git pull`; sentinel-отчёт фиксирует факт; fail-safe, исход не хуже текущего |
| TR-4 | `reset --hard origin/main` безвозвратно дискардит локальные правки deploy-базы, которые кто-то «нужными» считал | Низ. | Сред. | Это **ровно** инвариант: deploy-база = `origin/main` (источник истины — remote); deploy лишь fast-forward'ит. Наблюдаемость (D5) показывает сброшенное; скоуп — self-hosting; документировано (INFRA/README) |
| TR-5 | Гонка: hygiene на хосте чистит mount, пока контейнер читает deploy-базу | Низ. | Низ. | Деплой сериализован (serial-gate ORCH-088, один деплой за раз); hygiene в detached-хуке непосредственно перед pull; нормальный конвейер deploy-базу не читает в этот момент (worktree-изоляция). TOCTOU между `status` и `reset` пренебрежимо (один процесс) |
| TR-6 | Скоуп-leak: hygiene затронет прочие репо / синхронный деплой агентом | Низ. | Сред. | `applies(repo)` (локально, первым): пусто → self-hosting only; инжекция env только на self-deploy-пути (сам self-hosting-скоупленный, `self_deploy_applies`); TC-06 |
| TR-7 | Стейл `origin/main` (fetch не выполнился) → `reset --hard` сходится к устаревшему коммиту | Низ. | Низ. | `git fetch origin main` перед reset; при сбое fetch — `\|\| log` + продолжение; последующий `git pull origin main` (строка 226, не тронута) до-тянет недостающее; deploy промоутит build-once `SOURCE_IMAGE` (артефакт не зависит от дерева main clone) |
| TR-8 | Расширение объёма (касание `cancel_task`/`STAGE_TRANSITIONS`/QG) при реализации | Низ. | Сред. | ADR D4 явно запрещает расширение `cancel_task`; NFR-5 байт-в-байт; TC-09 структурно ассертит неизменность контрактов; трассировка ORCH-036/058/090 |
## Сводный вывод
Доминирующий класс — **деструктивная гигиена рядом с прод-rollback-состоянием** (TR-1/TR-2): низкая
вероятность, высокое влияние, **полностью** снимается контрактом сохранности (INV-HYGIENE-1/2) + явными
тестами (TC-03/TC-05/TC-09). Изменение аддитивно, под kill-switch (`checkout_hygiene_enabled`, дефолт
`True`; off → байт-в-байт до ORCH-112), never-raise, self-hosting-скоупленное, **не трогает**
`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схему БД/exit-code-контракт хука.
**Эскалация:** вводится новый leaf-компонент (`src/checkout_hygiene.py`) на глобальном пути прод-деплоя
+ правка прод-deploy-хука + сквозной ADR adr-0044 → рекомендуется лейбл **`arch:major-change`** для
reviewer-внимания (изменение safety-critical, в прод-deploy-пути). Возврат в анализ **не** требуется:
ТЗ удовлетворяется без нарушения принципов архитектуры (всё в Docker на одном сервере, без новых
зависимостей/очередей/k8s, без рестарта прода вне staging-гейта). Остаточный риск для прод-конвейера —
**низкий** при соблюдении INV-HYGIENE-1/2/3.
</content>